Created
October 18, 2020 09:57
-
-
Save ianchanning/201cb323e9ae90aec06c37b719f2dac1 to your computer and use it in GitHub Desktop.
coc.nvim debug log
This file has been truncated, but you can view the full file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11:54:54 DEBUG [transport] - request to vim: -1,nvim_get_api_info,[] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"get_api_info", | |
[] | |
], | |
-1 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -2,nvim_call_function,[ | |
"coc#util#path_replace_patterns", | |
[] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#path_replace_patterns", | |
[] | |
] | |
], | |
-2 | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"set_var", | |
[ | |
"coc_process_pid", | |
17368 | |
] | |
] | |
] | |
11:54:54 DEBUG [connection] - received notification: [ | |
"VimEnter", | |
[] | |
] | |
11:54:54 DEBUG [connection] - received request: 1,[ | |
"extensionStats", | |
[] | |
] | |
11:54:54 DEBUG [connection] - received response: -1,[ | |
null, | |
[ | |
1, | |
{ | |
"functions": [ | |
{ | |
"name": "nvim_buf_attach" | |
}, | |
{ | |
"name": "nvim_get_mode" | |
}, | |
{ | |
"name": "nvim_list_runtime_paths" | |
}, | |
{ | |
"name": "nvim_win_del_var" | |
}, | |
{ | |
"name": "nvim_tabpage_list_wins" | |
}, | |
{ | |
"name": "nvim_buf_del_var" | |
}, | |
{ | |
"name": "nvim_buf_get_mark" | |
}, | |
{ | |
"name": "nvim_tabpage_set_var" | |
}, | |
{ | |
"name": "nvim_create_namespace" | |
}, | |
{ | |
"name": "nvim_win_get_position" | |
}, | |
{ | |
"name": "nvim_win_set_height" | |
}, | |
{ | |
"name": "nvim_call_atomic" | |
}, | |
{ | |
"name": "nvim_buf_detach" | |
}, | |
{ | |
"name": "nvim_buf_line_count" | |
}, | |
{ | |
"name": "nvim_set_current_buf" | |
}, | |
{ | |
"name": "nvim_set_current_dir" | |
}, | |
{ | |
"name": "nvim_get_var" | |
}, | |
{ | |
"name": "nvim_del_current_line" | |
}, | |
{ | |
"name": "nvim_win_set_width" | |
}, | |
{ | |
"name": "nvim_out_write" | |
}, | |
{ | |
"name": "nvim_win_is_valid" | |
}, | |
{ | |
"name": "nvim_set_current_win" | |
}, | |
{ | |
"name": "nvim_get_current_tabpage" | |
}, | |
{ | |
"name": "nvim_tabpage_is_valid" | |
}, | |
{ | |
"name": "nvim_set_var" | |
}, | |
{ | |
"name": "nvim_win_get_height" | |
}, | |
{ | |
"name": "nvim_win_get_buf" | |
}, | |
{ | |
"name": "nvim_win_get_width" | |
}, | |
{ | |
"name": "nvim_buf_set_name" | |
}, | |
{ | |
"name": "nvim_subscribe" | |
}, | |
{ | |
"name": "nvim_get_current_win" | |
}, | |
{ | |
"name": "nvim_feedkeys" | |
}, | |
{ | |
"name": "nvim_get_vvar" | |
}, | |
{ | |
"name": "nvim_tabpage_get_number" | |
}, | |
{ | |
"name": "nvim_get_current_buf" | |
}, | |
{ | |
"name": "nvim_win_get_option" | |
}, | |
{ | |
"name": "nvim_win_get_cursor" | |
}, | |
{ | |
"name": "nvim_get_current_line" | |
}, | |
{ | |
"name": "nvim_win_get_var" | |
}, | |
{ | |
"name": "nvim_buf_get_var" | |
}, | |
{ | |
"name": "nvim_set_current_tabpage" | |
}, | |
{ | |
"name": "nvim_buf_clear_namespace" | |
}, | |
{ | |
"name": "nvim_err_write" | |
}, | |
{ | |
"name": "nvim_del_var" | |
}, | |
{ | |
"name": "nvim_call_dict_function" | |
}, | |
{ | |
"name": "nvim_set_current_line" | |
}, | |
{ | |
"name": "nvim_get_api_info" | |
}, | |
{ | |
"name": "nvim_unsubscribe" | |
}, | |
{ | |
"name": "nvim_get_option" | |
}, | |
{ | |
"name": "nvim_list_wins" | |
}, | |
{ | |
"name": "nvim_set_client_info" | |
}, | |
{ | |
"name": "nvim_win_set_cursor" | |
}, | |
{ | |
"name": "nvim_win_set_option" | |
}, | |
{ | |
"name": "nvim_eval" | |
}, | |
{ | |
"name": "nvim_tabpage_get_var" | |
}, | |
{ | |
"name": "nvim_buf_get_option" | |
}, | |
{ | |
"name": "nvim_tabpage_del_var" | |
}, | |
{ | |
"name": "nvim_buf_get_name" | |
}, | |
{ | |
"name": "nvim_list_bufs" | |
}, | |
{ | |
"name": "nvim_win_set_buf" | |
}, | |
{ | |
"name": "nvim_win_close" | |
}, | |
{ | |
"name": "nvim_command_output" | |
}, | |
{ | |
"name": "nvim_command" | |
}, | |
{ | |
"name": "nvim_tabpage_get_win" | |
}, | |
{ | |
"name": "nvim_win_set_var" | |
}, | |
{ | |
"name": "nvim_buf_add_highlight" | |
}, | |
{ | |
"name": "nvim_buf_set_var" | |
}, | |
{ | |
"name": "nvim_win_get_number" | |
}, | |
{ | |
"name": "nvim_strwidth" | |
}, | |
{ | |
"name": "nvim_buf_set_lines" | |
}, | |
{ | |
"name": "nvim_err_writeln" | |
}, | |
{ | |
"name": "nvim_buf_set_option" | |
}, | |
{ | |
"name": "nvim_list_tabpages" | |
}, | |
{ | |
"name": "nvim_set_option" | |
}, | |
{ | |
"name": "nvim_buf_get_lines" | |
}, | |
{ | |
"name": "nvim_buf_get_changedtick" | |
}, | |
{ | |
"name": "nvim_win_get_tabpage" | |
}, | |
{ | |
"name": "nvim_call_function" | |
}, | |
{ | |
"name": "nvim_buf_is_valid" | |
} | |
] | |
} | |
] | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -1,114ms | |
11:54:54 DEBUG [connection] - received response: -2,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -2,111ms | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"set_client_info", | |
[ | |
"coc", | |
{ | |
"major": 0, | |
"minor": 0, | |
"patch": 79 | |
}, | |
"remote", | |
{}, | |
{} | |
] | |
] | |
] | |
11:54:54 DEBUG [transport] - request to vim: -3,nvim_get_vvar,[ | |
"vim_did_enter" | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"get_vvar", | |
[ | |
"vim_did_enter" | |
] | |
], | |
-3 | |
] | |
11:54:54 DEBUG [connection] - received response: -3,[ | |
null, | |
1 | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -3,0ms | |
11:54:54 DEBUG [transport] - request to vim: -4,nvim_eval,[ | |
"&runtimepath" | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"&runtimepath" | |
] | |
], | |
-4 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -5,nvim_eval,[ | |
"&runtimepath" | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"&runtimepath" | |
] | |
], | |
-5 | |
] | |
11:54:54 DEBUG [connection] - received response: -4,[ | |
null, | |
"/home/channi16/.vim,/home/channi16/.vim/bundle/vim-solarized8,/home/channi16/.vim/bundle/onehalf/vim,/home/channi16/.vim/bundle/vim-sensible,/home/channi16/.vim/bundle/vim-fugitive,/home/channi16/.vim/bundle/vim-surround,/home/channi16/.vim/bundle/vim-unimpaired,/home/channi16/.vim/bundle/vim-repeat,/home/channi16/.vim/bundle/vim-obsession,/home/channi16/.vim/bundle/vim-vinegar,/home/channi16/.vim/bundle/vim-dadbod,/home/channi16/.vim/bundle/vim-commentary,/home/channi16/.vim/bundle/vim-airline,/home/channi16/.vim/bundle/vim-airline-themes,/home/channi16/.vim/bundle/vim-ripgrep,/home/channi16/.vim/bundle/fzf,/home/channi16/.vim/bundle/fzf.vim,/home/channi16/.vim/bundle/ack.vim,/home/channi16/.vim/bundle/zeavim.vim,/home/channi16/.vim/bundle/cream-showinvisibles,/home/channi16/.vim/bundle/coc.nvim,/home/channi16/.vim/bundle/delimitMate,/home/channi16/.vim/bundle/asyncrun.vim,/home/channi16/.vim/bundle/vim-javascript,/home/channi16/.vim/bundle/vim-jsx,/home/channi16/.vim/bundle/vim-graphql,/home/channi16/.vim/bundle/vim-jsdoc,/home/channi16/.vim/bundle/vim-markdown,/home/channi16/.vim/bundle/vim-markdown-composer,/home/channi16/.vim/bundle/vim-toml,/home/channi16/.vim/bundle/nerdtree,/home/channi16/.vim/bundle/mru,/var/lib/vim/addons,/etc/vim,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vim82/pack/dist/opt/matchit,/usr/share/vim/vimfiles/after,/etc/vim/after,/var/lib/vim/addons/after,/home/channi16/.vim/bundle/vim-javascript/after,/home/channi16/.vim/bundle/vim-jsx/after,/home/channi16/.vim/bundle/vim-graphql/after,/home/channi16/.vim/bundle/vim-markdown/after,/home/channi16/.vim/bundle/vim-markdown-composer/after,/home/channi16/.vim/after" | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -4,0ms | |
11:54:54 DEBUG [transport] - response of client cost: 1,14ms | |
11:54:54 DEBUG [connection] - send to vim: [ | |
1, | |
[ | |
null, | |
[ | |
{ | |
"id": "coc-eslint", | |
"isLocal": false, | |
"version": "1.3.2", | |
"description": "eslint extension for coc", | |
"exotic": false, | |
"uri": "", | |
"root": "/home/channi16/.config/coc/extensions/node_modules/coc-eslint", | |
"state": "unknown", | |
"packageJSON": { | |
"name": "coc-eslint", | |
"version": "1.3.2", | |
"description": "eslint extension for coc", | |
"main": "lib/index.js", | |
"publisher": "chemzqm", | |
"keywords": [ | |
"coc.nvim", | |
"eslint" | |
], | |
"engines": { | |
"coc": "^0.0.64" | |
}, | |
"scripts": { | |
"clean": "rimraf lib", | |
"build": "webpack", | |
"prepare": "npx npm-run-all clean build" | |
}, | |
"activationEvents": [ | |
"*" | |
], | |
"contributes": { | |
"commands": [ | |
{ | |
"title": "Fix all auto-fixable problems", | |
"category": "ESLint", | |
"command": "eslint.executeAutofix" | |
}, | |
{ | |
"title": "Create a '.eslintrc' config file", | |
"category": "ESLint", | |
"command": "eslint.createConfig" | |
} | |
], | |
"configuration": { | |
"type": "object", | |
"title": "Eslint", | |
"properties": { | |
"eslint.enable": { | |
"type": "boolean", | |
"default": true, | |
"description": "enable lint for files." | |
}, | |
"eslint.quiet": { | |
"type": "boolean", | |
"default": false, | |
"description": "Turns on quiet mode, which ignores warnings." | |
}, | |
"eslint.nodeEnv": { | |
"type": [ | |
"string", | |
"null" | |
], | |
"default": null, | |
"description": "The value of NODE_ENV to use when running eslint tasks." | |
}, | |
"eslint.trace.server": { | |
"type": "string", | |
"default": "off", | |
"enum": [ | |
"off", | |
"messages", | |
"verbose" | |
] | |
}, | |
"eslint.execArgv": { | |
"type": "array", | |
"default": [], | |
"items": { | |
"type": "string" | |
} | |
}, | |
"eslint.filetypes": { | |
"type": "array", | |
"default": [ | |
"javascript", | |
"javascriptreact", | |
"typescript", | |
"typescriptreact" | |
], | |
"items": { | |
"type": "string" | |
} | |
}, | |
"eslint.packageManager": { | |
"type": "string", | |
"default": "npm", | |
"enum": [ | |
"npm", | |
"yarn" | |
] | |
}, | |
"eslint.run": { | |
"type": "string", | |
"default": "onType", | |
"enum": [ | |
"onType", | |
"onSave" | |
] | |
}, | |
"eslint.runtime": { | |
"type": [ | |
"string", | |
"null" | |
], | |
"default": null, | |
"description": "The location of the node binary to run ESLint under." | |
}, | |
"eslint.nodePath": { | |
"type": [ | |
"string", | |
"null" | |
], | |
"default": null, | |
"description": "A path added to NODE_PATH when resolving the eslint module." | |
}, | |
"eslint.autoFix": { | |
"type": "boolean", | |
"default": true, | |
"description": "Enable auto fix feature" | |
}, | |
"eslint.options": { | |
"scope": "resource", | |
"type": "object", | |
"default": {}, | |
"description": "The eslint options object to provide args normally passed to eslint when executed from a command line (see http://eslint.org/docs/developer-guide/nodejs-api#cliengine)." | |
}, | |
"eslint.autoFixOnSave": { | |
"type": "boolean", | |
"default": false | |
}, | |
"eslint.autoFixSkipRules": { | |
"type": "array", | |
"items": { | |
"type": "string" | |
}, | |
"default": [], | |
"description": "Rules that should be skipped when autofixing." | |
}, | |
"eslint.codeAction.disableRuleComment": { | |
"scope": "resource", | |
"type": "object", | |
"default": { | |
"enable": true, | |
"location": "separateLine" | |
}, | |
"properties": { | |
"enable": { | |
"type": "boolean", | |
"default": true, | |
"description": "Show the disable code actions." | |
}, | |
"location": { | |
"type": "string", | |
"enum": [ | |
"separateLine", | |
"sameLine" | |
], | |
"default": "separateLine", | |
"description": "Configure the disable rule code action to insert the comment on the same line or a new line." | |
} | |
} | |
}, | |
"eslint.codeAction.showDocumentation": { | |
"scope": "resource", | |
"type": "object", | |
"default": { | |
"enable": true | |
}, | |
"properties": { | |
"enable": { | |
"type": "boolean", | |
"default": true, | |
"description": "Show the documentation code actions." | |
} | |
} | |
} | |
} | |
} | |
}, | |
"author": "[email protected]", | |
"license": "MIT", | |
"devDependencies": { | |
"@chemzqm/tsconfig": "^0.0.3", | |
"@types/eslint": "^7.2.2", | |
"@types/fast-diff": "^1.2.0", | |
"@types/node": "^10.12.0", | |
"coc.nvim": "0.0.79", | |
"eslint": "^7.9.0", | |
"fast-diff": "^1.2.0", | |
"rimraf": "^3.0.0", | |
"ts-loader": "^6.2.1", | |
"typescript": "^3.7.4", | |
"vscode-languageserver": "^6.1.1", | |
"vscode-languageserver-protocol": "3.15.3", | |
"vscode-languageserver-textdocument": "^1.0.0", | |
"vscode-uri": "^2.1.1", | |
"webpack": "^4.41.5", | |
"webpack-cli": "^3.3.10", | |
"which": "^2.0.2" | |
}, | |
"dependencies": {} | |
} | |
}, | |
{ | |
"id": "coc-json", | |
"isLocal": false, | |
"version": "1.2.6", | |
"description": "Json extension for coc.nvim", | |
"exotic": false, | |
"uri": "", | |
"root": "/home/channi16/.config/coc/extensions/node_modules/coc-json", | |
"state": "unknown", | |
"packageJSON": { | |
"name": "coc-json", | |
"version": "1.2.6", | |
"description": "Json extension for coc.nvim", | |
"main": "lib/index.js", | |
"publisher": "chemzqm", | |
"keywords": [ | |
"coc.nvim", | |
"json" | |
], | |
"engines": { | |
"coc": ">= 0.0.70" | |
}, | |
"scripts": { | |
"clean": "rimraf lib", | |
"watch": "webpack --watch", | |
"build": "webpack", | |
"prepare": "npx npm-run-all clean build" | |
}, | |
"activationEvents": [ | |
"onLanguage:json", | |
"onLanguage:jsonc" | |
], | |
"contributes": { | |
"configuration": { | |
"type": "object", | |
"title": "Json", | |
"properties": { | |
"json.enable": { | |
"type": "boolean", | |
"default": true, | |
"description": "Enable json server" | |
}, | |
"json.trace.server": { | |
"type": "string", | |
"default": "off", | |
"enum": [ | |
"off", | |
"messages", | |
"verbose" | |
] | |
}, | |
"json.execArgv": { | |
"type": "array", | |
"default": [], | |
"items": { | |
"type": "string" | |
} | |
}, | |
"json.format.enable": { | |
"type": "boolean", | |
"default": true, | |
"description": "Enable format for json server" | |
}, | |
"json.schemas": { | |
"type": "array", | |
"scope": "resource", | |
"description": "Schemas associations for json files", | |
"default": [], | |
"items": { | |
"type": "object", | |
"default": { | |
"fileMatch": [ | |
"/my-file" | |
], | |
"url": "schemaURL" | |
}, | |
"properties": { | |
"url": { | |
"type": "string", | |
"default": "/user.schema.json" | |
}, | |
"fileMatch": { | |
"type": "array", | |
"items": { | |
"type": "string", | |
"default": "MyFile.json" | |
}, | |
"minItems": 1, | |
"description": "File pattern to match." | |
}, | |
"schema": { | |
"$ref": "http://json-schema.org/draft-04/schema#", | |
"description": "Url of json schema, support file/url protocol." | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"author": "[email protected]", | |
"license": "MIT", | |
"devDependencies": { | |
"@chemzqm/tsconfig": "^0.0.3", | |
"@chemzqm/tslint-config": "^1.0.18", | |
"@types/node": "^11.13.10", | |
"coc.nvim": "^0.0.70", | |
"request-light": "^0.2.4", | |
"rimraf": "^2.6.3", | |
"ts-loader": "^6.0.3", | |
"tslint": "^5.16.0", | |
"typescript": "^3", | |
"vscode-json-languageservice": "3.3.0", | |
"vscode-languageserver": "5.3.0-next.7", | |
"vscode-uri": "2.0.1", | |
"webpack": "^4.34.0", | |
"webpack-cli": "^3.3.4" | |
}, | |
"dependencies": {} | |
} | |
} | |
] | |
] | |
] | |
11:54:54 DEBUG [connection] - received response: -5,[ | |
null, | |
"/home/channi16/.vim,/home/channi16/.vim/bundle/vim-solarized8,/home/channi16/.vim/bundle/onehalf/vim,/home/channi16/.vim/bundle/vim-sensible,/home/channi16/.vim/bundle/vim-fugitive,/home/channi16/.vim/bundle/vim-surround,/home/channi16/.vim/bundle/vim-unimpaired,/home/channi16/.vim/bundle/vim-repeat,/home/channi16/.vim/bundle/vim-obsession,/home/channi16/.vim/bundle/vim-vinegar,/home/channi16/.vim/bundle/vim-dadbod,/home/channi16/.vim/bundle/vim-commentary,/home/channi16/.vim/bundle/vim-airline,/home/channi16/.vim/bundle/vim-airline-themes,/home/channi16/.vim/bundle/vim-ripgrep,/home/channi16/.vim/bundle/fzf,/home/channi16/.vim/bundle/fzf.vim,/home/channi16/.vim/bundle/ack.vim,/home/channi16/.vim/bundle/zeavim.vim,/home/channi16/.vim/bundle/cream-showinvisibles,/home/channi16/.vim/bundle/coc.nvim,/home/channi16/.vim/bundle/delimitMate,/home/channi16/.vim/bundle/asyncrun.vim,/home/channi16/.vim/bundle/vim-javascript,/home/channi16/.vim/bundle/vim-jsx,/home/channi16/.vim/bundle/vim-graphql,/home/channi16/.vim/bundle/vim-jsdoc,/home/channi16/.vim/bundle/vim-markdown,/home/channi16/.vim/bundle/vim-markdown-composer,/home/channi16/.vim/bundle/vim-toml,/home/channi16/.vim/bundle/nerdtree,/home/channi16/.vim/bundle/mru,/var/lib/vim/addons,/etc/vim,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vim82/pack/dist/opt/matchit,/usr/share/vim/vimfiles/after,/etc/vim/after,/var/lib/vim/addons/after,/home/channi16/.vim/bundle/vim-javascript/after,/home/channi16/.vim/bundle/vim-jsx/after,/home/channi16/.vim/bundle/vim-graphql/after,/home/channi16/.vim/bundle/vim-markdown/after,/home/channi16/.vim/bundle/vim-markdown-composer/after,/home/channi16/.vim/after" | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -5,2ms | |
11:54:54 DEBUG [transport] - request to vim: -6,nvim_call_function,[ | |
"coc#util#vim_info", | |
[] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#vim_info", | |
[] | |
] | |
], | |
-6 | |
] | |
11:54:54 DEBUG [connection] - received response: -6,[ | |
null, | |
{ | |
"pid": 17367, | |
"version": "8021767", | |
"background": "light", | |
"columns": 173, | |
"completeOpt": "menu,preview", | |
"mode": "n", | |
"textprop": true, | |
"runtimepath": "/home/channi16/.vim,/home/channi16/.vim/bundle/vim-solarized8,/home/channi16/.vim/bundle/onehalf/vim,/home/channi16/.vim/bundle/vim-sensible,/home/channi16/.vim/bundle/vim-fugitive,/home/channi16/.vim/bundle/vim-surround,/home/channi16/.vim/bundle/vim-unimpaired,/home/channi16/.vim/bundle/vim-repeat,/home/channi16/.vim/bundle/vim-obsession,/home/channi16/.vim/bundle/vim-vinegar,/home/channi16/.vim/bundle/vim-dadbod,/home/channi16/.vim/bundle/vim-commentary,/home/channi16/.vim/bundle/vim-airline,/home/channi16/.vim/bundle/vim-airline-themes,/home/channi16/.vim/bundle/vim-ripgrep,/home/channi16/.vim/bundle/fzf,/home/channi16/.vim/bundle/fzf.vim,/home/channi16/.vim/bundle/ack.vim,/home/channi16/.vim/bundle/zeavim.vim,/home/channi16/.vim/bundle/cream-showinvisibles,/home/channi16/.vim/bundle/coc.nvim,/home/channi16/.vim/bundle/delimitMate,/home/channi16/.vim/bundle/asyncrun.vim,/home/channi16/.vim/bundle/vim-javascript,/home/channi16/.vim/bundle/vim-jsx,/home/channi16/.vim/bundle/vim-graphql,/home/channi16/.vim/bundle/vim-jsdoc,/home/channi16/.vim/bundle/vim-markdown,/home/channi16/.vim/bundle/vim-markdown-composer,/home/channi16/.vim/bundle/vim-toml,/home/channi16/.vim/bundle/nerdtree,/home/channi16/.vim/bundle/mru,/var/lib/vim/addons,/etc/vim,/usr/share/vim/vimfiles,/usr/share/vim/vim82,/usr/share/vim/vim82/pack/dist/opt/matchit,/usr/share/vim/vimfiles/after,/etc/vim/after,/var/lib/vim/addons/after,/home/channi16/.vim/bundle/vim-javascript/after,/home/channi16/.vim/bundle/vim-jsx/after,/home/channi16/.vim/bundle/vim-graphql/after,/home/channi16/.vim/bundle/vim-markdown/after,/home/channi16/.vim/bundle/vim-markdown-composer/after,/home/channi16/.vim/after", | |
"progpath": "/usr/bin/vim.gtk3", | |
"isCygwin": false, | |
"disabledSources": {}, | |
"isMacvim": false, | |
"floating": false, | |
"extensionRoot": "/home/channi16/.config/coc/extensions", | |
"lines": 44, | |
"cmdheight": 1, | |
"config": {}, | |
"isVim": true, | |
"workspaceFolders": null, | |
"pumevent": 1, | |
"isiTerm": 0, | |
"filetypeMap": {}, | |
"globalExtensions": [], | |
"locationlist": 1, | |
"colorscheme": "solarized8", | |
"vimCommands": [], | |
"watchExtensions": [], | |
"guicursor": "n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175" | |
} | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -6,25ms | |
11:54:54 DEBUG [transport] - request to vim: -7,nvim_list_bufs,[] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"list_bufs", | |
[] | |
], | |
-7 | |
] | |
11:54:54 DEBUG [connection] - received response: -7,[ | |
null, | |
[ | |
1, | |
2, | |
3, | |
4, | |
5, | |
6, | |
7, | |
8, | |
9, | |
10, | |
11, | |
12, | |
13, | |
14 | |
] | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -7,1ms | |
11:54:54 DEBUG [transport] - request to vim: -8,nvim_call_function,[ | |
"bufnr", | |
[ | |
"%" | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"bufnr", | |
[ | |
"%" | |
] | |
] | |
], | |
-8 | |
] | |
11:54:54 DEBUG [connection] - received response: -8,[ | |
null, | |
1 | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -8,0ms | |
11:54:54 DEBUG [transport] - request to vim: -9,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
1 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
1 | |
] | |
] | |
], | |
-9 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -10,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
2 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
2 | |
] | |
] | |
], | |
-10 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -11,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
3 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
3 | |
] | |
] | |
], | |
-11 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -12,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
4 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
4 | |
] | |
] | |
], | |
-12 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -13,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
5 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
5 | |
] | |
] | |
], | |
-13 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -14,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
6 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
6 | |
] | |
] | |
], | |
-14 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -15,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
7 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
7 | |
] | |
] | |
], | |
-15 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -16,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
8 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
8 | |
] | |
] | |
], | |
-16 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -17,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
9 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
9 | |
] | |
] | |
], | |
-17 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -18,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
10 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
10 | |
] | |
] | |
], | |
-18 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -19,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
11 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
11 | |
] | |
] | |
], | |
-19 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -20,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
12 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
12 | |
] | |
] | |
], | |
-20 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -21,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
13 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
13 | |
] | |
] | |
], | |
-21 | |
] | |
11:54:54 DEBUG [transport] - request to vim: -22,nvim_call_function,[ | |
"coc#util#get_bufoptions", | |
[ | |
14 | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_bufoptions", | |
[ | |
14 | |
] | |
] | |
], | |
-22 | |
] | |
11:54:54 DEBUG [connection] - received response: -9,[ | |
null, | |
{ | |
"changedtick": 4, | |
"variables": {}, | |
"winid": 1000, | |
"eol": 1, | |
"previewwindow": false, | |
"bufname": "innovatrix/images/api/service/services/CampaignService.js", | |
"fullpath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js", | |
"filetype": "javascript.jsx", | |
"buftype": "", | |
"iskeyword": "@,48-57,_,192-255,$", | |
"size": 51559 | |
} | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -9,1ms | |
11:54:54 DEBUG [connection] - received response: -10,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -10,1ms | |
11:54:54 DEBUG [connection] - received response: -11,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -11,1ms | |
11:54:54 DEBUG [connection] - received response: -12,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -12,1ms | |
11:54:54 DEBUG [connection] - received response: -13,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -13,1ms | |
11:54:54 DEBUG [connection] - received response: -14,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -14,1ms | |
11:54:54 DEBUG [connection] - received response: -15,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -15,1ms | |
11:54:54 DEBUG [connection] - received response: -16,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -16,1ms | |
11:54:54 DEBUG [connection] - received response: -17,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -17,1ms | |
11:54:54 DEBUG [connection] - received response: -18,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -18,1ms | |
11:54:54 DEBUG [connection] - received response: -19,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -19,1ms | |
11:54:54 DEBUG [connection] - received response: -20,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -20,0ms | |
11:54:54 DEBUG [transport] - request to vim: -23,nvim_call_function,[ | |
"getbufline", | |
[ | |
1, | |
1, | |
"$" | |
] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"getbufline", | |
[ | |
1, | |
1, | |
"$" | |
] | |
] | |
], | |
-23 | |
] | |
11:54:54 DEBUG [connection] - received response: -21,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -21,1ms | |
11:54:54 DEBUG [connection] - received response: -22,[ | |
null, | |
null | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -22,1ms | |
11:54:54 DEBUG [connection] - received response: -23,[ | |
null, | |
[ | |
"const log4js = require('log4js');", | |
"const { config } = require('cargo-service');", | |
"const { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');", | |
"const moment = require('moment-timezone');", | |
"const {", | |
" addCampaignManagerRoleToUser,", | |
" addUserToDxAuth,", | |
" getAllPossibleReviewers,", | |
" getPossibleCampaignManagers,", | |
" getScopedRightsForUser,", | |
" getUserFromDxAuth,", | |
" getUserFromDxAuthByDxAuthId,", | |
"} = require('./_duxisAuthService');", | |
"const DuxisAuthClient = require('./DuxisAuthClient');", | |
"const {", | |
" DOCUMENT_FIELD,", | |
" PROJECT_RECORD_FIELD,", | |
" DRAFT_INTAKE,", | |
" OPEN_INTAKE,", | |
" SUBMITTED_INTAKE,", | |
" BRUSSELS_TZ,", | |
"} = require('../constants/intakes');", | |
"", | |
"const log = log4js.getLogger('CampaignService');", | |
"", | |
"const SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);", | |
"const PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);", | |
"", | |
"function transformCampaign({", | |
" assessment_flows_group_id,", | |
" created_by,", | |
" created_on,", | |
" decision_date,", | |
" end_date,", | |
" has_evaluative_phases,", | |
" intake_end_date,", | |
" intake_start_date,", | |
" intake_type,", | |
" pitch_end,", | |
" pitch_start,", | |
" start_date,", | |
" updated_by,", | |
" updated_on,", | |
" ...rest", | |
"}) {", | |
" return {", | |
" assessmentFlowsGroupId: assessment_flows_group_id,", | |
" createdBy: created_by,", | |
" createdOn: created_on,", | |
" decisionDate: decision_date,", | |
" endDate: end_date,", | |
" hasEvaluativePhases: has_evaluative_phases,", | |
" intakeEndDate: intake_end_date,", | |
" intakeStartDate: intake_start_date,", | |
" intakeType: intake_type,", | |
" pitchEnd: pitch_end,", | |
" pitchStart: pitch_start,", | |
" startDate: start_date,", | |
" updatedBy: updated_by,", | |
" updatedOn: updated_on,", | |
" ...rest,", | |
" };", | |
"}", | |
"", | |
"class CampaignService {", | |
" constructor(store) {", | |
" this.store = store;", | |
" this.tableName = 'campaigns';", | |
" this.duxisAuthClient = new DuxisAuthClient();", | |
" }", | |
"", | |
" async createCampaigns(data, trx) {", | |
" try {", | |
" log.info('Writing campaigns...');", | |
"", | |
" const preSelectionPhase = await trx('phases')", | |
" .first('id')", | |
" .where('title', 'Pre-selection');", | |
" const selectionPhase = await trx('phases')", | |
" .first('id')", | |
" .where('title', 'Selection');", | |
" const projectPhase = await trx('phases')", | |
" .first('id')", | |
" .where('title', 'Project');", | |
"", | |
" let preSelId = uuid();", | |
" let selId = uuid();", | |
" let projectPhaseId = uuid();", | |
"", | |
" if (preSelectionPhase) {", | |
" preSelId = preSelectionPhase.id;", | |
" } else {", | |
" await trx('phases').insert({", | |
" id: preSelId,", | |
" title: 'Pre-selection',", | |
" });", | |
" }", | |
"", | |
" if (selectionPhase) {", | |
" selId = selectionPhase.id;", | |
" } else {", | |
" await trx('phases').insert({", | |
" id: selId,", | |
" title: 'Selection',", | |
" });", | |
" }", | |
"", | |
" if (projectPhase) {", | |
" projectPhaseId = projectPhase.id;", | |
" } else {", | |
" await trx('phases').insert({", | |
" id: projectPhaseId,", | |
" title: 'Project',", | |
" });", | |
" }", | |
"", | |
" await asyncForIn(data, async ({ phases, ...campaign }) => {", | |
" await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE", | |
" SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,", | |
" decision_date = ?, pitch_start = ?, pitch_end = ?;", | |
" `, [", | |
" campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,", | |
" campaign.decision_date, campaign.pitch_start, campaign.pitch_end,", | |
" ]);", | |
"", | |
" await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {", | |
" await trx.raw(`${trx('campaigns_phases').insert({", | |
" assessment_version_group_id: assessmentVersionGroupId,", | |
" deadline,", | |
" left_id: campaign.id,", | |
" right_id: title === 'Pre-selection' ? preSelId : selId,", | |
" order,", | |
" }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE", | |
" SET deadline = ?;", | |
" `, [deadline]);", | |
" });", | |
"", | |
" await trx.raw(`${trx('campaigns_phases')", | |
" .insert({", | |
" left_id: campaign.id,", | |
" right_id: projectPhaseId,", | |
" order: phases.length,", | |
" })", | |
" .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;", | |
" `, []);", | |
" });", | |
" log.info('Writing campaigns is done!');", | |
" } catch (e) {", | |
" log.warn(`createCampaigns ${e}`);", | |
" }", | |
" }", | |
"", | |
" async removeAll(trx) {", | |
" try {", | |
" await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);", | |
" await trx('campaigns_phases').del();", | |
" await trx('phases').del();", | |
" await trx(this.tableName).del();", | |
" await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);", | |
" } catch (e) {", | |
" log.warn(`removeAll: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async createCampaign({", | |
" name, phases, intakeType, intakeStartDate,", | |
" intakeEndDate, hasEvaluativePhases, intakeFormId,", | |
" }, duxisId) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" const profile = await trx('user_profiles')", | |
" .first('id').where('duxis_auth_id', duxisId);", | |
"", | |
" if (!profile) {", | |
" throw new Error('Unauthorized: user profile does not exist.');", | |
" }", | |
"", | |
" const campaignId = uuid();", | |
" const campaign = await trx('campaigns').insert({", | |
" id: campaignId,", | |
" created_by: profile.id,", | |
" created_on: new Date(),", | |
" updated_by: profile.id,", | |
" updated_on: new Date(),", | |
" name,", | |
" intake_type: intakeType,", | |
" intake_start_date: intakeStartDate,", | |
" intake_end_date: intakeEndDate,", | |
" has_evaluative_phases: hasEvaluativePhases,", | |
" });", | |
"", | |
" let phaseOrder = 0;", | |
" if (hasEvaluativePhases && phases && phases.length > 0) {", | |
" for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {", | |
" await trx('campaigns_phases').insert({", | |
" assessment_version_group_id: assessmentVersionGroupId,", | |
" deadline,", | |
" left_id: campaignId,", | |
" right_id: phaseId,", | |
" order: phaseOrder,", | |
" });", | |
" phaseOrder += 1;", | |
" }", | |
" }", | |
"", | |
" const projectPhase = await trx('phases')", | |
" .first('id')", | |
" .where('title', 'Project');", | |
"", | |
" if (projectPhase) {", | |
" await trx('campaigns_phases').insert({", | |
" left_id: campaignId,", | |
" right_id: projectPhase.id,", | |
" order: phaseOrder,", | |
" });", | |
" }", | |
"", | |
" // Link intake form with campaign if the intakeType is NOT creation", | |
" if (intakeType === 'APPLICATION' && intakeFormId) {", | |
" await trx('campaigns_intakes').insert({", | |
" left_id: campaignId,", | |
" right_id: intakeFormId,", | |
" });", | |
" }", | |
"", | |
" return { ...campaign, intakeFormId };", | |
" });", | |
" } catch (e) {", | |
" log.warn(`createCampaign: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async updateCampaign({", | |
" assessmentFlowsGroupId,", | |
" attendees,", | |
" campaignId,", | |
" hasEvaluativePhases,", | |
" intakeEndDate,", | |
" intakeFormId,", | |
" intakeStartDate,", | |
" intakeType,", | |
" name,", | |
" phases,", | |
" }, duxisId) {", | |
" try {", | |
" let campaign;", | |
" await this.store.knex.transaction(async (trx) => {", | |
" const profile = await trx('user_profiles')", | |
" .first('id').where('duxis_auth_id', duxisId);", | |
"", | |
" if (!profile) {", | |
" throw new Error('Unauthorized: user does not exist.');", | |
" }", | |
"", | |
" await trx('campaigns').update({", | |
" assessment_flows_group_id: assessmentFlowsGroupId,", | |
" attendees,", | |
" name,", | |
" intake_type: intakeType,", | |
" intake_start_date: intakeStartDate,", | |
" intake_end_date: intakeEndDate,", | |
" has_evaluative_phases: hasEvaluativePhases,", | |
" updated_by: profile.id,", | |
" updated_on: new Date(),", | |
" }).where('id', campaignId);", | |
"", | |
" // If no phases were explicitly provided we ignore it.", | |
" if (phases !== undefined) {", | |
" // Remove campaign phase links.", | |
" await trx('campaigns_phases').del().where('left_id', campaignId);", | |
"", | |
" let phaseOrder = 0;", | |
" if (hasEvaluativePhases && phases && phases.length > 0) {", | |
" for (const { deadline, phaseId, assessmentVersionId } of phases) {", | |
" const assessmentVersion = await trx('assessment_versions')", | |
" .first('id', 'group_id AS groupId')", | |
" .where('id', assessmentVersionId);", | |
" await trx('campaigns_phases').insert({", | |
" assessment_version_id: assessmentVersion.id,", | |
" assessment_version_group_id: assessmentVersion.groupId,", | |
" deadline,", | |
" left_id: campaignId,", | |
" right_id: phaseId,", | |
" order: phaseOrder,", | |
" });", | |
" phaseOrder += 1;", | |
" }", | |
" }", | |
"", | |
" const projectPhase = await trx('phases')", | |
" .first('id')", | |
" .where('title', 'Project');", | |
"", | |
" if (projectPhase) {", | |
" await trx('campaigns_phases').insert({", | |
" left_id: campaignId,", | |
" right_id: projectPhase.id,", | |
" order: phaseOrder,", | |
" });", | |
" }", | |
" }", | |
"", | |
" if (intakeFormId || intakeType === 'CREATION') {", | |
" await trx('campaigns_intakes')", | |
" .del()", | |
" .where('left_id', campaignId);", | |
" }", | |
"", | |
" if (intakeType === 'APPLICATION' && intakeFormId) {", | |
" await trx('campaigns_intakes').insert({", | |
" left_id: campaignId,", | |
" right_id: intakeFormId,", | |
" });", | |
" }", | |
"", | |
" campaign = await this.getCampaign(campaignId, trx);", | |
" });", | |
" return campaign;", | |
" } catch (e) {", | |
" log.warn(`updateCampaign: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async getCampaign(campaignId, existingTrx) {", | |
" try {", | |
" let campaign;", | |
" const now = moment().tz(BRUSSELS_TZ);", | |
" await this.store.withTransaction(existingTrx, async (trx) => {", | |
" campaign = await trx(this.tableName)", | |
" .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')", | |
" .where('id', campaignId)", | |
" .first();", | |
"", | |
" campaign.phases = await trx('phases')", | |
" .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')", | |
" .join('campaigns_phases', 'right_id', 'phases.id')", | |
" .where('left_id', campaignId)", | |
" .orderBy('campaigns_phases.order');", | |
"", | |
" const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;", | |
" campaign.isInFirstPhase = campaignFirstPhaseDeadline", | |
" ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)", | |
" : true;", | |
" campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);", | |
" const linkedIntake = await trx('campaigns_intakes')", | |
" .first('right_id AS id').where('left_id', campaignId);", | |
" campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;", | |
" });", | |
" return transformCampaign(campaign);", | |
" } catch (e) {", | |
" log.warn(`getCampaign: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async getPublicCampaign({ campaignName }) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" // Make sure the campaign exists", | |
" const publicCampaignData = await trx('campaigns')", | |
" .first('id', 'name', 'intake_end_date AS deadline')", | |
" .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well", | |
"", | |
" // If campaign wasn't found let's pass this INVALID_ID so we can do some error", | |
" // handling in the front-end", | |
" if (!publicCampaignData) {", | |
" return { id: 'INVALID_ID', callClosed: true };", | |
" }", | |
"", | |
" const now = moment().tz(BRUSSELS_TZ);", | |
" const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));", | |
" return { ...publicCampaignData, callClosed };", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getPublicCampaign: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async registerCampaignUser({ campaignId, user }) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" // Check intake_end for current campaign", | |
" const campaign = await trx('campaigns')", | |
" .first('intake_end_date AS deadline')", | |
" .where('id', campaignId);", | |
" if (!campaign) {", | |
" throw new Error('Call does not exist.');", | |
" }", | |
" const now = moment().tz(BRUSSELS_TZ);", | |
" const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;", | |
" if (now.isSameOrAfter(deadline)) {", | |
" throw new Error('Call is closed.');", | |
" }", | |
" // Try to register the user to auth0 and in our database", | |
" const dxResponse = await this.duxisAuthClient.getDuxisToken();", | |
" const { dxToken: strippedToken } = dxResponse;", | |
"", | |
" const sanitizedUsername = user.email.toLowerCase();", | |
"", | |
" const response = await addUserToDxAuth({", | |
" data: { firstName: user.firstName, lastName: user.lastName },", | |
" enabled: true,", | |
" provider: 'auth0',", | |
" roles: ['innovatrix/role/applicant'],", | |
" scoped_rights: [],", | |
" username: sanitizedUsername,", | |
" password: user.password,", | |
" }, strippedToken, true);", | |
"", | |
" // Check if we have auth0 or auth-store errors", | |
" if (response && response.errors && response.errors.length > 0) {", | |
" if (response.errors.find((e) => e.message.includes('user already exists'))) {", | |
" log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);", | |
" return {", | |
" id: 'USER_ALREADY_EXISTS',", | |
" };", | |
" }", | |
"", | |
" log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));", | |
" return {", | |
" id: 'UNKNOWN_ERROR',", | |
" };", | |
" }", | |
"", | |
" // Fetch the newly added user entry from our auth-store", | |
" const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(", | |
" sanitizedUsername,", | |
" strippedToken", | |
" );", | |
"", | |
" if (!newlyRegisteredUser) {", | |
" log.warn('registerCampaignUser: Did not find the newly registered user.');", | |
" return 'UNKNOWN_ERROR';", | |
" }", | |
"", | |
" const id = uuid();", | |
" const profile = {", | |
" duxis_auth_id: newlyRegisteredUser.id,", | |
" email: sanitizedUsername,", | |
" first_name: user.firstName,", | |
" has_logged_in: false,", | |
" id,", | |
" job_title: user.role,", | |
" last_name: user.lastName,", | |
" phone_number: user.phone,", | |
" salutation: user.salutation,", | |
" linked_in_profile: user.linkedIn,", | |
" };", | |
"", | |
" await trx('user_profiles')", | |
" .insert(profile);", | |
"", | |
" return { id };", | |
" });", | |
" } catch (e) {", | |
" log.warn(`registerCampaignUser: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async addOtherCampaignData(campaigns, trx) {", | |
" await asyncForIn(campaigns, async (campaign) => {", | |
" // Get all evaluative phases for this campaign.", | |
" campaign.phases = await trx('phases')", | |
" .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')", | |
" .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')", | |
" .where('left_id', campaign.id)", | |
" .orderBy('campaigns_phases.order');", | |
"", | |
" if (campaign.created_by) {", | |
" // Get and assign creator", | |
" const userProfile = await trx('user_profiles')", | |
" .first('first_name as firstName', 'last_name as lastName')", | |
" .where('id', campaign.created_by);", | |
" campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;", | |
" }", | |
"", | |
" if (campaign.updated_by) {", | |
" // Get and assign updater.", | |
" const userProfile = await trx('user_profiles')", | |
" .first('first_name as firstName', 'last_name as lastName')", | |
" .where('id', campaign.updated_by);", | |
" campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;", | |
" }", | |
"", | |
" // Get our most advanced phase for this campaign.", | |
" let campaignPhase = await trx('campaigns_projects')", | |
" .first('campaigns_phases.right_id AS phaseId')", | |
" .join('projects', 'projects.id', 'campaigns_projects.right_id')", | |
" .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')", | |
" .orderBy('campaigns_phases.order', 'DESC')", | |
" .where('campaigns_projects.left_id', campaign.id);", | |
"", | |
" // If there are no projects yet, we fallback on the first phase of the campaign.", | |
" if (!campaignPhase) {", | |
" campaignPhase = await trx('campaigns_phases')", | |
" .first('right_id AS phaseId')", | |
" .orderBy('order', 'DESC')", | |
" .where('left_id', campaign.id);", | |
" }", | |
"", | |
" if (PHASES_ENABLED) {", | |
" const projectsInLastPhase = await trx('projects')", | |
" .count('projects.id')", | |
" .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')", | |
" .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')", | |
" .where('projects.phase_id', function whereLastPhase() {", | |
" this.first('right_id AS id')", | |
" .from('campaigns_phases')", | |
" .whereRaw('left_id = campaigns_projects.left_id')", | |
" .orderBy('order', 'DESC');", | |
" })", | |
" .where('campaigns.id', campaign.id)", | |
" .first();", | |
" campaign.projectsInLastPhase = Number(projectsInLastPhase.count);", | |
" }", | |
"", | |
" const projectsInCampaign = await trx('campaigns_projects')", | |
" .select('projects.id AS id', 'projects.name AS name')", | |
" .join('projects', 'projects.id', 'campaigns_projects.right_id')", | |
" .where('left_id', campaign.id);", | |
"", | |
" campaign.projects = projectsInCampaign;", | |
"", | |
" campaign.numberOfProjects = (projectsInCampaign || []).length;", | |
"", | |
" campaign.phaseId = campaignPhase && campaignPhase.phaseId;", | |
"", | |
" const linkedIntake = await trx('campaigns_intakes')", | |
" .first('right_id AS id').where('left_id', campaign.id);", | |
" campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;", | |
" });", | |
" }", | |
"", | |
" async getCampaigns() {", | |
" try {", | |
" return this.store.knex.transaction(async (trx) => {", | |
" const campaigns = await this.store.getCollection(", | |
" this.tableName,", | |
" { ordering: [{ field: 'start_date', direction: 'desc' }], trx }", | |
" );", | |
" await this.addOtherCampaignData(campaigns, trx);", | |
" return campaigns.map(transformCampaign);", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getCampaigns: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async getScopedCampaigns(duxisId) {", | |
" try {", | |
" return this.store.knex.transaction(async (trx) => {", | |
" const profile = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user');", | |
" }", | |
" // Fetch all campaigns linked to the current user", | |
" const assignedCampaignIds = await trx('campaigns_managers')", | |
" .select('left_id as id')", | |
" .where('right_id', profile.id);", | |
"", | |
" // Fetch their data", | |
" const campaigns = await trx('campaigns')", | |
" .select('*')", | |
" .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));", | |
"", | |
" // Add additional data...", | |
" await this.addOtherCampaignData(campaigns, trx);", | |
"", | |
" // Transform the data and pass it to front-end", | |
" return campaigns.map(transformCampaign);", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getScopedCampaigns: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async getCampaignManagers({ campaignId }, duxisId) {", | |
" try {", | |
" return this.store.knex.transaction(async (trx) => {", | |
" const profile = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
"", | |
" // Get all the user_profile ids that are assigned for the current campaign", | |
" const managerIdsForCurrentCampaign = await trx('campaigns_managers')", | |
" .select('right_id AS id')", | |
" .where('left_id', campaignId);", | |
"", | |
" if (managerIdsForCurrentCampaign.length === 0) {", | |
" return [];", | |
" }", | |
"", | |
" const campaignManagers = await trx('user_profiles')", | |
" .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')", | |
" .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));", | |
"", | |
" return campaignManagers.map((manager) => ({", | |
" ...manager,", | |
" name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,", | |
" }));", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getCampaignManagers ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" // { campaignId: { projects: [ projectId, projectId, ... ]} }", | |
" async addProjects(campaignId, projectIds, trx) {", | |
" try {", | |
" await asyncForIn(projectIds.projects, async (projectId) => {", | |
" const writeObject = { left_id: campaignId, right_id: projectId };", | |
" await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);", | |
" });", | |
" } catch (e) {", | |
" log.warn(`addProjects ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async removeAllRelations(trx) {", | |
" try {", | |
" await trx('campaigns_projects').del();", | |
" } catch (e) {", | |
" log.warn(`removeAll: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async createPhase({ title }) {", | |
" try {", | |
" const phase = { id: uuid(), title };", | |
" await this.store.knex('phases').insert(phase);", | |
" return phase;", | |
" } catch (e) {", | |
" log.warn(`createPhase: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" // Used for dropdown with all available phases.", | |
" async getPhases() {", | |
" try {", | |
" return await this.store.knex('phases')", | |
" .select('id', 'title')", | |
" .orderBy('title');", | |
" } catch (e) {", | |
" log.warn(`getPhases: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async getAllPossibleReviewers(duxisId) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" // Check if the user exists", | |
" const profile = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
"", | |
" // Get a duxis token to make calls to the auth container", | |
" const dxResponse = await this.duxisAuthClient.getDuxisToken();", | |
" const { dxToken: strippedToken } = dxResponse;", | |
"", | |
" const response = await getAllPossibleReviewers(strippedToken);", | |
" const possibleReviewers = (response && response.items) || [];", | |
" const result = [];", | |
" await asyncForIn(possibleReviewers, async (reviewer) => {", | |
" const matchingProfile = await trx('user_profiles')", | |
" .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')", | |
" .where('duxis_auth_id', reviewer.id)", | |
" .andWhere('email', reviewer.username);", | |
" // Only push to the result array if we have a matching user_profile entry", | |
" if (matchingProfile) {", | |
" result.push({", | |
" ...matchingProfile,", | |
" ...reviewer,", | |
" name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,", | |
" profileId: matchingProfile.profileId,", | |
" });", | |
" }", | |
" });", | |
" const alphabeticallySortedReviewersList = result", | |
" // eslint-disable-next-line", | |
" .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));", | |
" return alphabeticallySortedReviewersList;", | |
" });", | |
" } catch (err) {", | |
" log.warn(`getAllPossibleReviewers: ${err}`);", | |
" throw new Error(err);", | |
" }", | |
" }", | |
"", | |
" // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them", | |
" async createCampaignReviewer({ user }, duxisId) {", | |
" try {", | |
" return this.store.knex.transaction(async (trx) => {", | |
" // Check if the user exists", | |
" const profile = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
"", | |
" // Try to register the user to auth0 and in our database", | |
" const dxResponse = await this.duxisAuthClient.getDuxisToken();", | |
" const { dxToken: strippedToken } = dxResponse;", | |
"", | |
" const sanitizedUsername = user.email.toLowerCase();", | |
"", | |
" const response = await addUserToDxAuth({", | |
" data: { firstName: user.firstName, lastName: user.lastName },", | |
" enabled: true,", | |
" provider: 'auth0',", | |
" roles: ['innovatrix/role/evaluator'],", | |
" scoped_rights: [],", | |
" username: sanitizedUsername,", | |
" }, strippedToken, false);", | |
"", | |
" // Check if we have auth0 or auth-store errors", | |
" if (response && response.errors && response.errors.length > 0) {", | |
" if (response.errors.find((e) => e.message.includes('user already exists'))) {", | |
" log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);", | |
" return {", | |
" id: 'USER_ALREADY_EXISTS',", | |
" };", | |
" }", | |
"", | |
" log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));", | |
" return {", | |
" id: 'UNKNOWN_ERROR',", | |
" };", | |
" }", | |
"", | |
" // Fetch the newly added user entry from our auth-store", | |
" const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(", | |
" sanitizedUsername,", | |
" strippedToken", | |
" );", | |
"", | |
" if (!newlyRegisteredUser) {", | |
" log.warn('createCampaignReviewer: Did not find the newly registered user.');", | |
" return 'UNKNOWN_ERROR';", | |
" }", | |
"", | |
" const id = uuid();", | |
" const newProfile = {", | |
" duxis_auth_id: newlyRegisteredUser.id,", | |
" email: sanitizedUsername,", | |
" first_name: user.firstName,", | |
" has_logged_in: false,", | |
" id,", | |
" last_name: user.lastName,", | |
" };", | |
"", | |
" await trx('user_profiles')", | |
" .insert(newProfile);", | |
"", | |
" return { id };", | |
" });", | |
" } catch (err) {", | |
" log.warn(`createCampaignReviewer: ${err}`);", | |
" throw new Error(err);", | |
" }", | |
" }", | |
"", | |
" async persistCampaignReviewers({ campaignId, reviewerIds }) {", | |
" if (!campaignId) {", | |
" throw new Error('campaignId is required.');", | |
" }", | |
" if (!reviewerIds || !Array.isArray(reviewerIds)) {", | |
" throw new Error('reviewerIds is required.');", | |
" }", | |
"", | |
" await this.store.knex.transaction(async (trx) => {", | |
" const deletedCampaignReviewers = await trx('campaigns_reviewers')", | |
" .select('right_id AS userProfileId')", | |
" .where('left_id', campaignId)", | |
" .whereNotIn('right_id', reviewerIds);", | |
"", | |
" // Insert new campaign reviewers", | |
" await asyncForIn(reviewerIds, async (id) => {", | |
" const exists = await trx('campaigns_reviewers')", | |
" .first('right_id')", | |
" .where('right_id', id)", | |
" .andWhere('left_id', campaignId);", | |
" if (!exists) {", | |
" await trx('campaigns_reviewers')", | |
" .insert({", | |
" left_id: campaignId,", | |
" right_id: id,", | |
" });", | |
" }", | |
" });", | |
"", | |
" if (deletedCampaignReviewers.length === 0) { return; }", | |
"", | |
" const currentCampaignProjects = await trx('campaigns_projects')", | |
" .select('right_id AS projectId')", | |
" .where('left_id', campaignId);", | |
"", | |
" const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);", | |
" const projectIds = currentCampaignProjects.map((c) => c.projectId);", | |
"", | |
" await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {", | |
" // Delete all project linked to the deleted reviewer(s)", | |
" await trx('projects_reviewers')", | |
" .del()", | |
" .whereIn('left_id', projectIds)", | |
" .andWhere('right_id', deletedReviewerId);", | |
"", | |
" // Delete campaign reviewer entry", | |
" await trx('campaigns_reviewers')", | |
" .del()", | |
" .where('right_id', deletedReviewerId)", | |
" .andWhere('left_id', campaignId);", | |
" });", | |
" });", | |
" }", | |
"", | |
" async getCampaignReviewers(campaignId) {", | |
" try {", | |
" return await this.store.knex('campaigns_reviewers')", | |
" .select('right_id AS id')", | |
" .where('left_id', campaignId);", | |
" } catch (e) {", | |
" log.warn(`getCampaignReviewers: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async getCampaignReviewerGroups(duxisId) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" // Check if the user exists", | |
" const profile = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
"", | |
" // Get all groups and their reviewers", | |
" const reviewerGroups = await trx('reviewer_groups')", | |
" .select('id', 'title', 'collapsed')", | |
" .orderBy('title');", | |
"", | |
" const result = [];", | |
" await asyncForIn(reviewerGroups, async (reviewerGroup) => {", | |
" const reviewerIds = await trx('reviewer_group_reviewers')", | |
" .select('right_id AS id')", | |
" .where('left_id', reviewerGroup.id);", | |
" result.push({", | |
" ...reviewerGroup,", | |
" reviewerIds: reviewerIds.map((i) => i.id),", | |
" });", | |
" });", | |
" return result;", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getCampaignReviewerGroups: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async persistCampaignReviewerGroups({ groups }, duxisId) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" // Check if the user exists", | |
" const profile = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
"", | |
" // Get all the currently stored groups' ids", | |
" const storedReviewerGroupIds = await trx('reviewer_groups')", | |
" .select('id');", | |
" // Check which groups we need to delete", | |
" const deletedGroupIds = storedReviewerGroupIds", | |
" .filter((r) => !groups.map((g) => g.id).includes(r.id))", | |
" .map((g) => g.id);", | |
" // Create/update groups", | |
" await asyncForIn(groups, async (group) => {", | |
" // Check if reviewer_group already exists...", | |
" const exists = await trx('reviewer_groups')", | |
" .first('id')", | |
" .where('id', group.id);", | |
" if (exists) {", | |
" // update if it does", | |
" await trx('reviewer_groups')", | |
" .update({", | |
" title: group.title,", | |
" collapsed: group.collapsed,", | |
" })", | |
" .where('id', group.id);", | |
" // First delete all reviewers, then we re-insert the current ones", | |
" // This way we don't need to check which ones to delete or update", | |
" await trx('reviewer_group_reviewers')", | |
" .del()", | |
" .where('left_id', group.id);", | |
" } else {", | |
" // create if it doesn't", | |
" await trx('reviewer_groups')", | |
" .insert({", | |
" id: group.id,", | |
" title: group.title,", | |
" collapsed: group.collapsed,", | |
" });", | |
" }", | |
" // Insert the reviewers", | |
" const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values", | |
" await asyncForIn(reviewerIds, async (reviewerId) => {", | |
" await trx('reviewer_group_reviewers')", | |
" .insert({", | |
" left_id: group.id,", | |
" right_id: reviewerId,", | |
" });", | |
" });", | |
" });", | |
"", | |
" // Finally remove deleted groups", | |
" await asyncForIn(deletedGroupIds, async (deletedGroupId) => {", | |
" await trx('reviewer_group_reviewers')", | |
" .del()", | |
" .where('left_id', deletedGroupId);", | |
" await trx('reviewer_groups')", | |
" .del()", | |
" .where('id', deletedGroupId);", | |
" });", | |
" });", | |
" } catch (e) {", | |
" log.warn(`createCampaignReviewerGroups: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async deleteCampaign(campaignId) {", | |
" try {", | |
" await this.store.knex.transaction(async (trx) => {", | |
" await trx('campaigns_phases')", | |
" .del()", | |
" .where('left_id', campaignId);", | |
" log.info('Deleted campaigns_phases WHERE left_id', campaignId);", | |
" await trx('campaigns_projects')", | |
" .del()", | |
" .where('left_id', campaignId);", | |
" log.info('Deleted campaigns_projects WHERE left_id', campaignId);", | |
" await trx('campaigns_reviewers')", | |
" .del()", | |
" .where('left_id', campaignId);", | |
" log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);", | |
" // Delete linked intakes", | |
" await trx('campaigns_intakes')", | |
" .del()", | |
" .where('left_id', campaignId);", | |
" await trx('campaigns')", | |
" .del()", | |
" .where('id', campaignId);", | |
" log.info('Deleted campaign WHERE id', campaignId);", | |
" });", | |
" } catch (e) {", | |
" log.warn(`deleteCampaign: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async makeCampaignsExport() {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" const phases = await trx('phases')", | |
" .select('id', 'title');", | |
" const projects = await trx('projects')", | |
" .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')", | |
" .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')", | |
" .where('deleted_on', null);", | |
" const projectDecisions = await trx('project_phase_statuses')", | |
" .select('status', 'project_id AS projectId', 'phase_id AS phaseId');", | |
"", | |
" const campaigns = await trx('campaigns')", | |
" .select('id', 'name');", | |
"", | |
" for (const campaign of campaigns) {", | |
" campaign.phases = (await trx('campaigns_phases')", | |
" .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')", | |
" .orderBy('order'));", | |
" }", | |
"", | |
" let evaluations;", | |
"", | |
" if (SALESFORCE_ENABLED) {", | |
" evaluations = await trx('evaluations')", | |
" .select(", | |
" 'evaluations.id', 'project_id AS projectId',", | |
" 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'", | |
" )", | |
" .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')", | |
" .where('submitted', true)", | |
" .orderBy('order');", | |
"", | |
" for (const evaluation of evaluations) {", | |
" // Only get ratings for quantified questions.", | |
" evaluation.ratings = await trx('evaluation_ratings')", | |
" .select(", | |
" 'evaluation_ratings.id', 'score', 'question_id AS questionId',", | |
" 'evaluation_ratings.domain_id AS domainId',", | |
" 'evaluation_ratings.criterium_id AS criteriumId',", | |
" 'criteria.pre_selection_threshold AS preSelectionThreshold',", | |
" 'criteria.selection_threshold AS selectionThreshold'", | |
" )", | |
" .join('questions', 'questions.id', 'evaluation_ratings.question_id')", | |
" .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')", | |
" .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');", | |
" }", | |
"", | |
" for (const project of projects) {", | |
" project.reviews = await trx('reviews')", | |
" .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')", | |
" .where('project_id', project.id);", | |
" }", | |
" } else {", | |
" evaluations = await trx('evaluations')", | |
" .select(", | |
" 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',", | |
" 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'", | |
" )", | |
" .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')", | |
" .where('completed', true)", | |
" .whereNot('reviewer_id', null)", | |
" .orderBy('order');", | |
"", | |
" for (const evaluation of evaluations) {", | |
" // Only get ratings for quantified questions.", | |
" evaluation.ratings = await trx('evaluation_ratings')", | |
" .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')", | |
" .join('questions', 'questions.id', 'evaluation_ratings.question_id')", | |
" .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');", | |
"", | |
" const review = await trx('new_reviews')", | |
" .first(", | |
" 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',", | |
" 'advice_type AS advice',", | |
" )", | |
" .join('question_options', 'question_options.id', 'new_reviews.question_option_id')", | |
" .join('questions', 'questions.id', 'question_options.question_id')", | |
" .where('new_reviews.evaluation_id', evaluation.id);", | |
"", | |
" evaluation.advice = review && review.advice;", | |
" }", | |
" }", | |
"", | |
" return {", | |
" campaigns,", | |
" evaluations,", | |
" phases,", | |
" projects,", | |
" projectDecisions,", | |
" };", | |
" });", | |
" } catch (e) {", | |
" throw new Error('makeCampaignsExport failed');", | |
" }", | |
" }", | |
"", | |
" async _checkRoles(userId, roleToCheck) {", | |
" const dxResponse = await this.duxisAuthClient.getDuxisToken();", | |
" const { dxToken: strippedToken } = dxResponse;", | |
" const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);", | |
" const { data: { getUser: { user: userData } } } = dxUser;", | |
" if (!userData || !userData.roles) {", | |
" // Should we throw an error?", | |
" return false;", | |
" }", | |
" return (userData.roles || []).includes(roleToCheck);", | |
" }", | |
"", | |
" async createCampaignManager({ campaignId, user }) {", | |
" try {", | |
" await this.store.knex.transaction(async (trx) => {", | |
" const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();", | |
" let userProfileId = user.id;", | |
" let userDuxisAuthId = user.duxisAuthId;", | |
" const sanitizedUsername = (user.email || '').toLowerCase();", | |
" // No id provided... so we assume this is going to be a new user in our database", | |
" if (!userProfileId) {", | |
" // Email is required field", | |
" if (!sanitizedUsername) {", | |
" throw new Error('The user needs an email to be invited.');", | |
" }", | |
" // Check if the email is already being used as a username in our auth-store", | |
" const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);", | |
" if (data.getUser.user) {", | |
" throw new Error('User with current email already exists.');", | |
" }", | |
" // Add the new user to our database", | |
" // with normal client role and campaign_manager role", | |
" await addUserToDxAuth({", | |
" data: { firstName: user.firstName, lastName: user.lastName },", | |
" enabled: true,", | |
" provider: 'auth0',", | |
" roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],", | |
" scoped_rights: [],", | |
" username: sanitizedUsername,", | |
" }, strippedToken, false);", | |
"", | |
" // Fetch the newly added user entry from our auth-store", | |
" const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(", | |
" sanitizedUsername,", | |
" strippedToken", | |
" );", | |
"", | |
" if (newlyAddedUser) {", | |
" const id = uuid();", | |
" userDuxisAuthId = newlyAddedUser.id;", | |
" // Add new user_profile entry", | |
" await trx('user_profiles')", | |
" .insert({", | |
" id,", | |
" duxis_auth_id: userDuxisAuthId,", | |
" email: sanitizedUsername,", | |
" first_name: user.firstName,", | |
" last_name: user.lastName,", | |
" has_logged_in: false,", | |
" });", | |
"", | |
" // Check if has been successfully created", | |
" const userEntry = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', userDuxisAuthId);", | |
"", | |
" // We set the newly created user_profile's id to link with the campaign", | |
" if (userEntry) {", | |
" userProfileId = userEntry.id;", | |
" } else {", | |
" throw new Error('Something went wrong while creating the new user.');", | |
" }", | |
" } else {", | |
" throw new Error('Something went wrong while getting the user from the auth database');", | |
" }", | |
" } else {", | |
" // The user has an id, so he should exist in our database", | |
" const profile = await trx('user_profiles')", | |
" .first('id', 'email')", | |
" .where('duxis_auth_id', userDuxisAuthId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
" // Add \"campaign manager\" role to the user if he does NOT have the role yet", | |
" // else we skip this step", | |
" const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');", | |
" if (!hasCampaignManagerRole) {", | |
" let roles;", | |
" let scopedRights;", | |
" const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);", | |
" const { data: { getUser: { user: userData } } } = dxUser;", | |
" if (!userData || !userData.roles) {", | |
" // Should we throw an error?", | |
" roles = [];", | |
" scopedRights = [];", | |
" } else {", | |
" roles = (userData.roles || []);", | |
" const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);", | |
" scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);", | |
" }", | |
" await addCampaignManagerRoleToUser({", | |
" data: userData.data,", | |
" provider: userData.provider,", | |
" roles,", | |
" scopedRights,", | |
" userId: userDuxisAuthId,", | |
" username: profile.email, // username in auth-store is the same as email in api-store", | |
" enabled: userData.enabled,", | |
" }, strippedToken);", | |
" }", | |
" }", | |
"", | |
" // Finally we link the user profile to the campaign...", | |
" await trx('campaigns_managers')", | |
" .insert({", | |
" right_id: userProfileId,", | |
" left_id: campaignId,", | |
" });", | |
" });", | |
" } catch (e) {", | |
" log.warn(`createCampaignManager: ${e}`);", | |
" throw new Error(e);", | |
" }", | |
" }", | |
"", | |
" async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {", | |
" try {", | |
" await this.store.knex.transaction(async (trx) => {", | |
" const profile = await trx('user_profiles')", | |
" .select('id')", | |
" .where('duxis_auth_id', duxisId);", | |
" if (!profile) {", | |
" throw new Error('Unauthorized user.');", | |
" }", | |
" await trx('campaigns_managers')", | |
" .del()", | |
" .where('right_id', userProfileId)", | |
" .andWhere('left_id', campaignId);", | |
" });", | |
" } catch (e) {", | |
" log.warn(`deleteCampaignManager: ${e}`);", | |
" throw new Error(e);", | |
" }", | |
" }", | |
"", | |
" async getPossibleCampaignManagers(searchString) {", | |
" try {", | |
" return this.store.knex.transaction(async (trx) => {", | |
" const dxResponse = await this.duxisAuthClient.getDuxisToken();", | |
" const { dxToken: strippedToken } = dxResponse;", | |
" const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);", | |
" const result = [];", | |
" await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {", | |
" const user = await trx('user_profiles')", | |
" .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')", | |
" .where('duxis_auth_id', possibleUser.id);", | |
" if (user) {", | |
" result.push(user);", | |
" }", | |
" });", | |
" return result;", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getPossibleCampaignManagers: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" _getIntakeCallStatus(id, intakes) {", | |
" const hasIntake = intakes.find((i) => i.intakeId === id);", | |
" if (hasIntake) {", | |
" return {", | |
" status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,", | |
" submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,", | |
" };", | |
" }", | |
" return {", | |
" status: OPEN_INTAKE,", | |
" submittedOn: null,", | |
" };", | |
" }", | |
"", | |
" async getCalls(duxisId) {", | |
" try {", | |
" return this.store.knex.transaction(async (trx) => {", | |
" const user = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
"", | |
" if (!user) {", | |
" throw new Error('Unauthorized: user does not exist.');", | |
" }", | |
"", | |
" const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');", | |
" const allCampaigns = await trx('campaigns')", | |
" .select(", | |
" 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',", | |
" 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'", | |
" )", | |
" .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')", | |
" .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')", | |
" .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!", | |
"", | |
" // fetch the current user's submitted/draft intakes, if any", | |
" // we should fetch an array of objects like:", | |
" // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],", | |
" const intakesCurrentUser = await trx('intake_answers')", | |
" .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')", | |
" .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')", | |
" .where('user_profile_id', user.id);", | |
"", | |
" const calls = {};", | |
" // This should only be filtered on the pitchEnd", | |
" // but let's add a fallback (for now) just in case no pitchEnd was set", | |
" // for open calls we have 3 states,", | |
" // 1) OPEN 2) DRAFT 3) SUBMITTED", | |
" calls.openCalls = allCampaigns", | |
" .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them", | |
" .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))", | |
" .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))", | |
" // Sort by soonest first", | |
" .sort((x, y) => x.intakeEndDate - y.intakeEndDate);", | |
" // for closed calls we only have 2 states,", | |
" // 1) DRAFT 2) SUBMITTED", | |
" calls.closedCalls = allCampaigns", | |
" .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))", | |
" .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))", | |
" // Sort by most recently passed first", | |
" .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status", | |
" .sort((x, y) => y.intakeEndDate - x.intakeEndDate);", | |
"", | |
" return calls;", | |
" });", | |
" } catch (e) {", | |
" log.warn(`getCalls: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" // Get a list of all calls", | |
" // ( = campaigns with an IntakeForm linked to them)", | |
" async manageCalls(duxisId) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" const user = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
"", | |
" if (!user) {", | |
" throw new Error('Unauthorized: user does not exist.');", | |
" }", | |
"", | |
" const calls = await trx('campaigns_intakes')", | |
" .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')", | |
" .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');", | |
"", | |
" return calls;", | |
" });", | |
" } catch (e) {", | |
" log.warn(`manageCalls: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"", | |
" async callOverview({ callId }, duxisId) {", | |
" try {", | |
" return await this.store.knex.transaction(async (trx) => {", | |
" const user = await trx('user_profiles')", | |
" .first('id')", | |
" .where('duxis_auth_id', duxisId);", | |
"", | |
" if (!user) {", | |
" throw new Error('Unauthorized: user does not exist.');", | |
" }", | |
"", | |
" const call = await trx('campaigns_intakes')", | |
" .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')", | |
" .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')", | |
" .where('campaigns.id', callId);", | |
"", | |
" if (!call) {", | |
" throw new Error('Call does not exist.');", | |
" }", | |
"", | |
" // Get all necessary data for the frontend", | |
" let result = {", | |
" ...call,", | |
" };", | |
"", | |
" // Get the answer data", | |
" const storedApplications = await trx('intake_answers')", | |
" .select(", | |
" 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',", | |
" 'user_profiles.email', 'user_profiles.first_name AS firstName',", | |
" 'user_profiles.last_name AS lastName', 'created_on AS created_on',", | |
" )", | |
" .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')", | |
" .where('campaign_id', callId)", | |
" .andWhere('intake_id', call.intakeId)", | |
" // sort by submitted_on first, drafts will be grouped together", | |
" .orderBy('submitted_on', 'desc')", | |
" // sort drafts by created_on", | |
" .orderBy('intake_answers.created_on', 'desc');", | |
"", | |
" const applications = [];", | |
" await asyncForIn(storedApplications, async ({ answers, ...rest }) => {", | |
" // parse the JSON", | |
" const parsedAnswers = JSON.parse(answers);", | |
" // find the provided PROJECT_RECORD_FIELD if it exists", | |
" const projectNameField = parsedAnswers", | |
" .map((section) => section.items)", | |
" .flat()", | |
" .find((field) => field.type === PROJECT_RECORD_FIELD);", | |
" // get the project name (if field exists)", | |
" const projectName = (projectNameField && projectNameField.value) || 'No project name provided';", | |
" // structure the object for the frontend", | |
" const structuredObject = {", | |
" ...rest,", | |
" projectName,", | |
" };", | |
" // add it to the list of applications", | |
" applications.push(structuredObject);", | |
" });", | |
"", | |
" result = {", | |
" ...result,", | |
" applications,", | |
" };", | |
"", | |
" const documents = [];", | |
" await asyncForIn(storedApplications, async ({ answers, ...rest }) => {", | |
" // parse the JSON", | |
" const parsedAnswers = JSON.parse(answers);", | |
" const intakeDocuments = await trx('intake_documents')", | |
" .select('intake_question_id AS questionId')", | |
" .where('intake_answer_id', rest.id);", | |
" const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);", | |
" // find the document fields with uploaded documents", | |
" const documentFields = parsedAnswers", | |
" .map((section) => section.items)", | |
" .flat()", | |
" .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));", | |
" // Only push to the results array if there effectively are", | |
" // uploaded documents available on a question", | |
" if (documentFields.length > 0) {", | |
" documents.push({", | |
" ...rest,", | |
" uploadedDocuments: documentFields,", | |
" });", | |
" }", | |
" });", | |
"", | |
" result = {", | |
" ...result,", | |
" documents,", | |
" };", | |
"", | |
" return result;", | |
" });", | |
" } catch (e) {", | |
" log.warn(`callOverview: ${e}`);", | |
" throw e;", | |
" }", | |
" }", | |
"}", | |
"", | |
"module.exports = CampaignService;" | |
] | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -23,3ms | |
11:54:54 DEBUG [transport] - request to vim: -24,nvim_call_function,[ | |
"win_getid", | |
[] | |
] | |
11:54:54 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"win_getid", | |
[] | |
] | |
], | |
-24 | |
] | |
11:54:54 DEBUG [connection] - received response: -24,[ | |
null, | |
1000 | |
] | |
11:54:54 DEBUG [transport] - response from vim cost: -24,4ms | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#_watch", | |
[ | |
"coc_sources_disable_map" | |
] | |
] | |
] | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_command", | |
[ | |
"sign define CocError linehl=CocErrorLine texthl=CocErrorSign text=>>" | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"sign define CocWarning linehl=CocWarningLine texthl=CocWarningSign text=⚠" | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"sign define CocInfo linehl=CocInfoLine texthl=CocInfoSign text=>>" | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"sign define CocHint linehl=CocHintLine texthl=CocHintSign text=>>" | |
] | |
] | |
] | |
] | |
] | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"command", | |
[ | |
"sign define CocSelected text=* texthl=CocSelectedText linehl=CocSelectedLine" | |
] | |
] | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"set_var", | |
[ | |
"coc_workspace_initialized", | |
1 | |
] | |
] | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"set_var", | |
[ | |
"WorkspaceFolders", | |
[ | |
"/home/channi16/imec/innovatrix" | |
] | |
] | |
] | |
] | |
11:54:55 DEBUG [transport] - request to vim: -25,nvim_command,[ | |
"source /tmp/vFnmXCJ/coc.nvim-17368/coc-17368.vim" | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"command", | |
[ | |
"source /tmp/vFnmXCJ/coc.nvim-17368/coc-17368.vim" | |
] | |
], | |
-25 | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"set_var", | |
[ | |
"coc_service_initialized", | |
1 | |
] | |
] | |
] | |
11:54:55 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocNvimInit" | |
] | |
] | |
] | |
] | |
11:54:55 DEBUG [connection] - received response: -25,[ | |
null, | |
0 | |
] | |
11:54:55 DEBUG [transport] - response from vim cost: -25,12ms | |
11:54:56 DEBUG [transport] - request to vim: -26,nvim_eval,[ | |
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]" | |
] | |
11:54:56 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]" | |
] | |
], | |
-26 | |
] | |
11:54:56 DEBUG [connection] - received response: -26,[ | |
null, | |
[ | |
1, | |
"n", | |
1, | |
531, | |
{ | |
"title": "" | |
} | |
] | |
] | |
11:54:56 DEBUG [transport] - response from vim cost: -26,1ms | |
11:54:56 DEBUG [transport] - request to vim: -27,nvim_call_atomic,[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_var", | |
[ | |
1, | |
"coc_diagnostic_info", | |
{ | |
"error": 0, | |
"warning": 0, | |
"information": 0, | |
"hint": 0, | |
"lnums": [ | |
0, | |
0, | |
0, | |
0 | |
] | |
} | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocDiagnosticChange" | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
11:54:56 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_var", | |
[ | |
1, | |
"coc_diagnostic_info", | |
{ | |
"error": 0, | |
"warning": 0, | |
"information": 0, | |
"hint": 0, | |
"lnums": [ | |
0, | |
0, | |
0, | |
0 | |
] | |
} | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocDiagnosticChange" | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
], | |
-27 | |
] | |
11:54:56 DEBUG [connection] - received response: -27,[ | |
null, | |
[ | |
[ | |
0, | |
0, | |
0 | |
], | |
null | |
] | |
] | |
11:54:56 DEBUG [transport] - response from vim cost: -27,1ms | |
11:54:56 DEBUG [transport] - request to vim: -28,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:54:56 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-28 | |
] | |
11:54:56 DEBUG [connection] - received response: -28,[ | |
null, | |
[ | |
1, | |
[ | |
530, | |
0 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:54:56 DEBUG [transport] - response from vim cost: -28,0ms | |
11:54:56 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertEnter", | |
1 | |
] | |
] | |
11:54:56 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#util#clear_highlights", | |
[] | |
] | |
] | |
] | |
11:54:56 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
11:54:56 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
531, | |
65 | |
] | |
] | |
] | |
11:54:57 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
531, | |
64 | |
] | |
] | |
] | |
11:54:57 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 531, | |
"col": 64, | |
"changedtick": 5, | |
"pre": " campaign.phaseId = campaignPhase && campaignPhase.phaseId" | |
} | |
] | |
] | |
11:54:57 DEBUG [transport] - request to vim: -29,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:54:57 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-29 | |
] | |
11:54:57 DEBUG [connection] - received response: -29,[ | |
null, | |
{ | |
"changedtick": 5, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:54:57 DEBUG [transport] - response from vim cost: -29,7ms | |
11:54:57 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertLeave", | |
1 | |
] | |
] | |
11:54:57 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMoved", | |
1, | |
[ | |
531, | |
63 | |
] | |
] | |
] | |
11:54:57 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMoved", | |
1, | |
[ | |
532, | |
1 | |
] | |
] | |
] | |
11:54:58 DEBUG [transport] - request to vim: -30,nvim_eval,[ | |
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]" | |
] | |
11:54:58 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]" | |
] | |
], | |
-30 | |
] | |
11:54:58 DEBUG [connection] - received response: -30,[ | |
null, | |
[ | |
1, | |
"n", | |
1, | |
532, | |
{ | |
"title": "" | |
} | |
] | |
] | |
11:54:58 DEBUG [transport] - response from vim cost: -30,1ms | |
11:54:58 DEBUG [transport] - request to vim: -31,nvim_call_atomic,[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_var", | |
[ | |
1, | |
"coc_diagnostic_info", | |
{ | |
"error": 1, | |
"warning": 0, | |
"information": 0, | |
"hint": 0, | |
"lnums": [ | |
531, | |
0, | |
0, | |
0 | |
] | |
} | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocDiagnosticChange" | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"sign place 1000 line=531 name=CocError buffer=1" | |
] | |
], | |
[ | |
"nvim_buf_add_highlight", | |
[ | |
1, | |
1082, | |
"CocErrorHighlight", | |
530, | |
63, | |
63 | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
11:54:58 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_var", | |
[ | |
1, | |
"coc_diagnostic_info", | |
{ | |
"error": 1, | |
"warning": 0, | |
"information": 0, | |
"hint": 0, | |
"lnums": [ | |
531, | |
0, | |
0, | |
0 | |
] | |
} | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocDiagnosticChange" | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"sign place 1000 line=531 name=CocError buffer=1" | |
] | |
], | |
[ | |
"nvim_buf_add_highlight", | |
[ | |
1, | |
1082, | |
"CocErrorHighlight", | |
530, | |
63, | |
63 | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
], | |
-31 | |
] | |
11:54:58 DEBUG [connection] - received response: -31,[ | |
null, | |
[ | |
[ | |
0, | |
0, | |
0, | |
0, | |
0 | |
], | |
null | |
] | |
] | |
11:54:58 DEBUG [transport] - response from vim cost: -31,2ms | |
11:54:58 DEBUG [transport] - request to vim: -32,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:54:58 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-32 | |
] | |
11:54:58 DEBUG [connection] - received response: -32,[ | |
null, | |
[ | |
1, | |
[ | |
531, | |
0 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:54:58 DEBUG [transport] - response from vim cost: -32,37ms | |
11:54:58 DEBUG [transport] - request to vim: -33,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:54:58 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-33 | |
] | |
11:54:58 DEBUG [connection] - received response: -33,[ | |
null, | |
{ | |
"changedtick": 5, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:54:58 DEBUG [transport] - response from vim cost: -33,8ms | |
11:54:59 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMoved", | |
1, | |
[ | |
531, | |
63 | |
] | |
] | |
] | |
11:54:59 DEBUG [transport] - request to vim: -34,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:54:59 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-34 | |
] | |
11:54:59 DEBUG [connection] - received response: -34,[ | |
null, | |
[ | |
1, | |
[ | |
530, | |
62 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:54:59 DEBUG [transport] - response from vim cost: -34,1ms | |
11:55:01 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMoved", | |
1, | |
[ | |
531, | |
64 | |
] | |
] | |
] | |
11:55:01 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChanged", | |
1, | |
7 | |
] | |
] | |
11:55:01 DEBUG [transport] - request to vim: -35,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-35 | |
] | |
11:55:01 DEBUG [connection] - received response: -35,[ | |
null, | |
{ | |
"changedtick": 7, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -35,7ms | |
11:55:01 DEBUG [transport] - request to vim: -36,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-36 | |
] | |
11:55:01 DEBUG [connection] - received response: -36,[ | |
null, | |
[ | |
1, | |
[ | |
530, | |
63 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -36,0ms | |
11:55:01 DEBUG [transport] - request to vim: -37,nvim_call_function,[ | |
"coc#float#get_float_mode", | |
[ | |
false, | |
false, | |
false | |
] | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#float#get_float_mode", | |
[ | |
false, | |
false, | |
false | |
] | |
] | |
], | |
-37 | |
] | |
11:55:01 DEBUG [connection] - received response: -37,[ | |
null, | |
[ | |
"n", | |
1, | |
[ | |
22, | |
65 | |
], | |
[ | |
531, | |
64 | |
], | |
{ | |
"columns": 173, | |
"lines": 44, | |
"cmdheight": 1 | |
} | |
] | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -37,1ms | |
11:55:01 DEBUG [transport] - request to vim: -38,nvim_call_function,[ | |
"coc#float#create_float_win", | |
[ | |
0, | |
0, | |
{ | |
"height": 1, | |
"width": 45, | |
"row": 1, | |
"col": 0, | |
"relative": "cursor" | |
} | |
] | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#float#create_float_win", | |
[ | |
0, | |
0, | |
{ | |
"height": 1, | |
"width": 45, | |
"row": 1, | |
"col": 0, | |
"relative": "cursor" | |
} | |
] | |
] | |
], | |
-38 | |
] | |
11:55:01 DEBUG [connection] - received response: -38,[ | |
null, | |
[ | |
1002, | |
15, | |
0 | |
] | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -38,2ms | |
11:55:01 DEBUG [transport] - request to vim: -39,nvim_call_atomic,[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"clearmatches", | |
[ | |
1002 | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_lines", | |
[ | |
15, | |
[ | |
"[eslint semi] [E] Missing semicolon. (semi)" | |
] | |
] | |
] | |
], | |
[ | |
"nvim_buf_add_highlight", | |
[ | |
15, | |
1086, | |
"CocErrorFloat", | |
0, | |
0, | |
43 | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"win_execute", | |
[ | |
1002, | |
"noa normal! gg0" | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"clearmatches", | |
[ | |
1002 | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_lines", | |
[ | |
15, | |
[ | |
"[eslint semi] [E] Missing semicolon. (semi)" | |
] | |
] | |
] | |
], | |
[ | |
"nvim_buf_add_highlight", | |
[ | |
15, | |
1086, | |
"CocErrorFloat", | |
0, | |
0, | |
43 | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"win_execute", | |
[ | |
1002, | |
"noa normal! gg0" | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
], | |
-39 | |
] | |
11:55:01 DEBUG [connection] - received response: -39,[ | |
null, | |
[ | |
[ | |
0, | |
0, | |
0, | |
"", | |
0 | |
], | |
null | |
] | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -39,1ms | |
11:55:01 DEBUG [transport] - request to vim: -40,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-40 | |
] | |
11:55:01 DEBUG [connection] - received response: -40,[ | |
null, | |
{ | |
"changedtick": 7, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -40,9ms | |
11:55:01 DEBUG [transport] - request to vim: -41,nvim_eval,[ | |
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]" | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#check_refresh(1),mode(),bufnr(\"%\"),line(\".\"),getloclist(bufwinid(1),{'title':1})]" | |
] | |
], | |
-41 | |
] | |
11:55:01 DEBUG [connection] - received response: -41,[ | |
null, | |
[ | |
1, | |
"n", | |
1, | |
531, | |
{ | |
"title": "" | |
} | |
] | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -41,1ms | |
11:55:01 DEBUG [transport] - request to vim: -42,nvim_call_atomic,[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_var", | |
[ | |
1, | |
"coc_diagnostic_info", | |
{ | |
"error": 0, | |
"warning": 0, | |
"information": 0, | |
"hint": 0, | |
"lnums": [ | |
0, | |
0, | |
0, | |
0 | |
] | |
} | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocDiagnosticChange" | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#unplace_signs", | |
[ | |
1, | |
[ | |
1000 | |
] | |
] | |
] | |
], | |
[ | |
"nvim_buf_clear_namespace", | |
[ | |
1, | |
1082, | |
0, | |
-1 | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#set_buf_var", | |
[ | |
1, | |
"coc_diagnostic_info", | |
{ | |
"error": 0, | |
"warning": 0, | |
"information": 0, | |
"hint": 0, | |
"lnums": [ | |
0, | |
0, | |
0, | |
0 | |
] | |
} | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#do_autocmd", | |
[ | |
"CocDiagnosticChange" | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#util#unplace_signs", | |
[ | |
1, | |
[ | |
1000 | |
] | |
] | |
] | |
], | |
[ | |
"nvim_buf_clear_namespace", | |
[ | |
1, | |
1082, | |
0, | |
-1 | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
], | |
-42 | |
] | |
11:55:01 DEBUG [connection] - received response: -42,[ | |
null, | |
[ | |
[ | |
0, | |
0, | |
0, | |
0, | |
0 | |
], | |
null | |
] | |
] | |
11:55:01 DEBUG [transport] - response from vim cost: -42,2ms | |
11:55:01 DEBUG [transport] - request to vim: -43,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:55:01 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-43 | |
] | |
11:55:02 DEBUG [connection] - received response: -43,[ | |
null, | |
[ | |
1, | |
[ | |
530, | |
63 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:55:02 DEBUG [transport] - response from vim cost: -43,53ms | |
11:55:02 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_call_function", | |
[ | |
"coc#float#close", | |
[ | |
1002 | |
] | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#float#close", | |
[ | |
0 | |
] | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
] | |
] | |
] | |
11:55:02 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"BufWinLeave", | |
15, | |
-1 | |
] | |
] | |
11:55:02 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#util#clear_pos_matches", | |
[ | |
"^Coc", | |
-1 | |
] | |
] | |
] | |
] | |
11:55:02 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"BufHidden", | |
15 | |
] | |
] | |
11:55:03 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMoved", | |
1, | |
[ | |
532, | |
1 | |
] | |
] | |
] | |
11:55:03 DEBUG [transport] - request to vim: -44,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:55:03 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-44 | |
] | |
11:55:03 DEBUG [connection] - received response: -44,[ | |
null, | |
[ | |
1, | |
[ | |
531, | |
0 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:55:03 DEBUG [transport] - response from vim cost: -44,2ms | |
11:55:06 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMoved", | |
1, | |
[ | |
531, | |
64 | |
] | |
] | |
] | |
11:55:06 DEBUG [transport] - request to vim: -45,nvim_eval,[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
11:55:06 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[bufnr(\"%\"),coc#util#cursor(),&filetype,mode(),get(b:,\"coc_diagnostic_disable\",0),get(w:,\"float\",0)]" | |
] | |
], | |
-45 | |
] | |
11:55:06 DEBUG [connection] - received response: -45,[ | |
null, | |
[ | |
1, | |
[ | |
530, | |
63 | |
], | |
"javascript.jsx", | |
"n", | |
0, | |
0 | |
] | |
] | |
11:55:06 DEBUG [transport] - response from vim cost: -45,1ms | |
11:55:06 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertEnter", | |
1 | |
] | |
] | |
11:55:06 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#util#clear_highlights", | |
[] | |
] | |
] | |
] | |
11:55:06 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"command", | |
[ | |
"redraw" | |
] | |
] | |
] | |
11:55:06 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
532, | |
7 | |
] | |
] | |
] | |
11:55:06 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 532, | |
"col": 7, | |
"changedtick": 8, | |
"pre": " " | |
} | |
] | |
] | |
11:55:07 DEBUG [transport] - request to vim: -46,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:07 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-46 | |
] | |
11:55:07 DEBUG [connection] - received response: -46,[ | |
null, | |
{ | |
"changedtick": 8, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n \n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:07 DEBUG [transport] - response from vim cost: -46,11ms | |
11:55:07 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
533, | |
7 | |
] | |
] | |
] | |
11:55:07 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 533, | |
"col": 7, | |
"changedtick": 9, | |
"pre": " " | |
} | |
] | |
] | |
11:55:07 DEBUG [transport] - request to vim: -47,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:07 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-47 | |
] | |
11:55:07 DEBUG [connection] - received response: -47,[ | |
null, | |
{ | |
"changedtick": 9, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n \n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:07 DEBUG [transport] - response from vim cost: -47,8ms | |
11:55:07 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
"c" | |
] | |
] | |
11:55:07 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
533, | |
8 | |
] | |
] | |
] | |
11:55:07 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 533, | |
"col": 8, | |
"changedtick": 10, | |
"pre": " c" | |
} | |
] | |
] | |
11:55:07 DEBUG [transport] - request to vim: -48,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:07 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-48 | |
] | |
11:55:07 DEBUG [transport] - request to vim: -49,nvim_eval,[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
11:55:07 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
], | |
-49 | |
] | |
11:55:08 DEBUG [connection] - received response: -48,[ | |
null, | |
{ | |
"changedtick": 10, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n c\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -48,3ms | |
11:55:08 DEBUG [transport] - request to vim: -50,nvim_call_function,[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
], | |
-50 | |
] | |
11:55:08 DEBUG [connection] - received response: -49,[ | |
null, | |
[ | |
[ | |
532, | |
7 | |
], | |
" c" | |
] | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -49,16ms | |
11:55:08 DEBUG [connection] - received response: -50,[ | |
null, | |
{ | |
"word": "c", | |
"bufnr": 1, | |
"col": 6, | |
"synname": "jsFuncBlock", | |
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js", | |
"blacklist": [], | |
"line": " c", | |
"filetype": "javascript.jsx", | |
"linenr": 533, | |
"input": "c", | |
"colnr": 8, | |
"changedtick": 10 | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -50,1ms | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
"o" | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
533, | |
9 | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 533, | |
"col": 9, | |
"changedtick": 11, | |
"pre": " co" | |
} | |
] | |
] | |
11:55:08 DEBUG [transport] - request to vim: -51,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-51 | |
] | |
11:55:08 DEBUG [transport] - request to vim: -52,nvim_eval,[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
], | |
-52 | |
] | |
11:55:08 DEBUG [connection] - received response: -51,[ | |
null, | |
{ | |
"changedtick": 11, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n co\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -51,4ms | |
11:55:08 DEBUG [transport] - request to vim: -53,nvim_call_function,[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
], | |
-53 | |
] | |
11:55:08 DEBUG [connection] - received response: -52,[ | |
null, | |
[ | |
[ | |
532, | |
8 | |
], | |
" co" | |
] | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -52,8ms | |
11:55:08 DEBUG [connection] - received response: -53,[ | |
null, | |
{ | |
"word": "co", | |
"bufnr": 1, | |
"col": 6, | |
"synname": "jsFuncBlock", | |
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js", | |
"blacklist": [], | |
"line": " co", | |
"filetype": "javascript.jsx", | |
"linenr": 533, | |
"input": "co", | |
"colnr": 9, | |
"changedtick": 11 | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -53,0ms | |
11:55:08 DEBUG [transport] - request to vim: -54,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-54 | |
] | |
11:55:08 DEBUG [connection] - received response: -54,[ | |
null, | |
{ | |
"changedtick": 11, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n co\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -54,3ms | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
"n" | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
533, | |
10 | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 533, | |
"col": 10, | |
"changedtick": 12, | |
"pre": " con" | |
} | |
] | |
] | |
11:55:08 DEBUG [transport] - request to vim: -55,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-55 | |
] | |
11:55:08 DEBUG [transport] - request to vim: -56,nvim_eval,[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
], | |
-56 | |
] | |
11:55:08 DEBUG [connection] - received response: -55,[ | |
null, | |
{ | |
"changedtick": 12, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n con\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -55,3ms | |
11:55:08 DEBUG [transport] - request to vim: -57,nvim_call_function,[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
], | |
-57 | |
] | |
11:55:08 DEBUG [transport] - request to vim: -58,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-58 | |
] | |
11:55:08 DEBUG [connection] - received response: -56,[ | |
null, | |
[ | |
[ | |
532, | |
9 | |
], | |
" con" | |
] | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -56,7ms | |
11:55:08 DEBUG [connection] - received response: -57,[ | |
null, | |
{ | |
"word": "con", | |
"bufnr": 1, | |
"col": 6, | |
"synname": "jsFuncBlock", | |
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js", | |
"blacklist": [], | |
"line": " con", | |
"filetype": "javascript.jsx", | |
"linenr": 533, | |
"input": "con", | |
"colnr": 10, | |
"changedtick": 12 | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -57,0ms | |
11:55:08 DEBUG [connection] - received response: -58,[ | |
null, | |
{ | |
"changedtick": 12, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n con\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -58,2ms | |
11:55:08 DEBUG [transport] - request to vim: -59,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-59 | |
] | |
11:55:08 DEBUG [connection] - received response: -59,[ | |
null, | |
{ | |
"changedtick": 12, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n con\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -59,4ms | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"command", | |
[ | |
"noa set completeopt=noselect,menuone" | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#_do_complete", | |
[ | |
6, | |
[ | |
{ | |
"word": "const", | |
"equal": 1, | |
"abbr": "const", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":0}" | |
}, | |
{ | |
"word": "config", | |
"equal": 1, | |
"abbr": "config", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":1}" | |
}, | |
{ | |
"word": "constants", | |
"equal": 1, | |
"abbr": "constants", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":3}" | |
}, | |
{ | |
"word": "container", | |
"equal": 1, | |
"abbr": "container", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":33}" | |
}, | |
{ | |
"word": "created_on", | |
"equal": 1, | |
"abbr": "created_on", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":6}" | |
}, | |
{ | |
"word": "constructor", | |
"equal": 1, | |
"abbr": "constructor", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":10}" | |
}, | |
{ | |
"word": "createdOn", | |
"equal": 1, | |
"abbr": "createdOn", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":8}" | |
}, | |
{ | |
"word": "count", | |
"equal": 1, | |
"abbr": "count", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":28}" | |
}, | |
{ | |
"word": "creation", | |
"equal": 1, | |
"abbr": "creation", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":17}" | |
} | |
], | |
-1 | |
] | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"MenuPopupChanged", | |
{ | |
"col": 6, | |
"row": 25, | |
"scrollbar": false, | |
"completed_item": {}, | |
"width": 16, | |
"height": 9, | |
"size": 9 | |
}, | |
24 | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedP", | |
1, | |
{ | |
"lnum": 533, | |
"col": 10, | |
"changedtick": 12, | |
"pre": " con" | |
} | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
"s" | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"MenuPopupChanged", | |
{ | |
"col": 6, | |
"row": 25, | |
"scrollbar": false, | |
"completed_item": {}, | |
"width": 16, | |
"height": 9, | |
"size": 9 | |
}, | |
24 | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedP", | |
1, | |
{ | |
"lnum": 533, | |
"col": 11, | |
"changedtick": 21, | |
"pre": " cons" | |
} | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#_do_complete", | |
[ | |
6, | |
[ | |
{ | |
"word": "const", | |
"equal": 1, | |
"abbr": "const", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":0}" | |
}, | |
{ | |
"word": "constants", | |
"equal": 1, | |
"abbr": "constants", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":3}" | |
}, | |
{ | |
"word": "constructor", | |
"equal": 1, | |
"abbr": "constructor", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":10}" | |
} | |
], | |
-1 | |
] | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CompleteDone", | |
{} | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"MenuPopupChanged", | |
{ | |
"col": 6, | |
"row": 25, | |
"scrollbar": false, | |
"completed_item": {}, | |
"width": 16, | |
"height": 3, | |
"size": 3 | |
}, | |
24 | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
"t" | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"MenuPopupChanged", | |
{ | |
"col": 6, | |
"row": 25, | |
"scrollbar": false, | |
"completed_item": {}, | |
"width": 16, | |
"height": 3, | |
"size": 3 | |
}, | |
24 | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedP", | |
1, | |
{ | |
"lnum": 533, | |
"col": 12, | |
"changedtick": 32, | |
"pre": " const" | |
} | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_function", | |
[ | |
"coc#_do_complete", | |
[ | |
6, | |
[ | |
{ | |
"word": "const", | |
"equal": 1, | |
"abbr": "const", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":0}" | |
}, | |
{ | |
"word": "constants", | |
"equal": 1, | |
"abbr": "constants", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":3}" | |
}, | |
{ | |
"word": "constructor", | |
"equal": 1, | |
"abbr": "constructor", | |
"menu": "[A]", | |
"user_data": "{\"cid\":1603014908,\"source\":\"around\",\"index\":10}" | |
} | |
], | |
-1 | |
] | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CompleteDone", | |
{} | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"MenuPopupChanged", | |
{ | |
"col": 6, | |
"row": 25, | |
"scrollbar": false, | |
"completed_item": {}, | |
"width": 16, | |
"height": 3, | |
"size": 3 | |
}, | |
24 | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CompleteDone", | |
{} | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
" " | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
533, | |
13 | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 533, | |
"col": 13, | |
"changedtick": 33, | |
"pre": " const " | |
} | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#notify", | |
[ | |
"call_atomic", | |
[ | |
[ | |
[ | |
"nvim_command", | |
[ | |
"noa set completeopt=menu,preview" | |
] | |
], | |
[ | |
"nvim_command", | |
[ | |
"let g:coc#_context['candidates'] = []" | |
] | |
], | |
[ | |
"nvim_call_function", | |
[ | |
"coc#_hide", | |
[] | |
] | |
] | |
] | |
] | |
] | |
] | |
11:55:08 DEBUG [transport] - request to vim: -60,nvim_eval,[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
], | |
-60 | |
] | |
11:55:08 DEBUG [connection] - received response: -60,[ | |
null, | |
[ | |
[ | |
532, | |
12 | |
], | |
" const " | |
] | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -60,3ms | |
11:55:08 DEBUG [transport] - request to vim: -61,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-61 | |
] | |
11:55:08 DEBUG [connection] - received response: -61,[ | |
null, | |
{ | |
"changedtick": 33, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const \n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -61,7ms | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"InsertCharPre", | |
"t" | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"CursorMovedI", | |
1, | |
[ | |
533, | |
14 | |
] | |
] | |
] | |
11:55:08 DEBUG [connection] - received notification: [ | |
"CocAutocmd", | |
[ | |
"TextChangedI", | |
1, | |
{ | |
"lnum": 533, | |
"col": 14, | |
"changedtick": 34, | |
"pre": " const t" | |
} | |
] | |
] | |
11:55:08 DEBUG [transport] - request to vim: -62,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-62 | |
] | |
11:55:08 DEBUG [transport] - request to vim: -63,nvim_eval,[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"eval", | |
[ | |
"[coc#util#cursor(), getline(\".\")]" | |
] | |
], | |
-63 | |
] | |
11:55:08 DEBUG [connection] - received response: -62,[ | |
null, | |
{ | |
"changedtick": 34, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const t\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set the newly created user_profile's id to link with the campaign\n if (userEntry) {\n userProfileId = userEntry.id;\n } else {\n throw new Error('Something went wrong while creating the new user.');\n }\n } else {\n throw new Error('Something went wrong while getting the user from the auth database');\n }\n } else {\n // The user has an id, so he should exist in our database\n const profile = await trx('user_profiles')\n .first('id', 'email')\n .where('duxis_auth_id', userDuxisAuthId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n // Add \"campaign manager\" role to the user if he does NOT have the role yet\n // else we skip this step\n const hasCampaignManagerRole = await this._checkRoles(userDuxisAuthId, 'innovatrix/role/scoped/campaign_manager');\n if (!hasCampaignManagerRole) {\n let roles;\n let scopedRights;\n const dxUser = await getUserFromDxAuthByDxAuthId(userDuxisAuthId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n roles = [];\n scopedRights = [];\n } else {\n roles = (userData.roles || []);\n const sr = await getScopedRightsForUser(userDuxisAuthId, strippedToken);\n scopedRights = sr.data.filterScopedRights.items.map((r) => r.id);\n }\n await addCampaignManagerRoleToUser({\n data: userData.data,\n provider: userData.provider,\n roles,\n scopedRights,\n userId: userDuxisAuthId,\n username: profile.email, // username in auth-store is the same as email in api-store\n enabled: userData.enabled,\n }, strippedToken);\n }\n }\n\n // Finally we link the user profile to the campaign...\n await trx('campaigns_managers')\n .insert({\n right_id: userProfileId,\n left_id: campaignId,\n });\n });\n } catch (e) {\n log.warn(`createCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async deleteCampaignManager({ campaignId, userProfileId }, duxisId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .select('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n await trx('campaigns_managers')\n .del()\n .where('right_id', userProfileId)\n .andWhere('left_id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaignManager: ${e}`);\n throw new Error(e);\n }\n }\n\n async getPossibleCampaignManagers(searchString) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxPossibleCampaignManagers = await getPossibleCampaignManagers(searchString, strippedToken);\n const result = [];\n await asyncForIn((dxPossibleCampaignManagers.items || []), async (possibleUser) => {\n const user = await trx('user_profiles')\n .first('id', 'first_name AS firstName', 'last_name AS lastName', 'email', 'duxis_auth_id AS duxisAuthId')\n .where('duxis_auth_id', possibleUser.id);\n if (user) {\n result.push(user);\n }\n });\n return result;\n });\n } catch (e) {\n log.warn(`getPossibleCampaignManagers: ${e}`);\n throw e;\n }\n }\n\n _getIntakeCallStatus(id, intakes) {\n const hasIntake = intakes.find((i) => i.intakeId === id);\n if (hasIntake) {\n return {\n status: hasIntake.status === SUBMITTED_INTAKE ? SUBMITTED_INTAKE : DRAFT_INTAKE,\n submittedOn: hasIntake.submittedOn ? hasIntake.submittedOn : null,\n };\n }\n return {\n status: OPEN_INTAKE,\n submittedOn: null,\n };\n }\n\n async getCalls(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const now = moment().tz(BRUSSELS_TZ).format('YYYY-MM-DD');\n const allCampaigns = await trx('campaigns')\n .select(\n 'campaigns.id', 'campaigns.name', 'intake_end_date AS intakeEndDate', 'pitch_end AS pitchEnd',\n 'intakes.id AS intakeId', 'intakes.deleted_on AS isIntakeDeleted'\n )\n .join('campaigns_intakes', 'campaigns.id', 'campaigns_intakes.left_id')\n .join('intakes', 'campaigns_intakes.right_id', 'intakes.id')\n .where('intake_type', 'APPLICATION'); // This is important, because we only want campaigns with an intake form assigned to them!\n\n // fetch the current user's submitted/draft intakes, if any\n // we should fetch an array of objects like:\n // [{ id: 'a051x0000044HjXAAU', intakeId: 'd9eeb730-d26d-11ea-aa67-af38bfd1b3df', status: DRAFT|SUBMITTED, submittedOn: '2020-07-31' }],\n const intakesCurrentUser = await trx('intake_answers')\n .select('campaigns_intakes.right_id as intakeId', 'campaign_id AS campaignId', 'status', 'submitted_on AS submittedOn')\n .join('campaigns_intakes', 'intake_answers.campaign_id', 'campaigns_intakes.left_id')\n .where('user_profile_id', user.id);\n\n const calls = {};\n // This should only be filtered on the pitchEnd\n // but let's add a fallback (for now) just in case no pitchEnd was set\n // for open calls we have 3 states,\n // 1) OPEN 2) DRAFT 3) SUBMITTED\n calls.openCalls = allCampaigns\n .filter((c) => !c.isIntakeDeleted) // make sure we filter out the campaigns with a deleted intake linked to them\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isSameOrAfter(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by soonest first\n .sort((x, y) => x.intakeEndDate - y.intakeEndDate);\n // for closed calls we only have 2 states,\n // 1) DRAFT 2) SUBMITTED\n calls.closedCalls = allCampaigns\n .filter((c) => moment.tz(c.pitchEnd || c.intakeEndDate, BRUSSELS_TZ).isBefore(now))\n .map((c) => ({ ...c, ...(this._getIntakeCallStatus(c.intakeId, intakesCurrentUser)) }))\n // Sort by most recently passed first\n .filter((c) => c.status !== OPEN_INTAKE) // we need to filter the OPEN_INTAKE status\n .sort((x, y) => y.intakeEndDate - x.intakeEndDate);\n\n return calls;\n });\n } catch (e) {\n log.warn(`getCalls: ${e}`);\n throw e;\n }\n }\n\n // Get a list of all calls\n // ( = campaigns with an IntakeForm linked to them)\n async manageCalls(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const calls = await trx('campaigns_intakes')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .select('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId');\n\n return calls;\n });\n } catch (e) {\n log.warn(`manageCalls: ${e}`);\n throw e;\n }\n }\n\n async callOverview({ callId }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const user = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n\n if (!user) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n const call = await trx('campaigns_intakes')\n .first('campaigns.id', 'campaigns.name', 'campaigns_intakes.right_id AS intakeId')\n .join('campaigns', 'campaigns.id', 'campaigns_intakes.left_id')\n .where('campaigns.id', callId);\n\n if (!call) {\n throw new Error('Call does not exist.');\n }\n\n // Get all necessary data for the frontend\n let result = {\n ...call,\n };\n\n // Get the answer data\n const storedApplications = await trx('intake_answers')\n .select(\n 'intake_answers.id', 'answers', 'status', 'submitted_on AS submittedOn',\n 'user_profiles.email', 'user_profiles.first_name AS firstName',\n 'user_profiles.last_name AS lastName', 'created_on AS created_on',\n )\n .join('user_profiles', 'user_profiles.id', 'intake_answers.user_profile_id')\n .where('campaign_id', callId)\n .andWhere('intake_id', call.intakeId)\n // sort by submitted_on first, drafts will be grouped together\n .orderBy('submitted_on', 'desc')\n // sort drafts by created_on\n .orderBy('intake_answers.created_on', 'desc');\n\n const applications = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n // find the provided PROJECT_RECORD_FIELD if it exists\n const projectNameField = parsedAnswers\n .map((section) => section.items)\n .flat()\n .find((field) => field.type === PROJECT_RECORD_FIELD);\n // get the project name (if field exists)\n const projectName = (projectNameField && projectNameField.value) || 'No project name provided';\n // structure the object for the frontend\n const structuredObject = {\n ...rest,\n projectName,\n };\n // add it to the list of applications\n applications.push(structuredObject);\n });\n\n result = {\n ...result,\n applications,\n };\n\n const documents = [];\n await asyncForIn(storedApplications, async ({ answers, ...rest }) => {\n // parse the JSON\n const parsedAnswers = JSON.parse(answers);\n const intakeDocuments = await trx('intake_documents')\n .select('intake_question_id AS questionId')\n .where('intake_answer_id', rest.id);\n const intakeDocumentsIds = intakeDocuments.map((i) => i.questionId);\n // find the document fields with uploaded documents\n const documentFields = parsedAnswers\n .map((section) => section.items)\n .flat()\n .filter((field) => field.type === DOCUMENT_FIELD && field.value && intakeDocumentsIds.includes(field.id));\n // Only push to the results array if there effectively are\n // uploaded documents available on a question\n if (documentFields.length > 0) {\n documents.push({\n ...rest,\n uploadedDocuments: documentFields,\n });\n }\n });\n\n result = {\n ...result,\n documents,\n };\n\n return result;\n });\n } catch (e) {\n log.warn(`callOverview: ${e}`);\n throw e;\n }\n }\n}\n\nmodule.exports = CampaignService;" | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -62,12ms | |
11:55:08 DEBUG [transport] - request to vim: -64,nvim_call_function,[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_complete_option", | |
[] | |
] | |
], | |
-64 | |
] | |
11:55:08 DEBUG [connection] - received response: -63,[ | |
null, | |
[ | |
[ | |
532, | |
13 | |
], | |
" const t" | |
] | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -63,30ms | |
11:55:08 DEBUG [connection] - received response: -64,[ | |
null, | |
{ | |
"word": "t", | |
"bufnr": 1, | |
"col": 12, | |
"synname": "jsFuncBlock", | |
"filepath": "/home/channi16/imec/innovatrix/images/api/service/services/CampaignService.js", | |
"blacklist": [], | |
"line": " const t", | |
"filetype": "javascript.jsx", | |
"linenr": 533, | |
"input": "t", | |
"colnr": 14, | |
"changedtick": 34 | |
} | |
] | |
11:55:08 DEBUG [transport] - response from vim cost: -64,1ms | |
11:55:08 DEBUG [transport] - request to vim: -65,nvim_call_function,[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
11:55:08 DEBUG [connection] - send to vim: [ | |
"call", | |
"coc#api#call", | |
[ | |
"call_function", | |
[ | |
"coc#util#get_content", | |
[ | |
1 | |
] | |
] | |
], | |
-65 | |
] | |
11:55:08 DEBUG [connection] - received response: -65,[ | |
null, | |
{ | |
"changedtick": 34, | |
"content": "const log4js = require('log4js');\nconst { config } = require('cargo-service');\nconst { asyncForEach, asyncForIn, uuid } = require('cargo-universal/utils');\nconst moment = require('moment-timezone');\nconst {\n addCampaignManagerRoleToUser,\n addUserToDxAuth,\n getAllPossibleReviewers,\n getPossibleCampaignManagers,\n getScopedRightsForUser,\n getUserFromDxAuth,\n getUserFromDxAuthByDxAuthId,\n} = require('./_duxisAuthService');\nconst DuxisAuthClient = require('./DuxisAuthClient');\nconst {\n DOCUMENT_FIELD,\n PROJECT_RECORD_FIELD,\n DRAFT_INTAKE,\n OPEN_INTAKE,\n SUBMITTED_INTAKE,\n BRUSSELS_TZ,\n} = require('../constants/intakes');\n\nconst log = log4js.getLogger('CampaignService');\n\nconst SALESFORCE_ENABLED = config.get('innovatrix.salesforceNoCreate.enable', false);\nconst PHASES_ENABLED = config.get('innovatrix.salesforceNoCreate.campaigns.phases', false);\n\nfunction transformCampaign({\n assessment_flows_group_id,\n created_by,\n created_on,\n decision_date,\n end_date,\n has_evaluative_phases,\n intake_end_date,\n intake_start_date,\n intake_type,\n pitch_end,\n pitch_start,\n start_date,\n updated_by,\n updated_on,\n ...rest\n}) {\n return {\n assessmentFlowsGroupId: assessment_flows_group_id,\n createdBy: created_by,\n createdOn: created_on,\n decisionDate: decision_date,\n endDate: end_date,\n hasEvaluativePhases: has_evaluative_phases,\n intakeEndDate: intake_end_date,\n intakeStartDate: intake_start_date,\n intakeType: intake_type,\n pitchEnd: pitch_end,\n pitchStart: pitch_start,\n startDate: start_date,\n updatedBy: updated_by,\n updatedOn: updated_on,\n ...rest,\n };\n}\n\nclass CampaignService {\n constructor(store) {\n this.store = store;\n this.tableName = 'campaigns';\n this.duxisAuthClient = new DuxisAuthClient();\n }\n\n async createCampaigns(data, trx) {\n try {\n log.info('Writing campaigns...');\n\n const preSelectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Pre-selection');\n const selectionPhase = await trx('phases')\n .first('id')\n .where('title', 'Selection');\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n let preSelId = uuid();\n let selId = uuid();\n let projectPhaseId = uuid();\n\n if (preSelectionPhase) {\n preSelId = preSelectionPhase.id;\n } else {\n await trx('phases').insert({\n id: preSelId,\n title: 'Pre-selection',\n });\n }\n\n if (selectionPhase) {\n selId = selectionPhase.id;\n } else {\n await trx('phases').insert({\n id: selId,\n title: 'Selection',\n });\n }\n\n if (projectPhase) {\n projectPhaseId = projectPhase.id;\n } else {\n await trx('phases').insert({\n id: projectPhaseId,\n title: 'Project',\n });\n }\n\n await asyncForIn(data, async ({ phases, ...campaign }) => {\n await trx.raw(`${trx(this.tableName).insert(campaign).toQuery()} ON CONFLICT (id) DO UPDATE\n SET name = ?, start_date = ?, end_date = ?, intake_start_date = ?, intake_end_date = ?,\n decision_date = ?, pitch_start = ?, pitch_end = ?;\n `, [\n campaign.name, campaign.start_date, campaign.end_date, campaign.intake_start_date, campaign.intake_end_date,\n campaign.decision_date, campaign.pitch_start, campaign.pitch_end,\n ]);\n\n await asyncForEach(phases, async ({ title, deadline, assessmentVersionGroupId }, order) => {\n await trx.raw(`${trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaign.id,\n right_id: title === 'Pre-selection' ? preSelId : selId,\n order,\n }).toQuery()} ON CONFLICT (left_id, right_id) DO UPDATE\n SET deadline = ?;\n `, [deadline]);\n });\n\n await trx.raw(`${trx('campaigns_phases')\n .insert({\n left_id: campaign.id,\n right_id: projectPhaseId,\n order: phases.length,\n })\n .toQuery()} ON CONFLICT (left_id, right_id) DO NOTHING;\n `, []);\n });\n log.info('Writing campaigns is done!');\n } catch (e) {\n log.warn(`createCampaigns ${e}`);\n }\n }\n\n async removeAll(trx) {\n try {\n await trx.raw(`ALTER TABLE ${this.tableName} DISABLE TRIGGER ALL;`);\n await trx('campaigns_phases').del();\n await trx('phases').del();\n await trx(this.tableName).del();\n await trx.raw(`ALTER TABLE ${this.tableName} ENABLE TRIGGER ALL;`);\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createCampaign({\n name, phases, intakeType, intakeStartDate,\n intakeEndDate, hasEvaluativePhases, intakeFormId,\n }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user profile does not exist.');\n }\n\n const campaignId = uuid();\n const campaign = await trx('campaigns').insert({\n id: campaignId,\n created_by: profile.id,\n created_on: new Date(),\n updated_by: profile.id,\n updated_on: new Date(),\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n });\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionGroupId } of phases) {\n await trx('campaigns_phases').insert({\n assessment_version_group_id: assessmentVersionGroupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n\n // Link intake form with campaign if the intakeType is NOT creation\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n return { ...campaign, intakeFormId };\n });\n } catch (e) {\n log.warn(`createCampaign: ${e}`);\n throw e;\n }\n }\n\n async updateCampaign({\n assessmentFlowsGroupId,\n attendees,\n campaignId,\n hasEvaluativePhases,\n intakeEndDate,\n intakeFormId,\n intakeStartDate,\n intakeType,\n name,\n phases,\n }, duxisId) {\n try {\n let campaign;\n await this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id').where('duxis_auth_id', duxisId);\n\n if (!profile) {\n throw new Error('Unauthorized: user does not exist.');\n }\n\n await trx('campaigns').update({\n assessment_flows_group_id: assessmentFlowsGroupId,\n attendees,\n name,\n intake_type: intakeType,\n intake_start_date: intakeStartDate,\n intake_end_date: intakeEndDate,\n has_evaluative_phases: hasEvaluativePhases,\n updated_by: profile.id,\n updated_on: new Date(),\n }).where('id', campaignId);\n\n // If no phases were explicitly provided we ignore it.\n if (phases !== undefined) {\n // Remove campaign phase links.\n await trx('campaigns_phases').del().where('left_id', campaignId);\n\n let phaseOrder = 0;\n if (hasEvaluativePhases && phases && phases.length > 0) {\n for (const { deadline, phaseId, assessmentVersionId } of phases) {\n const assessmentVersion = await trx('assessment_versions')\n .first('id', 'group_id AS groupId')\n .where('id', assessmentVersionId);\n await trx('campaigns_phases').insert({\n assessment_version_id: assessmentVersion.id,\n assessment_version_group_id: assessmentVersion.groupId,\n deadline,\n left_id: campaignId,\n right_id: phaseId,\n order: phaseOrder,\n });\n phaseOrder += 1;\n }\n }\n\n const projectPhase = await trx('phases')\n .first('id')\n .where('title', 'Project');\n\n if (projectPhase) {\n await trx('campaigns_phases').insert({\n left_id: campaignId,\n right_id: projectPhase.id,\n order: phaseOrder,\n });\n }\n }\n\n if (intakeFormId || intakeType === 'CREATION') {\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n }\n\n if (intakeType === 'APPLICATION' && intakeFormId) {\n await trx('campaigns_intakes').insert({\n left_id: campaignId,\n right_id: intakeFormId,\n });\n }\n\n campaign = await this.getCampaign(campaignId, trx);\n });\n return campaign;\n } catch (e) {\n log.warn(`updateCampaign: ${e}`);\n throw e;\n }\n }\n\n async getCampaign(campaignId, existingTrx) {\n try {\n let campaign;\n const now = moment().tz(BRUSSELS_TZ);\n await this.store.withTransaction(existingTrx, async (trx) => {\n campaign = await trx(this.tableName)\n .select('attendees', 'id', 'name', 'assessment_flows_group_id', 'amount_of_phases as phasesCount', 'intake_end_date as endDate')\n .where('id', campaignId)\n .first();\n\n campaign.phases = await trx('phases')\n .select('id', 'title', 'assessment_version_group_id as assessmentVersionGroupId', 'deadline')\n .join('campaigns_phases', 'right_id', 'phases.id')\n .where('left_id', campaignId)\n .orderBy('campaigns_phases.order');\n\n const campaignFirstPhaseDeadline = campaign.phases[0] && campaign.phases[0].deadline;\n campaign.isInFirstPhase = campaignFirstPhaseDeadline\n ? moment.tz(campaignFirstPhaseDeadline, BRUSSELS_TZ).isAfter(now)\n : true;\n campaign.intakeDeadlinePassed = campaign.endDate && !moment.tz(campaign.endDate, BRUSSELS_TZ).isAfter(now);\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaignId);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n return transformCampaign(campaign);\n } catch (e) {\n log.warn(`getCampaign: ${e}`);\n throw e;\n }\n }\n\n async getPublicCampaign({ campaignName }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Make sure the campaign exists\n const publicCampaignData = await trx('campaigns')\n .first('id', 'name', 'intake_end_date AS deadline')\n .whereRaw(`LOWER(name) = LOWER('${campaignName}')`); // allow fully lowercase campaign names as well\n\n // If campaign wasn't found let's pass this INVALID_ID so we can do some error\n // handling in the front-end\n if (!publicCampaignData) {\n return { id: 'INVALID_ID', callClosed: true };\n }\n\n const now = moment().tz(BRUSSELS_TZ);\n const callClosed = now.isAfter(moment.tz(publicCampaignData.deadline, BRUSSELS_TZ));\n return { ...publicCampaignData, callClosed };\n });\n } catch (e) {\n log.warn(`getPublicCampaign: ${e}`);\n throw e;\n }\n }\n\n async registerCampaignUser({ campaignId, user }) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check intake_end for current campaign\n const campaign = await trx('campaigns')\n .first('intake_end_date AS deadline')\n .where('id', campaignId);\n if (!campaign) {\n throw new Error('Call does not exist.');\n }\n const now = moment().tz(BRUSSELS_TZ);\n const deadline = campaign.deadline ? moment.tz(campaign.deadline, BRUSSELS_TZ) : now;\n if (now.isSameOrAfter(deadline)) {\n throw new Error('Call is closed.');\n }\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/applicant'],\n scoped_rights: [],\n username: sanitizedUsername,\n password: user.password,\n }, strippedToken, true);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`registerCampaignUser: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('registerCampaignUser: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('registerCampaignUser: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const profile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n job_title: user.role,\n last_name: user.lastName,\n phone_number: user.phone,\n salutation: user.salutation,\n linked_in_profile: user.linkedIn,\n };\n\n await trx('user_profiles')\n .insert(profile);\n\n return { id };\n });\n } catch (e) {\n log.warn(`registerCampaignUser: ${e}`);\n throw e;\n }\n }\n\n async addOtherCampaignData(campaigns, trx) {\n await asyncForIn(campaigns, async (campaign) => {\n // Get all evaluative phases for this campaign.\n campaign.phases = await trx('phases')\n .select('id', 'title', 'deadline', 'assessment_version_id AS assessmentVersionId', 'assessment_version_group_id as assessmentVersionGroupId', 'assessment_version_id AS lockedAssessmentVersion')\n .join('campaigns_phases', 'campaigns_phases.right_id', 'phases.id')\n .where('left_id', campaign.id)\n .orderBy('campaigns_phases.order');\n\n if (campaign.created_by) {\n // Get and assign creator\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.created_by);\n campaign.createdBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n if (campaign.updated_by) {\n // Get and assign updater.\n const userProfile = await trx('user_profiles')\n .first('first_name as firstName', 'last_name as lastName')\n .where('id', campaign.updated_by);\n campaign.updatedBy = userProfile.firstName && userProfile.lastName && `${userProfile.firstName} ${userProfile.lastName}`;\n }\n\n // Get our most advanced phase for this campaign.\n let campaignPhase = await trx('campaigns_projects')\n .first('campaigns_phases.right_id AS phaseId')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .join('campaigns_phases', 'campaigns_phases.left_id', 'campaigns_projects.left_id')\n .orderBy('campaigns_phases.order', 'DESC')\n .where('campaigns_projects.left_id', campaign.id);\n\n // If there are no projects yet, we fallback on the first phase of the campaign.\n if (!campaignPhase) {\n campaignPhase = await trx('campaigns_phases')\n .first('right_id AS phaseId')\n .orderBy('order', 'DESC')\n .where('left_id', campaign.id);\n }\n\n if (PHASES_ENABLED) {\n const projectsInLastPhase = await trx('projects')\n .count('projects.id')\n .rightOuterJoin('campaigns_projects', 'projects.id', 'campaigns_projects.right_id')\n .rightOuterJoin('campaigns', 'campaigns.id', 'campaigns_projects.left_id')\n .where('projects.phase_id', function whereLastPhase() {\n this.first('right_id AS id')\n .from('campaigns_phases')\n .whereRaw('left_id = campaigns_projects.left_id')\n .orderBy('order', 'DESC');\n })\n .where('campaigns.id', campaign.id)\n .first();\n campaign.projectsInLastPhase = Number(projectsInLastPhase.count);\n }\n\n const projectsInCampaign = await trx('campaigns_projects')\n .select('projects.id AS id', 'projects.name AS name')\n .join('projects', 'projects.id', 'campaigns_projects.right_id')\n .where('left_id', campaign.id);\n\n campaign.projects = projectsInCampaign;\n\n campaign.numberOfProjects = (projectsInCampaign || []).length;\n\n campaign.phaseId = campaignPhase && campaignPhase.phaseId;\n\n const t\n\n const linkedIntake = await trx('campaigns_intakes')\n .first('right_id AS id').where('left_id', campaign.id);\n campaign.intakeFormId = linkedIntake ? linkedIntake.id : null;\n });\n }\n\n async getCampaigns() {\n try {\n return this.store.knex.transaction(async (trx) => {\n const campaigns = await this.store.getCollection(\n this.tableName,\n { ordering: [{ field: 'start_date', direction: 'desc' }], trx }\n );\n await this.addOtherCampaignData(campaigns, trx);\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getScopedCampaigns(duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user');\n }\n // Fetch all campaigns linked to the current user\n const assignedCampaignIds = await trx('campaigns_managers')\n .select('left_id as id')\n .where('right_id', profile.id);\n\n // Fetch their data\n const campaigns = await trx('campaigns')\n .select('*')\n .whereIn('campaigns.id', assignedCampaignIds.map((c) => c.id));\n\n // Add additional data...\n await this.addOtherCampaignData(campaigns, trx);\n\n // Transform the data and pass it to front-end\n return campaigns.map(transformCampaign);\n });\n } catch (e) {\n log.warn(`getScopedCampaigns: ${e}`);\n throw e;\n }\n }\n\n async getCampaignManagers({ campaignId }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the user_profile ids that are assigned for the current campaign\n const managerIdsForCurrentCampaign = await trx('campaigns_managers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n\n if (managerIdsForCurrentCampaign.length === 0) {\n return [];\n }\n\n const campaignManagers = await trx('user_profiles')\n .select('email', 'first_name AS firstName', 'last_name AS lastName', 'id')\n .whereIn('id', managerIdsForCurrentCampaign.map((m) => m.id));\n\n return campaignManagers.map((manager) => ({\n ...manager,\n name: `${manager.firstName ? `${manager.firstName} ` : ''}${manager.lastName || ''}`,\n }));\n });\n } catch (e) {\n log.warn(`getCampaignManagers ${e}`);\n throw e;\n }\n }\n\n // { campaignId: { projects: [ projectId, projectId, ... ]} }\n async addProjects(campaignId, projectIds, trx) {\n try {\n await asyncForIn(projectIds.projects, async (projectId) => {\n const writeObject = { left_id: campaignId, right_id: projectId };\n await trx.raw(`${trx('campaigns_projects').insert(writeObject).toQuery()} ON CONFLICT DO NOTHING;`);\n });\n } catch (e) {\n log.warn(`addProjects ${e}`);\n throw e;\n }\n }\n\n async removeAllRelations(trx) {\n try {\n await trx('campaigns_projects').del();\n } catch (e) {\n log.warn(`removeAll: ${e}`);\n throw e;\n }\n }\n\n async createPhase({ title }) {\n try {\n const phase = { id: uuid(), title };\n await this.store.knex('phases').insert(phase);\n return phase;\n } catch (e) {\n log.warn(`createPhase: ${e}`);\n throw e;\n }\n }\n\n // Used for dropdown with all available phases.\n async getPhases() {\n try {\n return await this.store.knex('phases')\n .select('id', 'title')\n .orderBy('title');\n } catch (e) {\n log.warn(`getPhases: ${e}`);\n throw e;\n }\n }\n\n async getAllPossibleReviewers(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get a duxis token to make calls to the auth container\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const response = await getAllPossibleReviewers(strippedToken);\n const possibleReviewers = (response && response.items) || [];\n const result = [];\n await asyncForIn(possibleReviewers, async (reviewer) => {\n const matchingProfile = await trx('user_profiles')\n .first('first_name AS firstName', 'last_name AS lastName', 'id AS profileId')\n .where('duxis_auth_id', reviewer.id)\n .andWhere('email', reviewer.username);\n // Only push to the result array if we have a matching user_profile entry\n if (matchingProfile) {\n result.push({\n ...matchingProfile,\n ...reviewer,\n name: `${matchingProfile.lastName || ''}${matchingProfile.firstName ? ` ${matchingProfile.firstName}` : ''}`,\n profileId: matchingProfile.profileId,\n });\n }\n });\n const alphabeticallySortedReviewersList = result\n // eslint-disable-next-line\n .sort((a, b) => (a.lastName < b.lastName ? -1 : (a.lastName > b.lastName) ? 1 : 0));\n return alphabeticallySortedReviewersList;\n });\n } catch (err) {\n log.warn(`getAllPossibleReviewers: ${err}`);\n throw new Error(err);\n }\n }\n\n // We want to add a new user with the \"innovatrix/role/evaluator\" assigned to them\n async createCampaignReviewer({ user }, duxisId) {\n try {\n return this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Try to register the user to auth0 and in our database\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n\n const sanitizedUsername = user.email.toLowerCase();\n\n const response = await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/evaluator'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Check if we have auth0 or auth-store errors\n if (response && response.errors && response.errors.length > 0) {\n if (response.errors.find((e) => e.message.includes('user already exists'))) {\n log.warn(`createCampaignReviewer: user (${sanitizedUsername}) already exists.`);\n return {\n id: 'USER_ALREADY_EXISTS',\n };\n }\n\n log.warn('createCampaignReviewer: responseErrors =>', response.errors.map((e) => e.message));\n return {\n id: 'UNKNOWN_ERROR',\n };\n }\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyRegisteredUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (!newlyRegisteredUser) {\n log.warn('createCampaignReviewer: Did not find the newly registered user.');\n return 'UNKNOWN_ERROR';\n }\n\n const id = uuid();\n const newProfile = {\n duxis_auth_id: newlyRegisteredUser.id,\n email: sanitizedUsername,\n first_name: user.firstName,\n has_logged_in: false,\n id,\n last_name: user.lastName,\n };\n\n await trx('user_profiles')\n .insert(newProfile);\n\n return { id };\n });\n } catch (err) {\n log.warn(`createCampaignReviewer: ${err}`);\n throw new Error(err);\n }\n }\n\n async persistCampaignReviewers({ campaignId, reviewerIds }) {\n if (!campaignId) {\n throw new Error('campaignId is required.');\n }\n if (!reviewerIds || !Array.isArray(reviewerIds)) {\n throw new Error('reviewerIds is required.');\n }\n\n await this.store.knex.transaction(async (trx) => {\n const deletedCampaignReviewers = await trx('campaigns_reviewers')\n .select('right_id AS userProfileId')\n .where('left_id', campaignId)\n .whereNotIn('right_id', reviewerIds);\n\n // Insert new campaign reviewers\n await asyncForIn(reviewerIds, async (id) => {\n const exists = await trx('campaigns_reviewers')\n .first('right_id')\n .where('right_id', id)\n .andWhere('left_id', campaignId);\n if (!exists) {\n await trx('campaigns_reviewers')\n .insert({\n left_id: campaignId,\n right_id: id,\n });\n }\n });\n\n if (deletedCampaignReviewers.length === 0) { return; }\n\n const currentCampaignProjects = await trx('campaigns_projects')\n .select('right_id AS projectId')\n .where('left_id', campaignId);\n\n const deletedCampaignReviewersIds = deletedCampaignReviewers.map((r) => r.userProfileId);\n const projectIds = currentCampaignProjects.map((c) => c.projectId);\n\n await asyncForIn(deletedCampaignReviewersIds, async (deletedReviewerId) => {\n // Delete all project linked to the deleted reviewer(s)\n await trx('projects_reviewers')\n .del()\n .whereIn('left_id', projectIds)\n .andWhere('right_id', deletedReviewerId);\n\n // Delete campaign reviewer entry\n await trx('campaigns_reviewers')\n .del()\n .where('right_id', deletedReviewerId)\n .andWhere('left_id', campaignId);\n });\n });\n }\n\n async getCampaignReviewers(campaignId) {\n try {\n return await this.store.knex('campaigns_reviewers')\n .select('right_id AS id')\n .where('left_id', campaignId);\n } catch (e) {\n log.warn(`getCampaignReviewers: ${e}`);\n throw e;\n }\n }\n\n async getCampaignReviewerGroups(duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all groups and their reviewers\n const reviewerGroups = await trx('reviewer_groups')\n .select('id', 'title', 'collapsed')\n .orderBy('title');\n\n const result = [];\n await asyncForIn(reviewerGroups, async (reviewerGroup) => {\n const reviewerIds = await trx('reviewer_group_reviewers')\n .select('right_id AS id')\n .where('left_id', reviewerGroup.id);\n result.push({\n ...reviewerGroup,\n reviewerIds: reviewerIds.map((i) => i.id),\n });\n });\n return result;\n });\n } catch (e) {\n log.warn(`getCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async persistCampaignReviewerGroups({ groups }, duxisId) {\n try {\n return await this.store.knex.transaction(async (trx) => {\n // Check if the user exists\n const profile = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', duxisId);\n if (!profile) {\n throw new Error('Unauthorized user.');\n }\n\n // Get all the currently stored groups' ids\n const storedReviewerGroupIds = await trx('reviewer_groups')\n .select('id');\n // Check which groups we need to delete\n const deletedGroupIds = storedReviewerGroupIds\n .filter((r) => !groups.map((g) => g.id).includes(r.id))\n .map((g) => g.id);\n // Create/update groups\n await asyncForIn(groups, async (group) => {\n // Check if reviewer_group already exists...\n const exists = await trx('reviewer_groups')\n .first('id')\n .where('id', group.id);\n if (exists) {\n // update if it does\n await trx('reviewer_groups')\n .update({\n title: group.title,\n collapsed: group.collapsed,\n })\n .where('id', group.id);\n // First delete all reviewers, then we re-insert the current ones\n // This way we don't need to check which ones to delete or update\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', group.id);\n } else {\n // create if it doesn't\n await trx('reviewer_groups')\n .insert({\n id: group.id,\n title: group.title,\n collapsed: group.collapsed,\n });\n }\n // Insert the reviewers\n const reviewerIds = (group.reviewerIds || []).filter((r) => r); // mitigate storing null or undefined values\n await asyncForIn(reviewerIds, async (reviewerId) => {\n await trx('reviewer_group_reviewers')\n .insert({\n left_id: group.id,\n right_id: reviewerId,\n });\n });\n });\n\n // Finally remove deleted groups\n await asyncForIn(deletedGroupIds, async (deletedGroupId) => {\n await trx('reviewer_group_reviewers')\n .del()\n .where('left_id', deletedGroupId);\n await trx('reviewer_groups')\n .del()\n .where('id', deletedGroupId);\n });\n });\n } catch (e) {\n log.warn(`createCampaignReviewerGroups: ${e}`);\n throw e;\n }\n }\n\n async deleteCampaign(campaignId) {\n try {\n await this.store.knex.transaction(async (trx) => {\n await trx('campaigns_phases')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_phases WHERE left_id', campaignId);\n await trx('campaigns_projects')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_projects WHERE left_id', campaignId);\n await trx('campaigns_reviewers')\n .del()\n .where('left_id', campaignId);\n log.info('Deleted campaigns_reviewers WHERE left_id', campaignId);\n // Delete linked intakes\n await trx('campaigns_intakes')\n .del()\n .where('left_id', campaignId);\n await trx('campaigns')\n .del()\n .where('id', campaignId);\n log.info('Deleted campaign WHERE id', campaignId);\n });\n } catch (e) {\n log.warn(`deleteCampaign: ${e}`);\n throw e;\n }\n }\n\n async makeCampaignsExport() {\n try {\n return await this.store.knex.transaction(async (trx) => {\n const phases = await trx('phases')\n .select('id', 'title');\n const projects = await trx('projects')\n .select('id', 'left_id AS campaignId', 'phase_id AS phaseId')\n .join('campaigns_projects', 'campaigns_projects.right_id', 'projects.id')\n .where('deleted_on', null);\n const projectDecisions = await trx('project_phase_statuses')\n .select('status', 'project_id AS projectId', 'phase_id AS phaseId');\n\n const campaigns = await trx('campaigns')\n .select('id', 'name');\n\n for (const campaign of campaigns) {\n campaign.phases = (await trx('campaigns_phases')\n .select('right_id AS phaseId', 'assessment_version_group_id AS asssessmentVersionGroupId')\n .orderBy('order'));\n }\n\n let evaluations;\n\n if (SALESFORCE_ENABLED) {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('submitted', true)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select(\n 'evaluation_ratings.id', 'score', 'question_id AS questionId',\n 'evaluation_ratings.domain_id AS domainId',\n 'evaluation_ratings.criterium_id AS criteriumId',\n 'criteria.pre_selection_threshold AS preSelectionThreshold',\n 'criteria.selection_threshold AS selectionThreshold'\n )\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .fullOuterJoin('criteria', 'criteria.id', 'evaluation_ratings.criterium_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n }\n\n for (const project of projects) {\n project.reviews = await trx('reviews')\n .select('id', 'overall_advice AS advice', 'phase_id AS phaseId', 'reviewer_id AS reviewerId')\n .where('project_id', project.id);\n }\n } else {\n evaluations = await trx('evaluations')\n .select(\n 'evaluations.id', 'reviewer_id AS reviewerId', 'project_id AS projectId',\n 'assessment_version_id AS assessmentVersionId', 'assessment_versions.group_id AS assessmentVersionGroupId'\n )\n .join('assessment_versions', 'assessment_versions.id', 'evaluations.assessment_version_id')\n .where('completed', true)\n .whereNot('reviewer_id', null)\n .orderBy('order');\n\n for (const evaluation of evaluations) {\n // Only get ratings for quantified questions.\n evaluation.ratings = await trx('evaluation_ratings')\n .select('evaluation_ratings.id', 'score', 'question_id AS questionId', 'questions.threshold')\n .join('questions', 'questions.id', 'evaluation_ratings.question_id')\n .where('questions.type', 'MULTIPLE_CHOICE_SINGLE_SELECTION_QUANTIFIED');\n\n const review = await trx('new_reviews')\n .first(\n 'new_reviews.id', 'question_id AS questionId', 'question_group_id AS questionGroupId',\n 'advice_type AS advice',\n )\n .join('question_options', 'question_options.id', 'new_reviews.question_option_id')\n .join('questions', 'questions.id', 'question_options.question_id')\n .where('new_reviews.evaluation_id', evaluation.id);\n\n evaluation.advice = review && review.advice;\n }\n }\n\n return {\n campaigns,\n evaluations,\n phases,\n projects,\n projectDecisions,\n };\n });\n } catch (e) {\n throw new Error('makeCampaignsExport failed');\n }\n }\n\n async _checkRoles(userId, roleToCheck) {\n const dxResponse = await this.duxisAuthClient.getDuxisToken();\n const { dxToken: strippedToken } = dxResponse;\n const dxUser = await getUserFromDxAuthByDxAuthId(userId, strippedToken);\n const { data: { getUser: { user: userData } } } = dxUser;\n if (!userData || !userData.roles) {\n // Should we throw an error?\n return false;\n }\n return (userData.roles || []).includes(roleToCheck);\n }\n\n async createCampaignManager({ campaignId, user }) {\n try {\n await this.store.knex.transaction(async (trx) => {\n const { dxToken: strippedToken } = await this.duxisAuthClient.getDuxisToken();\n let userProfileId = user.id;\n let userDuxisAuthId = user.duxisAuthId;\n const sanitizedUsername = (user.email || '').toLowerCase();\n // No id provided... so we assume this is going to be a new user in our database\n if (!userProfileId) {\n // Email is required field\n if (!sanitizedUsername) {\n throw new Error('The user needs an email to be invited.');\n }\n // Check if the email is already being used as a username in our auth-store\n const { data } = await getUserFromDxAuth(sanitizedUsername, strippedToken);\n if (data.getUser.user) {\n throw new Error('User with current email already exists.');\n }\n // Add the new user to our database\n // with normal client role and campaign_manager role\n await addUserToDxAuth({\n data: { firstName: user.firstName, lastName: user.lastName },\n enabled: true,\n provider: 'auth0',\n roles: ['innovatrix/role/client', 'innovatrix/role/scoped/campaign_manager'],\n scoped_rights: [],\n username: sanitizedUsername,\n }, strippedToken, false);\n\n // Fetch the newly added user entry from our auth-store\n const { data: { getUser: { user: newlyAddedUser } } } = await getUserFromDxAuth(\n sanitizedUsername,\n strippedToken\n );\n\n if (newlyAddedUser) {\n const id = uuid();\n userDuxisAuthId = newlyAddedUser.id;\n // Add new user_profile entry\n await trx('user_profiles')\n .insert({\n id,\n duxis_auth_id: userDuxisAuthId,\n email: sanitizedUsername,\n first_name: user.firstName,\n last_name: user.lastName,\n has_logged_in: false,\n });\n\n // Check if has been successfully created\n const userEntry = await trx('user_profiles')\n .first('id')\n .where('duxis_auth_id', userDuxisAuthId);\n\n // We set th |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment