diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index ab497c5ed79..9de2dc36c2e 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -7,6 +7,7 @@ CREATE TABLE `Project` ( `Name` VARCHAR(255) NOT NULL, `Alias` char(4) NOT NULL, `recruitmentTarget` INT(6) Default NULL, + `showSummaryOnLogin` BOOLEAN DEFAULT TRUE, PRIMARY KEY (`ProjectID`), UNIQUE KEY `u_ProjectName` (`Name`) ) ENGINE = InnoDB DEFAULT CHARSET=utf8; @@ -2502,6 +2503,14 @@ CREATE TABLE `publication_users_edit_perm_rel` ( CONSTRAINT `FK_publication_users_edit_perm_rel_UserID` FOREIGN KEY (`UserID`) REFERENCES `users` (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET='utf8'; +CREATE TABLE Login_Summary_Statistics ( + Title VARCHAR(255), + Project VARCHAR(255), + Value INT, + QueryOrder INT, + PRIMARY KEY (Title, Project) +); + CREATE TABLE dataquery_queries ( QueryID int(10) unsigned NOT NULL AUTO_INCREMENT, Query JSON NOT NULL, @@ -2561,7 +2570,7 @@ CREATE TABLE dataquery_study_queries_rel ( -- to a saved query but is chosen by admins, a dashboard query -- shows the number of matching results on the LORIS dashboard. Name varchar(255) NOT NULL, - PinType enum('topquery', 'dashboard'), + PinType enum('topquery', 'dashboard', 'loginpage'), FOREIGN KEY (QueryID) REFERENCES dataquery_queries(QueryID), FOREIGN KEY (PinnedBy) REFERENCES users(ID), CONSTRAINT unique_pin UNIQUE (QueryID, PinType) diff --git a/SQL/9999-99-99-drop_tables.sql b/SQL/9999-99-99-drop_tables.sql index c40942b9815..e510d4386fe 100644 --- a/SQL/9999-99-99-drop_tables.sql +++ b/SQL/9999-99-99-drop_tables.sql @@ -5,6 +5,7 @@ DROP TABLE IF EXISTS dataquery_shared_queries_rel; DROP TABLE IF EXISTS dataquery_run_queries; DROP TABLE IF EXISTS dataquery_query_names; DROP TABLE IF EXISTS dataquery_queries; +DROP TABLE IF EXISTS `Login_Summary_Statistics`; -- 0000-00-05-ElectrophysiologyTables.sql DROP TABLE IF EXISTS `physiological_event_parameter_category_level`; diff --git a/SQL/Login_Summary_Statistics/01_Female Participant.sql b/SQL/Login_Summary_Statistics/01_Female Participant.sql new file mode 100644 index 00000000000..4caf299cce0 --- /dev/null +++ b/SQL/Login_Summary_Statistics/01_Female Participant.sql @@ -0,0 +1,9 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(DISTINCT c.CandID) AS count + FROM candidate c + JOIN Project ON c.RegistrationProjectID = Project.ProjectID +WHERE Project.showSummaryOnLogin = 1 + AND c.Sex = 'Female' + AND Entity_type = 'Human' +GROUP BY Project.Name WITH ROLLUP \ No newline at end of file diff --git a/SQL/Login_Summary_Statistics/02_Male Participant.sql b/SQL/Login_Summary_Statistics/02_Male Participant.sql new file mode 100644 index 00000000000..410524301de --- /dev/null +++ b/SQL/Login_Summary_Statistics/02_Male Participant.sql @@ -0,0 +1,9 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(DISTINCT c.CandID) AS count + FROM candidate c + JOIN Project ON c.RegistrationProjectID = Project.ProjectID +WHERE Project.showSummaryOnLogin = 1 + AND c.Sex = 'Male' + AND Entity_type = 'Human' +GROUP BY Project.Name WITH ROLLUP \ No newline at end of file diff --git a/SQL/Login_Summary_Statistics/03_Site.sql b/SQL/Login_Summary_Statistics/03_Site.sql new file mode 100644 index 00000000000..6216b41c69e --- /dev/null +++ b/SQL/Login_Summary_Statistics/03_Site.sql @@ -0,0 +1,8 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(DISTINCT psc.CenterID) AS count + FROM psc + JOIN session s ON s.CenterID = psc.CenterID + JOIN Project ON s.ProjectID = Project.ProjectID +WHERE Project.showSummaryOnLogin = 1 +GROUP BY Project.Name WITH ROLLUP \ No newline at end of file diff --git a/SQL/Login_Summary_Statistics/04_Visit.sql b/SQL/Login_Summary_Statistics/04_Visit.sql new file mode 100644 index 00000000000..a1fe4b0fd88 --- /dev/null +++ b/SQL/Login_Summary_Statistics/04_Visit.sql @@ -0,0 +1,7 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(CandID) AS count + FROM session s + JOIN Project ON s.ProjectID = Project.ProjectID +WHERE Project.showSummaryOnLogin = 1 +GROUP BY Project.Name WITH ROLLUP \ No newline at end of file diff --git a/SQL/Login_Summary_Statistics/05_Instrument.sql b/SQL/Login_Summary_Statistics/05_Instrument.sql new file mode 100644 index 00000000000..cc4fe0edc86 --- /dev/null +++ b/SQL/Login_Summary_Statistics/05_Instrument.sql @@ -0,0 +1,8 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(DISTINCT TestID) AS count + FROM flag f + JOIN session s ON s.ID = f.SessionID + JOIN Project ON s.ProjectID = Project.ProjectID +WHERE Project.showSummaryOnLogin = 1 +GROUP BY Project.Name WITH ROLLUP; \ No newline at end of file diff --git a/SQL/Login_Summary_Statistics/06_Scan.sql b/SQL/Login_Summary_Statistics/06_Scan.sql new file mode 100644 index 00000000000..423169f8c57 --- /dev/null +++ b/SQL/Login_Summary_Statistics/06_Scan.sql @@ -0,0 +1,8 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(FileID) AS count + FROM files f + JOIN session s ON s.ID = f.SessionID + JOIN Project ON s.ProjectID = Project.ProjectID +WHERE Project.showSummaryOnLogin = 1 +GROUP BY Project.Name WITH ROLLUP; \ No newline at end of file diff --git a/SQL/Login_Summary_Statistics/07_EEG Recording.sql b/SQL/Login_Summary_Statistics/07_EEG Recording.sql new file mode 100644 index 00000000000..f1ed1ccf961 --- /dev/null +++ b/SQL/Login_Summary_Statistics/07_EEG Recording.sql @@ -0,0 +1,17 @@ +SELECT + IFNULL(Project.Name, 'All Projects') as ProjectName, + COUNT(PhysiologicalFileID) AS count +FROM physiological_parameter_file ppf +LEFT JOIN physiological_file USING (PhysiologicalFileID) +LEFT JOIN physiological_output_type USING (PhysiologicalOutputTypeID) +LEFT JOIN Project ON ppf.ProjectID = Project.ProjectID +WHERE ( + ParameterTypeID = ( + SELECT ParameterTypeID + FROM parameter_type + WHERE Name = 'RecordingDuration' + ) +) +-- AND OutputTypeName = 'raw' +AND Project.showSummaryOnLogin = 1 +GROUP BY Project.Name WITH ROLLUP \ No newline at end of file diff --git a/SQL/New_patches/2024-12-18-Login_statistics.sql b/SQL/New_patches/2024-12-18-Login_statistics.sql new file mode 100644 index 00000000000..34d8b6a4e31 --- /dev/null +++ b/SQL/New_patches/2024-12-18-Login_statistics.sql @@ -0,0 +1,13 @@ +CREATE TABLE Login_Summary_Statistics ( + Title VARCHAR(255), + Project VARCHAR(255), + Value INT, + QueryOrder INT, + PRIMARY KEY (Title, Project) +); + +ALTER TABLE dataquery_study_queries_rel +MODIFY COLUMN PinType enum('topquery','dashboard', 'loginpage') DEFAULT NULL; + +ALTER TABLE Project +ADD COLUMN showSummaryOnLogin BOOLEAN DEFAULT TRUE; \ No newline at end of file diff --git a/modules/dataquery/jsx/welcome.adminquerymodal.tsx b/modules/dataquery/jsx/welcome.adminquerymodal.tsx index 52350901a29..ff45d1c4bb4 100644 --- a/modules/dataquery/jsx/welcome.adminquerymodal.tsx +++ b/modules/dataquery/jsx/welcome.adminquerymodal.tsx @@ -17,12 +17,18 @@ function AdminQueryModal(props: { QueryID: number, defaultName: string, closeModal: () => void, - onSubmit: (name: string, topQuery: boolean, dashboardQuery: boolean) + onSubmit: ( + name: string, + topQuery: boolean, + dashboardQuery: boolean, + loginQuery: boolean, + ) => void, }) { const [queryName, setQueryName] = useState(props.defaultName || ''); const [topQuery, setTopQuery] = useState(true); const [dashboardQuery, setDashboardQuery] = useState(true); + const [loginQuery, setLoginQuery] = useState(true); /** * Convert the onSubmit callback to a promise function of the format * expected by jsx/Modal. @@ -39,20 +45,20 @@ function AdminQueryModal(props: { reject(); return; } - if (!topQuery && !dashboardQuery) { + if (!topQuery && !dashboardQuery && !loginQuery) { swal.fire({ type: 'error', - text: 'Must pin as study query or pin to dashboard.', + text: 'Must pin as study query, to dashboard, or to the login page.', }); reject(); return; } - resolve([queryName.trim(), topQuery, dashboardQuery]); + resolve([queryName.trim(), topQuery, dashboardQuery, loginQuery]); }); if (props.onSubmit) { - sbmt = sbmt.then((val: [string, boolean, boolean]) => { - const [name, topq, dashq] = val; - props.onSubmit(name, topq, dashq); + sbmt = sbmt.then((val: [string, boolean, boolean, boolean]) => { + const [name, topq, dashq, loginq] = val; + props.onSubmit(name, topq, dashq, loginq); }); } return sbmt; @@ -87,6 +93,14 @@ function AdminQueryModal(props: { setDashboardQuery(value) } /> + + setLoginQuery(value) + } + /> ; diff --git a/modules/dataquery/jsx/welcome.tsx b/modules/dataquery/jsx/welcome.tsx index 40140048675..86103fcee4c 100644 --- a/modules/dataquery/jsx/welcome.tsx +++ b/modules/dataquery/jsx/welcome.tsx @@ -221,8 +221,7 @@ function QueryList(props: { const [fullQuery, setFullQuery] = useState(!props.defaultCollapsed); const [unpinAdminQuery, setUnpinAdminQuery] = useState(null); - const [adminPinAction, setAdminPinAction] - = useState<'top'|'dashboard'|'top,dashboard'>('top'); + const [adminPinAction, setAdminPinAction] = useState(''); useEffect(() => { const modules = new Set(); @@ -300,14 +299,17 @@ function QueryList(props: { setAdminModalID(null); setQueryName(null); - let param; - if (adminPinAction == 'top') { - param = 'adminname=' + encodeURIComponent(name); - } else if (adminPinAction == 'dashboard') { - param = 'dashboardname=' + encodeURIComponent(name); - } else if (adminPinAction == 'top,dashboard') { - param = 'adminname=' + encodeURIComponent(name) - + '&dashboardname=' + encodeURIComponent(name); + let param = ''; + if (adminPinAction.includes('top')) { + param += 'adminname=' + encodeURIComponent(name); + } + if (adminPinAction.includes('dashboard')) { + if (param != '') param += '&'; + param += 'dashboardname=' + encodeURIComponent(name); + } + if (adminPinAction.includes('login')) { + if (param != '') param += '&'; + param += 'loginname=' + encodeURIComponent(name); } fetch( '/dataquery/queries/' + id @@ -363,16 +365,15 @@ function QueryList(props: { />); const adminModal = adminModalID == null ? '' : { - if (topQ && dashboardQ) { - setAdminPinAction('top,dashboard'); - } else if (topQ) { - setAdminPinAction('top'); - } else if (dashboardQ) { - setAdminPinAction('dashboard'); - } else { + onSubmit={(name, topQ, dashboardQ, loginQ) => { + let boolList = ''; + if (topQ) boolList += 'top'; + if (dashboardQ) boolList += 'dashboard'; + if (loginQ) boolList += 'login'; + if (!topQ && !dashboardQ && !loginQ) { throw new Error('Modal promise should not have resolved'); } + setAdminPinAction(boolList); setQueryName(name); }} closeModal={() => setAdminModalID(null)} diff --git a/modules/dataquery/php/endpoints/queries/query.class.inc b/modules/dataquery/php/endpoints/queries/query.class.inc index 221617ad9a3..5c8aee43486 100644 --- a/modules/dataquery/php/endpoints/queries/query.class.inc +++ b/modules/dataquery/php/endpoints/queries/query.class.inc @@ -110,6 +110,20 @@ class Query extends \LORIS\Http\Endpoint } } + if (isset($params['loginname'])) { + if ($params['loginname'] == '') { + $query->removeAdminPinnedQuery('loginpage'); + $resp['loginpage'] = 'Unpinned query from login page'; + } else { + $query->setAdminPinnedQuery( + $user, + $params['loginname'], + 'loginpage' + ); + $resp['loginpage'] = 'Pinned query to login page'; + } + } + if (isset($params['name'])) { $query->setQueryName($user, $params['name']); $resp['Name'] = 'Named query'; diff --git a/modules/dataquery/php/provisioners/studyqueries.class.inc b/modules/dataquery/php/provisioners/studyqueries.class.inc index 84bda337b53..7eb7f5aa385 100644 --- a/modules/dataquery/php/provisioners/studyqueries.class.inc +++ b/modules/dataquery/php/provisioners/studyqueries.class.inc @@ -15,8 +15,8 @@ class StudyQueries extends \LORIS\Data\Provisioners\DBRowProvisioner * the pinned study queries. * * @param protected \LORIS\LorisInstance $loris - The LORIS object - * @param string $pintype - The pin type. Either - * "dashboard" or "study" + * @param string $pintype - The pin type. "dashboard" + * or "study" or "loginpage" */ function __construct(protected \LORIS\LorisInstance $loris, $pintype) { diff --git a/modules/dataquery/php/query.class.inc b/modules/dataquery/php/query.class.inc index 62d23192fd2..2a819a81349 100644 --- a/modules/dataquery/php/query.class.inc +++ b/modules/dataquery/php/query.class.inc @@ -366,7 +366,6 @@ class Query implements \LORIS\StudyEntities\AccessibleResource, 'Name' => $name, ] ); - } /** @@ -492,7 +491,7 @@ class Query implements \LORIS\StudyEntities\AccessibleResource, * @param string $name The name that the admin thinks should be * used for the query * @param string $type The type of querypin to remove. Either - * 'topquery' or 'dashboard' + * 'topquery' or 'dashboard' or 'loginpage' * * @return void */ @@ -517,7 +516,7 @@ class Query implements \LORIS\StudyEntities\AccessibleResource, * Remove this query from the list of queries pinned by an admin. * * @param string $type The type of querypin to remove. Either - * 'topquery' or 'dashboard' + * 'topquery' or 'dashboard' or 'loginpage' * * @return void */ diff --git a/modules/dataquery/static/schema.yml b/modules/dataquery/static/schema.yml index 4a20647f237..37052374732 100644 --- a/modules/dataquery/static/schema.yml +++ b/modules/dataquery/static/schema.yml @@ -109,6 +109,12 @@ paths: description: The admin name to pin the query to the dashboard as. If the empty string, will be unpinned. schema: type: string + - name: loginpagename + in: query + style: pipeDelimited + description: The admin name to pin the query to the login page as. If the empty string, will be unpinned. + schema: + type: string - name: name in: query style: pipeDelimited diff --git a/modules/dataquery/test/TestPlan.md b/modules/dataquery/test/TestPlan.md index bf919a98b8b..ba9d19aef72 100644 --- a/modules/dataquery/test/TestPlan.md +++ b/modules/dataquery/test/TestPlan.md @@ -21,8 +21,8 @@ 13. Click the `Pin` icon to pin some queries. 1. With and empty text in the `query name` text field, click the `Submit` button. 2. Assert that: the error message `Must provide a query name to pin query as.` is triggered. - 3. Unchecking all checkboxes (i.e. `Pin Study Query` and `Pin Dashboard Summary`). - 4. Assert that: clicking `Submit` triggers the error message `Must pin as study query or pin to dashboard.`. + 3. Unchecking all checkboxes (i.e. `Pin Study Query` and `Pin Dashboard Summary` and `Pin to Login Page`). + 4. Assert that: clicking `Submit` triggers the error message `Must pin as study query, to dashboard, or to the login page.`. 5. Check the `Pin Study Query` checkbox and click the submit button. 6. Assert that: the query is now pinned at the top of the page in the `Study Queries` panel. 7. Go to LORIS main page by clicking the `LORIS` name in the top-left corner. @@ -34,8 +34,8 @@ 13. Assert that: the query is displayed inside the right-side `Study Queries` panel. 14. Click the pinned query. 15. Assert that: the confirmation message `Query loaded` is displayed and query can immediately be executed. - 16. Try pinning a query with both `Pin Study Query` and `Pin Dashboard Summary` options. - 17. Assert that: both `Study Queries` in the dataquery module **AND** `Study Queries` in LORIS welcome page are displayed. + 16. Try pinning a query with `Pin Study Query`, `Pin Dashboard Summary` and `Pin to Login Page` options. + 17. Assert that: `Study Queries` in the dataquery module **AND** `Study Queries` in LORIS welcome page **AND** `Data in LORIS` on the LORIS Login Page are displayed. 14. Assert that: the query is now pinned at the top of the page, in `Study Queries` panel. 15. Go back to `LORIS main page`. 16. Assert that: `starred queries` are available in the right side `Starred Queries` panel. diff --git a/modules/login/README.md b/modules/login/README.md index 78d20c8396e..fff9a2c97ba 100644 --- a/modules/login/README.md +++ b/modules/login/README.md @@ -38,6 +38,15 @@ to provide references on the LORIS landing page. The configuration setting "StudyLogo" is the URL for an image to show above the login box. +If a query is pinned to the login page from the dataquery module, then +after the tools/update_login_summary_statistics.php tool is run, a summary of +all the pinned queries will be displayed on the login page including the count. +If a query returns more than one row, the name will be appended with an 's'. +Pinned queries must include the column Project in order to be displayed. +Queries can also be added to project/tools/Login_Summary_Statistics to +be added to the calculations for the statistics on the login page. + + ## Interactions with LORIS The login module redirects to the dashboard upon successful login. diff --git a/modules/login/assets/summaryStatisticsPhone.js b/modules/login/assets/summaryStatisticsPhone.js new file mode 100644 index 00000000000..15e0f73cb2e --- /dev/null +++ b/modules/login/assets/summaryStatisticsPhone.js @@ -0,0 +1,199 @@ +import * as React from 'react'; +const SvgComponent = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgComponent; diff --git a/modules/login/css/login.css b/modules/login/css/login.css index db93807a816..62156d53003 100644 --- a/modules/login/css/login.css +++ b/modules/login/css/login.css @@ -4,3 +4,37 @@ cursor: pointer; color: grey; } + +.study-description { + display: flex; + flex-direction: row; +} + +.stats-phone { + position: relative; + width: 300px; + margin: auto; +} + +.stats-screen { + position: absolute; + left: 8%; + top: 10%; + width: 55%; + height: 90%; +} + +.stats-copy { + position: absolute; + bottom: 20px; +} + +.stats-copy:hover { + opacity: 1; +} + +@media screen and (max-width: 768px) { + .study-description { + flex-direction: column-reverse; + } +} \ No newline at end of file diff --git a/modules/login/jsx/loginIndex.js b/modules/login/jsx/loginIndex.js index b0f6c254e0e..26b4bce3e03 100644 --- a/modules/login/jsx/loginIndex.js +++ b/modules/login/jsx/loginIndex.js @@ -14,6 +14,7 @@ import { PasswordElement, ButtonElement, } from 'jsx/Form'; +import SummaryStatistics from './summaryStatistics'; /** * Login form. @@ -53,6 +54,7 @@ class Login extends Component { requestAccount: null, expiredPassword: null, }, + summaryStatistics: null, isLoaded: false, }; // Bind component instance to custom methods @@ -67,8 +69,7 @@ class Login extends Component { * Executes after component mounts. */ componentDidMount() { - this.fetchData() - .then(() => this.setState({isLoaded: true})); + this.fetchData(); } /** @@ -91,8 +92,18 @@ class Login extends Component { // request account setup. state.component.requestAccount = json.requestAccount; state.oidc = json.oidc; - state.isLoaded = true; this.setState(state); + }).then(() => { + fetch(window.location.origin + '/login/summary_statistics', { + method: 'GET', + }) + .then((resp) => resp.json()) + .then((json) => { + this.setState({ + summaryStatistics: json, + isLoaded: true, + }); + }); }).catch((error) => { this.setState({error: true}); console.error(error); @@ -274,9 +285,19 @@ class Login extends Component { collapsing={false} bold={true} > - {study} +
+ { + this.state.summaryStatistics + && + } + {study} +
+
+
); diff --git a/modules/login/jsx/summaryStatistics.js b/modules/login/jsx/summaryStatistics.js new file mode 100644 index 00000000000..15bed5b3109 --- /dev/null +++ b/modules/login/jsx/summaryStatistics.js @@ -0,0 +1,73 @@ +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; +import {SelectElement} from 'jsx/Form'; +import SummaryStatisticsPhone from '../assets/summaryStatisticsPhone.js'; + +/** + * Login Summary Statistics + * + * @author Saagar Arya + * @version 1.0.0 + */ +const SummaryStatistics = ({data}) => { + const [selectedProject, setSelectedProject] = useState( + data.projects.includes('All Projects') + ? data.projects.indexOf('All Projects') + : '0' + ); + + return ( +
+ + {/* Phone screen */} +
+ {/* Project Selector */} + setSelectedProject(value)} + emptyOption={false} + /> +
Data in LORIS:
+ {/* Statistics */} + {data.statistics.map((statistic, index) => { + if ( + statistic.Project === data.projects[selectedProject] && + statistic.Value > 0 + ) { + return ( +
+ + {statistic.Value.toLocaleString('fr-CA')}{' '} + + {statistic.Title} + {statistic.Value !== 1 ? 's' : ''} +
+ ); + } + return null; + })} + {/* Copy to clipboard */} +
+ navigator.clipboard.writeText(data.csv)} + /> +
+
+
+ ); +}; + +SummaryStatistics.propTypes = { + data: PropTypes.object.isRequired, +}; + +export default SummaryStatistics; diff --git a/modules/login/php/summary_statistics.class.inc b/modules/login/php/summary_statistics.class.inc new file mode 100644 index 00000000000..2028b8f7a03 --- /dev/null +++ b/modules/login/php/summary_statistics.class.inc @@ -0,0 +1,106 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Summary_Statistics extends \NDB_Page implements ETagCalculator +{ + /** + * This function will return a json object for login summary statistics module. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + // Ensure GET or POST request. + switch ($request->getMethod()) { + case 'GET': + $DB = $this->loris->getDatabaseConnection(); + $response = $DB->pselect( + "SELECT * FROM Login_Summary_Statistics + WHERE Value > 0 + ORDER BY QueryOrder ASC", + [], + ); + if (count($response) == 0) { + return new \LORIS\Http\Response\JsonResponse(null); + } + $projects = $DB->pselectCol( + "SELECT DISTINCT Project FROM Login_Summary_Statistics", + [] + ); + + // csv version for exporting to clipboard + $reusable_response = iterator_to_array($response); + $csv = "Project,Statistic,Value\n"; + foreach ($reusable_response as $row) { + $csv .= $row['Project'].",".$row['Title'].",".$row['Value'] . "\n"; + } + + return new \LORIS\Http\Response\JsonResponse( + [ + 'statistics' => $reusable_response, + 'projects' => $projects, + 'csv' => $csv, + ] + ); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + /** + * An ETagCalculator provides the ability to calculate an ETag for + * an incoming HTTP request. + * + * @param ServerRequestInterface $request The incoming PSR7 request. + * + * @return string The value to use for the ETag header. + */ + public function ETag(ServerRequestInterface $request): string + { + return md5(json_encode((string) $this->handle($request)->getBody())); + } + + /** + * Return an array of valid HTTP methods for this endpoint + * + * @return string[] Valid versions + */ + protected function allowedMethods(): array + { + return [ + 'GET', + ]; + } + + /** + * Returns true if the user has permission to access + * the Login module + * + * @param \User $user The user whose access is being checked + * + * @return bool true if user has permission + */ + function _hasAccess(\User $user) : bool + { + return true; + } + +} diff --git a/raisinbread/RB_files/RB_Project.sql b/raisinbread/RB_files/RB_Project.sql index e281498aa23..b78da343472 100644 --- a/raisinbread/RB_files/RB_Project.sql +++ b/raisinbread/RB_files/RB_Project.sql @@ -1,9 +1,9 @@ SET FOREIGN_KEY_CHECKS=0; TRUNCATE TABLE `Project`; LOCK TABLES `Project` WRITE; -INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`) VALUES (1,'Pumpernickel','PUMP',200); -INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`) VALUES (2,'Rye','RYE',150); -INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`) VALUES (3,'Challah','CHA',250); -INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`) VALUES (4,'DCP','DCP',0); +INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`, `showSummaryOnLogin`) VALUES (1,'Pumpernickel','PUMP',200,1); +INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`, `showSummaryOnLogin`) VALUES (2,'Rye','RYE',150,1); +INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`, `showSummaryOnLogin`) VALUES (3,'Challah','CHA',250,1); +INSERT INTO `Project` (`ProjectID`, `Name`, `Alias`, `recruitmentTarget`, `showSummaryOnLogin`) VALUES (4,'DCP','DCP',0,0); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; diff --git a/tools/update_login_summary_statistics.php b/tools/update_login_summary_statistics.php new file mode 100644 index 00000000000..031f7f40572 --- /dev/null +++ b/tools/update_login_summary_statistics.php @@ -0,0 +1,167 @@ + + * @license Loris license + * @link https://www.github.com/aces/Loris/ + */ + +require_once "generic_includes.php"; +require_once __DIR__ + . "/../modules/dataquery/php/query.class.inc"; +require_once __DIR__ + . "/../modules/dataquery/php/endpoints/queries/query/run.class.inc"; +require_once __DIR__ + . "/../modules/dataquery/php/queryrun.class.inc"; +require_once __DIR__ + . "/../modules/dataquery/php/querydataprovisioner.class.inc"; +require_once __DIR__ + . "/../modules/dataquery/php/querydataprovisioner.class.inc"; +require_once __DIR__ + . "/../modules/dataquery/php/provisioners/studyqueries.class.inc"; + +// Get list of projects +$projects = $DB->pselectCol( + "SELECT Name FROM Project + WHERE showSummaryOnLogin='1'", + [] +); + +$pinnedqueries = $DB->pselectWithIndexKey( + "SELECT dq.QueryID, Query, dsq.Name + FROM dataquery_queries dq + LEFT JOIN dataquery_study_queries_rel dsq ON + (dq.QueryID=dsq.QueryID) + WHERE dsq.PinType=:pintype + GROUP BY QueryID, Query, dsq.Name + ORDER BY QueryID", + ['pintype' => 'loginpage'], + 'QueryID' +); +$user = (new \User())->factory('admin'); +$data = ['All Projects' => []]; +$DB->run("DELETE FROM Login_Summary_Statistics", []); +foreach ($projects as $project) { + $data[$project] = []; +} +$order = 0; +$queryToOrder = []; +foreach ($pinnedqueries as $pin) { + $queryInProjects = []; + $queryID = $pin['QueryID']; + $queryName = $pin['Name']; + $queryToOrder[$queryName] = $order; + $order++; + + // Initialize the counts + $data['All Projects'][$queryName] = -1; + foreach ($projects as $project) { + $data[$project][$queryName] = 0; + } + + // Get the data of the query + $runner = new \LORIS\dataquery\endpoints\Queries\query\Run( + $lorisInstance, + $queryID + ); + + $response = $runner->runQuery($user, $queryID)->getBody(); + $result = []; + while ($response->eof() == false) { + $next = $response->read(1024); + if (!empty($next)) { + $result[] = $next; + $inAProject = false; + foreach ($projects as $project) { + if (strpos($next, $project) !== false) { + $data[$project][$queryName] += 1; + $inAProject = true; + $queryInProjects[$project] = $project; + } + } + // Only add to All Projects if the query is in at least one project + if ($inAProject) { + $data['All Projects'][$queryName] += 1; + } + } + } + // If the query only has data in one project, remove from the All Projects tab + if (count($queryInProjects) == 1) { + unset($data['All Projects'][$queryName]); + } else if (count($queryInProjects) == 0) { + print_r( + "--- ERROR: Query $queryName has no data in any project. Please delete " + . "this query, and re-save with the Project column selected.\n" + ); + } + $order++; +} + +// Try to get SQL queries from ../project/tools/Login_Summary_Statistics +// Otherwise get them from SQL/Login_Summary_Statistics +// Filename should be the desired name of the query, with a "#_" infront +// Example: 01_Site.sql where 01 means it is the first query +$folder = __DIR__ . "/../project/tools/Login_Summary_Statistics/"; +if (!is_dir($folder)) { + print_r( + "Folder $folder does not exist, " + . "using ../SQL/Login_Summary_Statistics instead \n" + ); + $folder = __DIR__ . "/../SQL/Login_Summary_Statistics/"; +} + +$files = scandir($folder); +foreach ($files as $file) { + if (is_file($folder . $file)) { + print_r("Reading SQL File $file\n"); + $result = $DB->pselect(file_get_contents($folder . $file), []); + $queryName = pathinfo($file, PATHINFO_FILENAME); + $queryOrder = explode("_", $queryName)[0]; + $queryName = explode("_", $queryName)[1]; + $queryToOrder[$queryName] = $queryOrder; + foreach ($result as $row) { + $data[$row['ProjectName']][$queryName] = $row['count']; + } + } +} + +print_r($data); +foreach ($data as $project => $values) { + foreach ($values as $title => $value) { + $DB->insertOnDuplicateUpdate( + 'Login_Summary_Statistics', + [ + 'Project' => $project, + 'Title' => $title, + 'value' => $value, + 'QueryOrder' => $queryToOrder[$title] + ] + ); + } +} \ No newline at end of file