From d41412d60aa42774d088582a3ed78374f74dbfc4 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 11:17:13 -0400 Subject: [PATCH 01/19] [policies] Add infrastructure to track user decisions to policies --- SQL/0000-00-00-schema.sql | 32 ++++ .../2025-05-22-Introduce-Policy-Decisions.sql | 27 ++++ htdocs/PolicyDecision.php | 36 +++++ jsx/PolicyButton.js | 101 ++++++++++++ php/libraries/Utility.class.inc | 145 ++++++++++++++++++ raisinbread/RB_files/RB_policies.sql | 7 + .../RB_files/RB_user_policy_decision.sql | 5 + .../UserPageDecorationMiddleware.php | 2 + webpack.config.ts | 1 + 9 files changed, 356 insertions(+) create mode 100644 SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql create mode 100644 htdocs/PolicyDecision.php create mode 100644 jsx/PolicyButton.js create mode 100644 raisinbread/RB_files/RB_policies.sql create mode 100644 raisinbread/RB_files/RB_user_policy_decision.sql diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index 269f9356ca2..a7dff13780a 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -1421,6 +1421,38 @@ CREATE TABLE `user_account_history` ( PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- ******************************** +-- tables for policies +-- ******************************** + +CREATE TABLE policies ( + PolicyID INT AUTO_INCREMENT PRIMARY KEY, + Name VARCHAR(255) NOT NULL, + Version VARCHAR(50) NOT NULL, + ModuleID INT NOT NULL, + PolicyRenewalTime INT DEFAULT 7, + PolicyRenewalTimeUnit enum('D','Y','M','H') DEFAULT 'D', + Content TEXT NULL, + SwalTitle VARCHAR(255) DEFAULT 'Terms Of Use', + HeaderButton enum('Y','N') DEFAULT 'Y', + HeaderButtonText VARCHAR(255) DEFAULT 'Terms Of Use', + Active enum('Y','N') DEFAULT 'Y', + AcceptButtonText VARCHAR(255) DEFAULT '', + DeclineButtonText VARCHAR(255) DEFAULT '', + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, + UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE user_policy_decision ( + ID INT AUTO_INCREMENT PRIMARY KEY, + UserID INT NOT NULL, + PolicyID INT NOT NULL, + Decision enum('Accepted','Declined') NOT NULL, + DecisionDate DATETIME DEFAULT CURRENT_TIMESTAMP +); + + -- ******************************** -- user_login_history tables -- ******************************** diff --git a/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql b/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql new file mode 100644 index 00000000000..d2057e42b77 --- /dev/null +++ b/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql @@ -0,0 +1,27 @@ +CREATE TABLE policies ( + PolicyID INT AUTO_INCREMENT PRIMARY KEY, + Name VARCHAR(255) NOT NULL, + Version VARCHAR(50) NOT NULL, + ModuleID INT NOT NULL, -- Show in the header for a module + PolicyRenewalTime INT DEFAULT 7, -- Number of days before the policy is renewed + PolicyRenewalTimeUnit enum('D','Y','M','H') DEFAULT 'D', -- Unit of the renewal time + Content TEXT NULL, + SwalTitle VARCHAR(255) DEFAULT 'Terms Of Use', + HeaderButton enum('Y','N') DEFAULT 'Y', + HeaderButtonText VARCHAR(255) DEFAULT 'Terms Of Use', + Active enum('Y','N') DEFAULT 'Y', + AcceptButton enum('Y','N') DEFAULT 'Y', + AcceptButtonText VARCHAR(255) DEFAULT 'Accept', + DeclineButton enum('Y','N') DEFAULT 'Y', + DeclineButtonText VARCHAR(255) DEFAULT 'Decline', + CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, + UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE user_policy_decision ( + ID INT AUTO_INCREMENT PRIMARY KEY, + UserID INT NOT NULL, + PolicyID INT NOT NULL, + Decision enum('Accepted','Declined') NOT NULL, + DecisionDate DATETIME DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/htdocs/PolicyDecision.php b/htdocs/PolicyDecision.php new file mode 100644 index 00000000000..30c0f90e97f --- /dev/null +++ b/htdocs/PolicyDecision.php @@ -0,0 +1,36 @@ + + * @license Loris license + * @link https://github.com/aces/Loris + */ + + +set_include_path( + get_include_path() . ":" . + __DIR__ . "/../project/libraries:" . + __DIR__ . "/../php/libraries" +); + +require_once __DIR__ . "/../vendor/autoload.php"; +// Ensures the user is logged in, and parses the config file. +require_once "NDB_Client.class.inc"; +$client = new NDB_Client(); +$anonymous = ($client->initialize() === false); +if ($anonymous) { + // If the user is not logged in, we cannot save the policy decision. + exit; +} + +\Utility::saveUserPolicyDecision( + $_REQUEST['ModuleName'], + $_REQUEST['PolicyName'], + $_REQUEST['decision'] +); \ No newline at end of file diff --git a/jsx/PolicyButton.js b/jsx/PolicyButton.js new file mode 100644 index 00000000000..2cf51ba4e8f --- /dev/null +++ b/jsx/PolicyButton.js @@ -0,0 +1,101 @@ +/** + * This file contains React component for the Policy Button. + * + * @author Saagar Arya + * @version 2.0.0 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import Swal from 'sweetalert2'; + +/** + * PolicyButton Component + * @param {object} props - The component props. + * @param {object} props.onClickPolicy - The policy object containing title + * and content that should appear when the button is pressed. + * @param {object} [props.buttonStyle] - Optional style object for the button. + * @param {object} [props.popUpPolicy] - Optional policy object for pop-up + * policy that needs renewal. + * @param {string} [props.buttonText] - Optional text for the button. + * @param {boolean} [props.anon] - Optional flag to indicate if the user is anonymous. + * @param {function} [props.callback] - Optional callback function to execute after the policy decision. + */ +const PolicyButton = ({ + onClickPolicy, + popUpPolicy, + buttonStyle, + buttonText, + anon, + callback, +}) => { + if (popUpPolicy && popUpPolicy.needsRenewal) { + fireSwal(popUpPolicy); + } + if (onClickPolicy) { + return { + fireSwal(onClickPolicy, anon, callback); + }} + > + {buttonText || onClickPolicy.HeaderButtonText} + ; + } +}; + +const fireSwal = (policy, anon, callback) => { + Swal.fire({ + title: policy.SwalTitle, + html: policy.Content, + confirmButtonText: policy.AcceptButtonText, + cancelButtonText: policy.DeclineButtonText, + showCancelButton: policy.DeclineButtonText, + allowOutsideClick: false, + }).then((decision) => { + if (callback) { + callback(decision); + } + if (!anon) { + $.ajax(loris.BaseURL+'/PolicyDecision.php', { + method: 'POST', + data: { + ...policy, + decision: decision.value == true ? 'Accepted' : 'Declined', + }, + dataType: 'json', + }); + if (decision.value != true) { + window.location.href = loris.BaseURL; + } + } + }); +}; + +PolicyButton.propTypes = { + onClickPolicy: PropTypes.shape({ + SwalTitle: PropTypes.string.isRequired, + Content: PropTypes.string.isRequired, + AcceptButtonText: PropTypes.string.isRequired, + DeclineButtonText: PropTypes.string.isRequired, + }).isRequired, + onClickPolicy: PropTypes.object.isRequired, + popUpPolicy: PropTypes.shape({ + needsRenewal: PropTypes.bool, + SwalTitle: PropTypes.string, + Content: PropTypes.string, + AcceptButtonText: PropTypes.string, + DeclineButtonText: PropTypes.string, + }), + buttonStyle: PropTypes.object, + buttonText: PropTypes.string, + anon: PropTypes.bool, + callback: PropTypes.func, +}; + +window.PolicyButton = PolicyButton; + +export {PolicyButton, fireSwal}; diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index 513de8c7da8..938426076e8 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -758,6 +758,151 @@ class Utility ); } + /** + * Get latest Policy for the given Modile Name and Policy Name + * This is the CCNA Version for now, the LORIS Core version will + * likely return more fields. + * + * @param string $moduleName The module name + * @param string $policyName The policy name + * + * @return array The policy details + * @throws \DatabaseException + */ + static function getPolicy(string $moduleName, string $policyName ='', bool $headerButton = false) { + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + // If no policy name is given, then we return the latest policy for the module + if (!empty($policyName)) { + $policyName = 'AND p.Name = '. $DB->quote($policyName); + } + // If we want just a policy that goes in the header, then we need HeaderButton = 'Y' + if ($headerButton == true) { + $headerWhere = "('Y')"; + } else { + $headerWhere = "('Y', 'N')"; + } + return $DB->pselectRow( + "SELECT + p.Name as PolicyName, + Version, + Content, + SwalTitle, + HeaderButtonText, + AcceptButtonText, + DeclineButtonText, + m.Name as ModuleName, + PolicyID, + PolicyRenewalTime, + PolicyRenewalTimeUnit + FROM policies p + JOIN modules m ON m.ID = p.ModuleID + WHERE m.Name = :moduleName + $policyName + AND p.Active = 'Y' + AND p.HeaderButton IN $headerWhere + AND Version = ( + SELECT MAX(Version) + FROM policies + WHERE ModuleID = m.ID + AND Name = p.Name + AND p.HeaderButton IN $headerWhere + ) + ", + [ + 'moduleName' => $moduleName + ] + ); + } + + /** + * Get a User's latest decision for a Policy, and whether or not they need to renew. + * + * @param string $moduleName The module name + * @param string $policyName The policy name + * + * @return array The decision details + * @throws \DatabaseException + */ + static function getLatestPolicyDecision(string $moduleName, string $policyName ='', int $userID = null) { + $client = new NDB_Client(); + $anonymous = ($client->initialize() === false); + if ($anonymous) { + // If the user is anonymous, we cannot get their decision + return []; + } + + $userID = $userID ?? User::singleton()->getID(); + + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + + $policyInfo = self::getPolicy($moduleName, $policyName, false) ?? []; + if (empty($policyInfo)) { + return []; + } + $decision = $DB->pselectRow( + "SELECT Decision, DecisionDate + FROM user_policy_decision + WHERE + UserID = :uid + AND PolicyID=:policyID + AND DecisionDate = ( + SELECT MAX(DecisionDate) + FROM user_policy_decision + WHERE + UserID = :uid + AND PolicyID=:policyID + ) + ", + [ + 'uid' => $userID, + 'policyID'=> $policyInfo['PolicyID'] + ] + ) ?? []; + error_log(print_r($decision, true)); + $needsRenewal = true; + if ($decision && $policyInfo['PolicyRenewalTime'] > 0) { + $decisionDate = new \DateTime($decision['DecisionDate']); + $interval = new \DateInterval("P{$policyInfo['PolicyRenewalTime']}{$policyInfo['PolicyRenewalTimeUnit']}"); + $renewalDate = $decisionDate->add($interval); + error_log("Renewal Date: " . $renewalDate->format('Y-m-d H:i:s')); + if ($renewalDate > new \DateTime()) { + $needsRenewal = false; + } + } + return [ + ...$policyInfo, + 'needsRenewal' => $needsRenewal, + ]; + } + + /** + * Save a User's Policy decision + * + * @param string $ModuleName The Module Name + * @param string $PolicyName The Policy Name + * @param string $decision The decision made by the current user + * + * @return void + * @throws \DatabaseException + * + */ + static function saveUserPolicyDecision(string $ModuleName, string $PolicyName, string $decision) { + $user = User::singleton(); + $db = \NDB_Factory::singleton()->database(); + + $policy = self::getPolicy($ModuleName, $PolicyName, false); + $db->insert( + 'user_policy_decision', + [ + 'UserID' => $user->getId(), + 'PolicyID' => $policy['PolicyID'], + 'Decision' => $decision, + ] + ); + } + /** * Get all the instruments which currently exist for a given visit label * in the database. diff --git a/raisinbread/RB_files/RB_policies.sql b/raisinbread/RB_files/RB_policies.sql new file mode 100644 index 00000000000..ac752ee4575 --- /dev/null +++ b/raisinbread/RB_files/RB_policies.sql @@ -0,0 +1,7 @@ +SET FOREIGN_KEY_CHECKS=0; +TRUNCATE TABLE `policies`; +LOCK TABLES `policies` WRITE; +INSERT INTO `policies` (`PolicyID`, `Name`, `Version`, `ModuleID`, `PolicyRenewalTime`, `PolicyRenewalTimeUnit`, `Content`, `SwalTitle`, `HeaderButton`, `HeaderButtonText`, `Active`, `AcceptButtonText`, `DeclineButtonText`, `CreatedAt`, `UpdatedAt`) VALUES (1,'dataquery_example','v1',49,7,'D','By using this Data Query Tool, you acknowledge that you know it is in beta and may not work as expected. You also agree to use it responsibly and not to misuse the data.','Terms Of Use','Y','Terms Of Use','Y','Yes, I accept','Decline','2025-05-28 10:54:21','2025-05-28 10:54:21'); +INSERT INTO `policies` (`PolicyID`, `Name`, `Version`, `ModuleID`, `PolicyRenewalTime`, `PolicyRenewalTimeUnit`, `Content`, `SwalTitle`, `HeaderButton`, `HeaderButtonText`, `Active`, `AcceptButtonText`, `DeclineButtonText`, `CreatedAt`, `UpdatedAt`) VALUES (2,'login_example','v1',28,7,'D','By using this LORIS instance you acknowledge that you know it is filled with test data, and not real user data.','Terms Of Use','Y','Terms Of Use','Y','Accept','','2025-05-28 10:54:23','2025-05-28 10:54:23'); +UNLOCK TABLES; +SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_user_policy_decision.sql b/raisinbread/RB_files/RB_user_policy_decision.sql new file mode 100644 index 00000000000..1eb9aa14838 --- /dev/null +++ b/raisinbread/RB_files/RB_user_policy_decision.sql @@ -0,0 +1,5 @@ +SET FOREIGN_KEY_CHECKS=0; +TRUNCATE TABLE `user_policy_decision`; +LOCK TABLES `user_policy_decision` WRITE; +UNLOCK TABLES; +SET FOREIGN_KEY_CHECKS=1; diff --git a/src/Middleware/UserPageDecorationMiddleware.php b/src/Middleware/UserPageDecorationMiddleware.php index f562d8757be..3effdc069c0 100644 --- a/src/Middleware/UserPageDecorationMiddleware.php +++ b/src/Middleware/UserPageDecorationMiddleware.php @@ -272,6 +272,8 @@ function ($a, $b) { if ($page instanceof \NDB_Page) { $tpl_data['breadcrumbs'] = $page->getBreadcrumbs(); + $tpl_data['header_policy'] = $page->getHeaderPolicy(); + $tpl_data['pop_up_policy'] = $page->getPolicyThatNeedsRenewal(); } // Assign the console template variable as the very, very last thing. diff --git a/webpack.config.ts b/webpack.config.ts index cb71dc58b4d..ce349abf7b9 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -347,6 +347,7 @@ configs.push({ StaticDataTable: './jsx/StaticDataTable.js', MultiSelectDropdown: './jsx/MultiSelectDropdown.js', Breadcrumbs: './jsx/Breadcrumbs.js', + PolicyButton: './jsx/PolicyButton.js', CSSGrid: './jsx/CSSGrid.js', Help: './jsx/Help.js', ...getModulesEntries(), From 4cf2f1c92af95ca2a6f94c46eaa964702a0b2e3d Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 13:16:55 -0400 Subject: [PATCH 02/19] Add rest of the files --- SQL/9999-99-99-drop_tables.sql | 2 ++ jsx/PolicyButton.js | 4 +-- modules/login/jsx/loginIndex.js | 13 ++++++++ modules/login/jsx/requestAccount.js | 35 +++++++++++++++++++++- modules/login/php/authentication.class.inc | 1 + php/libraries/NDB_Page.class.inc | 22 ++++++++++++++ smarty/templates/main.tpl | 13 ++++++++ 7 files changed, 87 insertions(+), 3 deletions(-) diff --git a/SQL/9999-99-99-drop_tables.sql b/SQL/9999-99-99-drop_tables.sql index e510d4386fe..f74c20120e1 100644 --- a/SQL/9999-99-99-drop_tables.sql +++ b/SQL/9999-99-99-drop_tables.sql @@ -127,6 +127,8 @@ DROP TABLE IF EXISTS `server_processes`; DROP TABLE IF EXISTS `StatisticsTabs`; DROP TABLE IF EXISTS `user_login_history`; DROP TABLE IF EXISTS `user_account_history`; +DROP TABLE IF EXISTS `policies`; +DROP TABLE IF EXISTS `user_policy_decision`; DROP TABLE IF EXISTS `data_integrity_flag`; DROP TABLE IF EXISTS `certification_training_quiz_answers`; DROP TABLE IF EXISTS `certification_training_quiz_questions`; diff --git a/jsx/PolicyButton.js b/jsx/PolicyButton.js index 2cf51ba4e8f..b01269462cc 100644 --- a/jsx/PolicyButton.js +++ b/jsx/PolicyButton.js @@ -26,8 +26,8 @@ const PolicyButton = ({ popUpPolicy, buttonStyle, buttonText, - anon, - callback, + anon=false, + callback=() => {}, }) => { if (popUpPolicy && popUpPolicy.needsRenewal) { fireSwal(popUpPolicy); diff --git a/modules/login/jsx/loginIndex.js b/modules/login/jsx/loginIndex.js index 26b4bce3e03..fff7886ccd6 100644 --- a/modules/login/jsx/loginIndex.js +++ b/modules/login/jsx/loginIndex.js @@ -14,6 +14,7 @@ import { PasswordElement, ButtonElement, } from 'jsx/Form'; +import {PolicyButton} from 'jsx/PolicyButton'; import SummaryStatistics from './summaryStatistics'; /** @@ -210,6 +211,16 @@ class Login extends Component { class={'col-xs-12 col-sm-12 col-md-12 text-danger'} /> ) : null; + const policy = this.state.component.requestAccount + ? <> +
+ + : null; const oidc = this.state.oidc ? this.getOIDCLinks() : ''; const login = (
@@ -257,6 +268,8 @@ class Login extends Component {
this.setMode('request')} style={{cursor: 'pointer'}}>Request Account +
+ {policy}
{oidc}
diff --git a/modules/login/jsx/requestAccount.js b/modules/login/jsx/requestAccount.js index 501d5c4e6c0..200a878be0b 100644 --- a/modules/login/jsx/requestAccount.js +++ b/modules/login/jsx/requestAccount.js @@ -11,6 +11,7 @@ import { CheckboxElement, ButtonElement, } from 'jsx/Form'; +import {PolicyButton} from '../../../jsx/PolicyButton'; /** * Request account form. @@ -45,8 +46,10 @@ class RequestAccount extends Component { ? this.props.data.captcha : '', error: '', + viewedPolicy: false, }, request: false, + policy: this.props.data.policy || null, }; this.setForm = this.setForm.bind(this); this.handleSubmit = this.handleSubmit.bind(this); @@ -75,7 +78,16 @@ class RequestAccount extends Component { */ handleSubmit(e) { e.preventDefault(); - + if (this.props.data.policy && !this.state.form.viewedPolicy) { + let title = this.props.data.policy.SwalTitle; + swal.fire({ + title: title + ' not accepted', + text: 'You must accept the ' + title + ' before requesting an account.', + icon: 'error', + }); + e.stopPropagation(); + return; + } const state = JSON.parse(JSON.stringify(this.state)); fetch( window.location.origin + '/login/Signup', { @@ -156,6 +168,26 @@ class RequestAccount extends Component {
) : null; + const policy = this.state.policy ? ( + <> + { + this.setState({ + form: { + ...this.state.form, + viewedPolicy: true, + }, + }); + }} + /> +

+ + ) : null; const request = !this.state.request ? (
+ {policy} {captcha} getHeaderPolicy(); $values['requestAccount'] = $requestAccountData; if ($this->loris->hasModule('oidc')) { diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index 7622e9a433c..b7999b2d191 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -861,6 +861,7 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface $baseurl . '/js/jquery.fileupload.js', $baseurl . '/bootstrap/js/bootstrap.min.js', $baseurl . '/js/components/Breadcrumbs.js', + $baseurl . '/js/components/HeaderPolicyButton.js', $baseurl . "/js/util/queryString.js", $baseurl . '/js/components/Help.js', ]; @@ -936,6 +937,27 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface ); } + /** + * Returns the latest Policy for this page with a HeaderButton + */ + public function getHeaderPolicy(): array { + return \Utility::getPolicy( + $this->Module->getName(), + '', + true + ) ?? []; + } + + /** + * Returns the latest Policy for this page that the user needs to renew + */ + public function getPolicyThatNeedsRenewal(): array { + return \Utility::getLatestPolicyDecision( + $this->Module->getName(), + '', + ); + } + /** * Converts the results of this form to a JSON format to be retrieved * with ?format=json diff --git a/smarty/templates/main.tpl b/smarty/templates/main.tpl index ec0c8574873..7f8a75e5361 100644 --- a/smarty/templates/main.tpl +++ b/smarty/templates/main.tpl @@ -73,6 +73,16 @@ toggleIcon.classList.add('glyphicon-chevron-down'); } }); + + headerPolicyRoot = ReactDOM.createRoot( + document.getElementById("headerPolicyButton") + ); + headerPolicyRoot.render( + React.createElement(PolicyButton, { + onClickPolicy: {$header_policy|json_encode}, + popUpPolicy: {$pop_up_policy|json_encode}, + }) + ); }); @@ -143,6 +153,9 @@ {/if} + {if $header_policy} + + {/if} From 83777163713c5cc7336e1b42c3fbbbc90eae4df0 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 13:31:42 -0400 Subject: [PATCH 03/19] Fix for lint --- php/libraries/NDB_Page.class.inc | 10 ++++- php/libraries/Utility.class.inc | 66 ++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index b7999b2d191..0e919804a7e 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -939,8 +939,11 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface /** * Returns the latest Policy for this page with a HeaderButton + * + * @return array */ - public function getHeaderPolicy(): array { + public function getHeaderPolicy(): array + { return \Utility::getPolicy( $this->Module->getName(), '', @@ -950,8 +953,11 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface /** * Returns the latest Policy for this page that the user needs to renew + * + * @return array */ - public function getPolicyThatNeedsRenewal(): array { + public function getPolicyThatNeedsRenewal(): array + { return \Utility::getLatestPolicyDecision( $this->Module->getName(), '', diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index 938426076e8..ea554365926 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -762,14 +762,21 @@ class Utility * Get latest Policy for the given Modile Name and Policy Name * This is the CCNA Version for now, the LORIS Core version will * likely return more fields. - * - * @param string $moduleName The module name - * @param string $policyName The policy name - * + * + * @param string $moduleName The module name + * @param string $policyName The policy name + * @param bool $headerButton If true, only return policies that are + * have HeaderButton = 'Y' + * * @return array The policy details * @throws \DatabaseException */ - static function getPolicy(string $moduleName, string $policyName ='', bool $headerButton = false) { + static function getPolicy( + string $moduleName, + string $policyName ='', + bool $headerButton = false + ) + { $factory = \NDB_Factory::singleton(); $DB = $factory->database(); // If no policy name is given, then we return the latest policy for the module @@ -816,15 +823,20 @@ class Utility } /** - * Get a User's latest decision for a Policy, and whether or not they need to renew. - * - * @param string $moduleName The module name - * @param string $policyName The policy name - * + * Get a User's latest policy decision, and whether or not they need to renew. + * + * @param string $moduleName The module name + * @param string $policyName The policy name + * @param int|null $userID The user ID, or current user if null + * * @return array The decision details * @throws \DatabaseException */ - static function getLatestPolicyDecision(string $moduleName, string $policyName ='', int $userID = null) { + static function getLatestPolicyDecision( + string $moduleName, + string $policyName = '', + int $userID = null) + { $client = new NDB_Client(); $anonymous = ($client->initialize() === false); if ($anonymous) { @@ -834,9 +846,9 @@ class Utility $userID = $userID ?? User::singleton()->getID(); - $factory = \NDB_Factory::singleton(); - $DB = $factory->database(); - + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + $policyInfo = self::getPolicy($moduleName, $policyName, false) ?? []; if (empty($policyInfo)) { return []; @@ -856,17 +868,17 @@ class Utility ) ", [ - 'uid' => $userID, - 'policyID'=> $policyInfo['PolicyID'] + 'uid' => $userID, + 'policyID' => $policyInfo['PolicyID'] ] ) ?? []; - error_log(print_r($decision, true)); $needsRenewal = true; if ($decision && $policyInfo['PolicyRenewalTime'] > 0) { $decisionDate = new \DateTime($decision['DecisionDate']); - $interval = new \DateInterval("P{$policyInfo['PolicyRenewalTime']}{$policyInfo['PolicyRenewalTimeUnit']}"); - $renewalDate = $decisionDate->add($interval); - error_log("Renewal Date: " . $renewalDate->format('Y-m-d H:i:s')); + $interval = new \DateInterval( + "P{$policyInfo['PolicyRenewalTime']}{$policyInfo['PolicyRenewalTimeUnit']}" + ); + $renewalDate = $decisionDate->add($interval); if ($renewalDate > new \DateTime()) { $needsRenewal = false; } @@ -883,20 +895,24 @@ class Utility * @param string $ModuleName The Module Name * @param string $PolicyName The Policy Name * @param string $decision The decision made by the current user - * + * * @return void * @throws \DatabaseException - * */ - static function saveUserPolicyDecision(string $ModuleName, string $PolicyName, string $decision) { + static function saveUserPolicyDecision( + string $ModuleName, + string $PolicyName, + string $decision + ) + { $user = User::singleton(); - $db = \NDB_Factory::singleton()->database(); + $db = \NDB_Factory::singleton()->database(); $policy = self::getPolicy($ModuleName, $PolicyName, false); $db->insert( 'user_policy_decision', [ - 'UserID' => $user->getId(), + 'UserID' => $user->getId(), 'PolicyID' => $policy['PolicyID'], 'Decision' => $decision, ] From 1079cf273d3b0bb55569049c4022574dd0f5481e Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 13:41:25 -0400 Subject: [PATCH 04/19] Login test plan --- modules/login/test/Request_Account_test_plan.md | 1 + php/libraries/NDB_Page.class.inc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/login/test/Request_Account_test_plan.md b/modules/login/test/Request_Account_test_plan.md index 34226150693..b06270ab2e1 100644 --- a/modules/login/test/Request_Account_test_plan.md +++ b/modules/login/test/Request_Account_test_plan.md @@ -6,6 +6,7 @@ 3. Verify that verification code is enforced (if and only if re-captcha has been activated by the project) 4. Verify that clicking "Submit" button with valid form data will load page acknowledging receipt (Thank you page) 5. Verify "Return to Loris login page" link on Thank you page works +6. Verify that if a policy is added to the project for module `login`, the policy is displayed on the Request Account form page, and it is required that the policy has been viewed to request an account. ### Approving new User Account Request: 6. Log in as another user who has permission: `user_accounts` (User Management) and does not have permission: `user_accounts_multisite` (Across all sites create and edit users). Verify that new account request notification is counted in Dashboard (count has incremented). diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index 0e919804a7e..e1325210c1c 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -861,7 +861,7 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface $baseurl . '/js/jquery.fileupload.js', $baseurl . '/bootstrap/js/bootstrap.min.js', $baseurl . '/js/components/Breadcrumbs.js', - $baseurl . '/js/components/HeaderPolicyButton.js', + $baseurl . '/js/components/PolicyButton.js', $baseurl . "/js/util/queryString.js", $baseurl . '/js/components/Help.js', ]; From cd193a6abb21f23f69f69aa9e304e4c02ec1e7fb Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 13:50:05 -0400 Subject: [PATCH 05/19] Update unit test --- test/unittests/NDB_PageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unittests/NDB_PageTest.php b/test/unittests/NDB_PageTest.php index de823fbf69a..bcb73684db7 100644 --- a/test/unittests/NDB_PageTest.php +++ b/test/unittests/NDB_PageTest.php @@ -816,6 +816,7 @@ public function testGetJSDependencies() '/js/jquery.fileupload.js', '/bootstrap/js/bootstrap.min.js', '/js/components/Breadcrumbs.js', + '/js/components/PolicyButton.js', '/js/util/queryString.js', '/js/components/Help.js', ], From ef87966e6faca611c2a5ec3927c04d8010651747 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 13:55:48 -0400 Subject: [PATCH 06/19] Lint fixes --- php/libraries/Utility.class.inc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index ea554365926..c9b9fcee3c1 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -775,15 +775,15 @@ class Utility string $moduleName, string $policyName ='', bool $headerButton = false - ) - { + ) { $factory = \NDB_Factory::singleton(); $DB = $factory->database(); - // If no policy name is given, then we return the latest policy for the module + // If no policy name is given, we return the latest policy for the module if (!empty($policyName)) { $policyName = 'AND p.Name = '. $DB->quote($policyName); } - // If we want just a policy that goes in the header, then we need HeaderButton = 'Y' + // If we want just a policy that goes in the header, + // then we need HeaderButton = 'Y' if ($headerButton == true) { $headerWhere = "('Y')"; } else { @@ -835,8 +835,8 @@ class Utility static function getLatestPolicyDecision( string $moduleName, string $policyName = '', - int $userID = null) - { + int $userID = null + ) { $client = new NDB_Client(); $anonymous = ($client->initialize() === false); if ($anonymous) { @@ -876,7 +876,8 @@ class Utility if ($decision && $policyInfo['PolicyRenewalTime'] > 0) { $decisionDate = new \DateTime($decision['DecisionDate']); $interval = new \DateInterval( - "P{$policyInfo['PolicyRenewalTime']}{$policyInfo['PolicyRenewalTimeUnit']}" + "P{$policyInfo['PolicyRenewalTime']}". + "{$policyInfo['PolicyRenewalTimeUnit']}" ); $renewalDate = $decisionDate->add($interval); if ($renewalDate > new \DateTime()) { @@ -891,10 +892,10 @@ class Utility /** * Save a User's Policy decision - * + * * @param string $ModuleName The Module Name * @param string $PolicyName The Policy Name - * @param string $decision The decision made by the current user + * @param string $decision The decision made by the current user * * @return void * @throws \DatabaseException @@ -903,8 +904,7 @@ class Utility string $ModuleName, string $PolicyName, string $decision - ) - { + ) { $user = User::singleton(); $db = \NDB_Factory::singleton()->database(); From db25ee7b98edfc9715b07632a4e24133acf9d7de Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 14:05:38 -0400 Subject: [PATCH 07/19] Fix spacing --- php/libraries/Utility.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index c9b9fcee3c1..5f333c1572b 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -853,7 +853,7 @@ class Utility if (empty($policyInfo)) { return []; } - $decision = $DB->pselectRow( + $decision = $DB->pselectRow( "SELECT Decision, DecisionDate FROM user_policy_decision WHERE From 2bbb94223d3826d9b2bdbae1ce28fd8af1cd9ffc Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 14:34:49 -0400 Subject: [PATCH 08/19] Fix spacing again --- src/Middleware/UserPageDecorationMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/UserPageDecorationMiddleware.php b/src/Middleware/UserPageDecorationMiddleware.php index 3effdc069c0..c58ffaf772f 100644 --- a/src/Middleware/UserPageDecorationMiddleware.php +++ b/src/Middleware/UserPageDecorationMiddleware.php @@ -271,7 +271,7 @@ function ($a, $b) { $tpl_data['FormAction'] = $page->FormAction ?? ''; if ($page instanceof \NDB_Page) { - $tpl_data['breadcrumbs'] = $page->getBreadcrumbs(); + $tpl_data['breadcrumbs'] = $page->getBreadcrumbs(); $tpl_data['header_policy'] = $page->getHeaderPolicy(); $tpl_data['pop_up_policy'] = $page->getPolicyThatNeedsRenewal(); } From a444d7aa44ec51fba6e6ac9856eeea5e7a1c3c29 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 14:47:21 -0400 Subject: [PATCH 09/19] Attempt to fix tests --- htdocs/PolicyDecision.php | 2 +- php/libraries/Utility.class.inc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htdocs/PolicyDecision.php b/htdocs/PolicyDecision.php index 30c0f90e97f..256432d059a 100644 --- a/htdocs/PolicyDecision.php +++ b/htdocs/PolicyDecision.php @@ -26,7 +26,7 @@ $anonymous = ($client->initialize() === false); if ($anonymous) { // If the user is not logged in, we cannot save the policy decision. - exit; + exit(0); } \Utility::saveUserPolicyDecision( diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index 5f333c1572b..8ae8e56016a 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -819,7 +819,7 @@ class Utility [ 'moduleName' => $moduleName ] - ); + ) ?? []; } /** @@ -835,7 +835,7 @@ class Utility static function getLatestPolicyDecision( string $moduleName, string $policyName = '', - int $userID = null + int $userID ) { $client = new NDB_Client(); $anonymous = ($client->initialize() === false); From b69ea56aface7ba389d6941780f54453fcf785e9 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 28 May 2025 14:53:06 -0400 Subject: [PATCH 10/19] One more time --- php/libraries/Utility.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index 8ae8e56016a..d2d8481cea9 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -834,7 +834,7 @@ class Utility */ static function getLatestPolicyDecision( string $moduleName, - string $policyName = '', + string $policyName, int $userID ) { $client = new NDB_Client(); From 21a89f0caf2a4e51b009059338560d11185a55c6 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Mon, 14 Jul 2025 19:59:15 -0400 Subject: [PATCH 11/19] Rework to not have htdocs --- htdocs/PolicyDecision.php | 36 ---- jsx/PolicyButton.js | 23 ++- modules/login/jsx/loginIndex.js | 24 +-- modules/login/jsx/requestAccount.js | 35 ++-- modules/login/php/authentication.class.inc | 5 +- php/libraries/NDB_Page.class.inc | 164 ++++++++++++++++-- php/libraries/Utility.class.inc | 164 +----------------- .../UserPageDecorationMiddleware.php | 5 +- webpack.config.ts | 1 + 9 files changed, 203 insertions(+), 254 deletions(-) delete mode 100644 htdocs/PolicyDecision.php diff --git a/htdocs/PolicyDecision.php b/htdocs/PolicyDecision.php deleted file mode 100644 index 256432d059a..00000000000 --- a/htdocs/PolicyDecision.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @license Loris license - * @link https://github.com/aces/Loris - */ - - -set_include_path( - get_include_path() . ":" . - __DIR__ . "/../project/libraries:" . - __DIR__ . "/../php/libraries" -); - -require_once __DIR__ . "/../vendor/autoload.php"; -// Ensures the user is logged in, and parses the config file. -require_once "NDB_Client.class.inc"; -$client = new NDB_Client(); -$anonymous = ($client->initialize() === false); -if ($anonymous) { - // If the user is not logged in, we cannot save the policy decision. - exit(0); -} - -\Utility::saveUserPolicyDecision( - $_REQUEST['ModuleName'], - $_REQUEST['PolicyName'], - $_REQUEST['decision'] -); \ No newline at end of file diff --git a/jsx/PolicyButton.js b/jsx/PolicyButton.js index b01269462cc..f31921d7ee6 100644 --- a/jsx/PolicyButton.js +++ b/jsx/PolicyButton.js @@ -11,6 +11,7 @@ import Swal from 'sweetalert2'; /** * PolicyButton Component + * * @param {object} props - The component props. * @param {object} props.onClickPolicy - The policy object containing title * and content that should appear when the button is pressed. @@ -60,14 +61,20 @@ const fireSwal = (policy, anon, callback) => { callback(decision); } if (!anon) { - $.ajax(loris.BaseURL+'/PolicyDecision.php', { - method: 'POST', - data: { - ...policy, - decision: decision.value == true ? 'Accepted' : 'Declined', - }, - dataType: 'json', - }); + fetch( + loris.BaseURL + + '/policy_tracker/policies', + { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + ...policy, + decision: decision.value == true ? 'Accepted' : 'Declined', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); if (decision.value != true) { window.location.href = loris.BaseURL; } diff --git a/modules/login/jsx/loginIndex.js b/modules/login/jsx/loginIndex.js index fff7886ccd6..caa2f308911 100644 --- a/modules/login/jsx/loginIndex.js +++ b/modules/login/jsx/loginIndex.js @@ -14,8 +14,8 @@ import { PasswordElement, ButtonElement, } from 'jsx/Form'; -import {PolicyButton} from 'jsx/PolicyButton'; import SummaryStatistics from './summaryStatistics'; +import {PolicyButton} from 'jsx/PolicyButton'; /** * Login form. @@ -211,16 +211,16 @@ class Login extends Component { class={'col-xs-12 col-sm-12 col-md-12 text-danger'} /> ) : null; - const policy = this.state.component.requestAccount - ? <> -
- - : null; + const policy = this.state.component.requestAccount.policy; + const policyButton = policy ? + + : null; const oidc = this.state.oidc ? this.getOIDCLinks() : ''; const login = (
@@ -269,7 +269,7 @@ class Login extends Component { this.setMode('request')} style={{cursor: 'pointer'}}>Request Account
- {policy} + {policyButton}
{oidc}
diff --git a/modules/login/jsx/requestAccount.js b/modules/login/jsx/requestAccount.js index 200a878be0b..c07f172227e 100644 --- a/modules/login/jsx/requestAccount.js +++ b/modules/login/jsx/requestAccount.js @@ -11,7 +11,7 @@ import { CheckboxElement, ButtonElement, } from 'jsx/Form'; -import {PolicyButton} from '../../../jsx/PolicyButton'; +import PolicyButton from 'jsx/PolicyButton'; /** * Request account form. @@ -169,24 +169,21 @@ class RequestAccount extends Component {
) : null; const policy = this.state.policy ? ( - <> - { - this.setState({ - form: { - ...this.state.form, - viewedPolicy: true, - }, - }); - }} - /> -

- + { + this.setState({ + form: { + ...this.state.form, + viewedPolicy: true, + }, + }); + }} + /> ) : null; const request = !this.state.request ? (
diff --git a/modules/login/php/authentication.class.inc b/modules/login/php/authentication.class.inc index fa4a8c5e9d2..b86dd001594 100644 --- a/modules/login/php/authentication.class.inc +++ b/modules/login/php/authentication.class.inc @@ -130,8 +130,9 @@ class Authentication extends \NDB_Page implements ETagCalculator } $requestAccountData['site'] = \Utility::getSiteList(); $requestAccountData['project'] = \Utility::getProjectList(); - $requestAccountData['policy'] = $this->getHeaderPolicy(); - $values['requestAccount'] = $requestAccountData; + $loris = $request->getAttribute("loris"); + $requestAccountData['policy'] = $this->getHeaderPolicy($loris); + $values['requestAccount'] = $requestAccountData; if ($this->loris->hasModule('oidc')) { $DB = $this->loris->getDatabaseConnection(); diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index e1325210c1c..7c8d922a458 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -937,30 +937,172 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface ); } + /** + * Get latest Policy for this module with an optional Policy Name + * + * @param \LORIS\LorisInstance $loris The LORIS instance + * @param string $policyName The policy name + * @param bool $headerButton If true, only return policies that + * have HeaderButton = 'Y' + * + * @return array The policy details + * @throws \DatabaseException + */ + function getPolicy( + \LORIS\LorisInstance $loris, + string $policyName ='', + bool $headerButton = false + ) { + $moduleName = $this->Module->getName(); + $DB = $loris->getDatabaseConnection(); + // If no policy name is given, we return the latest policy for the module + if (!empty($policyName)) { + $policyName = 'AND p.Name = '. $DB->quote($policyName); + } + // If we want just a policy that goes in the header, + // then we need HeaderButton = 'Y' + if ($headerButton == true) { + $headerWhere = "('Y')"; + } else { + $headerWhere = "('Y', 'N')"; + } + return $DB->pselectRow( + "SELECT + p.Name as PolicyName, + Version, + Content, + SwalTitle, + HeaderButtonText, + AcceptButtonText, + DeclineButtonText, + m.Name as ModuleName, + PolicyID, + PolicyRenewalTime, + PolicyRenewalTimeUnit + FROM policies p + JOIN modules m ON m.ID = p.ModuleID + WHERE m.Name = :moduleName + $policyName + AND p.Active = 'Y' + AND p.HeaderButton IN $headerWhere + AND Version = ( + SELECT MAX(Version) + FROM policies + WHERE ModuleID = m.ID + AND Name = p.Name + AND p.HeaderButton IN $headerWhere + ) + ", + [ + 'moduleName' => $moduleName + ] + ) ?? []; + } + /** * Returns the latest Policy for this page with a HeaderButton * + * @param \LORIS\LorisInstance $loris The LORIS instance + * * @return array */ - public function getHeaderPolicy(): array - { - return \Utility::getPolicy( - $this->Module->getName(), + public function getHeaderPolicy( + \LORIS\LorisInstance $loris + ): array { + return $this->getPolicy( + $loris, '', true ) ?? []; } /** - * Returns the latest Policy for this page that the user needs to renew + * Get a User's latest policy decision, and whether or not they need to renew. * - * @return array + * @param \LORIS\LorisInstance $loris The LORIS instance + * @param string $policyName The policy name + * @param int|null $userID The user ID, or current user if null + * + * @return array The decision details + * @throws \DatabaseException + * @access public */ - public function getPolicyThatNeedsRenewal(): array - { - return \Utility::getLatestPolicyDecision( - $this->Module->getName(), - '', + public function getLatestPolicyDecision( + \LORIS\LorisInstance $loris, + string $policyName, + int $userID + ) { + $client = new NDB_Client(); + $userID = $userID ?? User::singleton()->getID(); + $DB = $loris->getDatabaseConnection(); + + $policyInfo = $this->getPolicy($loris, $policyName, false) ?? []; + if (empty($policyInfo)) { + return []; + } + $decision = $DB->pselectRow( + "SELECT Decision, DecisionDate + FROM user_policy_decision + WHERE + UserID = :uid + AND PolicyID=:policyID + AND DecisionDate = ( + SELECT MAX(DecisionDate) + FROM user_policy_decision + WHERE + UserID = :uid + AND PolicyID=:policyID + ) + ", + [ + 'uid' => $userID, + 'policyID' => $policyInfo['PolicyID'] + ] + ) ?? []; + $needsRenewal = true; + if ($decision && $policyInfo['PolicyRenewalTime'] > 0) { + $decisionDate = new \DateTime($decision['DecisionDate']); + $interval = new \DateInterval( + "P{$policyInfo['PolicyRenewalTime']}". + "{$policyInfo['PolicyRenewalTimeUnit']}" + ); + $renewalDate = $decisionDate->add($interval); + if ($renewalDate > new \DateTime()) { + $needsRenewal = false; + } + } + return [ + ...$policyInfo, + 'needsRenewal' => $needsRenewal, + ]; + } + + /** + * Save a User's Policy decision + * + * @param \LORIS\LorisInstance $loris The LORIS instance + * @param string $PolicyName The Policy Name + * @param string $decision The decision made by the current user + * + * @return void + * @throws \DatabaseException + */ + function saveUserPolicyDecision( + \LORIS\LorisInstance $loris, + string $PolicyName, + string $decision + ) { + $user = User::singleton(); + $DB = $this->loris->getDatabaseConnection(); + + $policy = $this->getPolicy($loris, $PolicyName, false); + $DB->insert( + 'user_policy_decision', + [ + 'UserID' => $user->getId(), + 'PolicyID' => $policy['PolicyID'], + 'Decision' => $decision, + ] ); } diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index d2d8481cea9..61598e33c2f 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -758,167 +758,6 @@ class Utility ); } - /** - * Get latest Policy for the given Modile Name and Policy Name - * This is the CCNA Version for now, the LORIS Core version will - * likely return more fields. - * - * @param string $moduleName The module name - * @param string $policyName The policy name - * @param bool $headerButton If true, only return policies that are - * have HeaderButton = 'Y' - * - * @return array The policy details - * @throws \DatabaseException - */ - static function getPolicy( - string $moduleName, - string $policyName ='', - bool $headerButton = false - ) { - $factory = \NDB_Factory::singleton(); - $DB = $factory->database(); - // If no policy name is given, we return the latest policy for the module - if (!empty($policyName)) { - $policyName = 'AND p.Name = '. $DB->quote($policyName); - } - // If we want just a policy that goes in the header, - // then we need HeaderButton = 'Y' - if ($headerButton == true) { - $headerWhere = "('Y')"; - } else { - $headerWhere = "('Y', 'N')"; - } - return $DB->pselectRow( - "SELECT - p.Name as PolicyName, - Version, - Content, - SwalTitle, - HeaderButtonText, - AcceptButtonText, - DeclineButtonText, - m.Name as ModuleName, - PolicyID, - PolicyRenewalTime, - PolicyRenewalTimeUnit - FROM policies p - JOIN modules m ON m.ID = p.ModuleID - WHERE m.Name = :moduleName - $policyName - AND p.Active = 'Y' - AND p.HeaderButton IN $headerWhere - AND Version = ( - SELECT MAX(Version) - FROM policies - WHERE ModuleID = m.ID - AND Name = p.Name - AND p.HeaderButton IN $headerWhere - ) - ", - [ - 'moduleName' => $moduleName - ] - ) ?? []; - } - - /** - * Get a User's latest policy decision, and whether or not they need to renew. - * - * @param string $moduleName The module name - * @param string $policyName The policy name - * @param int|null $userID The user ID, or current user if null - * - * @return array The decision details - * @throws \DatabaseException - */ - static function getLatestPolicyDecision( - string $moduleName, - string $policyName, - int $userID - ) { - $client = new NDB_Client(); - $anonymous = ($client->initialize() === false); - if ($anonymous) { - // If the user is anonymous, we cannot get their decision - return []; - } - - $userID = $userID ?? User::singleton()->getID(); - - $factory = \NDB_Factory::singleton(); - $DB = $factory->database(); - - $policyInfo = self::getPolicy($moduleName, $policyName, false) ?? []; - if (empty($policyInfo)) { - return []; - } - $decision = $DB->pselectRow( - "SELECT Decision, DecisionDate - FROM user_policy_decision - WHERE - UserID = :uid - AND PolicyID=:policyID - AND DecisionDate = ( - SELECT MAX(DecisionDate) - FROM user_policy_decision - WHERE - UserID = :uid - AND PolicyID=:policyID - ) - ", - [ - 'uid' => $userID, - 'policyID' => $policyInfo['PolicyID'] - ] - ) ?? []; - $needsRenewal = true; - if ($decision && $policyInfo['PolicyRenewalTime'] > 0) { - $decisionDate = new \DateTime($decision['DecisionDate']); - $interval = new \DateInterval( - "P{$policyInfo['PolicyRenewalTime']}". - "{$policyInfo['PolicyRenewalTimeUnit']}" - ); - $renewalDate = $decisionDate->add($interval); - if ($renewalDate > new \DateTime()) { - $needsRenewal = false; - } - } - return [ - ...$policyInfo, - 'needsRenewal' => $needsRenewal, - ]; - } - - /** - * Save a User's Policy decision - * - * @param string $ModuleName The Module Name - * @param string $PolicyName The Policy Name - * @param string $decision The decision made by the current user - * - * @return void - * @throws \DatabaseException - */ - static function saveUserPolicyDecision( - string $ModuleName, - string $PolicyName, - string $decision - ) { - $user = User::singleton(); - $db = \NDB_Factory::singleton()->database(); - - $policy = self::getPolicy($ModuleName, $PolicyName, false); - $db->insert( - 'user_policy_decision', - [ - 'UserID' => $user->getId(), - 'PolicyID' => $policy['PolicyID'], - 'Decision' => $decision, - ] - ); - } - /** * Get all the instruments which currently exist for a given visit label * in the database. @@ -931,8 +770,7 @@ class Utility */ static function getVisitInstruments(string $visit_label): array { - $factory = \NDB_Factory::singleton(); - $DB = $factory->database(); + $db = $lorisInstance->getDatabaseConnection(); $test_names = $DB->pselectColWithIndexKey( "SELECT DISTINCT t.Test_name, t.Full_name diff --git a/src/Middleware/UserPageDecorationMiddleware.php b/src/Middleware/UserPageDecorationMiddleware.php index 3e590b78c10..0f232a583fd 100644 --- a/src/Middleware/UserPageDecorationMiddleware.php +++ b/src/Middleware/UserPageDecorationMiddleware.php @@ -272,10 +272,9 @@ function ($a, $b) { if ($page instanceof \NDB_Page) { $tpl_data['breadcrumbs'] = $page->getBreadcrumbs(); - $tpl_data['header_policy'] = $page->getHeaderPolicy(); - $tpl_data['pop_up_policy'] = $page->getPolicyThatNeedsRenewal(); + $tpl_data['header_policy'] = $page->getHeaderPolicy($loris); + $tpl_data['pop_up_policy'] = $page->getLatestPolicyDecision($loris, '', $user->getId()); } - // Assign the console template variable as the very, very last thing. $tpl_data['console'] = htmlspecialchars( ob_get_contents(), diff --git a/webpack.config.ts b/webpack.config.ts index 508d45dd383..399048b23b9 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -33,6 +33,7 @@ const lorisModules: Record = { datadict: ['dataDictIndex'], dataquery: ['index'], data_release: ['dataReleaseIndex'], + policy_tracker: ['policyTrackerIndex'], dictionary: ['dataDictIndex'], dqt: [ 'components/expansionpanels', From 1c79bd6723c355fcd4642125091a734234ea283c Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Mon, 14 Jul 2025 20:01:48 -0400 Subject: [PATCH 12/19] Forgot to add new policy_tracker module --- modules/policy_tracker/help/policy_tracker.md | 6 ++ modules/policy_tracker/php/module.class.inc | 77 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 modules/policy_tracker/help/policy_tracker.md create mode 100644 modules/policy_tracker/php/module.class.inc diff --git a/modules/policy_tracker/help/policy_tracker.md b/modules/policy_tracker/help/policy_tracker.md new file mode 100644 index 00000000000..244b85992b2 --- /dev/null +++ b/modules/policy_tracker/help/policy_tracker.md @@ -0,0 +1,6 @@ +# Policy Tracker + +This module provides an endpoint for other pages to manage policy acceptance. + +### Future work: +A Filterable Data Table to display the policies that have been accepted by users. \ No newline at end of file diff --git a/modules/policy_tracker/php/module.class.inc b/modules/policy_tracker/php/module.class.inc new file mode 100644 index 00000000000..bcb940084c6 --- /dev/null +++ b/modules/policy_tracker/php/module.class.inc @@ -0,0 +1,77 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace LORIS\policy_tracker; +use \Psr\Http\Message\ServerRequestInterface; +use \Psr\Http\Message\ResponseInterface; + +/** + * Class module implements the basic LORIS module functionality + * + * @category Core + * @package Main + * @subpackage Login + * @author Saagar Arya + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Module extends \Module +{ + /** + * Implements the PSR15 RequestHandler interface for this module. The API + * module does some preliminary verification of the request, converts the + * version from the URL to a request attribute, and then falls back on the + * default LORIS page handler. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $body = json_decode($request->getBody()->getContents(), true); + switch ($request->getMethod()) { + case 'POST': + $module = $this->loris->getModule($body['ModuleName']); + $page = new \NDB_Page( + $this->loris, + $module, + '', + '', + '', + ); + $page->saveUserPolicyDecision( + $this->loris, + $body['PolicyName'], + $body['decision'] + ); + return new \LORIS\Http\Response\JSON\OK( + ['message' => 'Policy decision saved successfully'] + ); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed(['POST']); + } + } + + /** + * {@inheritDoc} + * + * @return string The human readable name for this module + */ + public function getLongName() : string + { + return dgettext("policy_tracker", "Policy Tracker"); + } +} From daddb8c01d602de7125e3209c37c738d1cfcd05a Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Mon, 14 Jul 2025 20:04:43 -0400 Subject: [PATCH 13/19] Remove leftover code from webpack --- webpack.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/webpack.config.ts b/webpack.config.ts index 399048b23b9..508d45dd383 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -33,7 +33,6 @@ const lorisModules: Record = { datadict: ['dataDictIndex'], dataquery: ['index'], data_release: ['dataReleaseIndex'], - policy_tracker: ['policyTrackerIndex'], dictionary: ['dataDictIndex'], dqt: [ 'components/expansionpanels', From df87735cdc63e7d8db1bbce73ad279aa82539f81 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Mon, 14 Jul 2025 20:07:46 -0400 Subject: [PATCH 14/19] Reverse the fix for a lint test to make it work again? --- php/libraries/NDB_Page.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index 7c8d922a458..de3fb894887 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -1002,7 +1002,7 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface /** * Returns the latest Policy for this page with a HeaderButton * - * @param \LORIS\LorisInstance $loris The LORIS instance + * @param \LORIS\LorisInstance $loris The LORIS instance * * @return array */ From 471a605490b00184da2c707e9c37863f313252b7 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Mon, 14 Jul 2025 20:15:55 -0400 Subject: [PATCH 15/19] Fix phan tests --- modules/policy_tracker/php/module.class.inc | 6 +++--- php/libraries/NDB_Page.class.inc | 3 +-- php/libraries/Utility.class.inc | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/policy_tracker/php/module.class.inc b/modules/policy_tracker/php/module.class.inc index bcb940084c6..79cbcd140e3 100644 --- a/modules/policy_tracker/php/module.class.inc +++ b/modules/policy_tracker/php/module.class.inc @@ -44,7 +44,7 @@ class Module extends \Module $body = json_decode($request->getBody()->getContents(), true); switch ($request->getMethod()) { case 'POST': - $module = $this->loris->getModule($body['ModuleName']); + $module = $this->loris->getModule($body['ModuleName'] ?? ''); $page = new \NDB_Page( $this->loris, $module, @@ -54,8 +54,8 @@ class Module extends \Module ); $page->saveUserPolicyDecision( $this->loris, - $body['PolicyName'], - $body['decision'] + $body['PolicyName'] ?? '', + $body['decision'] ?? '' ); return new \LORIS\Http\Response\JSON\OK( ['message' => 'Policy decision saved successfully'] diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index de3fb894887..d7a6ef7e90c 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -1030,9 +1030,8 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface public function getLatestPolicyDecision( \LORIS\LorisInstance $loris, string $policyName, - int $userID + int|null $userID ) { - $client = new NDB_Client(); $userID = $userID ?? User::singleton()->getID(); $DB = $loris->getDatabaseConnection(); diff --git a/php/libraries/Utility.class.inc b/php/libraries/Utility.class.inc index 61598e33c2f..513de8c7da8 100644 --- a/php/libraries/Utility.class.inc +++ b/php/libraries/Utility.class.inc @@ -770,7 +770,8 @@ class Utility */ static function getVisitInstruments(string $visit_label): array { - $db = $lorisInstance->getDatabaseConnection(); + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); $test_names = $DB->pselectColWithIndexKey( "SELECT DISTINCT t.Test_name, t.Full_name From 7add0e03b135281b7f88d3e558692ecd4dcc050f Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 17 Jul 2025 11:28:03 -0400 Subject: [PATCH 16/19] Requested changes and enforce when declined --- SQL/0000-00-00-schema.sql | 10 +- SQL/0000-00-01-Modules.sql | 1 + .../2025-05-22-Introduce-Policy-Decisions.sql | 12 +- SQL/sql_for_abstract.sql | 391 ++++++++++++++++++ php/libraries/NDB_Page.class.inc | 32 +- raisinbread/RB_files/RB_modules.sql | 1 + raisinbread/RB_files/RB_policies.sql | 4 +- 7 files changed, 419 insertions(+), 32 deletions(-) create mode 100644 SQL/sql_for_abstract.sql diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index f32bb7a853a..3196f384902 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -1434,17 +1434,17 @@ CREATE TABLE `user_account_history` ( CREATE TABLE policies ( PolicyID INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255) NOT NULL, - Version VARCHAR(50) NOT NULL, + Version INT NOT NULL, ModuleID INT NOT NULL, PolicyRenewalTime INT DEFAULT 7, PolicyRenewalTimeUnit enum('D','Y','M','H') DEFAULT 'D', Content TEXT NULL, - SwalTitle VARCHAR(255) DEFAULT 'Terms Of Use', + SwalTitle VARCHAR(255) DEFAULT 'Terms of Use', HeaderButton enum('Y','N') DEFAULT 'Y', - HeaderButtonText VARCHAR(255) DEFAULT 'Terms Of Use', + HeaderButtonText VARCHAR(255) DEFAULT 'Terms of Use', Active enum('Y','N') DEFAULT 'Y', - AcceptButtonText VARCHAR(255) DEFAULT '', - DeclineButtonText VARCHAR(255) DEFAULT '', + AcceptButtonText VARCHAR(255) DEFAULT 'Accept', + DeclineButtonText VARCHAR(255) DEFAULT 'Decline', CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); diff --git a/SQL/0000-00-01-Modules.sql b/SQL/0000-00-01-Modules.sql index 2562b9ae10e..f69435841e6 100644 --- a/SQL/0000-00-01-Modules.sql +++ b/SQL/0000-00-01-Modules.sql @@ -55,5 +55,6 @@ INSERT INTO modules (Name, Active) VALUES ('electrophysiology_uploader', 'Y'); INSERT INTO modules (Name, Active) VALUES ('dataquery', 'Y'); INSERT INTO modules (Name, Active) VALUES ('schedule_module', 'Y'); INSERT INTO modules (Name, Active) VALUES ('redcap', 'N'); +INSERT INTO modules (Name, Active) VALUES ('policy_tracker', 'Y'); ALTER TABLE issues ADD CONSTRAINT `fk_issues_7` FOREIGN KEY (`module`) REFERENCES `modules` (`ID`); diff --git a/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql b/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql index d2057e42b77..a66662c05b8 100644 --- a/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql +++ b/SQL/New_patches/2025-05-22-Introduce-Policy-Decisions.sql @@ -1,18 +1,16 @@ CREATE TABLE policies ( PolicyID INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255) NOT NULL, - Version VARCHAR(50) NOT NULL, + Version INT NOT NULL, ModuleID INT NOT NULL, -- Show in the header for a module PolicyRenewalTime INT DEFAULT 7, -- Number of days before the policy is renewed PolicyRenewalTimeUnit enum('D','Y','M','H') DEFAULT 'D', -- Unit of the renewal time Content TEXT NULL, - SwalTitle VARCHAR(255) DEFAULT 'Terms Of Use', + SwalTitle VARCHAR(255) DEFAULT 'Terms of Use', HeaderButton enum('Y','N') DEFAULT 'Y', - HeaderButtonText VARCHAR(255) DEFAULT 'Terms Of Use', + HeaderButtonText VARCHAR(255) DEFAULT 'Terms of Use', Active enum('Y','N') DEFAULT 'Y', - AcceptButton enum('Y','N') DEFAULT 'Y', AcceptButtonText VARCHAR(255) DEFAULT 'Accept', - DeclineButton enum('Y','N') DEFAULT 'Y', DeclineButtonText VARCHAR(255) DEFAULT 'Decline', CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, UpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP @@ -24,4 +22,6 @@ CREATE TABLE user_policy_decision ( PolicyID INT NOT NULL, Decision enum('Accepted','Declined') NOT NULL, DecisionDate DATETIME DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file +); + +INSERT INTO modules (`Name`, `Active`) VALUES ('policy_tracker','Y'); \ No newline at end of file diff --git a/SQL/sql_for_abstract.sql b/SQL/sql_for_abstract.sql new file mode 100644 index 00000000000..60626afddb8 --- /dev/null +++ b/SQL/sql_for_abstract.sql @@ -0,0 +1,391 @@ +SELECT count(DISTINCT cr.ResolvedID) +FROM conflicts_resolved cr +JOIN ( + SELECT f.CommentID + FROM flag f + JOIN session s ON f.SessionID = s.ID + WHERE s.Visit_label IN ( + 'Initial_Assessment_Screening', + 'Initial_MRI', + 'Repeat_Initial_Assessment_Screening', + 'Clinical_Assessment', + 'Neuropsychology_Assessment' + ) + -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) + AND Test_name NOT IN ( +'Annual_Follow_up_Phone_Call', +'Autopsy_Banked_Tissue_Inventory', +'Autopsy_Neuropathology_Report', +'Clinical_Current_Medication', +'Clinical_Current_Medication_FU', +'Clinical_Diagnosis_Confirmation', +'Clinical_Family_History', +'Clinical_Family_History_FU', +'Clinical_Hobbies_And_Leisure_Activities', +'Clinical_Initial_Symptoms', +'Clinical_Lumbar_Puncture_Procedure_Report', +'Clinical_Medical_History', +'Clinical_Medical_History_FU', +'Clinical_Mental_Health_History', +'Clinical_Mental_Health_History_FU', +'Clinical_Past_Medications', +'Clinical_Past_Medications_FU', +'Clinical_Pre_Gait_Assessment', +'Clinical_Signs_Symptoms', +'Clinical_Sleep_Work_Diary', +'Clinical_Smoking', +'Clinical_Surgical_History', +'Clinical_Surgical_History_FU', +'Clinical_UPDRS_III_IV', +'General_Health_Alcohol_Consumption', +'General_Health_Biosample_Collection', +'General_Health_Caregiving_Assessment', +'General_Health_Falls_Balance', +'General_Health_Gait_Assessment', +'General_Health_Grip_Strength', +'General_Health_Nutrition', +'General_Health_Physical_Activity', +'General_Health_Physical_Activity_FU', +'General_Health_Sleep', +'General_Health_Vision', +'mri_parameter_form', +'PI_Caregiving_Assessment', +'PI_General_Information', +'PI_General_Information_FU', +'Reappraisal_Initial_Diagnosis_Reappraisal', +'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', +'Screening_CERAD_Recog', +'Screening_Driving_History', +'Screening_Driving_History_FU', +'Screening_Education', +'Screening_Education_FU', +'Screening_Employment_History', +'Screening_Employment_History_FU', +'Screening_Languages', +'Screening_Logical_Memory_Delayed', +'Screening_MOCA', +'Screening_Reproductive_History' +) +) fs ON fs.CommentID = cr.CommentID1; + +SELECT count(*) +FROM information_schema.columns +WHERE table_name NOT IN ( +'Annual_Follow_up_Phone_Call', +'Autopsy_Banked_Tissue_Inventory', +'Autopsy_Neuropathology_Report', +'Clinical_Current_Medication', +'Clinical_Current_Medication_FU', +'Clinical_Diagnosis_Confirmation', +'Clinical_Family_History', +'Clinical_Family_History_FU', +'Clinical_Hobbies_And_Leisure_Activities', +'Clinical_Initial_Symptoms', +'Clinical_Lumbar_Puncture_Procedure_Report', +'Clinical_Medical_History', +'Clinical_Medical_History_FU', +'Clinical_Mental_Health_History', +'Clinical_Mental_Health_History_FU', +'Clinical_Past_Medications', +'Clinical_Past_Medications_FU', +'Clinical_Pre_Gait_Assessment', +'Clinical_Signs_Symptoms', +'Clinical_Sleep_Work_Diary', +'Clinical_Smoking', +'Clinical_Surgical_History', +'Clinical_Surgical_History_FU', +'Clinical_UPDRS_III_IV', +'General_Health_Alcohol_Consumption', +'General_Health_Biosample_Collection', +'General_Health_Caregiving_Assessment', +'General_Health_Falls_Balance', +'General_Health_Gait_Assessment', +'General_Health_Grip_Strength', +'General_Health_Nutrition', +'General_Health_Physical_Activity', +'General_Health_Physical_Activity_FU', +'General_Health_Sleep', +'General_Health_Vision', +'mri_parameter_form', +'PI_Caregiving_Assessment', +'PI_General_Information', +'PI_General_Information_FU', +'Reappraisal_Initial_Diagnosis_Reappraisal', +'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', +'Screening_CERAD_Recog', +'Screening_Driving_History', +'Screening_Driving_History_FU', +'Screening_Education', +'Screening_Education_FU', +'Screening_Employment_History', +'Screening_Employment_History_FU', +'Screening_Languages', +'Screening_Logical_Memory_Delayed', +'Screening_MOCA', +'Screening_Reproductive_History' +) + +SELECT count(DISTINCT cr.ConflictID) +FROM conflicts_unresolved cr +JOIN ( + SELECT f.CommentID + FROM flag f + JOIN session s ON f.SessionID = s.ID + WHERE s.Visit_label LIKE '%Time_3%' + -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) +-- AND Test_name NOT IN ( +-- 'Annual_Follow_up_Phone_Call', +-- 'Autopsy_Banked_Tissue_Inventory', +-- 'Autopsy_Neuropathology_Report', +-- 'Clinical_Current_Medication', +-- 'Clinical_Current_Medication_FU', +-- 'Clinical_Diagnosis_Confirmation', +-- 'Clinical_Family_History', +-- 'Clinical_Family_History_FU', +-- 'Clinical_Hobbies_And_Leisure_Activities', +-- 'Clinical_Initial_Symptoms', +-- 'Clinical_Lumbar_Puncture_Procedure_Report', +-- 'Clinical_Medical_History', +-- 'Clinical_Medical_History_FU', +-- 'Clinical_Mental_Health_History', +-- 'Clinical_Mental_Health_History_FU', +-- 'Clinical_Past_Medications', +-- 'Clinical_Past_Medications_FU', +-- 'Clinical_Pre_Gait_Assessment', +-- 'Clinical_Signs_Symptoms', +-- 'Clinical_Sleep_Work_Diary', +-- 'Clinical_Smoking', +-- 'Clinical_Surgical_History', +-- 'Clinical_Surgical_History_FU', +-- 'Clinical_UPDRS_III_IV', +-- 'General_Health_Alcohol_Consumption', +-- 'General_Health_Biosample_Collection', +-- 'General_Health_Caregiving_Assessment', +-- 'General_Health_Falls_Balance', +-- 'General_Health_Gait_Assessment', +-- 'General_Health_Grip_Strength', +-- 'General_Health_Nutrition', +-- 'General_Health_Physical_Activity', +-- 'General_Health_Physical_Activity_FU', +-- 'General_Health_Sleep', +-- 'General_Health_Vision', +-- 'mri_parameter_form', +-- 'PI_Caregiving_Assessment', +-- 'PI_General_Information', +-- 'PI_General_Information_FU', +-- 'Reappraisal_Initial_Diagnosis_Reappraisal', +-- 'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', +-- 'Screening_CERAD_Recog', +-- 'Screening_Driving_History', +-- 'Screening_Driving_History_FU', +-- 'Screening_Education', +-- 'Screening_Education_FU', +-- 'Screening_Employment_History', +-- 'Screening_Employment_History_FU', +-- 'Screening_Languages', +-- 'Screening_Logical_Memory_Delayed', +-- 'Screening_MOCA', +-- 'Screening_Reproductive_History' +-- ) +) fs ON fs.CommentID = cr.CommentID1; + +SELECT count(f.CommentID) + FROM flag f + JOIN session s ON f.SessionID = s.ID + WHERE s.Visit_label IN ( + 'Initial_Assessment_Screening', + 'Initial_MRI', + 'Repeat_Initial_Assessment_Screening', + 'Clinical_Assessment', + 'Neuropsychology_Assessment' + ) + AND CommentID LIKE 'DDE_%' + AND Data_entry IS NOT NULL + -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) + AND Test_name NOT IN ( + 'Annual_Follow_up_Phone_Call', + 'Autopsy_Banked_Tissue_Inventory' + 'Autopsy_Neuropathology_Report', + 'Clinical_Current_Medication', + 'Clinical_Current_Medication_FU', + 'Clinical_Diagnosis_Confirmation', + 'Clinical_Family_History', + 'Clinical_Family_History_FU', + 'Clinical_Hobbies_And_Leisure_Activities', + 'Clinical_Initial_Symptoms', + 'Clinical_Lumbar_Puncture_Procedure_Report', + 'Clinical_Medical_History', + 'Clinical_Medical_History_FU', + 'Clinical_Mental_Health_History', + 'Clinical_Mental_Health_History_FU', + 'Clinical_Past_Medications', + 'Clinical_Past_Medications_FU', + 'Clinical_Pre_Gait_Assessment', + 'Clinical_Signs_Symptoms', + 'Clinical_Sleep_Work_Diary', + 'Clinical_Smoking', + 'Clinical_Surgical_History', + 'Clinical_Surgical_History_FU', + 'Clinical_UPDRS_III_IV', + 'General_Health_Alcohol_Consumption', + 'General_Health_Biosample_Collection', + 'General_Health_Caregiving_Assessment', + 'General_Health_Falls_Balance', + 'General_Health_Gait_Assessment', + 'General_Health_Grip_Strength', + 'General_Health_Nutrition', + 'General_Health_Physical_Activity', + 'General_Health_Physical_Activity_FU', + 'General_Health_Sleep', + 'General_Health_Vision', + 'mri_parameter_form', + 'PI_Caregiving_Assessment', + 'PI_General_Information', + 'PI_General_Information_FU', + 'Reappraisal_Initial_Diagnosis_Reappraisal', + 'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', + 'Screening_CERAD_Recog', + 'Screening_Driving_History', + 'Screening_Driving_History_FU', + 'Screening_Education', + 'Screening_Education_FU', + 'Screening_Employment_History', + 'Screening_Employment_History_FU', + 'Screening_Languages', + 'Screening_Logical_Memory_Delayed', + 'Screening_MOCA', + 'Screening_Reproductive_History' + ); + + SELECT count(f.CommentID) + FROM flag f + JOIN session s ON f.SessionID = s.ID + WHERE s.Visit_label LIKE '%Time_3%' + AND Data_entry IS NOT NULL + AND CommentID LIKE 'DDE_%' + -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) + AND Test_name NOT IN ( + 'Annual_Follow_up_Phone_Call', + 'Autopsy_Banked_Tissue_Inventory', + 'Autopsy_Neuropathology_Report', + 'Clinical_Current_Medication', + 'Clinical_Current_Medication_FU', + 'Clinical_Diagnosis_Confirmation', + 'Clinical_Family_History', + 'Clinical_Family_History_FU', + 'Clinical_Hobbies_And_Leisure_Activities', + 'Clinical_Initial_Symptoms', + 'Clinical_Lumbar_Puncture_Procedure_Report', + 'Clinical_Medical_History', + 'Clinical_Medical_History_FU', + 'Clinical_Mental_Health_History', + 'Clinical_Mental_Health_History_FU', + 'Clinical_Past_Medications', + 'Clinical_Past_Medications_FU', + 'Clinical_Pre_Gait_Assessment', + 'Clinical_Signs_Symptoms', + 'Clinical_Sleep_Work_Diary', + 'Clinical_Smoking', + 'Clinical_Surgical_History', + 'Clinical_Surgical_History_FU', + 'Clinical_UPDRS_III_IV', + 'General_Health_Alcohol_Consumption', + 'General_Health_Biosample_Collection', + 'General_Health_Caregiving_Assessment', + 'General_Health_Falls_Balance', + 'General_Health_Gait_Assessment', + 'General_Health_Grip_Strength', + 'General_Health_Nutrition', + 'General_Health_Physical_Activity', + 'General_Health_Physical_Activity_FU', + 'General_Health_Sleep', + 'General_Health_Vision', + 'mri_parameter_form', + 'PI_Caregiving_Assessment', + 'PI_General_Information', + 'PI_General_Information_FU', + 'Reappraisal_Initial_Diagnosis_Reappraisal', + 'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', + 'Screening_CERAD_Recog', + 'Screening_Driving_History', + 'Screening_Driving_History_FU', + 'Screening_Education', + 'Screening_Education_FU', + 'Screening_Employment_History', + 'Screening_Employment_History_FU', + 'Screening_Languages', + 'Screening_Logical_Memory_Delayed', + 'Screening_MOCA', + 'Screening_Reproductive_History' + ) + + +SELECT med_name, COUNT(*) AS total_count +FROM ( + SELECT med_name_1 AS med_name FROM Clinical_Current_Medication WHERE med_name_1 IS NOT NULL + UNION ALL + SELECT med_name_2 FROM Clinical_Current_Medication WHERE med_name_2 IS NOT NULL + UNION ALL + SELECT med_name_3 FROM Clinical_Current_Medication WHERE med_name_3 IS NOT NULL + UNION ALL + SELECT med_name_4 FROM Clinical_Current_Medication WHERE med_name_4 IS NOT NULL + UNION ALL + SELECT med_name_5 FROM Clinical_Current_Medication WHERE med_name_5 IS NOT NULL + UNION ALL + SELECT med_name_6 FROM Clinical_Current_Medication WHERE med_name_6 IS NOT NULL + UNION ALL + SELECT med_name_7 FROM Clinical_Current_Medication WHERE med_name_7 IS NOT NULL + UNION ALL + SELECT med_name_8 FROM Clinical_Current_Medication WHERE med_name_8 IS NOT NULL + -- Continue this pattern up to med_name_32 + UNION ALL + SELECT med_name_9 FROM Clinical_Current_Medication WHERE med_name_9 IS NOT NULL + UNION ALL + SELECT med_name_10 FROM Clinical_Current_Medication WHERE med_name_10 IS NOT NULL + UNION ALL + SELECT med_name_11 FROM Clinical_Current_Medication WHERE med_name_11 IS NOT NULL + UNION ALL + SELECT med_name_12 FROM Clinical_Current_Medication WHERE med_name_12 IS NOT NULL + UNION ALL + SELECT med_name_13 FROM Clinical_Current_Medication WHERE med_name_13 IS NOT NULL + UNION ALL + SELECT med_name_14 FROM Clinical_Current_Medication WHERE med_name_14 IS NOT NULL + UNION ALL + SELECT med_name_15 FROM Clinical_Current_Medication WHERE med_name_15 IS NOT NULL + UNION ALL + SELECT med_name_16 FROM Clinical_Current_Medication WHERE med_name_16 IS NOT NULL + UNION ALL + SELECT med_name_17 FROM Clinical_Current_Medication WHERE med_name_17 IS NOT NULL + UNION ALL + SELECT med_name_18 FROM Clinical_Current_Medication WHERE med_name_18 IS NOT NULL + UNION ALL + SELECT med_name_19 FROM Clinical_Current_Medication WHERE med_name_19 IS NOT NULL + UNION ALL + SELECT med_name_20 FROM Clinical_Current_Medication WHERE med_name_20 IS NOT NULL + UNION ALL + SELECT med_name_21 FROM Clinical_Current_Medication WHERE med_name_21 IS NOT NULL + UNION ALL + SELECT med_name_22 FROM Clinical_Current_Medication WHERE med_name_22 IS NOT NULL + UNION ALL + SELECT med_name_23 FROM Clinical_Current_Medication WHERE med_name_23 IS NOT NULL + UNION ALL + SELECT med_name_24 FROM Clinical_Current_Medication WHERE med_name_24 IS NOT NULL + UNION ALL + SELECT med_name_25 FROM Clinical_Current_Medication WHERE med_name_25 IS NOT NULL + UNION ALL + SELECT med_name_26 FROM Clinical_Current_Medication WHERE med_name_26 IS NOT NULL + UNION ALL + SELECT med_name_27 FROM Clinical_Current_Medication WHERE med_name_27 IS NOT NULL + UNION ALL + SELECT med_name_28 FROM Clinical_Current_Medication WHERE med_name_28 IS NOT NULL + UNION ALL + SELECT med_name_29 FROM Clinical_Current_Medication WHERE med_name_29 IS NOT NULL + UNION ALL + SELECT med_name_30 FROM Clinical_Current_Medication WHERE med_name_30 IS NOT NULL + UNION ALL + SELECT med_name_31 FROM Clinical_Current_Medication WHERE med_name_31 IS NOT NULL + UNION ALL + SELECT med_name_32 FROM Clinical_Current_Medication WHERE med_name_32 IS NOT NULL +) AS all_meds +GROUP BY med_name +ORDER BY total_count DESC; diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index d7a6ef7e90c..fd836abac4b 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -966,7 +966,7 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface } else { $headerWhere = "('Y', 'N')"; } - return $DB->pselectRow( + return iterator_to_array($DB->pselect( "SELECT p.Name as PolicyName, Version, @@ -985,18 +985,13 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface $policyName AND p.Active = 'Y' AND p.HeaderButton IN $headerWhere - AND Version = ( - SELECT MAX(Version) - FROM policies - WHERE ModuleID = m.ID - AND Name = p.Name - AND p.HeaderButton IN $headerWhere - ) + ORDER BY p.Version DESC + LIMIT 1 ", [ 'moduleName' => $moduleName ] - ) ?? []; + ))[0] ?? []; } /** @@ -1039,27 +1034,26 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface if (empty($policyInfo)) { return []; } - $decision = $DB->pselectRow( + $decision = iterator_to_array($DB->pselect( "SELECT Decision, DecisionDate FROM user_policy_decision WHERE UserID = :uid AND PolicyID=:policyID - AND DecisionDate = ( - SELECT MAX(DecisionDate) - FROM user_policy_decision - WHERE - UserID = :uid - AND PolicyID=:policyID - ) + ORDER BY DecisionDate DESC + LIMIT 1 ", [ 'uid' => $userID, 'policyID' => $policyInfo['PolicyID'] ] - ) ?? []; + ))[0] ?? []; $needsRenewal = true; - if ($decision && $policyInfo['PolicyRenewalTime'] > 0) { + if ( + $decision + && $decision['Decision'] == 'Accepted' + && $policyInfo['PolicyRenewalTime'] > 0 + ) { $decisionDate = new \DateTime($decision['DecisionDate']); $interval = new \DateInterval( "P{$policyInfo['PolicyRenewalTime']}". diff --git a/raisinbread/RB_files/RB_modules.sql b/raisinbread/RB_files/RB_modules.sql index 8d3b64376b6..02c66566447 100644 --- a/raisinbread/RB_files/RB_modules.sql +++ b/raisinbread/RB_files/RB_modules.sql @@ -50,5 +50,6 @@ INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (47,'electrophysiology_upl INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (48,'schedule_module','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (49,'dataquery','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (50,'oidc','N'); +INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (51,'policy_tracker','Y'); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_policies.sql b/raisinbread/RB_files/RB_policies.sql index ac752ee4575..03ed0ae1f71 100644 --- a/raisinbread/RB_files/RB_policies.sql +++ b/raisinbread/RB_files/RB_policies.sql @@ -1,7 +1,7 @@ SET FOREIGN_KEY_CHECKS=0; TRUNCATE TABLE `policies`; LOCK TABLES `policies` WRITE; -INSERT INTO `policies` (`PolicyID`, `Name`, `Version`, `ModuleID`, `PolicyRenewalTime`, `PolicyRenewalTimeUnit`, `Content`, `SwalTitle`, `HeaderButton`, `HeaderButtonText`, `Active`, `AcceptButtonText`, `DeclineButtonText`, `CreatedAt`, `UpdatedAt`) VALUES (1,'dataquery_example','v1',49,7,'D','By using this Data Query Tool, you acknowledge that you know it is in beta and may not work as expected. You also agree to use it responsibly and not to misuse the data.','Terms Of Use','Y','Terms Of Use','Y','Yes, I accept','Decline','2025-05-28 10:54:21','2025-05-28 10:54:21'); -INSERT INTO `policies` (`PolicyID`, `Name`, `Version`, `ModuleID`, `PolicyRenewalTime`, `PolicyRenewalTimeUnit`, `Content`, `SwalTitle`, `HeaderButton`, `HeaderButtonText`, `Active`, `AcceptButtonText`, `DeclineButtonText`, `CreatedAt`, `UpdatedAt`) VALUES (2,'login_example','v1',28,7,'D','By using this LORIS instance you acknowledge that you know it is filled with test data, and not real user data.','Terms Of Use','Y','Terms Of Use','Y','Accept','','2025-05-28 10:54:23','2025-05-28 10:54:23'); +INSERT INTO `policies` (`PolicyID`, `Name`, `Version`, `ModuleID`, `PolicyRenewalTime`, `PolicyRenewalTimeUnit`, `Content`, `SwalTitle`, `HeaderButton`, `HeaderButtonText`, `Active`, `AcceptButtonText`, `DeclineButtonText`, `CreatedAt`, `UpdatedAt`) VALUES (1,'dataquery_example',1,49,7,'D','By using this Data Query Tool, you acknowledge that you know it is in beta and may not work as expected. You also agree to use it responsibly and not to misuse the data.','Terms of Use','Y','Terms of Use','Y','Yes, I accept','Decline','2025-05-28 10:54:21','2025-05-28 10:54:21'); +INSERT INTO `policies` (`PolicyID`, `Name`, `Version`, `ModuleID`, `PolicyRenewalTime`, `PolicyRenewalTimeUnit`, `Content`, `SwalTitle`, `HeaderButton`, `HeaderButtonText`, `Active`, `AcceptButtonText`, `DeclineButtonText`, `CreatedAt`, `UpdatedAt`) VALUES (2,'login_example',1,28,7,'D','By using this LORIS instance you acknowledge that you know it is filled with test data, and not real user data.','Terms of Use','Y','Terms of Use','Y','Accept','','2025-05-28 10:54:23','2025-05-28 10:54:23'); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; From 2db791cab238c209a172d85205281d5cf7c81848 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 17 Jul 2025 12:22:34 -0400 Subject: [PATCH 17/19] Fix formatting --- php/libraries/NDB_Page.class.inc | 33 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index fd836abac4b..4dcae7181c8 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -1034,23 +1034,24 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface if (empty($policyInfo)) { return []; } - $decision = iterator_to_array($DB->pselect( - "SELECT Decision, DecisionDate - FROM user_policy_decision - WHERE - UserID = :uid - AND PolicyID=:policyID - ORDER BY DecisionDate DESC - LIMIT 1 - ", - [ - 'uid' => $userID, - 'policyID' => $policyInfo['PolicyID'] - ] - ))[0] ?? []; + $decision = iterator_to_array( + $DB->pselect( + "SELECT Decision, DecisionDate + FROM user_policy_decision + WHERE + UserID = :uid + AND PolicyID=:policyID + ORDER BY DecisionDate DESC + LIMIT 1 + ", + [ + 'uid' => $userID, + 'policyID' => $policyInfo['PolicyID'] + ] + ) + )[0] ?? []; $needsRenewal = true; - if ( - $decision + if ($decision && $decision['Decision'] == 'Accepted' && $policyInfo['PolicyRenewalTime'] > 0 ) { From d4589e9f29638add167e3e3a148ff56f824b16c1 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 17 Jul 2025 12:38:22 -0400 Subject: [PATCH 18/19] Fix formatting again --- php/libraries/NDB_Page.class.inc | 54 +++++++++++++++++--------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/php/libraries/NDB_Page.class.inc b/php/libraries/NDB_Page.class.inc index 4dcae7181c8..52af7425c5e 100644 --- a/php/libraries/NDB_Page.class.inc +++ b/php/libraries/NDB_Page.class.inc @@ -966,32 +966,34 @@ class NDB_Page extends \LORIS\Http\Endpoint implements RequestHandlerInterface } else { $headerWhere = "('Y', 'N')"; } - return iterator_to_array($DB->pselect( - "SELECT - p.Name as PolicyName, - Version, - Content, - SwalTitle, - HeaderButtonText, - AcceptButtonText, - DeclineButtonText, - m.Name as ModuleName, - PolicyID, - PolicyRenewalTime, - PolicyRenewalTimeUnit - FROM policies p - JOIN modules m ON m.ID = p.ModuleID - WHERE m.Name = :moduleName - $policyName - AND p.Active = 'Y' - AND p.HeaderButton IN $headerWhere - ORDER BY p.Version DESC - LIMIT 1 - ", - [ - 'moduleName' => $moduleName - ] - ))[0] ?? []; + return iterator_to_array( + $DB->pselect( + "SELECT + p.Name as PolicyName, + Version, + Content, + SwalTitle, + HeaderButtonText, + AcceptButtonText, + DeclineButtonText, + m.Name as ModuleName, + PolicyID, + PolicyRenewalTime, + PolicyRenewalTimeUnit + FROM policies p + JOIN modules m ON m.ID = p.ModuleID + WHERE m.Name = :moduleName + $policyName + AND p.Active = 'Y' + AND p.HeaderButton IN $headerWhere + ORDER BY p.Version DESC + LIMIT 1 + ", + [ + 'moduleName' => $moduleName + ] + ) + )[0] ?? []; } /** From ff7a9c7ece5322bbf7141ec8e3c51634c0aefa8d Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 30 Jul 2025 15:31:53 -0400 Subject: [PATCH 19/19] Requested cleanp --- SQL/sql_for_abstract.sql | 391 -------------------------------------- jsx/PolicyButton.js | 2 +- smarty/templates/main.tpl | 4 +- 3 files changed, 3 insertions(+), 394 deletions(-) delete mode 100644 SQL/sql_for_abstract.sql diff --git a/SQL/sql_for_abstract.sql b/SQL/sql_for_abstract.sql deleted file mode 100644 index 60626afddb8..00000000000 --- a/SQL/sql_for_abstract.sql +++ /dev/null @@ -1,391 +0,0 @@ -SELECT count(DISTINCT cr.ResolvedID) -FROM conflicts_resolved cr -JOIN ( - SELECT f.CommentID - FROM flag f - JOIN session s ON f.SessionID = s.ID - WHERE s.Visit_label IN ( - 'Initial_Assessment_Screening', - 'Initial_MRI', - 'Repeat_Initial_Assessment_Screening', - 'Clinical_Assessment', - 'Neuropsychology_Assessment' - ) - -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) - AND Test_name NOT IN ( -'Annual_Follow_up_Phone_Call', -'Autopsy_Banked_Tissue_Inventory', -'Autopsy_Neuropathology_Report', -'Clinical_Current_Medication', -'Clinical_Current_Medication_FU', -'Clinical_Diagnosis_Confirmation', -'Clinical_Family_History', -'Clinical_Family_History_FU', -'Clinical_Hobbies_And_Leisure_Activities', -'Clinical_Initial_Symptoms', -'Clinical_Lumbar_Puncture_Procedure_Report', -'Clinical_Medical_History', -'Clinical_Medical_History_FU', -'Clinical_Mental_Health_History', -'Clinical_Mental_Health_History_FU', -'Clinical_Past_Medications', -'Clinical_Past_Medications_FU', -'Clinical_Pre_Gait_Assessment', -'Clinical_Signs_Symptoms', -'Clinical_Sleep_Work_Diary', -'Clinical_Smoking', -'Clinical_Surgical_History', -'Clinical_Surgical_History_FU', -'Clinical_UPDRS_III_IV', -'General_Health_Alcohol_Consumption', -'General_Health_Biosample_Collection', -'General_Health_Caregiving_Assessment', -'General_Health_Falls_Balance', -'General_Health_Gait_Assessment', -'General_Health_Grip_Strength', -'General_Health_Nutrition', -'General_Health_Physical_Activity', -'General_Health_Physical_Activity_FU', -'General_Health_Sleep', -'General_Health_Vision', -'mri_parameter_form', -'PI_Caregiving_Assessment', -'PI_General_Information', -'PI_General_Information_FU', -'Reappraisal_Initial_Diagnosis_Reappraisal', -'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', -'Screening_CERAD_Recog', -'Screening_Driving_History', -'Screening_Driving_History_FU', -'Screening_Education', -'Screening_Education_FU', -'Screening_Employment_History', -'Screening_Employment_History_FU', -'Screening_Languages', -'Screening_Logical_Memory_Delayed', -'Screening_MOCA', -'Screening_Reproductive_History' -) -) fs ON fs.CommentID = cr.CommentID1; - -SELECT count(*) -FROM information_schema.columns -WHERE table_name NOT IN ( -'Annual_Follow_up_Phone_Call', -'Autopsy_Banked_Tissue_Inventory', -'Autopsy_Neuropathology_Report', -'Clinical_Current_Medication', -'Clinical_Current_Medication_FU', -'Clinical_Diagnosis_Confirmation', -'Clinical_Family_History', -'Clinical_Family_History_FU', -'Clinical_Hobbies_And_Leisure_Activities', -'Clinical_Initial_Symptoms', -'Clinical_Lumbar_Puncture_Procedure_Report', -'Clinical_Medical_History', -'Clinical_Medical_History_FU', -'Clinical_Mental_Health_History', -'Clinical_Mental_Health_History_FU', -'Clinical_Past_Medications', -'Clinical_Past_Medications_FU', -'Clinical_Pre_Gait_Assessment', -'Clinical_Signs_Symptoms', -'Clinical_Sleep_Work_Diary', -'Clinical_Smoking', -'Clinical_Surgical_History', -'Clinical_Surgical_History_FU', -'Clinical_UPDRS_III_IV', -'General_Health_Alcohol_Consumption', -'General_Health_Biosample_Collection', -'General_Health_Caregiving_Assessment', -'General_Health_Falls_Balance', -'General_Health_Gait_Assessment', -'General_Health_Grip_Strength', -'General_Health_Nutrition', -'General_Health_Physical_Activity', -'General_Health_Physical_Activity_FU', -'General_Health_Sleep', -'General_Health_Vision', -'mri_parameter_form', -'PI_Caregiving_Assessment', -'PI_General_Information', -'PI_General_Information_FU', -'Reappraisal_Initial_Diagnosis_Reappraisal', -'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', -'Screening_CERAD_Recog', -'Screening_Driving_History', -'Screening_Driving_History_FU', -'Screening_Education', -'Screening_Education_FU', -'Screening_Employment_History', -'Screening_Employment_History_FU', -'Screening_Languages', -'Screening_Logical_Memory_Delayed', -'Screening_MOCA', -'Screening_Reproductive_History' -) - -SELECT count(DISTINCT cr.ConflictID) -FROM conflicts_unresolved cr -JOIN ( - SELECT f.CommentID - FROM flag f - JOIN session s ON f.SessionID = s.ID - WHERE s.Visit_label LIKE '%Time_3%' - -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) --- AND Test_name NOT IN ( --- 'Annual_Follow_up_Phone_Call', --- 'Autopsy_Banked_Tissue_Inventory', --- 'Autopsy_Neuropathology_Report', --- 'Clinical_Current_Medication', --- 'Clinical_Current_Medication_FU', --- 'Clinical_Diagnosis_Confirmation', --- 'Clinical_Family_History', --- 'Clinical_Family_History_FU', --- 'Clinical_Hobbies_And_Leisure_Activities', --- 'Clinical_Initial_Symptoms', --- 'Clinical_Lumbar_Puncture_Procedure_Report', --- 'Clinical_Medical_History', --- 'Clinical_Medical_History_FU', --- 'Clinical_Mental_Health_History', --- 'Clinical_Mental_Health_History_FU', --- 'Clinical_Past_Medications', --- 'Clinical_Past_Medications_FU', --- 'Clinical_Pre_Gait_Assessment', --- 'Clinical_Signs_Symptoms', --- 'Clinical_Sleep_Work_Diary', --- 'Clinical_Smoking', --- 'Clinical_Surgical_History', --- 'Clinical_Surgical_History_FU', --- 'Clinical_UPDRS_III_IV', --- 'General_Health_Alcohol_Consumption', --- 'General_Health_Biosample_Collection', --- 'General_Health_Caregiving_Assessment', --- 'General_Health_Falls_Balance', --- 'General_Health_Gait_Assessment', --- 'General_Health_Grip_Strength', --- 'General_Health_Nutrition', --- 'General_Health_Physical_Activity', --- 'General_Health_Physical_Activity_FU', --- 'General_Health_Sleep', --- 'General_Health_Vision', --- 'mri_parameter_form', --- 'PI_Caregiving_Assessment', --- 'PI_General_Information', --- 'PI_General_Information_FU', --- 'Reappraisal_Initial_Diagnosis_Reappraisal', --- 'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', --- 'Screening_CERAD_Recog', --- 'Screening_Driving_History', --- 'Screening_Driving_History_FU', --- 'Screening_Education', --- 'Screening_Education_FU', --- 'Screening_Employment_History', --- 'Screening_Employment_History_FU', --- 'Screening_Languages', --- 'Screening_Logical_Memory_Delayed', --- 'Screening_MOCA', --- 'Screening_Reproductive_History' --- ) -) fs ON fs.CommentID = cr.CommentID1; - -SELECT count(f.CommentID) - FROM flag f - JOIN session s ON f.SessionID = s.ID - WHERE s.Visit_label IN ( - 'Initial_Assessment_Screening', - 'Initial_MRI', - 'Repeat_Initial_Assessment_Screening', - 'Clinical_Assessment', - 'Neuropsychology_Assessment' - ) - AND CommentID LIKE 'DDE_%' - AND Data_entry IS NOT NULL - -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) - AND Test_name NOT IN ( - 'Annual_Follow_up_Phone_Call', - 'Autopsy_Banked_Tissue_Inventory' - 'Autopsy_Neuropathology_Report', - 'Clinical_Current_Medication', - 'Clinical_Current_Medication_FU', - 'Clinical_Diagnosis_Confirmation', - 'Clinical_Family_History', - 'Clinical_Family_History_FU', - 'Clinical_Hobbies_And_Leisure_Activities', - 'Clinical_Initial_Symptoms', - 'Clinical_Lumbar_Puncture_Procedure_Report', - 'Clinical_Medical_History', - 'Clinical_Medical_History_FU', - 'Clinical_Mental_Health_History', - 'Clinical_Mental_Health_History_FU', - 'Clinical_Past_Medications', - 'Clinical_Past_Medications_FU', - 'Clinical_Pre_Gait_Assessment', - 'Clinical_Signs_Symptoms', - 'Clinical_Sleep_Work_Diary', - 'Clinical_Smoking', - 'Clinical_Surgical_History', - 'Clinical_Surgical_History_FU', - 'Clinical_UPDRS_III_IV', - 'General_Health_Alcohol_Consumption', - 'General_Health_Biosample_Collection', - 'General_Health_Caregiving_Assessment', - 'General_Health_Falls_Balance', - 'General_Health_Gait_Assessment', - 'General_Health_Grip_Strength', - 'General_Health_Nutrition', - 'General_Health_Physical_Activity', - 'General_Health_Physical_Activity_FU', - 'General_Health_Sleep', - 'General_Health_Vision', - 'mri_parameter_form', - 'PI_Caregiving_Assessment', - 'PI_General_Information', - 'PI_General_Information_FU', - 'Reappraisal_Initial_Diagnosis_Reappraisal', - 'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', - 'Screening_CERAD_Recog', - 'Screening_Driving_History', - 'Screening_Driving_History_FU', - 'Screening_Education', - 'Screening_Education_FU', - 'Screening_Employment_History', - 'Screening_Employment_History_FU', - 'Screening_Languages', - 'Screening_Logical_Memory_Delayed', - 'Screening_MOCA', - 'Screening_Reproductive_History' - ); - - SELECT count(f.CommentID) - FROM flag f - JOIN session s ON f.SessionID = s.ID - WHERE s.Visit_label LIKE '%Time_3%' - AND Data_entry IS NOT NULL - AND CommentID LIKE 'DDE_%' - -- AND s.SubprojectID IN (1,2,3,4,5,6,7,8,9,10,14,17,18) - AND Test_name NOT IN ( - 'Annual_Follow_up_Phone_Call', - 'Autopsy_Banked_Tissue_Inventory', - 'Autopsy_Neuropathology_Report', - 'Clinical_Current_Medication', - 'Clinical_Current_Medication_FU', - 'Clinical_Diagnosis_Confirmation', - 'Clinical_Family_History', - 'Clinical_Family_History_FU', - 'Clinical_Hobbies_And_Leisure_Activities', - 'Clinical_Initial_Symptoms', - 'Clinical_Lumbar_Puncture_Procedure_Report', - 'Clinical_Medical_History', - 'Clinical_Medical_History_FU', - 'Clinical_Mental_Health_History', - 'Clinical_Mental_Health_History_FU', - 'Clinical_Past_Medications', - 'Clinical_Past_Medications_FU', - 'Clinical_Pre_Gait_Assessment', - 'Clinical_Signs_Symptoms', - 'Clinical_Sleep_Work_Diary', - 'Clinical_Smoking', - 'Clinical_Surgical_History', - 'Clinical_Surgical_History_FU', - 'Clinical_UPDRS_III_IV', - 'General_Health_Alcohol_Consumption', - 'General_Health_Biosample_Collection', - 'General_Health_Caregiving_Assessment', - 'General_Health_Falls_Balance', - 'General_Health_Gait_Assessment', - 'General_Health_Grip_Strength', - 'General_Health_Nutrition', - 'General_Health_Physical_Activity', - 'General_Health_Physical_Activity_FU', - 'General_Health_Sleep', - 'General_Health_Vision', - 'mri_parameter_form', - 'PI_Caregiving_Assessment', - 'PI_General_Information', - 'PI_General_Information_FU', - 'Reappraisal_Initial_Diagnosis_Reappraisal', - 'Reappraisal_Initial_Diagnosis_Reappraisal_IMPACT_AD', - 'Screening_CERAD_Recog', - 'Screening_Driving_History', - 'Screening_Driving_History_FU', - 'Screening_Education', - 'Screening_Education_FU', - 'Screening_Employment_History', - 'Screening_Employment_History_FU', - 'Screening_Languages', - 'Screening_Logical_Memory_Delayed', - 'Screening_MOCA', - 'Screening_Reproductive_History' - ) - - -SELECT med_name, COUNT(*) AS total_count -FROM ( - SELECT med_name_1 AS med_name FROM Clinical_Current_Medication WHERE med_name_1 IS NOT NULL - UNION ALL - SELECT med_name_2 FROM Clinical_Current_Medication WHERE med_name_2 IS NOT NULL - UNION ALL - SELECT med_name_3 FROM Clinical_Current_Medication WHERE med_name_3 IS NOT NULL - UNION ALL - SELECT med_name_4 FROM Clinical_Current_Medication WHERE med_name_4 IS NOT NULL - UNION ALL - SELECT med_name_5 FROM Clinical_Current_Medication WHERE med_name_5 IS NOT NULL - UNION ALL - SELECT med_name_6 FROM Clinical_Current_Medication WHERE med_name_6 IS NOT NULL - UNION ALL - SELECT med_name_7 FROM Clinical_Current_Medication WHERE med_name_7 IS NOT NULL - UNION ALL - SELECT med_name_8 FROM Clinical_Current_Medication WHERE med_name_8 IS NOT NULL - -- Continue this pattern up to med_name_32 - UNION ALL - SELECT med_name_9 FROM Clinical_Current_Medication WHERE med_name_9 IS NOT NULL - UNION ALL - SELECT med_name_10 FROM Clinical_Current_Medication WHERE med_name_10 IS NOT NULL - UNION ALL - SELECT med_name_11 FROM Clinical_Current_Medication WHERE med_name_11 IS NOT NULL - UNION ALL - SELECT med_name_12 FROM Clinical_Current_Medication WHERE med_name_12 IS NOT NULL - UNION ALL - SELECT med_name_13 FROM Clinical_Current_Medication WHERE med_name_13 IS NOT NULL - UNION ALL - SELECT med_name_14 FROM Clinical_Current_Medication WHERE med_name_14 IS NOT NULL - UNION ALL - SELECT med_name_15 FROM Clinical_Current_Medication WHERE med_name_15 IS NOT NULL - UNION ALL - SELECT med_name_16 FROM Clinical_Current_Medication WHERE med_name_16 IS NOT NULL - UNION ALL - SELECT med_name_17 FROM Clinical_Current_Medication WHERE med_name_17 IS NOT NULL - UNION ALL - SELECT med_name_18 FROM Clinical_Current_Medication WHERE med_name_18 IS NOT NULL - UNION ALL - SELECT med_name_19 FROM Clinical_Current_Medication WHERE med_name_19 IS NOT NULL - UNION ALL - SELECT med_name_20 FROM Clinical_Current_Medication WHERE med_name_20 IS NOT NULL - UNION ALL - SELECT med_name_21 FROM Clinical_Current_Medication WHERE med_name_21 IS NOT NULL - UNION ALL - SELECT med_name_22 FROM Clinical_Current_Medication WHERE med_name_22 IS NOT NULL - UNION ALL - SELECT med_name_23 FROM Clinical_Current_Medication WHERE med_name_23 IS NOT NULL - UNION ALL - SELECT med_name_24 FROM Clinical_Current_Medication WHERE med_name_24 IS NOT NULL - UNION ALL - SELECT med_name_25 FROM Clinical_Current_Medication WHERE med_name_25 IS NOT NULL - UNION ALL - SELECT med_name_26 FROM Clinical_Current_Medication WHERE med_name_26 IS NOT NULL - UNION ALL - SELECT med_name_27 FROM Clinical_Current_Medication WHERE med_name_27 IS NOT NULL - UNION ALL - SELECT med_name_28 FROM Clinical_Current_Medication WHERE med_name_28 IS NOT NULL - UNION ALL - SELECT med_name_29 FROM Clinical_Current_Medication WHERE med_name_29 IS NOT NULL - UNION ALL - SELECT med_name_30 FROM Clinical_Current_Medication WHERE med_name_30 IS NOT NULL - UNION ALL - SELECT med_name_31 FROM Clinical_Current_Medication WHERE med_name_31 IS NOT NULL - UNION ALL - SELECT med_name_32 FROM Clinical_Current_Medication WHERE med_name_32 IS NOT NULL -) AS all_meds -GROUP BY med_name -ORDER BY total_count DESC; diff --git a/jsx/PolicyButton.js b/jsx/PolicyButton.js index f31921d7ee6..a8afcf20400 100644 --- a/jsx/PolicyButton.js +++ b/jsx/PolicyButton.js @@ -36,7 +36,7 @@ const PolicyButton = ({ if (onClickPolicy) { return { diff --git a/smarty/templates/main.tpl b/smarty/templates/main.tpl index 97c7b31303b..adddaf6668a 100644 --- a/smarty/templates/main.tpl +++ b/smarty/templates/main.tpl @@ -75,7 +75,7 @@ }); headerPolicyRoot = ReactDOM.createRoot( - document.getElementById("headerPolicyButton") + document.getElementById("header-policy-button") ); headerPolicyRoot.render( React.createElement(PolicyButton, { @@ -154,7 +154,7 @@ {/if} {if $header_policy} - + {/if}