From c78c6cd6e2170d2b28d0cbc7299a30fcbc16c7eb Mon Sep 17 00:00:00 2001 From: Jordan Schultz Date: Wed, 10 Jan 2024 20:02:10 -0800 Subject: [PATCH 01/44] Refactored refresh from server, fixed bug stacking interval callbacks --- RedcapNotifications.php | 4 +- assets/scripts/NotificationController.js | 64 +++++++++++++----------- assets/scripts/jsmo.js | 6 +++ 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 378404f..90efec3 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -531,7 +531,7 @@ private function getDismissedNotifications($dismissal_pid, $user) { public function injectREDCapNotifs(){ global $Proj; - $notifs_jsmo = $this->getUrl("assets/scripts/jsmo.js", true); + $jsmo = $this->getUrl("assets/scripts/jsmo.js", true); $utility_js = $this->getUrl("assets/scripts/utility.js", true); $notif_cls = $this->getUrl("assets/scripts/Notification.js", true); @@ -559,7 +559,7 @@ public function injectREDCapNotifs(){ - + - - + + @@ -548,10 +560,11 @@ public function injectREDCapNotifs(){ * @param $user * @return string */ - public function clean_user($user){ - $user = str_replace(" ","_", $user); - $user = str_replace("[","", $user); - $user = str_replace("]","", $user); + public function clean_user($user) + { + $user = str_replace(" ", "_", $user); + $user = str_replace("[", "", $user); + $user = str_replace("]", "", $user); return $user; } @@ -561,9 +574,10 @@ public function clean_user($user){ * @param * @return array */ - public function getForceRefreshSetting(){ - $existing_json = $this->getSystemSetting("force_refresh_ts"); - $existing_arr = empty($existing_json) ? array() : json_decode($existing_json, true); + public function getForceRefreshSetting() + { + $existing_json = $this->getSystemSetting("force_refresh_ts"); + $existing_arr = empty($existing_json) ? array() : json_decode($existing_json, true); return $existing_arr; } @@ -571,13 +585,14 @@ public function getForceRefreshSetting(){ /** * Set a new notif record id with timestamp to force refresh * - * @param $record, $last_ts + * @param $record , $last_ts * @return void */ - public function setForceRefreshSetting($record, $last_ts){ + public function setForceRefreshSetting($record, $last_ts) + { $existing_arr = $this->getForceRefreshSetting(); - if(!array_key_exists($record, $existing_arr)){ + if (!array_key_exists($record, $existing_arr)) { $existing_arr[$record] = null; } @@ -592,13 +607,14 @@ public function setForceRefreshSetting($record, $last_ts){ * @param * @return void */ - public function emDebugForCustomUseridList(){ - $temp = $this->getSystemSetting("user-specific-log-list"); - $temp = str_replace(" ", "", $temp); - $custom_log_list = empty($temp) ? [] : explode(",", $temp); + public function emDebugForCustomUseridList() + { + $temp = $this->getSystemSetting("user-specific-log-list"); + $temp = str_replace(" ", "", $temp); + $custom_log_list = empty($temp) ? [] : explode(",", $temp); $cur_user = $this->getUser()->getUsername(); - if(in_array($cur_user, $custom_log_list)){ + if (in_array($cur_user, $custom_log_list)) { $args = func_get_args(); $this->emDebug("REDCapNotifs Custom Debug for $cur_user", $args); } @@ -606,7 +622,8 @@ public function emDebugForCustomUseridList(){ /* AJAX HANDLING IN HERE INSTEAD OF A STAND ALONE PAGE? */ - public function redcap_module_ajax($action, $payload, $project_id, $record, $instrument, $event_id, $repeat_instance, $survey_hash, $response_id, $survey_queue_hash, $page, $page_full, $user_id, $group_id) { + public function redcap_module_ajax($action, $payload, $project_id, $record, $instrument, $event_id, $repeat_instance, $survey_hash, $response_id, $survey_queue_hash, $page, $page_full, $user_id, $group_id) + { // $this->emDebug(func_get_args()); // $this->emDebug("is redcap_module_ajax a reserved name?", // $action, @@ -622,7 +639,7 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins //NO LONGER SEPARATE ACTIONS, THEY ALL FLOW THROUGH QUEUE //REMOVE THIS SWITCH WHEN WORKFLOW FINALIZED - switch($action){ + switch ($action) { case "get_full_payload": case "save_dismissals": case "check_forced_refresh": @@ -638,37 +655,22 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins // NO // x APPEND TO QUEUE and return empty payload - - if( $jobQueue = ProcessQueue::getJobQueue($this) ){ - $job_id = session_id() . "_" . $action; - $json_str = $jobQueue->getValue($job_id); - $result = array(); - - if(empty($json_str)){ - //NOT IN QUEUE SO ADD IT AND SAVE IT AND RETURN EMPTY ARRAY with property indicating in QUEUE - $payload = $payload ?? []; - - if(!($action == "save_dismissals" && empty($payload["dismiss_notifs"]))){ - $this->emDebug("not in queue, make new job in queue for $job_id", $payload); - $jobQueue->setValue($job_id, json_encode($payload)); - $jobQueue->save(); - } - }else{ - $json = json_decode($json_str, 1); - - $if_no_results_update_params = json_encode($payload); - //FOUND IN QUEUE, LETS SEE IF IT HAS RESULTS YET? IF SO RETURN THOSE - if(array_key_exists("results", $json)){ - $result = $json["results"]; - // Damn i think need to delete in real time, cause forced refresh. still clear every 24 minutes anyway. - $if_no_results_update_params = null; - } - $jobQueue->setValue($job_id, $if_no_results_update_params); - $jobQueue->save(); + $apiObject = $this->getAPIObject(); + if ($apiObject) { + $project_id = null; + $admin_rights = null; + $project_status = null; + if(defined('PROJECT_ID')){ + $project_id = PROJECT_ID; + global $Proj; + $project_status = $Proj->project['status']; } - - $return_o["results"] = $result; - $return_o["success"] = true; + if(defined('ADMIN_RIGHTS')){ + $admin_rights = ADMIN_RIGHTS; + } + $return_o = $apiObject->getNotifications($project_id, $project_status, $admin_rights); + } else { + throw new \Exception("No notifications"); } break; @@ -687,16 +689,17 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins * @return void * @throws \GuzzleHttp\Exception\GuzzleException */ - public function processJobQueue() { + public function processJobQueue() + { //GET THE JOB QUEUE AND ALL THE UN PROCESSED JOBS AND DO ALL OF THEM AND STUFF THEM IN $processed_job array - $current_queue = ProcessQueue::getUnProcessedJobs($this); - $processed_job = array(); - foreach($current_queue as $job_id => $json_string) { - [$session_id, $action] = explode('_', $job_id, 2); - $payload = json_decode($json_string,1); - - if(isset($payload["results"]) || empty($payload["user"]) || empty($action)){ - continue; + $current_queue = ProcessQueue::getUnProcessedJobs($this); + $processed_job = array(); + foreach ($current_queue as $job_id => $json_string) { + [$session_id, $action] = explode('_', $job_id, 2); + $payload = json_decode($json_string, 1); + + if (isset($payload["results"]) || empty($payload["user"]) || empty($action)) { + continue; } $user_name = $payload["user"]; @@ -708,9 +711,9 @@ public function processJobQueue() { case "get_full_payload" : try { $apiObject = $this->getAPIObject(); - if($apiObject){ + if ($apiObject) { $apiObject->getNotifications(); - }else{ + } else { throw new \Exception("No notifications"); } } catch (\Exception $e) { @@ -722,10 +725,10 @@ public function processJobQueue() { case "check_forced_refresh" : try { - $last_updated = new DateTime($payload["last_updated"]); - $force_results = $payload["results"] = $this->getForceRefreshSetting(); + $last_updated = new DateTime($payload["last_updated"]); + $force_results = $payload["results"] = $this->getForceRefreshSetting(); - $dates = array_map(function($date) { + $dates = array_map(function ($date) { return new DateTime($date); }, $force_results); @@ -743,31 +746,31 @@ public function processJobQueue() { case "save_dismissals" : $dismiss_notifs = $payload['dismiss_notifs']; - $new_timestamp = new DateTime(); - $now = $new_timestamp->format('Y-m-d H:i:s'); + $new_timestamp = new DateTime(); + $now = $new_timestamp->format('Y-m-d H:i:s'); - $dismissalPid = $this->getSystemProjectIDs('dismissal-pid'); - if(count($dismiss_notifs)){ - $data = array(); + $dismissalPid = $this->getSystemProjectIDs('dismissal-pid'); + if (count($dismiss_notifs)) { + $data = array(); $return_ids = array(); - foreach($dismiss_notifs as $notif){ - $newRecordId = REDCap::reserveNewRecordId($dismissalPid); - $data[] = array( - "record_id" => $newRecordId, - "note_record_id" => $notif["record_id"], - "note_name" => $notif['note_name'], - "note_username" => $notif['note_username'], - "note_dismissal_datetime" => $now + foreach ($dismiss_notifs as $notif) { + $newRecordId = REDCap::reserveNewRecordId($dismissalPid); + $data[] = array( + "record_id" => $newRecordId, + "note_record_id" => $notif["record_id"], + "note_name" => $notif['note_name'], + "note_username" => $notif['note_username'], + "note_dismissal_datetime" => $now ); - $return_ids[] = $notif["record_id"]; + $return_ids[] = $notif["record_id"]; } - $results = REDCap::saveData($dismissalPid, 'json', json_encode($data)); + $results = REDCap::saveData($dismissalPid, 'json', json_encode($data)); $this->emDebug("need to return the dismissed record_ids", $return_ids); $this->emDebug("Save Return results: " . json_encode($results) . " for notification: " . json_encode($dismissData)); - $payload["results"] = $return_ids; + $payload["results"] = $return_ids; $processed_job[$job_id] = $payload; - }else{ + } else { $this->emError("Cannot save dismissed notification because record set was empty or there was invalid data"); } break; @@ -776,7 +779,7 @@ public function processJobQueue() { //NOW LOOP THROUGH THAT ARRAY AND SAVE BACK TO THE PARAMETERS WITH TEH $jobQueue Obje - if(!empty($processed_job)){ + if (!empty($processed_job)) { $jobQueue = ProcessQueue::getJobQueue($this); $jobQueue->setValues($processed_job); $jobQueue->save(); @@ -791,9 +794,10 @@ public function processJobQueue() { * @return void * @throws \GuzzleHttp\Exception\GuzzleException */ - public function clearJobQueue(){ + public function clearJobQueue() + { $result = false; - if( $jobQueue = ProcessQueue::getJobQueue($this) ){ + if ($jobQueue = ProcessQueue::getJobQueue($this)) { $jobQueue->delete(); $result = true; } @@ -807,9 +811,9 @@ public function clearJobQueue(){ public function getAPIObject(): \Stanford\RedcapNotificationsAPI\RedcapNotificationsAPI { // check Notification EM is enabled first. - if(!$this->APIObject AND $this->framework->isModuleEnabled($this->getSystemSetting('notification-api'))){ + if (!$this->APIObject and $this->framework->isModuleEnabled($this->getSystemSetting('notification-api'))) { $this->setAPIObject(\ExternalModules\ExternalModules::getModuleInstance($this->getSystemSetting('notification-api'))); - }else{ + } else { REDCap::logEvent('REDCap Notification EM is not Enabled.'); } return $this->APIObject; @@ -824,10 +828,10 @@ public function setAPIObject(\Stanford\RedcapNotificationsAPI\RedcapNotification } - } -function isValid($date, $format = 'Y-m-d'){ +function isValid($date, $format = 'Y-m-d') +{ $dt = DateTime::createFromFormat($format, $date); return $dt && $dt->format($format) === $date; } From e4f7137f94e467ef2d2f7b90f16a15b3ccf6fd23 Mon Sep 17 00:00:00 2001 From: ihabz Date: Thu, 8 Feb 2024 16:50:07 -0800 Subject: [PATCH 17/44] fix datatype error. --- RedcapNotifications.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 61e7659..7be8ab7 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -808,7 +808,7 @@ public function clearJobQueue() /** * @return \Stanford\RedcapNotificationsAPI\RedcapNotificationsAPI */ - public function getAPIObject(): \Stanford\RedcapNotificationsAPI\RedcapNotificationsAPI + public function getAPIObject() { // check Notification EM is enabled first. if (!$this->APIObject and $this->framework->isModuleEnabled($this->getSystemSetting('notification-api'))) { From 1fb17dc98f1a2dc9896b22acde515886608649b4 Mon Sep 17 00:00:00 2001 From: ihabz Date: Fri, 9 Feb 2024 10:32:37 -0800 Subject: [PATCH 18/44] add notification dismissal --- RedcapNotifications.php | 184 ++++++---------------------------------- config.json | 17 +--- 2 files changed, 28 insertions(+), 173 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 7be8ab7..733c018 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -1,4 +1,5 @@ getSystemSetting("force_refresh_ts"); - $existing_arr = empty($existing_json) ? array() : json_decode($existing_json, true); - - return $existing_arr; - } - - /** - * Set a new notif record id with timestamp to force refresh - * - * @param $record , $last_ts - * @return void - */ - public function setForceRefreshSetting($record, $last_ts) - { - $existing_arr = $this->getForceRefreshSetting(); - - if (!array_key_exists($record, $existing_arr)) { - $existing_arr[$record] = null; - } - - $existing_arr[$record] = $last_ts; - $this->setSystemSetting("force_refresh_ts", json_encode($existing_arr)); - } - /** * display emdebugs only for custom comma delimited list of userids to debug for select subset of userids to try to find out why they constantly callback for notif payloads @@ -641,8 +610,8 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins //REMOVE THIS SWITCH WHEN WORKFLOW FINALIZED switch ($action) { case "get_full_payload": - case "save_dismissals": - case "check_forced_refresh": + + // case "check_forced_refresh": // CHECK // IS QUEUE AVAILABLE? @@ -660,12 +629,12 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins $project_id = null; $admin_rights = null; $project_status = null; - if(defined('PROJECT_ID')){ + if (defined('PROJECT_ID')) { $project_id = PROJECT_ID; global $Proj; $project_status = $Proj->project['status']; } - if(defined('ADMIN_RIGHTS')){ + if (defined('ADMIN_RIGHTS')) { $admin_rights = ADMIN_RIGHTS; } $return_o = $apiObject->getNotifications($project_id, $project_status, $admin_rights); @@ -673,7 +642,29 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins throw new \Exception("No notifications"); } break; + case "save_dismissals": + $dismiss_notifs = $payload['dismiss_notifs']; + if (count($dismiss_notifs)) { + try { + $apiObject = $this->getAPIObject(); + if ($apiObject) { + foreach ($dismiss_notifs as $notif) { + if(!$apiObject->dismissNotification($notif["record_id"])){ + throw new \Exception("Cant dismiss Notification '" .$notif["record_id"]. "'"); + }; + $return_o[] = $notif["record_id"]; + } + $this->emDebug("need to return the dismissed record_ids", $return_o); + } else { + throw new \Exception("No notifications"); + } + } catch (\Exception $e) { + return $e->getMessage(); + }; + } else { + $this->emError("Cannot save dismissed notification because record set was empty or there was invalid data"); + } default: $this->emError("Invalid Action"); break; @@ -684,127 +675,6 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins } - /** - * this cron will process Refresh Requests for Notifications Payloads by User - * @return void - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function processJobQueue() - { - //GET THE JOB QUEUE AND ALL THE UN PROCESSED JOBS AND DO ALL OF THEM AND STUFF THEM IN $processed_job array - $current_queue = ProcessQueue::getUnProcessedJobs($this); - $processed_job = array(); - foreach ($current_queue as $job_id => $json_string) { - [$session_id, $action] = explode('_', $job_id, 2); - $payload = json_decode($json_string, 1); - - if (isset($payload["results"]) || empty($payload["user"]) || empty($action)) { - continue; - } - - $user_name = $payload["user"]; - - // LOOP THROUGH THE QUEUE AND PROCESS BASED ON ACTION AND SAVED $payload - // ONCE OUTPUT IS GATHERERED SAVE IT BACK INTO THE QUEUE UNDER - // SAVE THE QUEUE BACK INTO THE LOG TABLE - switch ($action) { - case "get_full_payload" : - try { - $apiObject = $this->getAPIObject(); - if ($apiObject) { - $apiObject->getNotifications(); - } else { - throw new \Exception("No notifications"); - } - } catch (\Exception $e) { - //Entities::createException($e->getMessage()); - return []; - } - - break; - - case "check_forced_refresh" : - try { - $last_updated = new DateTime($payload["last_updated"]); - $force_results = $payload["results"] = $this->getForceRefreshSetting(); - - $dates = array_map(function ($date) { - return new DateTime($date); - }, $force_results); - - // Get the latest date from the array - $max_date = max($dates); - - //Only include if any force is newer than the last updated - if ($max_date > $last_updated) { - $processed_job[$job_id] = $payload; - } - } catch (\Exception $e) { - //Entities::createException($e->getMessage()); - } - break; - - case "save_dismissals" : - $dismiss_notifs = $payload['dismiss_notifs']; - $new_timestamp = new DateTime(); - $now = $new_timestamp->format('Y-m-d H:i:s'); - - $dismissalPid = $this->getSystemProjectIDs('dismissal-pid'); - if (count($dismiss_notifs)) { - $data = array(); - $return_ids = array(); - foreach ($dismiss_notifs as $notif) { - $newRecordId = REDCap::reserveNewRecordId($dismissalPid); - $data[] = array( - "record_id" => $newRecordId, - "note_record_id" => $notif["record_id"], - "note_name" => $notif['note_name'], - "note_username" => $notif['note_username'], - "note_dismissal_datetime" => $now - ); - $return_ids[] = $notif["record_id"]; - } - $results = REDCap::saveData($dismissalPid, 'json', json_encode($data)); - $this->emDebug("need to return the dismissed record_ids", $return_ids); - $this->emDebug("Save Return results: " . json_encode($results) . " for notification: " . json_encode($dismissData)); - - $payload["results"] = $return_ids; - $processed_job[$job_id] = $payload; - } else { - $this->emError("Cannot save dismissed notification because record set was empty or there was invalid data"); - } - break; - } - } - - - //NOW LOOP THROUGH THAT ARRAY AND SAVE BACK TO THE PARAMETERS WITH TEH $jobQueue Obje - if (!empty($processed_job)) { - $jobQueue = ProcessQueue::getJobQueue($this); - $jobQueue->setValues($processed_job); - $jobQueue->save(); - } - - $this->emDebug("cron jobs processed : ", count($processed_job)); - return $processed_job; - } - - /** - * this cron will clear Job Queues every 24 minutes, the average - * @return void - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function clearJobQueue() - { - $result = false; - if ($jobQueue = ProcessQueue::getJobQueue($this)) { - $jobQueue->delete(); - $result = true; - } - - return $result; - } - /** * @return \Stanford\RedcapNotificationsAPI\RedcapNotificationsAPI */ diff --git a/config.json b/config.json index 405cc79..c823ce9 100644 --- a/config.json +++ b/config.json @@ -146,22 +146,7 @@ "type": "text" } ], - "crons": [ - { - "cron_name": "redcap_notifs_refresh_queue", - "cron_description": "This cron will run every N seconds to process notif payload refresh requests.", - "method": "processJobQueue", - "cron_frequency": "60", - "cron_max_run_time": "300" - }, - { - "cron_name": "redcap_notifs_queue_cleanup", - "cron_description": "This cron will run every 24 minutes (max php session time) to delete queued jobs.", - "method": "clearJobQueue", - "cron_frequency": "1440", - "cron_max_run_time": "300" - } - ], + "crons": [], "compatibility": { "php-version-min": "", "php-version-max": "", From c1dc013c66abaf7b0d6e2c7e36a1aa52bacf343f Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Mon, 12 Feb 2024 19:06:59 -0800 Subject: [PATCH 19/44] removing uneccesary files --- classes/ProcessQueue.php | 83 ------- classes/SimpleEmLogObject.php | 402 ---------------------------------- cron/processJobQueue.php | 8 - 3 files changed, 493 deletions(-) delete mode 100644 classes/ProcessQueue.php delete mode 100644 classes/SimpleEmLogObject.php delete mode 100644 cron/processJobQueue.php diff --git a/classes/ProcessQueue.php b/classes/ProcessQueue.php deleted file mode 100644 index 58fd01f..0000000 --- a/classes/ProcessQueue.php +++ /dev/null @@ -1,83 +0,0 @@ -module */ - - /** - * The object holds QUEUED PROCESSING REQUESTS in the EM LOG TABLE - * EM LOG table already has: record, timestamp - */ - - CONST OBJECT_NAME = 'QueuedJob'; // This is the 'name' of the object and stored in the message column - - - public function __construct($module, $type = self::OBJECT_NAME, $log_id = null, $limit_params = []) - { - parent::__construct($module, $type, $log_id, $limit_params); - } - - - /** - * Get The PROCESSED PAYLOAD (IF DONE) - * @param $module obj - * @return array - */ - public static function getUnProcessedJobs($module) { - $jobq = ProcessQueue::getJobQueue($module); - $jobs_in_q = $jobq->getObjectParameters(); - - $unprocessed = array_filter($jobs_in_q, function ($item, $key) { - // Skip 'record_id' - if ($key === 'record_id') return false; - - // Decode the JSON - $data = json_decode($item, true); - - // Check for 'results' key - return !isset($data['results']); - }, ARRAY_FILTER_USE_BOTH); - - return $unprocessed; - } - - - /** - * Create an instance of this ProcessQueue - * @return obj - */ - public static function createJobQueue($module){ - return new ProcessQueue($module); - } - - - /** - * Get existing or new Job QUeue - * @param $module obj - * @return obj - */ - public static function getJobQueue($module) { - //first check if existing jobQueue - $jobQueue = self::queryObjects($module, self::OBJECT_NAME); - if(empty($jobQueue)){ - $jobQueue = ProcessQueue::createJobQueue($module); - $jobQueue->save(); - }else{ - $jobQueue = current($jobQueue); - } - - return $jobQueue; - } - -} diff --git a/classes/SimpleEmLogObject.php b/classes/SimpleEmLogObject.php deleted file mode 100644 index a9502eb..0000000 --- a/classes/SimpleEmLogObject.php +++ /dev/null @@ -1,402 +0,0 @@ -module = $module; - $this->type = $type; - // $this->module->emDebug("Constructor for $type!"); - - if($log_id) { - // Try to get all available EAV parameter entries for log_id - if (empty($limit_params)) { - // Get all params for the log_id - $sql = "select distinct name from redcap_external_modules_log_parameters where log_id=?"; - $result = $module->query($sql, $log_id); - - while ($row = $result->fetch_assoc()) { - $limit_params[] = $row['name']; - } - } - $columns = array_merge(static::MAIN_COLUMNS, $limit_params); - - - // Query all data - $sql = "select " . implode(", ", $columns) . " where log_id=? and message=?"; - - // $module->emDebug("Load Sql: " . $sql); - $q = $module->queryLogs($sql, [$log_id, $type]); - - if ($row = $q->fetch_assoc()) { - foreach ($row as $key=>$val) { - if (property_exists($this, $key)) { -// $this->module->emDebug("Setting property $key to $val"); - $this->$key = $val; - } else { -// $this->module->emDebug("Setting object_parameter $key to $val"); - $this->object_parameters[$key] = $val; - } - } - } else { - $this->last_error = "Requested log_id $log_id not found for type $type"; - $this->module->emDebug($this->last_error); - throw new Exception ($this->last_error); - } - } else { - // Create a new object - not yet saved - $this->module->emDebug("Creating new $type"); - } - } - - - /** - * Set object value by key pair - * If null, remove from object_properties - * If unchanged, do not mark as dirty - * @param string $name - * @param $val - * @return void - */ - public function setValue($name, $val) { - if(is_array($val)) { - $val = json_encode($val); - $this->module->emDebug("Input $name is array - casting to json for storage"); - } - if(is_object($val)) { - $val = json_encode($val); - $this->module->emDebug("Input $name is object - casting to json for storage"); - } - - if(property_exists($this,$name)) { - // Is object property - if (in_array($name, self::UPDATABLE_COLUMNS)) { - if ($this->$name != $val) { - // $this->module->emDebug("Setting property $name value " . ($this->$name ? "" : "from $this->$name ") . "to $val"); - $this->$name = $val; - $this->dirty_columns[$name] = $val; - } else { - // No change in property value - // $this->module->emDebug("Property $name remains unchanged as $val"); - } - } else { - $this->last_error = "The property $name is not updatable."; - $this->module->emDebug($this->last_error); - // Could throw and exception here but going to just swallow this for now - } - } else { - // Must be a parameter - if (isset($this->object_parameters[$name])) { - // Existing parameter - if (is_null($val) || $val == '') { - // Null or empty parameter values are not supported - skip and mark for removal - $this->dirty_parameters[] = $name; - unset($this->object_parameters[$name]); - } else if ($this->object_parameters[$name] == $val) { - // Skip - no change to value - // $this->module->emDebug("The parameter $name remains unchanged as $val"); - } else { - // Update - // $this->module->emDebug("Updating parameter $name from " . $this->object_parameters[$name] . " to $val"); - $this->object_parameters[$name] = $val; - $this->dirty_parameters[] = $name; - } - } else { - // New parameter - if (is_null($val) || $val == '') { - // Null or empty parameter values are not supported - $this->module->emDebug("Skipping $name -- null/empty parameters are not allowed"); - } else { - // Create - // $this->module->emDebug("Creating parameter $name as $val"); - $this->object_parameters[$name] = $val; - $this->dirty_parameters[] = $name; - } - } - } - } - - - /** - * Set object values by an associative array - * @param array $arr - * @return bool - */ - public function setValues($arr) { - if (!is_array($arr)) { - $this->module->emDebug("Input is not an array"); - return false; - } - foreach ($arr as $k => $v) { - $this->setValue($k, $v); - } - return true; - } - - - /** - * Get a value by a key - * If key doesn't exist, return null - * @param string $k - * @return mixed - */ - public function getValue($k) { - if(property_exists($this,$k)) { - // if($k=="project_id") $this->module->emDebug("$k PROPERTY EXISTS"); - $value = $this->$k; - } else if (isset($this->object_parameters[$k])) { - // if($k=="project_id") $this->module->emDebug("$k PARAMETER EXISTS"); - $value = $this->object_parameters[$k]; - } else { - $this->module->emDebug("Unable to identify requested value by key $k"); - $value = null; - } - return $value; - } - - - /** - * Get the log_id for the object - * @return mixed - */ - public function getId() { - return $this->log_id; - } - - - /** - * Get the object_parameters - * @return mixed - */ - public function getObjectParameters() { - return $this->object_parameters; - } - - - /** - * Save the object, only modifying the object_parameters - * @return void - * @throws Exception - */ - public function save() { - if ($this->log_id) { - $this->module->emDebug("has logid", $this->log_id); - // For saving existing log_ids - // $this->module->emDebug("DIRTY IN SAVE: ", $this->dirty_parameters, $this->dirty_columns); - // We only update dirty parameters - $this->dirty_parameters = array_unique($this->dirty_parameters); - - // Loop through all parameters - foreach ($this->object_parameters as $k => $v) { - // Only update dirty parameters - if (in_array($k, $this->dirty_parameters)) { - // Update/Insert parameter - if ($this->validateParameter($k, $v)) { - $this->module->emDebug("Updating parameter $k to $v"); - // UPSERT THE VALUE - $sql = "INSERT INTO redcap_external_modules_log_parameters (log_id,name,value) " . - " VALUES (?,?,?) ON DUPLICATE KEY UPDATE value=?"; - // $this->module->emDebug($sql); - $params = [$this->log_id, $k, $v, $v]; - $result = $this->module->query($sql, $params); - if (!$result) { - $this->module->emDebug("QUERY FAILED: ", $sql, $params, $result); - } - - // Remove from dirty parameters - $this->dirty_parameters = array_diff($this->dirty_parameters, [$k]); - } else { - // Invalid key or value - } - } else { - // Skip parameter - wasn't dirty - } - } - - // To remove a parameter from an object, you setValue to null which makes it dirty but unsets it - // from the object. Therefore, any parameters left as dirty should be deleted. - foreach ($this->dirty_parameters as $name) { - $sql = "delete from redcap_external_modules_log_parameters where log_id=? and name=? limit 1"; - $result = $this->module->query($sql, [$this->log_id, $name]); - $this->module->emDebug("Deleted parameter $name for log id $this->log_id", $result); - } - - if (!empty($this->dirty_columns)) { - foreach ($this->dirty_columns as $col => $val) { - if (in_array($col, self::UPDATABLE_COLUMNS)) { - $sql = "update redcap_external_modules_log set " . $col . "=? where log_id=?"; - $result = $this->module->query($sql, [$this->$col, $this->log_id]); - $this->module->emDebug("Updated $col to " . $this->$col); - } else { - $this->module->emError("You cannot update column $col on a previously saved object"); - } - } - // You cannot update these columns on an already saved log_id - $this->module->emError("You cannot update column values on an already saved object $this->log_id", $this->dirty_columns); - } - } else { - // Create New Log Entry (merging columns and parameters) - $parameters = array_merge($this->dirty_columns, $this->object_parameters); -// $this->module->emDebug("About to save: " , $parameters); - $this->log_id = $this->module->log($this->type, $parameters); - } - - // Clear object - $this->dirty_parameters=[]; - } - - - /** - * Delete from database - * @return bool - */ - public function delete() { - // Remove this log_id - if ($this->log_id) { - $result = $this->module->removeLogs("log_id = ?", [$this->log_id]); - $this->module->emDebug("Removed log $this->log_id with result: " . json_encode($result)); - - return true; - } else { - $this->module->emDebug("This object hasn't been saved. Cannot delete."); - return false; - } - } - - - /** - * Modified from Framework function - * @param string $name - * @param mixed $value - * @return bool - * @throws Exception - */ - private function validateParameter($name, $value) - { - $type = gettype($value); - if(!in_array($type, ['boolean', 'integer', 'double', 'string', 'NULL'])){ - throw new Exception("The type '$type' for the '$name' parameter is not supported."); - } - else if (isset(AbstractExternalModule::$RESERVED_LOG_PARAMETER_NAMES_FLIPPED[$name])) { - throw new Exception("The '$name' parameter name is set automatically and cannot be overridden."); - } - else if($value === null){ - // There's no point in storing null values in the database. - // If a parameter is missing, queries will return null for it anyway. - // unset($parameters[$name]); - return false; - } - else if(strpos($name, "'") !== false){ - throw new Exception("Single quotes are not allowed in parameter names."); - } - else if(mb_strlen($name, '8bit') > ExternalModules::LOG_PARAM_NAME_SIZE_LIMIT){ - throw new Exception(ExternalModules::tt('em_errors_160', ExternalModules::LOG_PARAM_NAME_SIZE_LIMIT)); - } - else if(mb_strlen($value, '8bit') > ExternalModules::LOG_PARAM_VALUE_SIZE_LIMIT){ - throw new Exception(ExternalModules::tt('em_errors_161', ExternalModules::LOG_PARAM_VALUE_SIZE_LIMIT)); - } - return true; - } - - - #### STATIC METHODS #### - /** - * Get all of the matching log ids for the object - * @param $module - * @param $object_type - * @param $filter_clause - * @param $parameters - * @return array - * @throws Exception - */ - public static function queryIds($module, $object_type, $filter_clause = "", $parameters = []) { - $framework = new \ExternalModules\Framework($module); - - // Trim leading where if it exists - if (substr(trim(mb_strtolower($filter_clause)),0,5) === "where") { - $filter_clause = substr(trim($filter_clause),5); - } - - $question_mark_count = count_chars($filter_clause)[ord("?")]; - if (count($parameters) != $question_mark_count) { - throw Exception ("query filter must have parameter for each question mark"); - } - - - // Add type filter - $sql = "select log_id where message = ?" . (empty($filter_clause) ? "" : " and " . $filter_clause); - $params = array_merge([$object_type], $parameters); - -// $module->emDebug("queryIds() object type", $object_type, $filter_clause, $parameters); -// $module->emDebug("in queryIds", $sql, $params); - $result = $framework->queryLogs($sql,$params); - $ids = []; - while ($row = $result->fetch_assoc()) { - $ids[] = $row['log_id']; - } - return $ids; - } - - /** - * Return an array of objects instead of ids for the matching results - * @param $module - * @param $object_type - * @param $filter_clause - * @param $parameters - * @return array - * @throws Exception - */ - public static function queryObjects($module, $object_type, $filter_clause = "", $parameters = []) { - $ids = static::queryIds($module,$object_type,$filter_clause,$parameters); - $results = []; - - foreach ($ids as $id) { - $obj = new static($module, $object_type, $id); - $results[] = $obj; - } - - return $results; - } - - -} diff --git a/cron/processJobQueue.php b/cron/processJobQueue.php deleted file mode 100644 index e34c45f..0000000 --- a/cron/processJobQueue.php +++ /dev/null @@ -1,8 +0,0 @@ -processJobQueue(); - -echo "
";
-print_r($return);

From 8d700bfcd7b1c1571b83f102d7dd0f88e63fa17a Mon Sep 17 00:00:00 2001
From: Irvin Szeto 
Date: Mon, 12 Feb 2024 19:26:30 -0800
Subject: [PATCH 20/44]  woops forgot to remove the include line

---
 RedcapNotifications.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/RedcapNotifications.php b/RedcapNotifications.php
index 70e960a..1a1517a 100644
--- a/RedcapNotifications.php
+++ b/RedcapNotifications.php
@@ -3,7 +3,6 @@
 namespace Stanford\RedcapNotifications;
 require_once "vendor/autoload.php";
 require_once "emLoggerTrait.php";
-require_once "classes/ProcessQueue.php";
 require_once "classes/CacheInterface.php";
 require_once "classes/Redis.php";
 require_once "classes/Database.php";

From ccb6cf91db0719a9877414094470eff4cccce761 Mon Sep 17 00:00:00 2001
From: ihabz 
Date: Tue, 13 Feb 2024 09:27:08 -0800
Subject: [PATCH 21/44] remove cache from config.json

---
 config.json | 35 -----------------------------------
 1 file changed, 35 deletions(-)

diff --git a/config.json b/config.json
index 26ecc05..1c2743f 100644
--- a/config.json
+++ b/config.json
@@ -56,41 +56,6 @@
             "name": "
REDCap Notification EM
", "type": "descriptive" }, - { - "key": "notification-cache", - "name": "Where do you want to cache notifications? (Default: Database)", - "type": "dropdown", - "choices": [ - { - "name": "Database", - "value": "database" - }, - { - "name": "Redis", - "value": "redis" - } - ] - }, - { - "key": "redis-host", - "name": "Redis Host", - "required": true, - "type": "text", - "branchingLogic": { - "field": "notification-cache", - "value": "redis" - } - }, - { - "key": "redis-port", - "name": "Redis Port", - "required": true, - "type": "text", - "branchingLogic": { - "field": "notification-cache", - "value": "redis" - } - }, { "key": "instructions", "name": "For First time setup

Please check the boxes above to enable this EM on all projects by default and to hide this EM from all non-admins", From 2e3946107a60336c7d69cd38737d2cc7ec6f9442 Mon Sep 17 00:00:00 2001 From: ihabz Date: Tue, 13 Feb 2024 10:13:09 -0800 Subject: [PATCH 22/44] clean config.json --- config.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/config.json b/config.json index 1c2743f..89f02bd 100644 --- a/config.json +++ b/config.json @@ -61,18 +61,6 @@ "name": "For First time setup

Please check the boxes above to enable this EM on all projects by default and to hide this EM from all non-admins", "type": "descriptive" }, - { - "key": "notification-pid", - "name": "Notifications PID Project ID where notifications are created and stored", - "required": true, - "type": "project-id" - }, - { - "key": "dismissal-pid", - "name": "Notifications that were dismissed PID Project ID where notifications were viewed and dismissed", - "required": true, - "type": "project-id" - }, { "key": "redcap-notifs-snooze-minutes", "name": "Time in minutes that the snooze button will hide REDCap Notifications", From 507041a6a008e480e8f5ef43236f7f4323999e28 Mon Sep 17 00:00:00 2001 From: Jordan Schultz Date: Tue, 13 Feb 2024 12:58:39 -0800 Subject: [PATCH 23/44] Retooling javascript to work with new backend --- RedcapNotifications.php | 2 +- assets/scripts/NotificationController.js | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index ad715b7..9f52ef8 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -539,7 +539,7 @@ public function injectREDCapNotifs() //Initialize JSMO $this->initializeJavascriptModuleObject(); - $this->processJobQueue(); +// $this->processJobQueue(); ?> diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index 27dd359..80acc2a 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -84,8 +84,14 @@ class NotificationController { if (this.isStale()) { let _this = this; this.refreshFromServer().then(function (data) { - // SUCCESFUL, parse Notifs and store in this.notif - let response = decode_object(data); + let response = {} + let arr = [] + for(let i in data) { + console.log(i) + arr.push(JSON.parse(data[i])) + } + response['notifs'] = arr + console.log('inside', response) if (response) { console.log("Refresh from server promise resolved", response); _this.parseNotifications(response); @@ -138,7 +144,7 @@ class NotificationController { }; const response = await this.parent.callAjax2("get_full_payload", data) - + return response // Response from API will always return success 200 , ensure it has results key for processing, if not recurse if(!(response['results']).hasOwnProperty("notifs")){ console.log('it is in_queue, call ajax again in 90 sec') @@ -149,7 +155,7 @@ class NotificationController { } } else { console.log('') - return response['results'] + return response } } @@ -168,6 +174,7 @@ class NotificationController { "notifs": data["notifs"], "snooze_expire": { "banner": null, "modal": null } }; + console.log(this.payload) // this.parent.Log("fresh load from server" + JSON.stringify(this.payload), "info"); //fresh payload, need to clear out notifs cache. @@ -476,6 +483,7 @@ class NotificationController { // Generate array of notifications here for use later. generateNotificationArray() { + console.log('in generate', this.payload) if (this.payload.notifs.length) { var dismissed_ids = []; @@ -485,7 +493,7 @@ class NotificationController { for (var i in this.payload.notifs) { var notif = new Notification(this.payload.notifs[i], this); - + console.log(notif) //if in dimissed queue dont show if ($.inArray(notif.getRecordId(), dismissed_ids) > -1) { notif.setDismissed(); From 774ed952318bdda98a8ef3e61f0df071672610be Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Tue, 13 Feb 2024 14:51:23 -0800 Subject: [PATCH 24/44] adjusting the dimsiss_notif and cleaning up some client side functions no longer needed... may need to do some adjustements to the function resolveDismissed() depending on what is returned --- assets/scripts/Notification.js | 5 +- assets/scripts/NotificationController.js | 93 ++++-------------------- 2 files changed, 17 insertions(+), 81 deletions(-) diff --git a/assets/scripts/Notification.js b/assets/scripts/Notification.js index f095cb9..65a2b6d 100644 --- a/assets/scripts/Notification.js +++ b/assets/scripts/Notification.js @@ -92,10 +92,11 @@ class Notification { } dismissNotif(){ - this.setDismissed(); - + //CALL PARENT FUNCTION TO SEND TO SERVER this.parent.dismissNotif(this.notif.key); + //UPDATE UI + this.setDismissed(); this.domjq.fadeOut("fast", function(){ $(this).remove(); }); diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index 68f1e44..a9720d3 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -64,11 +64,6 @@ class NotificationController { //first time just call it , then interval 30 seconds there after this.showNotifications(); } - if (this.payload.client.dismissed.length) { - //first time just call it , then interval 30 seconds there after - console.log("used to call this.dismissNotifs()"); - // this.dismissNotifs(); - } this.pollNotifsDisplay(); } @@ -197,52 +192,10 @@ class NotificationController { this.showNotifications(); } - //Function that checks which notifications have been altered & Flag set on the server (to update UI & determine what content to pull) - getForceRefresh() { - var _this = this; - var data = { - "user" : _this.user, - "last_updated" : _this.getLastUpdate() - }; - - if (this.getEndpointStatus()) { - _this.parent.callAjax("check_forced_refresh", data, function (response) { - var result = response.results; - if (result) { - var forced_refresh_list = decode_object(result); - var force_record_ids = Object.keys(forced_refresh_list); - - for (var i in _this.notif_objs) { - var notif_o = _this.notif_objs[i]; - if ($.inArray(notif_o.getRecordId(), force_record_ids) > -1) { - var check_force = new Date(_this.getLastUpdate()) < new Date(forced_refresh_list[notif_o.getRecordId()]); - - if (check_force) { - //one match is enough to refresh entire payload - _this.force_refresh = true; - // _this.parent.Log("Notif " + notif_o.getRecordId() + " needs force refresh at " + forced_refresh_list[notif_o.getRecordId()], {}); - console.log('getForceRefresh') - _this.loadNotifications(); - break; - } - } - } - } - }, function (err) { - _this.setEndpointFalse(err); - }); - } - } - - startPolling() { - this.pollNotifsDisplay(); - } - pollNotifsDisplay() { let _this = this; this.notifDisplayIntervalID = setInterval(function () { if (_this.isStale()) { - console.log('loadNotifications in pollNotifsDisplay') _this.loadNotifications(); } else if (_this.payload.server.updated) { _this.showNotifications(); @@ -434,43 +387,25 @@ class NotificationController { } } - dismissNotif(data) { - this.payload.client.dismissed.push(data); - // localStorage.setItem(this.redcap_notif_storage_key, JSON.stringify(this.payload)); - - - //TODO, WHAT IF THEY HIT "dismiss all"? - this.dismissNotifs(); - } - - //Remove from future payloads. - dismissNotifs() { - if (this.payload.client.dismissed.length && this.getEndpointStatus()) { - // this.parent.Log("polling dismiss " + this.payload.client.dismissed.length + " items", {}); + dismissNotif(notif_key) { + //THIS MAY NOT BE NECESSARY ANYMORE + this.payload.client.dismissed.push(notif_key); + localStorage.setItem(this.redcap_notif_storage_key, JSON.stringify(this.payload)); - var _this = this; - var data = { - "dismiss_notifs": this.payload.client.dismissed, - "user" : _this.user + //PHP CLASS APPEARS TO BE LOOKING FOR AN ARRAY SO WRAPPING IN [] + var _this = this; + _this.parent.callAjax("save_dismissals", [notif_key], function (result) { + var result = result.results; + if (result.length) { + _this.resolveDismissed(result); } - - _this.parent.callAjax("save_dismissals", data, function (result) { - var result = result.results; - if (result.length) { - // _this.parent.Log("dismissNotif Sucess", {}); - _this.resolveDismissed(result); - } - }, function (err) { - _this.setEndpointFalse(err); - }); - } else { - // this.parent.Log("no notifs to dismiss yet", "misc"); - } + }, function (err) { + _this.setEndpointFalse(err); + }); } resolveDismissed(remove_notifs) { - // remove_notifs.find((el) => this.payload.client.dismissed) - + // WILL NEED TO SEE WHAT THE RETURN IS AND ADJUST THIS var i = this.payload.client.dismissed.length; while (i--) { if ($.inArray(this.payload.client.dismissed[i]["record_id"], remove_notifs) > -1) { From 6d98540cf6e7e4814dce153c2310a197656f4ba0 Mon Sep 17 00:00:00 2001 From: ihabz Date: Tue, 13 Feb 2024 14:59:10 -0800 Subject: [PATCH 25/44] fix minor bug in dismiss endpoint. --- RedcapNotifications.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 14206b6..e243097 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -631,13 +631,13 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins } break; case "save_dismissals": - $dismiss_notifs = $payload['dismiss_notifs']; + $dismiss_notifs = $payload; if (count($dismiss_notifs)) { try { $apiObject = $this->getAPIObject(); if ($apiObject) { foreach ($dismiss_notifs as $notif) { - if(!$apiObject->dismissNotification($notif["record_id"])){ + if(!$apiObject->dismissNotification($notif)){ throw new \Exception("Cant dismiss Notification '" .$notif["record_id"]. "'"); }; $return_o[] = $notif["record_id"]; From e57ee96fb76a56a6eb4cfac8a5958b8188ef4dd5 Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Fri, 16 Feb 2024 10:44:07 -0800 Subject: [PATCH 26/44] removing unecceary dismiss checks and carrying over snooze_expire --- assets/scripts/NotificationController.js | 171 +++++------------------ 1 file changed, 37 insertions(+), 134 deletions(-) diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index a9720d3..f86c942 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -18,8 +18,6 @@ class NotificationController { "client": { "downloaded": null, "offset_hours": null, - "dismissed": [], - "request_update": null }, "notifs": [], "snooze_expire": { "banner": null, "modal": null } @@ -56,15 +54,8 @@ class NotificationController { //Function called once to begin setInterval upon page load initialize() { - //load and parse notifs + //load and parse and show notifs this.loadNotifications(); - - //KICK OFF POLL TO SHOW NOTIFS (IF NOT SNOOZED) - if (this.payload.server.updated) { - //first time just call it , then interval 30 seconds there after - this.showNotifications(); - } - this.pollNotifsDisplay(); } @@ -72,59 +63,28 @@ class NotificationController { * */ loadNotifications() { - if (localStorage.getItem(this.redcap_notif_storage_key)) { - this.payload = JSON.parse(localStorage.getItem(this.redcap_notif_storage_key)) - } - - if (this.isStale()) { - console.log("is stale so pull new from BE"); - let _this = this; - this.refreshFromServer().then(function (data) { - let response = {} - let arr = [] - for(let i in data) { - let parsed = JSON.parse(data[i]) - parsed['key'] = i - arr.push(parsed) - } + let _this = this; + this.refreshFromServer().then(function (data) { + let response = {} + let arr = [] - response['notifs'] = arr - console.log('inside', response) - if (response) { - console.log("Refresh from server promise resolved", response); - _this.parseNotifications(response); + for(let i in data) { + if (i != "38_PROD:DEV_ALLUSERS_3") { + continue; } - }).catch(function (err) { - console.log(err) - console.log("why is it getting rejected? uncomment once figure out why"); - // _this.setEndpointFalse(err); - - // Run this when promise was rejected via reject() - // _this.parent.Log("Error loading or parsing notifs, do nothing they just wont see the notifs this time"); - }); - } else { - this.generateNotificationArray(); - } - } - - isStale() { - if (this.force_refresh) { - return true; - } + let parsed = JSON.parse(data[i]) + parsed['key'] = i + arr.push(parsed) + } - if (this.payload.server.updated) { //Default payload entry is null - let hours_since_last_updated; - hours_since_last_updated = getDifferenceInHours(new Date(this.getOffsetTime(this.payload.server.updated)), Date.now()); - if (hours_since_last_updated < this.refresh_limit) { - if (this.getEndpointStatus()) { //Ensure the endpoint is not offline - // this.parent.Log("Constant refresh Ajax could it be cause of offset time?", { "hours_since_last_updated": hours_since_last_updated, "date_now": Date.now(), "offset_time": new Date(this.getOffsetTime(this.payload.server.updated)) }) - } - return false; + response['notifs'] = arr + if (response) { + _this.parseNotifications(response); } - } - // this.parent.Log("notif payload isStale() " + hours_since_last_updated + " hours since last updated"); - return true; + }).catch(function (err) { + console.log("error?", err) + }); } /** @@ -143,21 +103,15 @@ class NotificationController { const response = await this.parent.callAjax2("get_full_payload", data) return response - // Response from API will always return success 200 , ensure it has results key for processing, if not recurse - if(!(response['results']).hasOwnProperty("notifs")){ - console.log('it is in_queue, call ajax again in 90 sec') - if(!this.refreshFromServerRef) { // Ensure there is no currently running iteration before recursive call - this.refreshFromServerRef = setTimeout(() => { - this.refreshFromServer(notif_type) - }, 90000) - } - } else { - console.log('') - return response - } } parseNotifications(data) { + let snooze_expire ; + if (localStorage.getItem(this.redcap_notif_storage_key)) { + this.payload = JSON.parse(localStorage.getItem(this.redcap_notif_storage_key)); + snooze_expire = this.payload.snooze_expire; + } + var client_date_time = getClientDateTime(); var client_offset = getDifferenceInHours(new Date(data["server_time"]), new Date(client_date_time)) + "h"; @@ -166,14 +120,10 @@ class NotificationController { "client": { "downloaded": client_date_time, "offset_hours": client_offset, - "dismissed": [], - "request_update": null }, "notifs": data["notifs"], - "snooze_expire": { "banner": null, "modal": null } + "snooze_expire": snooze_expire }; - console.log(this.payload) - // this.parent.Log("fresh load from server" + JSON.stringify(this.payload), "info"); //fresh payload, need to clear out notifs cache. this.notif_objs = []; @@ -183,11 +133,6 @@ class NotificationController { localStorage.setItem(this.redcap_notif_storage_key,JSON.stringify(this.payload)); } - if (this.force_refresh) { - //TODO DOES IT MAKE SENSE TO LOAD JUST NEW STUFF SINCE THE LAST UPDATE AND CONCATING , OR JUST PULL ENTIRELY NEW FRESH BATCH? - this.force_refresh = false; - } - //i just do this? this.showNotifications(); } @@ -195,14 +140,20 @@ class NotificationController { pollNotifsDisplay() { let _this = this; this.notifDisplayIntervalID = setInterval(function () { - if (_this.isStale()) { - _this.loadNotifications(); - } else if (_this.payload.server.updated) { - _this.showNotifications(); - } + _this.showNotifications(); }, this.default_polling_int); } + // Generate array of notifications here for use later. + generateNotificationArray() { + if (this.payload.notifs.length) { + for (var i in this.payload.notifs) { + var notif = new Notification(this.payload.notifs[i], this); + this.notif_objs.push(notif); + } + } + } + setEndpointFalse(err) { this.serverOK = false; if (err) { @@ -388,64 +339,16 @@ class NotificationController { } dismissNotif(notif_key) { - //THIS MAY NOT BE NECESSARY ANYMORE - this.payload.client.dismissed.push(notif_key); - localStorage.setItem(this.redcap_notif_storage_key, JSON.stringify(this.payload)); - //PHP CLASS APPEARS TO BE LOOKING FOR AN ARRAY SO WRAPPING IN [] var _this = this; + console.log("dismiss notif", notif_key); _this.parent.callAjax("save_dismissals", [notif_key], function (result) { var result = result.results; - if (result.length) { - _this.resolveDismissed(result); - } }, function (err) { _this.setEndpointFalse(err); }); } - resolveDismissed(remove_notifs) { - // WILL NEED TO SEE WHAT THE RETURN IS AND ADJUST THIS - var i = this.payload.client.dismissed.length; - while (i--) { - if ($.inArray(this.payload.client.dismissed[i]["record_id"], remove_notifs) > -1) { - this.payload.client.dismissed.splice(i, 1); - localStorage.setItem(this.redcap_notif_storage_key, JSON.stringify(this.payload)); - } - } - - var i = this.payload.notifs.length; - while (i--) { - if ($.inArray(this.payload.notifs[i]["record_id"], remove_notifs) > -1) { - this.payload.notifs.splice(i, 1); - localStorage.setItem(this.redcap_notif_storage_key, JSON.stringify(this.payload)); - } - } - } - - // Generate array of notifications here for use later. - generateNotificationArray() { - console.log('in generate', this.payload) - if (this.payload.notifs.length) { - var dismissed_ids = []; - - for (var i in this.payload.client.dismissed) { - dismissed_ids.push(this.payload.client.dismissed[i]["record_id"]); - } - - for (var i in this.payload.notifs) { - var notif = new Notification(this.payload.notifs[i], this); - console.log(notif) - //if in dimissed queue dont show - if ($.inArray(notif.getRecordId(), dismissed_ids) > -1) { - notif.setDismissed(); - } - - this.notif_objs.push(notif); - } - } - } - snoozeNotifs(notif_type) { var snooze_expire = this.calcSnoozeExpiration(); this.payload.snooze_expire[notif_type] = snooze_expire; From a7fc427021b011640a930935f78ff4c18d35a987 Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Fri, 16 Feb 2024 14:06:07 -0800 Subject: [PATCH 27/44] rmeoiveng a debug line and using the callAjax2 method --- assets/scripts/NotificationController.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index f86c942..8f29d00 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -69,15 +69,11 @@ class NotificationController { let arr = [] for(let i in data) { - if (i != "38_PROD:DEV_ALLUSERS_3") { - continue; - } let parsed = JSON.parse(data[i]) parsed['key'] = i arr.push(parsed) } - response['notifs'] = arr if (response) { _this.parseNotifications(response); @@ -341,8 +337,7 @@ class NotificationController { dismissNotif(notif_key) { //PHP CLASS APPEARS TO BE LOOKING FOR AN ARRAY SO WRAPPING IN [] var _this = this; - console.log("dismiss notif", notif_key); - _this.parent.callAjax("save_dismissals", [notif_key], function (result) { + _this.parent.callAjax2("save_dismissals", [notif_key], function (result) { var result = result.results; }, function (err) { _this.setEndpointFalse(err); From 07d6b008ddb17c746dcff3810cbe9cac25c7670e Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Fri, 16 Feb 2024 15:12:18 -0800 Subject: [PATCH 28/44] added a default value for snooze_expire --- assets/scripts/NotificationController.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index 8f29d00..6197943 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -118,7 +118,7 @@ class NotificationController { "offset_hours": client_offset, }, "notifs": data["notifs"], - "snooze_expire": snooze_expire + "snooze_expire": (snooze_expire ?? { "banner": null, "modal": null }) }; //fresh payload, need to clear out notifs cache. @@ -338,9 +338,9 @@ class NotificationController { //PHP CLASS APPEARS TO BE LOOKING FOR AN ARRAY SO WRAPPING IN [] var _this = this; _this.parent.callAjax2("save_dismissals", [notif_key], function (result) { - var result = result.results; + }, function (err) { - _this.setEndpointFalse(err); + console.log("dismissNotif", err); }); } From 24f354dad394ae27f626e84897966fb61e312b40 Mon Sep 17 00:00:00 2001 From: ihabz Date: Fri, 16 Feb 2024 15:37:53 -0800 Subject: [PATCH 29/44] add mysql lock to prevent duplicate user dismiss key. fix minor bug. --- RedcapNotifications.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index e243097..1f1a69d 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -640,7 +640,7 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins if(!$apiObject->dismissNotification($notif)){ throw new \Exception("Cant dismiss Notification '" .$notif["record_id"]. "'"); }; - $return_o[] = $notif["record_id"]; + $return_o[] = $notif; } $this->emDebug("need to return the dismissed record_ids", $return_o); From 5bd44ff9e3df6df079c80c62a46cfd7a9e2a79c3 Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Fri, 16 Feb 2024 16:04:46 -0800 Subject: [PATCH 30/44] dismiss all ajax was firing too quick , so added small delay...now it works as expected --- assets/scripts/NotificationController.js | 46 +++++++++++++++--------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index 6197943..aedca7d 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -230,6 +230,29 @@ class NotificationController { } } + + /** + * Clicks on visible buttons within a specified container with a delay. + * @param {jQuery} container - The jQuery object representing the container. + * @param {number} delay - The delay between clicks in milliseconds. + */ + async clickButtonsWithDelay(container, delay) { + if (container.find(".dismissable").length) { + const buttons = container.find(".dismissable .notif_hdr button").toArray(); + + for (const button of buttons) { + const $button = $(button); + if ($button.is(":visible")) { + $button.trigger("click"); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + } + + /** + * Bind all on + /** * Bind all onClick events to the banner notifications * @param banner JQuery variable containing the UI as String @@ -238,13 +261,9 @@ class NotificationController { let _this = this; banner.find(".dismiss_all").click(function () { - if (banner.find(".dismissable").length) { - banner.find(".dismissable .notif_hdr button").each(function () { - if ($(this).is(":visible")) { - $(this).trigger("click"); - } - }); - } + _this.clickButtonsWithDelay(banner, 100).then(() => { + // Additional actions after dismissing all notifications, if needed + }); }); banner.find(".hide_notifs").click(function () { @@ -265,16 +284,9 @@ class NotificationController { let _this = this; modal.find(".dismiss_all").click(function () { - // _this.parent.Log("dismmiss all dismissable modal", "debug"); - if (modal.find(".dismissable").length) { - // _this.parent.Log("how many modal notifs to dismiss? " + html_cont["modal"].find(".dismissable .notif_hdr button").length, "debug"); - - modal.find(".dismissable .notif_hdr button").each(function () { - if ($(this).is(":visible")) { - $(this).trigger("click"); - } - }); - } + _this.clickButtonsWithDelay(modal, 100).then(() => { + // Additional actions after dismissing all notifications, if needed + }); }); modal.find(".hide_notifs").click(function () { From 6c398244c3140f98c4e8bd74900b326f3c6533b7 Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Wed, 21 Feb 2024 09:00:42 -0800 Subject: [PATCH 31/44] hide dismiss_all button if no notifs are dismisssable after an action, hide notif UI for banner and modal if no notifs are left after action --- assets/scripts/Notification.js | 6 +++-- assets/scripts/NotificationController.js | 30 ++++++++++++++++++++++++ assets/styles/redcap_notifs.css | 8 ++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/assets/scripts/Notification.js b/assets/scripts/Notification.js index 65a2b6d..abbdb18 100644 --- a/assets/scripts/Notification.js +++ b/assets/scripts/Notification.js @@ -92,13 +92,15 @@ class Notification { } dismissNotif(){ - //CALL PARENT FUNCTION TO SEND TO SERVER - this.parent.dismissNotif(this.notif.key); + let _this = this; //UPDATE UI this.setDismissed(); this.domjq.fadeOut("fast", function(){ $(this).remove(); + + //CALL PARENT FUNCTION TO SEND TO SERVER + _this.parent.dismissNotif(_this.notif.key); }); } diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index aedca7d..0139a3e 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -335,6 +335,14 @@ class NotificationController { } } + //SHOW "dismiss_all" button if any notifs is dismissable + if(html_cont["modal"].find(".dismissbtn").length){ + html_cont["modal"].find(".dismiss_all").addClass("has_dismiss"); + } + if(html_cont["banner"].find(".dismissbtn").length) { + html_cont["banner"].find(".dismiss_all").addClass("has_dismiss"); + } + for (let notif_style in html_cont) { if (html_cont[notif_style].find(".notif.alert").length) { if (notif_style == "banner") { @@ -354,6 +362,28 @@ class NotificationController { }, function (err) { console.log("dismissNotif", err); }); + + _this.removeNotificationByKey(notif_key); + } + + removeNotificationByKey(keyToRemove) { + this.notif_objs = this.notif_objs.filter(notification => notification.notif.key !== keyToRemove); + + //IF NO MORE DISMISSABLE THEN REMOVE THE "dismiss_all" button + if(this.banner_jq && !this.banner_jq.find(".dismissbtn").length){ + this.banner_jq.find(".dismiss_all").removeClass("has_dismiss"); + } + if(this.modal_jq && !this.modal_jq.find(".dismissbtn").length){ + this.modal_jq.find(".dismiss_all").removeClass("has_dismiss"); + } + + //IF NO MORE NOTIFS THEN HIDE RESPECTIVE UIs + if(this.banner_jq && !this.banner_jq.find(".notif").length){ + this.hideNotifs("banner"); + } + if(this.modal_jq && !this.modal_jq.find(".notif").length){ + this.hideNotifs("modal"); + } } snoozeNotifs(notif_type) { diff --git a/assets/styles/redcap_notifs.css b/assets/styles/redcap_notifs.css index 266ba9e..cbf3d95 100644 --- a/assets/styles/redcap_notifs.css +++ b/assets/styles/redcap_notifs.css @@ -32,10 +32,16 @@ display:none; } -#container .dismiss_all { +#container .dismiss_all, +#redcap_modal_notifs .dismiss_all, +#redcap_banner_notifs .dismiss_all { display:none; } +#redcap_banner_notifs .dismiss_all.has_dismiss, +#redcap_modal_notifs .dismiss_all.has_dismiss{ + display:inline-block; +} From 2115dec0deb4c6f7e790e07c676e851a8d63149b Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Wed, 21 Feb 2024 09:14:55 -0800 Subject: [PATCH 32/44] adding css so that the modal will grow only up to a fixed height, and then introduce an inline vertical scroll after that --- assets/styles/redcap_notifs.css | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/assets/styles/redcap_notifs.css b/assets/styles/redcap_notifs.css index cbf3d95..c5562e3 100644 --- a/assets/styles/redcap_notifs.css +++ b/assets/styles/redcap_notifs.css @@ -140,6 +140,37 @@ background-repeat: no-repeat; background-position: 0 0; } + + + + + + +.modal-dialog { + max-width: 950px; /* Your existing max-width */ + margin: 20px auto; /* Adjust margin for vertical spacing from the viewport's top */ + display: flex; + flex-direction: column; +} + +.modal-content { + display: flex; + flex-direction: column; + height: auto; /* Allows the modal content to grow based on content, up to the viewport's size */ +} + +.modal-body { + overflow-y: auto; /* Enable scrolling when content overflows */ + max-height: 50vh; /* Maximum height before scrolling kicks in */ + flex-grow: 1; /* Allows the body to expand within the constraints of the modal-content */ +} + + + + + + + /* growler style From ac8b89617e62dae52dd4e978efa9d69508b2fafa Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Wed, 21 Feb 2024 13:48:35 -0800 Subject: [PATCH 33/44] fixing CSS for home / survey page display , and adding height limit for banners --- RedcapNotifications.php | 5 +++-- assets/scripts/NotificationController.js | 9 ++++++--- assets/styles/redcap_notifs.css | 15 +++++++++++++-- config.json | 1 + 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 1f1a69d..1998bb1 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -65,7 +65,9 @@ function redcap_every_page_top($project_id) 'Surveys/invite_participants.php', 'DataEntry/record_status_dashboard.php', 'DataExport/index.php', - 'UserRights/index.php' + 'UserRights/index.php', + 'surveys/index.php', + 'Home/index.php' ]; if (in_array(PAGE, $allowed_pages)) @@ -538,7 +540,6 @@ public function injectREDCapNotifs() //Initialize JSMO $this->initializeJavascriptModuleObject(); -// $this->processJobQueue(); ?> diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index 0139a3e..50cca69 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -31,7 +31,7 @@ class NotificationController { constructor({ current_user, dev_prod_status, - page, + current_page, parent, project_id, refresh_limit, @@ -39,12 +39,11 @@ class NotificationController { php_session }) { - this.user = current_user; this.parent = parent; this.snooze_duration = snooze_duration; this.refresh_limit = refresh_limit; - this.page = page; + this.page = current_page; this.project_id = project_id; this.dev_prod_status = dev_prod_status; this.redcap_notif_storage_key = `redcapNotifications_${this.user}`; @@ -181,8 +180,10 @@ class NotificationController { if (!this.isSnoozed("banner") && this.banner_jq && this.banner_jq.find(".notif.alert").length) { if (!$("#redcap_banner_notifs").length && ($("#subheader").length || $("#container").length)) { if (this.getCurPage() == "surveys/index.php") { + this.banner_jq.addClass("on_survey_page"); $("#container").prepend(this.banner_jq); } else { + // console.log("banner",this.getCurPage()); if ($("#subheader").length) { $("#subheader").after(this.banner_jq); } else if ($("#control_center_window").length) { @@ -200,8 +201,10 @@ class NotificationController { if (!$("#redcap_notifs_blocker").length) { $("body").append(opaque); if (this.getCurPage() == "surveys/index.php") { + this.modal_jq.addClass("on_survey_page"); $("#container").append(this.modal_jq); } else { + // console.log("modal",this.getCurPage()); $("body").append(this.modal_jq); } } diff --git a/assets/styles/redcap_notifs.css b/assets/styles/redcap_notifs.css index c5562e3..b9588c4 100644 --- a/assets/styles/redcap_notifs.css +++ b/assets/styles/redcap_notifs.css @@ -28,8 +28,11 @@ width:96%; } -#container .dismissable button { - display:none; +#redcap_modal_notifs.on_survey_page .dismiss_all, +#redcap_banner_notifs.on_survey_page .dismiss_all, +#redcap_modal_notifs.on_survey_page .dismissable button , +#redcap_banner_notifs.on_survey_page .dismissable button { + display:none !important; } #container .dismiss_all, @@ -168,6 +171,14 @@ +#redcap_banner_notifs .notif_cont_system, +#redcap_banner_notifs .notif_cont_project { + max-height: 50vh; /* Example: Adjust this value based on your needs */ + overflow-y: auto; /* Enable vertical scroll on overflow */ + margin-bottom: 10px; /* Optional: Adds space between notification containers */ + padding:1px; +} + diff --git a/config.json b/config.json index 89f02bd..728e622 100644 --- a/config.json +++ b/config.json @@ -40,6 +40,7 @@ "MyAction", "refresh", "dismiss", + "get_full_payload", "force_refresh" ], "project-settings": [ From a62e4bb2f79d8b8991b899748af65ae6670ace86 Mon Sep 17 00:00:00 2001 From: ihabz Date: Mon, 26 Feb 2024 15:34:04 -0800 Subject: [PATCH 34/44] fix html component. --- assets/scripts/Notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/scripts/Notification.js b/assets/scripts/Notification.js index abbdb18..e7af0c3 100644 --- a/assets/scripts/Notification.js +++ b/assets/scripts/Notification.js @@ -57,7 +57,7 @@ class Notification { notif_jq.find(".notif_bdy .headline").text(this.getSubject()); if(this.getMessage()){ - notif_jq.find(".notif_bdy .lead").text(this.getMessage()); + notif_jq.find(".notif_bdy .lead").html(this.getMessage()); }else{ notif_jq.find(".notif_bdy .lead").remove(); } From 7c970f00133402cab776144aa64e67b18b36d590 Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Mon, 26 Feb 2024 15:38:22 -0800 Subject: [PATCH 35/44] putting all of them into the same container so the maximum expansion will be just one, or do we need them to be grouped by type? --- assets/scripts/NotificationController.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/scripts/NotificationController.js b/assets/scripts/NotificationController.js index 50cca69..0027727 100644 --- a/assets/scripts/NotificationController.js +++ b/assets/scripts/NotificationController.js @@ -326,7 +326,8 @@ class NotificationController { if (!notif.isDismissed() && !notif.isFuture() && !notif.isExpired() && notif.displayOnPage()) { //force surveys to be modals no matter what let notif_type = notif.getType(); - let notif_cont = notif.getTarget() == "survey" ? ".notif_cont_project" : ".notif_cont_" + notif.getTarget(); + // let notif_cont = notif.getTarget() == "survey" ? ".notif_cont_project" : ".notif_cont_" + notif.getTarget(); + let notif_cont = ".notif_cont_system"; let jqunit = notif.getJQUnit(); @@ -484,8 +485,12 @@ class NotificationController { -

-
+
+ +
+
+ +
` ); } From 98e665ce34a3d644e3d258a7fabe29e6bc9bad89 Mon Sep 17 00:00:00 2001 From: ihabz Date: Thu, 21 Mar 2024 14:12:04 -0700 Subject: [PATCH 36/44] when notification is loaded log that it has been viewed. --- RedcapNotifications.php | 67 +++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 1998bb1..59c0281 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -592,7 +592,8 @@ public function emDebugForCustomUseridList() /* AJAX HANDLING IN HERE INSTEAD OF A STAND ALONE PAGE? */ - public function redcap_module_ajax($action, $payload, $project_id, $record, $instrument, $event_id, $repeat_instance, $survey_hash, $response_id, $survey_queue_hash, $page, $page_full, $user_id, $group_id) { + public function redcap_module_ajax($action, $payload, $project_id, $record, $instrument, $event_id, $repeat_instance, $survey_hash, $response_id, $survey_queue_hash, $page, $page_full, $user_id, $group_id) + { $return_o = ["success" => false]; //NO LONGER SEPARATE ACTIONS, THEY ALL FLOW THROUGH QUEUE @@ -600,7 +601,7 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins switch ($action) { case "get_full_payload": - // case "check_forced_refresh": + // case "check_forced_refresh": // CHECK // IS QUEUE AVAILABLE? @@ -627,33 +628,38 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins $admin_rights = ADMIN_RIGHTS; } $return_o = $apiObject->getNotifications($project_id, $project_status, $admin_rights); + + // log viewed notifications for projects when user is logged in and do it one time only. + if($project_id and defined('USERID')){ + $this->logNotificationsView($project_id, $return_o); + } } else { throw new \Exception("No notifications"); } break; case "save_dismissals": $dismiss_notifs = $payload; - if (count($dismiss_notifs)) { - try { - $apiObject = $this->getAPIObject(); - if ($apiObject) { - foreach ($dismiss_notifs as $notif) { - if(!$apiObject->dismissNotification($notif)){ - throw new \Exception("Cant dismiss Notification '" .$notif["record_id"]. "'"); - }; - $return_o[] = $notif; - } - $this->emDebug("need to return the dismissed record_ids", $return_o); - - } else { - throw new \Exception("No notifications"); + if (count($dismiss_notifs)) { + try { + $apiObject = $this->getAPIObject(); + if ($apiObject) { + foreach ($dismiss_notifs as $notif) { + if (!$apiObject->dismissNotification($notif)) { + throw new \Exception("Cant dismiss Notification '" . $notif["record_id"] . "'"); + }; + $return_o[] = $notif; } - } catch (\Exception $e) { - return $e->getMessage(); - }; - } else { - $this->emError("Cannot save dismissed notification because record set was empty or there was invalid data"); - } + $this->emDebug("need to return the dismissed record_ids", $return_o); + + } else { + throw new \Exception("No notifications"); + } + } catch (\Exception $e) { + return $e->getMessage(); + }; + } else { + $this->emError("Cannot save dismissed notification because record set was empty or there was invalid data"); + } default: $this->emError("Invalid Action"); break; @@ -664,6 +670,23 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins } + private function logNotificationsView($projectId, $notifications) + { + $log_event_table = REDCap::getLogEventTable($projectId); + + $notifications = implode("\n", array_keys($notifications)); + $user = USERID; + $sql = sprintf("select count(ts) as count + from $log_event_table + where description = 'Notifications Viewed' AND data_values = '%s' AND project_id = %d and user = '%s'", db_escape($notifications), db_escape($projectId), db_escape($user)); + + $q = db_query($sql); + $row = db_fetch_assoc($q); + if($row['count'] == 0){ + \REDCap::logEvent('Notifications Viewed', $notifications); + } + } + /** * @return \Stanford\RedcapNotificationsAPI\RedcapNotificationsAPI */ From ab4657efd18ad7d43c6c2f3d3174b2ed99e2e065 Mon Sep 17 00:00:00 2001 From: Irvin Szeto Date: Tue, 30 Apr 2024 12:21:24 -0700 Subject: [PATCH 37/44] updating the skull and crossboanes default danger icon --- assets/scripts/Notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/scripts/Notification.js b/assets/scripts/Notification.js index e7af0c3..eeccbdf 100644 --- a/assets/scripts/Notification.js +++ b/assets/scripts/Notification.js @@ -35,7 +35,7 @@ class Notification { default_icon = { "info": ``, "warning": ``, - "danger": `` + "danger": `` } constructor(notif, parent){ From baca3863fcc8f205681aa6831f0ae2496e64d43f Mon Sep 17 00:00:00 2001 From: ihabz Date: Wed, 1 May 2024 11:29:22 -0700 Subject: [PATCH 38/44] change get project id to return array of pids instead of one. --- assets/scripts/Notification.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/scripts/Notification.js b/assets/scripts/Notification.js index eeccbdf..27e37c3 100644 --- a/assets/scripts/Notification.js +++ b/assets/scripts/Notification.js @@ -137,13 +137,15 @@ class Notification { //NEED TO CHECK CURRENT PAGE CONTEXT TO DETERMINE IF NOTIFS SHOULD DISPLAY (PROJECT, SURVEY, or SYSTEM) if( page_project_id && this.isProjectNotif() && !this.isExcluded() && this.isCorrectProjectStatus() ){ //project notif, page is in project context - if(page_project_id == this.getProjId() || this.getProjId() == ""){ + + + if(this.getProjIds().includes(page_project_id) || this.getProjIds().length == 0){ //project notif, specified project id = current projoect context return true; } }else if( this.isSurveyNotif() && this.parent.getCurPage() == "surveys/index.php" ){ const global_var_pid = pid; //UGH - if(this.getProjId() == global_var_pid){ + if(this.getProjIds().includes(global_var_pid)){ return true; } }else if( this.isSystemNotif() && !page_project_id){ @@ -173,8 +175,8 @@ class Notification { getEndDate(){ return this.notif.note_end_dt; } - getProjId(){ - return this.notif.note_project_id; + getProjIds(){ + return this.notif.note_project_id.split(","); } getName(){ return this.notif.note_name; From b8e30283ff344726434e34de4bfa313b2ada494a Mon Sep 17 00:00:00 2001 From: ihabz Date: Mon, 6 May 2024 12:52:56 -0700 Subject: [PATCH 39/44] add unit tests --- RedcapNotifications.php | 9 ++++---- tests/RedcapNotificationsTest.php | 37 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tests/RedcapNotificationsTest.php diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 59c0281..967bd11 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -1,7 +1,6 @@ $this->clean_user($cur_user), + "current_user" => $this->cleanUser($cur_user), "snooze_duration" => $snooze_duration, "refresh_limit" => $refresh_limit, "current_page" => PAGE, @@ -562,7 +561,7 @@ public function injectREDCapNotifs() * @param $user * @return string */ - public function clean_user($user) + public function cleanUser($user) { $user = str_replace(" ", "_", $user); $user = str_replace("[", "", $user); diff --git a/tests/RedcapNotificationsTest.php b/tests/RedcapNotificationsTest.php new file mode 100644 index 0000000..e8cfd7b --- /dev/null +++ b/tests/RedcapNotificationsTest.php @@ -0,0 +1,37 @@ +assertEquals($expected, $this->cleanUser('[clean user]')); + } + + public function testExcludeProjects() + { + /** @var \Stanford\RedcapNotifications\RedcapNotifications $this */ + + $expected = [1]; + $projects = [1, 2]; + $exclude = '2'; + $this->assertEquals(json_encode($expected), json_encode($this->excludeProjects($projects, $exclude))); + } + + public function testThisProjectExcluded() + { + /** @var \Stanford\RedcapNotifications\RedcapNotifications $this */ + + $expected = true; + $this->assertEquals($expected, $this->thisProjectExcluded(1, '1')); + } +} \ No newline at end of file From 6d22e8a34b8335fc816aed7f719df9e0506c876f Mon Sep 17 00:00:00 2001 From: ihabz Date: Tue, 7 May 2024 16:21:02 -0700 Subject: [PATCH 40/44] do not create notification viewed log if no notification exists. --- RedcapNotifications.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 967bd11..523a743 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -629,7 +629,7 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins $return_o = $apiObject->getNotifications($project_id, $project_status, $admin_rights); // log viewed notifications for projects when user is logged in and do it one time only. - if($project_id and defined('USERID')){ + if($project_id and defined('USERID') and !empty($return_o)){ $this->logNotificationsView($project_id, $return_o); } } else { From 71022b56ba3c878b3b7bc53208cdd697dd9bfa1a Mon Sep 17 00:00:00 2001 From: ihabz Date: Thu, 1 Aug 2024 15:08:51 -0700 Subject: [PATCH 41/44] fix minor php errors. --- RedcapNotifications.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 523a743..5bc0641 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -533,7 +533,7 @@ public function injectREDCapNotifs() "refresh_limit" => $refresh_limit, "current_page" => PAGE, "project_id" => !empty($Proj) ? $Proj->project_id : null, - "dev_prod_status" => !empty($Proj) ? $Proj->status : null, + "dev_prod_status" => !empty($Proj) ? $Proj->project['status'] : null, "php_session" => session_id() ); From 35960aeaa0e50ede408591a5c46219bda77c44fe Mon Sep 17 00:00:00 2001 From: ihabz Date: Tue, 3 Sep 2024 17:36:01 -0700 Subject: [PATCH 42/44] fix for notifications project status. --- assets/scripts/Notification.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/scripts/Notification.js b/assets/scripts/Notification.js index 27e37c3..f17f075 100644 --- a/assets/scripts/Notification.js +++ b/assets/scripts/Notification.js @@ -231,7 +231,8 @@ class Notification { let dev_prod_status = this.parent.getDevProdStatus(); let notif_dev_prod = this.notif["project_status"] == "" ? null : parseInt(this.notif["project_status"]); - if( dev_prod_status ){ + if(notif_dev_prod != null){ + if( dev_prod_status == "1"){ //PROD, ONLY if(!notif_dev_prod){ return false; @@ -243,6 +244,8 @@ class Notification { return false; } } + } + //let it pass! return true; From ddf14ef9c73fbb4ee8a4efd66bbf5268c6c515a5 Mon Sep 17 00:00:00 2001 From: ihabz Date: Wed, 4 Sep 2024 11:49:59 -0700 Subject: [PATCH 43/44] new update project status EM to automatically update project status if days elapsed. --- config.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/config.json b/config.json index 728e622..a631f16 100644 --- a/config.json +++ b/config.json @@ -20,11 +20,7 @@ "institution": "Stanford University" } ], - "framework-version": 9, - "permissions": [ - "redcap_every_page_top", - "redcap_save_record" - ], + "framework-version": 14, "enable-every-page-hooks-on-system-pages": true, "links": { "project": [] From 69ab9090d832bf3cafa4f326d204e97517380abf Mon Sep 17 00:00:00 2001 From: ihabz Date: Fri, 20 Sep 2024 09:28:33 -0700 Subject: [PATCH 44/44] update log message. --- RedcapNotifications.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/RedcapNotifications.php b/RedcapNotifications.php index 5bc0641..d75bdc7 100644 --- a/RedcapNotifications.php +++ b/RedcapNotifications.php @@ -672,7 +672,7 @@ public function redcap_module_ajax($action, $payload, $project_id, $record, $ins private function logNotificationsView($projectId, $notifications) { $log_event_table = REDCap::getLogEventTable($projectId); - + $records = $notifications;; $notifications = implode("\n", array_keys($notifications)); $user = USERID; $sql = sprintf("select count(ts) as count @@ -682,7 +682,12 @@ private function logNotificationsView($projectId, $notifications) $q = db_query($sql); $row = db_fetch_assoc($q); if($row['count'] == 0){ - \REDCap::logEvent('Notifications Viewed', $notifications); + $temp = []; + foreach ($records as $record) { + $record = json_decode($record, true); + $temp[] = $record['note_subject']; + } + \REDCap::logEvent( USERID . ' viewed Notifications', implode("\n", $temp)); } }