PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/api/database/queue.class.php

https://github.com/patrickdemond/beartooth
PHP | 1125 lines | 805 code | 96 blank | 224 comment | 108 complexity | d8feeb552a8d0c426924073cbbff866b MD5 | raw file
  1. <?php
  2. /**
  3. * queue.class.php
  4. *
  5. * @author Patrick Emond <emondpd@mcmaster.ca>
  6. * @filesource
  7. */
  8. namespace beartooth\database;
  9. use cenozo\lib, cenozo\log, beartooth\util;
  10. /**
  11. * queue: record
  12. */
  13. class queue extends \cenozo\database\record
  14. {
  15. /**
  16. * Constructor
  17. *
  18. * The constructor either creates a new object which can then be insert into the database by
  19. * calling the {@link save} method, or, if an primary key is provided then the row with the
  20. * requested primary id will be loaded.
  21. * This method overrides the parent constructor because of custom sql required by each queue.
  22. * @author Patrick Emond <emondpd@mcmaster.ca>
  23. * @param integer $id The primary key for this object.
  24. * @access public
  25. */
  26. public function __construct( $id = NULL )
  27. {
  28. parent::__construct( $id );
  29. }
  30. public static function get_ranked_participant_list( $modifier = NULL, $count = false )
  31. {
  32. // repopulate all ranked queues
  33. $queue_mod = lib::create( 'database\modifier' );
  34. $queue_mod->where( 'rank', '!=', NULL );
  35. foreach( static::select( $queue_mod ) as $db_queue )
  36. $db_queue->populate_time_specific();
  37. $sql = sprintf(
  38. 'SELECT %s '.
  39. 'FROM participant '.
  40. 'JOIN queue_has_participant ON participant.id = queue_has_participant.participant_id '.
  41. 'JOIN queue ON queue_has_participant.queue_id = queue.id '.
  42. 'AND queue.rank IS NOT NULL '.
  43. 'LEFT JOIN site ON queue_has_participant.site_id = site.id '.
  44. 'LEFT JOIN qnaire ON queue_has_participant.qnaire_id = qnaire.id '.
  45. 'LEFT JOIN address ON queue_has_participant.address_id = address.id %s',
  46. $count ? 'COUNT( DISTINCT participant.id )' : 'DISTINCT participant.id',
  47. is_null( $modifier ) ? '' : $modifier->get_sql() );
  48. if( $count )
  49. {
  50. return intval( static::db()->get_one( $sql ) );
  51. }
  52. else
  53. {
  54. $ids = static::db()->get_col( $sql );
  55. $records = array();
  56. foreach( $ids as $id ) $records[] = lib::create( 'database\participant', $id );
  57. return $records;
  58. }
  59. }
  60. public static function get_ranked_participant_count( $modifier = NULL, $count = false )
  61. {
  62. return static::get_ranked_participant_list( $modifier, true );
  63. }
  64. /**
  65. * Override parent get_record_list() method to dynamically populate time-specific queues
  66. * @author Patrick Emond <emondpd@mcmaster.ca>
  67. * @param string $record_type The type of record.
  68. * @param modifier $modifier A modifier to apply to the list or count.
  69. * @param boolean $inverted Whether to invert the count (count records NOT in the joining table).
  70. * @param boolean $count If true then this method returns the count instead of list of records.
  71. * @param boolean $distinct Whether to use the DISTINCT sql keyword
  72. * @return array( record ) | int
  73. * @access protected
  74. */
  75. public function get_record_list(
  76. $record_type, $modifier = NULL, $inverted = false, $count = false, $distinct = true )
  77. {
  78. // if we're getting a participant list/count for a time-specific column, populate it first
  79. if( 'participant' == $record_type ) $this->populate_time_specific();
  80. // if the queue's site has been set, add its restriction to the query
  81. if( !is_null( $this->db_site ) )
  82. {
  83. $service_id = lib::create( 'business\session' )->get_service()->id;
  84. if( is_null( $modifier ) ) $modifier = lib::create( 'database\modifier' );
  85. $modifier->where( 'queue_has_participant.site_id', '=', $this->db_site->id );
  86. }
  87. // now call the parent method as usual
  88. return parent::get_record_list( $record_type, $modifier, $inverted, $count, $distinct );
  89. }
  90. /**
  91. * Generates the query list.
  92. *
  93. * This method is called internally by the {@link repopulate} method in order to generate
  94. * the proper SQL to complete the repopulation of queues.
  95. * @author Patrick Emond <emondpd@mcmaster.ca>
  96. * @access protected
  97. * @static
  98. */
  99. protected static function generate_query_list()
  100. {
  101. $participant_class_name = lib::get_class_name( 'database\participant' );
  102. $phone_call_class_name = lib::get_class_name( 'database\phone_call' );
  103. // define the SQL for each queue
  104. $queue_list = array(
  105. 'all',
  106. 'finished',
  107. 'ineligible',
  108. 'inactive',
  109. 'refused consent',
  110. 'condition',
  111. 'eligible',
  112. 'qnaire',
  113. 'qnaire waiting',
  114. 'appointment',
  115. 'assigned',
  116. 'quota disabled',
  117. 'outside calling time',
  118. 'callback',
  119. 'upcoming callback',
  120. 'assignable callback',
  121. 'new participant',
  122. 'old participant' );
  123. foreach( $queue_list as $queue )
  124. {
  125. $parts = self::get_query_parts( $queue );
  126. $from_sql = '';
  127. $first = true;
  128. // reverse order to make sure the join works
  129. foreach( array_reverse( $parts['from'] ) as $from )
  130. {
  131. $from_sql .= sprintf( $first ? 'FROM %s' : ', %s', $from );
  132. $first = false;
  133. }
  134. $join_sql = '';
  135. foreach( $parts['join'] as $join ) $join_sql .= ' '.$join;
  136. $where_sql = 'WHERE true';
  137. foreach( $parts['where'] as $where ) $where_sql .= ' AND '.$where;
  138. self::$query_list[$queue] =
  139. sprintf( 'SELECT <SELECT_PARTICIPANT> %s %s %s',
  140. $from_sql,
  141. $join_sql,
  142. $where_sql );
  143. }
  144. // now add the sql for each call back status, grouping machine message, machine no message,
  145. // not reached, disconnected and wrong number into a single "not reached" category
  146. $phone_call_status_list = $phone_call_class_name::get_enum_values( 'status' );
  147. $remove_list = array(
  148. 'machine message',
  149. 'machine no message',
  150. 'disconnected',
  151. 'wrong number' );
  152. $phone_call_status_list = array_diff( $phone_call_status_list, $remove_list );
  153. foreach( $phone_call_status_list as $phone_call_status )
  154. {
  155. // ignore statuses which result in deactivating phone numbers
  156. if( 'disconnected' != $phone_call_status && 'wrong number' != $phone_call_status )
  157. {
  158. $queue_list = array(
  159. 'phone call status',
  160. 'phone call status waiting',
  161. 'phone call status ready' );
  162. foreach( $queue_list as $queue )
  163. {
  164. $parts = self::get_query_parts( $queue, $phone_call_status );
  165. $from_sql = '';
  166. $first = true;
  167. // reverse order to make sure the join works
  168. foreach( array_reverse( $parts['from'] ) as $from )
  169. {
  170. $from_sql .= sprintf( $first ? 'FROM %s' : ', %s', $from );
  171. $first = false;
  172. }
  173. $join_sql = '';
  174. foreach( $parts['join'] as $join ) $join_sql .= ' '.$join;
  175. $where_sql = 'WHERE true';
  176. foreach( $parts['where'] as $where ) $where_sql .= ' AND '.$where;
  177. $queue_name = str_replace( 'phone call status', $phone_call_status, $queue );
  178. self::$query_list[$queue_name] =
  179. sprintf( 'SELECT <SELECT_PARTICIPANT> %s %s %s',
  180. $from_sql,
  181. $join_sql,
  182. $where_sql );
  183. }
  184. }
  185. }
  186. }
  187. /**
  188. * Re-populates a queue's participant list
  189. *
  190. * This method is used to pupulate all non-time-specific queues.
  191. * Only non time-specific queues are affected by this function, to populate time-specific
  192. * queues use the populate_time_specific() method instead.
  193. * @author Patrick Emond <emondpd@mcmaster.ca>
  194. * @param database\participant $db_participant If provided then only that participant will
  195. * be affected by the operation.
  196. * @access public
  197. * @static
  198. */
  199. static public function repopulate( $db_participant = NULL )
  200. {
  201. $database_class_name = lib::get_class_name( 'database\database' );
  202. $session = lib::create( 'business\session' );
  203. $db_user = $session->get_user();
  204. // block with a semaphore
  205. $session->acquire_semaphore();
  206. // make sure the temporary table exists
  207. static::create_participant_for_queue( $db_participant );
  208. // make sure the queue list cache exists
  209. static::create_queue_list_cache();
  210. $modifier = lib::create( 'database\modifier' );
  211. $modifier->order( 'id' );
  212. foreach( static::select( $modifier ) as $db_queue )
  213. {
  214. $columns = sprintf(
  215. 'DISTINCT participant_for_queue.id, %s, '.
  216. 'IFNULL( service_has_participant_preferred_site_id, jurisdiction_site_id ), '.
  217. 'effective_qnaire_id, '.
  218. 'start_qnaire_date, '.
  219. 'first_address_id',
  220. $database_class_name::format_string( $db_queue->id ) );
  221. $sql = sprintf(
  222. 'DELETE FROM queue_has_participant WHERE queue_id = %s ',
  223. $database_class_name::format_string( $db_queue->id ) );
  224. if( !is_null( $db_participant ) )
  225. $sql .= sprintf( ' AND participant_id = %s ',
  226. $database_class_name::format_string( $db_participant->id ) );
  227. static::db()->execute( $sql );
  228. // only populate queues which are not time-specific
  229. if( !$db_queue->time_specific )
  230. {
  231. static::db()->execute( sprintf(
  232. 'INSERT INTO queue_has_participant( '.
  233. 'participant_id, queue_id, site_id, qnaire_id, start_qnaire_date, address_id ) %s',
  234. $db_queue->get_sql( $columns ) ) );
  235. }
  236. }
  237. $session->release_semaphore();
  238. }
  239. /**
  240. * Re-populates a time-specific queue
  241. *
  242. * This method is used to populate queues which are dependent on the exact time.
  243. * Only time-specific queues are affected by this function, to populate non time-specific
  244. * queues use the repopulate() static method instead.
  245. * @author Patrick Emond <emondpd@mcmaster.ca>
  246. * @access protected
  247. */
  248. protected function populate_time_specific()
  249. {
  250. // do nothing if this isn't a time-specific queue
  251. if( !$this->time_specific ) return;
  252. $database_class_name = lib::get_class_name( 'database\database' );
  253. $session = lib::create( 'business\session' );
  254. $db_user = $session->get_user();
  255. // block with a semaphore
  256. $session->acquire_semaphore();
  257. // make sure the queue list cache exists and get the queue's parent
  258. static::create_queue_list_cache();
  259. $db_parent_queue = self::$queue_list_cache[$this->name]['parent'];
  260. // determine what date/time to view the queues
  261. if( is_null( self::$viewing_date ) )
  262. {
  263. $viewing_date = 'UTC_TIMESTAMP()';
  264. $check_time = true;
  265. }
  266. else
  267. {
  268. // put double quotes around the date since it is being inserted into sql below
  269. $viewing_date = sprintf( '"%s"', self::$viewing_date );
  270. $check_time = false;
  271. }
  272. // fill in the settings
  273. $setting_manager = lib::create( 'business\setting_manager' );
  274. static::db()->execute( sprintf(
  275. 'DELETE FROM queue_has_participant WHERE queue_id = %s',
  276. $database_class_name::format_string( $this->id ) ) );
  277. // populate appointment upcomming/assignable/missed queues
  278. if( ' appointment' == substr( $this->name, -12 ) )
  279. {
  280. $appointment_pre_window =
  281. $setting_manager->get_setting( 'appointment', 'call pre-window', $this->db_site );
  282. $appointment_post_window =
  283. $setting_manager->get_setting( 'appointment', 'call post-window', $this->db_site );
  284. $sql = sprintf(
  285. 'INSERT INTO queue_has_participant( '.
  286. 'participant_id, queue_id, site_id, qnaire_id, start_qnaire_date, address_id ) '.
  287. 'SELECT DISTINCT queue_has_participant.participant_id, %s, site_id, qnaire_id, start_qnaire_date, '.
  288. 'queue_has_participant.address_id '.
  289. 'FROM queue_has_participant '.
  290. 'JOIN appointment ON queue_has_participant.participant_id = appointment.participant_id '.
  291. 'AND appointment.assignment_id IS NULL '.
  292. 'WHERE queue_id = %s AND ',
  293. $database_class_name::format_string( $this->id ),
  294. $database_class_name::format_string( $db_parent_queue->id ) );
  295. if( 'upcoming appointment' == $this->name )
  296. {
  297. $sql .= sprintf(
  298. $check_time ? '%s < appointment.datetime - INTERVAL %d MINUTE'
  299. : 'DATE( %s ) < DATE( appointment.datetime )',
  300. $viewing_date,
  301. $appointment_pre_window );
  302. }
  303. else if( 'assignable appointment' == $this->name )
  304. {
  305. $sql .= sprintf(
  306. $check_time ? '%s >= appointment.datetime - INTERVAL %d MINUTE AND '.
  307. '%s <= appointment.datetime + INTERVAL %d MINUTE'
  308. : 'DATE( %s ) = DATE( appointment.datetime )',
  309. $viewing_date,
  310. $appointment_pre_window,
  311. $viewing_date,
  312. $appointment_post_window );
  313. }
  314. else if( 'missed appointment' == $this->name )
  315. {
  316. $sql .= sprintf(
  317. $check_time ? '%s > appointment.datetime + INTERVAL %d MINUTE'
  318. : 'DATE( %s ) > DATE( appointment.datetime )',
  319. $viewing_date,
  320. $appointment_post_window );
  321. }
  322. static::db()->execute( $sql );
  323. }
  324. // populate callback upcoming/assignable queues
  325. else if( ' callback' == substr( $this->name, -9 ) )
  326. {
  327. $callback_pre_window =
  328. $setting_manager->get_setting( 'callback', 'call pre-window', $this->db_site );
  329. $sql = sprintf(
  330. 'INSERT INTO queue_has_participant( '.
  331. 'participant_id, queue_id, site_id, qnaire_id, start_qnaire_date, address_id ) '.
  332. 'SELECT DISTINCT queue_has_participant.participant_id, %s, site_id, qnaire_id, start_qnaire_date, '.
  333. 'queue_has_participant.address_id '.
  334. 'FROM queue_has_participant '.
  335. 'JOIN callback ON queue_has_participant.participant_id = callback.participant_id '.
  336. 'AND callback.assignment_id IS NULL '.
  337. 'WHERE queue_id = %s AND ',
  338. $database_class_name::format_string( $this->id ),
  339. $database_class_name::format_string( $db_parent_queue->id ) );
  340. if( 'upcoming callback' == $this->name )
  341. {
  342. $sql .= sprintf(
  343. $check_time ? '%s < callback.datetime - INTERVAL %d MINUTE'
  344. : 'DATE( %s ) < DATE( callback.datetime )',
  345. $viewing_date,
  346. $callback_pre_window );
  347. }
  348. else if( 'assignable callback' == $this->name )
  349. {
  350. $sql .= sprintf(
  351. $check_time ? '%s >= callback.datetime - INTERVAL %d MINUTE'
  352. : 'DATE( %s ) = DATE( callback.datetime )',
  353. $viewing_date,
  354. $callback_pre_window );
  355. }
  356. static::db()->execute( $sql );
  357. }
  358. // populate "last call waiting" queues
  359. else if( ' waiting' == substr( $this->name, -8 ) || ' ready' == substr( $this->name, -6 ) )
  360. {
  361. $call_type = ' waiting' == substr( $this->name, -8 )
  362. ? substr( $this->name, 0, -8 )
  363. : substr( $this->name, 0, -6 );
  364. $callback_timing =
  365. $setting_manager->get_setting( 'callback timing', $call_type, $this->db_site );
  366. $sql = sprintf(
  367. 'INSERT INTO queue_has_participant( '.
  368. 'participant_id, queue_id, site_id, qnaire_id, start_qnaire_date, address_id ) '.
  369. 'SELECT DISTINCT queue_has_participant.participant_id, %s, site_id, qnaire_id, start_qnaire_date, '.
  370. 'queue_has_participant.address_id '.
  371. 'FROM queue_has_participant '.
  372. 'JOIN participant_last_interview '.
  373. 'ON queue_has_participant.participant_id = participant_last_interview.participant_id '.
  374. 'JOIN interview_last_assignment '.
  375. 'ON participant_last_interview.interview_id = interview_last_assignment.interview_id '.
  376. 'JOIN assignment_last_phone_call '.
  377. 'ON interview_last_assignment.assignment_id = assignment_last_phone_call.assignment_id '.
  378. 'JOIN phone_call ON phone_call.id = assignment_last_phone_call.phone_call_id '.
  379. 'WHERE queue_id = %s AND ',
  380. $database_class_name::format_string( $this->id ),
  381. $database_class_name::format_string( $db_parent_queue->id ) );
  382. if( ' waiting' == substr( $this->name, -8 ) )
  383. {
  384. $sql .= sprintf(
  385. $check_time ? '%s < phone_call.end_datetime + INTERVAL %d MINUTE' :
  386. 'DATE( %s ) < DATE( phone_call.end_datetime + INTERVAL %d MINUTE )',
  387. $viewing_date,
  388. $callback_timing );
  389. }
  390. else // ' ready' == substr( $this->name, -6 )
  391. {
  392. $sql .= sprintf(
  393. $check_time ? '%s >= phone_call.end_datetime + INTERVAL %d MINUTE' :
  394. 'DATE( %s ) >= DATE( phone_call.end_datetime + INTERVAL %d MINUTE )',
  395. $viewing_date,
  396. $callback_timing );
  397. }
  398. static::db()->execute( $sql );
  399. }
  400. else
  401. {
  402. $session->release_semaphore();
  403. throw lib::create( 'exception\runtime',
  404. sprintf( 'No rules to populate time-specific queue "%s"', $this->name ),
  405. __METHOD__ );
  406. }
  407. $session->release_semaphore();
  408. }
  409. /**
  410. * The site to restrict the queue to.
  411. * @author Patrick Emond <emondpd@mcmaster.ca>
  412. * @param site $db_site
  413. * @access public
  414. */
  415. public function set_site( $db_site = NULL )
  416. {
  417. $this->db_site = $db_site;
  418. // reset the query list
  419. self::$query_list = array();
  420. }
  421. /**
  422. * Gets the parts of the query for a particular queue.
  423. * @author Patrick Emond <emondpd@mcmaster.ca>
  424. * @param string $queue The name of the queue to get the query parts for
  425. * @param string $phone_call_status The name of which phone call status to get the query parts
  426. * for (or NULL when the queue type is not based on phone call status)
  427. * @return associative array
  428. * @throws exception\argument
  429. * @access protected
  430. * @static
  431. */
  432. protected static function get_query_parts( $queue, $phone_call_status = NULL )
  433. {
  434. // start by getting the queue and parent queue objects from the cache
  435. $queue_name = is_null( $phone_call_status )
  436. ? $queue
  437. : str_replace( 'phone call status', $phone_call_status, $queue );
  438. $db_queue = self::$queue_list_cache[$queue_name]['object'];
  439. if( is_null( $db_queue ) ) // invalid queue name
  440. throw lib::create( 'exception\runtime',
  441. sprintf( 'Cannot find queue named "%s"', $queue_name ), __METHOD__ );
  442. $db_parent_queue = self::$queue_list_cache[$queue_name]['parent'];
  443. // if this is a time-specific queue then return a query which will return no rows
  444. if( $db_queue->time_specific )
  445. return array(
  446. 'from' => array( 'participant_for_queue' ),
  447. 'join' => array(),
  448. 'where' => array( 'false' ) );
  449. // determine what date/time to view the queues
  450. if( is_null( self::$viewing_date ) )
  451. {
  452. $viewing_date = 'UTC_TIMESTAMP()';
  453. $check_time = true;
  454. }
  455. else
  456. {
  457. // put double quotes around the date since it is being inserted into sql below
  458. $viewing_date = sprintf( '"%s"', self::$viewing_date );
  459. $check_time = false;
  460. }
  461. $participant_class_name = lib::get_class_name( 'database\participant' );
  462. // an array containing all of the qnaire queue's direct children queues
  463. $qnaire_children = array(
  464. 'qnaire waiting', 'appointment', 'assigned', 'quota disabled',
  465. 'outside calling time', 'callback', 'new participant', 'old participant' );
  466. // join to the quota table based on site, region, gender and age group
  467. $quota_join =
  468. 'LEFT JOIN quota '.
  469. 'ON quota.site_id = jurisdiction_site_id '.
  470. 'AND quota.region_id = primary_region_id '.
  471. 'AND quota.gender = participant_gender '.
  472. 'AND quota.age_group_id = participant_age_group_id '.
  473. 'LEFT JOIN quota_state '.
  474. 'ON quota.id = quota_state.quota_id';
  475. $appointment_join =
  476. 'LEFT JOIN appointment '.
  477. 'ON appointment.participant_id = participant_for_queue.id '.
  478. 'AND '.
  479. '( '.
  480. '( '.
  481. 'effective_qnaire_type = "home" '.
  482. 'AND appointment.address_id IS NOT NULL '.
  483. ') '.
  484. 'OR '.
  485. '( '.
  486. 'effective_qnaire_type = "site" '.
  487. 'AND appointment.address_id IS NULL '.
  488. ') '.
  489. ')';
  490. // checks to make sure a participant is hours
  491. if( $check_time )
  492. {
  493. $localtime = localtime( time(), true );
  494. $offset = $localtime['tm_isdst']
  495. ? 'first_address_timezone_offset + first_address_daylight_savings'
  496. : 'first_address_timezone_offset';
  497. $calling_time_sql = sprintf(
  498. '( '.
  499. 'first_address_timezone_offset IS NULL OR '.
  500. 'first_address_daylight_savings IS NULL OR '.
  501. '( '.
  502. 'TIME( %s + INTERVAL %s*60 MINUTE ) >= "<CALLING_START_TIME>" AND '.
  503. 'TIME( %s + INTERVAL %s*60 MINUTE ) < "<CALLING_END_TIME>" '.
  504. ') '.
  505. ')',
  506. $viewing_date,
  507. $offset,
  508. $viewing_date,
  509. $offset );
  510. }
  511. // get the parent queue's query parts
  512. if( is_null( $phone_call_status ) )
  513. {
  514. if( !is_null( $db_parent_queue ) ) $parts = self::get_query_parts( $db_parent_queue->name );
  515. }
  516. else if( 'phone call status' == $queue )
  517. {
  518. $parts = self::get_query_parts( 'old participant' );
  519. }
  520. else
  521. {
  522. $parts = self::get_query_parts( 'phone call status', $phone_call_status );
  523. }
  524. // now determine the sql parts for the given queue
  525. if( 'all' == $queue )
  526. {
  527. // NOTE: when updating this query database\participant::get_queue_data()
  528. // should also be updated as it performs a very similar query
  529. $parts = array(
  530. 'from' => array( 'participant_for_queue' ),
  531. 'join' => array(),
  532. 'where' => array( '<SITE_TEST>' ) );
  533. }
  534. else if( 'finished' == $queue )
  535. {
  536. // no effective_qnaire_id means no qnaires left to complete
  537. $parts['where'][] = 'effective_qnaire_id IS NULL';
  538. }
  539. else
  540. {
  541. // effective_qnaire_id is the either the next qnaire to work on or the one in progress
  542. $parts['where'][] = 'effective_qnaire_id IS NOT NULL';
  543. if( 'ineligible' == $queue )
  544. {
  545. // ineligible means either inactive or with a "final" state
  546. $parts['join'][] =
  547. 'LEFT JOIN participant_for_queue_phone_count '.
  548. 'ON participant_for_queue_phone_count.person_id = participant_person_id';
  549. $parts['where'][] =
  550. '( '.
  551. 'participant_active = false '.
  552. 'OR participant_state_id IS NOT NULL '.
  553. 'OR phone_count = 0 '.
  554. 'OR last_consent_accept = 0 '.
  555. ')';
  556. }
  557. else if( 'inactive' == $queue )
  558. {
  559. $parts['where'][] = 'participant_active = false';
  560. }
  561. else if( 'refused consent' == $queue )
  562. {
  563. $parts['where'][] = 'participant_active = true';
  564. $parts['where'][] = 'last_consent_accept = 0';
  565. }
  566. else if( 'condition' == $queue )
  567. {
  568. $parts['where'][] = 'participant_active = true';
  569. $parts['where'][] =
  570. '( '.
  571. 'last_consent_accept IS NULL '.
  572. 'OR last_consent_accept = 1 '.
  573. ')';
  574. $parts['where'][] = 'participant_state_id IS NOT NULL';
  575. }
  576. else if( 'eligible' == $queue )
  577. {
  578. // active participant who does not have a "final" state and has at least one phone number
  579. $parts['join'][] =
  580. 'LEFT JOIN participant_for_queue_phone_count '.
  581. 'ON participant_for_queue_phone_count.person_id = participant_person_id';
  582. $parts['where'][] = 'participant_active = true';
  583. $parts['where'][] = 'participant_state_id IS NULL';
  584. $parts['where'][] = 'phone_count > 0';
  585. $parts['where'][] =
  586. '( '.
  587. 'last_consent_accept IS NULL OR '.
  588. 'last_consent_accept = 1 '.
  589. ')';
  590. }
  591. else if( 'qnaire' == $queue )
  592. {
  593. // no additional parts needed
  594. }
  595. // we must process all of the qnaire queue's direct children as a whole
  596. else if( in_array( $queue, $qnaire_children ) )
  597. {
  598. if( 'qnaire waiting' == $queue )
  599. {
  600. // the current qnaire cannot start before start_qnaire_date
  601. $parts['where'][] = 'start_qnaire_date IS NOT NULL';
  602. $parts['where'][] = sprintf( 'start_qnaire_date > DATE( %s )',
  603. $viewing_date );
  604. }
  605. else
  606. {
  607. // the current qnaire cannot start before start_qnaire_date
  608. $parts['where'][] = sprintf(
  609. '( '.
  610. 'start_qnaire_date IS NULL OR '.
  611. 'start_qnaire_date <= DATE( %s ) '.
  612. ')',
  613. $viewing_date );
  614. $parts['join'][] = $appointment_join;
  615. if( 'appointment' == $queue )
  616. {
  617. // participants with a future appointment
  618. $parts['where'][] = 'appointment.id IS NOT NULL';
  619. }
  620. else
  621. {
  622. // participants without a future appointment
  623. $parts['where'][] = 'appointment.id IS NULL';
  624. if( 'assigned' == $queue )
  625. {
  626. // participants who are currently assigned
  627. $parts['where'][] =
  628. '( last_assignment_id IS NOT NULL AND last_assignment_end_datetime IS NULL )';
  629. }
  630. else
  631. {
  632. // participants who are NOT currently assigned
  633. $parts['where'][] =
  634. '( last_assignment_id IS NULL OR last_assignment_end_datetime IS NOT NULL )';
  635. $parts['join'][] = $quota_join;
  636. if( 'quota disabled' == $queue )
  637. {
  638. // who belong to a quota which is disabled
  639. $parts['where'][] = 'quota_state.disabled = true';
  640. // and who are ot marked to override quota
  641. $parts['where'][] = 'participant_override_quota = false';
  642. }
  643. else
  644. {
  645. // who belong to a quota which is not disabled or doesn't exist
  646. $parts['where'][] =
  647. '( quota_state.disabled IS NULL OR '.
  648. 'quota_state.disabled = false OR '.
  649. 'participant_override_quota = true )';
  650. if( 'outside calling time' == $queue )
  651. {
  652. // outside of the calling time
  653. $parts['where'][] = $check_time
  654. ? 'NOT '.$calling_time_sql
  655. : 'NOT true'; // purposefully a negative tautology
  656. }
  657. else
  658. {
  659. // within the calling time
  660. $parts['where'][] = $check_time
  661. ? $calling_time_sql
  662. : 'true'; // purposefully a tautology
  663. if( 'callback' == $queue )
  664. {
  665. // link to callback table and make sure the callback hasn't been assigned
  666. // (by design, there can only ever one unassigned callback per participant)
  667. $parts['from'][] = 'callback';
  668. $parts['where'][] = 'callback.participant_id = participant_for_queue.id';
  669. $parts['where'][] = 'callback.assignment_id IS NULL';
  670. }
  671. else
  672. {
  673. // Make sure there is no unassigned callback. By design there can only be one of
  674. // per participant, so if the callback is null then the participant has no pending
  675. // callbacks.
  676. $parts['join'][] =
  677. 'LEFT JOIN callback '.
  678. 'ON callback.participant_id = participant_for_queue.id '.
  679. 'AND callback.assignment_id IS NULL';
  680. $parts['where'][] = 'callback.id IS NULL';
  681. if( 'new participant' == $queue )
  682. {
  683. // If there is a start_qnaire_date then the current qnaire has never been
  684. // started, the exception is for participants who have no appointment
  685. $parts['where'][] =
  686. '( '.
  687. 'start_qnaire_date IS NOT NULL OR '.
  688. 'last_assignment_id IS NULL '.
  689. ')';
  690. }
  691. else // old participant
  692. {
  693. // add the last phone call's information
  694. $parts['from'][] = 'phone_call';
  695. $parts['from'][] = 'assignment_last_phone_call';
  696. $parts['where'][] =
  697. 'assignment_last_phone_call.assignment_id = last_assignment_id';
  698. $parts['where'][] =
  699. 'phone_call.id = assignment_last_phone_call.phone_call_id';
  700. // make sure the current interview's qnaire matches the effective qnaire,
  701. // otherwise this participant has never been assigned
  702. $parts['where'][] = 'current_interview_qnaire_id = effective_qnaire_id';
  703. }
  704. }
  705. }
  706. }
  707. }
  708. }
  709. }
  710. }
  711. else if( 'phone call status' == $queue )
  712. {
  713. // a phone call status has been included (all remaining queues require it)
  714. if( is_null( $phone_call_status ) )
  715. throw lib::create( 'exception\argument',
  716. 'phone_call_status', $phone_call_status, __METHOD__ );
  717. $parts['where'][] = 'not reached' == $phone_call_status
  718. ? 'phone_call.status IN ( "machine message","machine no message",'.
  719. '"disconnected","wrong number","not reached" )'
  720. : sprintf( 'phone_call.status = "%s"', $phone_call_status );
  721. }
  722. else // invalid queue name
  723. {
  724. throw lib::create( 'exception\argument', 'queue', $queue, __METHOD__ );
  725. }
  726. }
  727. return $parts;
  728. }
  729. /**
  730. * Get the query for this queue.
  731. * @author Patrick Emond <emondpd@mcmaster.ca>
  732. * @param string $select_participant_sql The text to put in place of the first occurance of
  733. * <SELECT_PARTICIPANT>
  734. * @return string
  735. * @access protected
  736. */
  737. protected function get_sql( $select_participant_sql )
  738. {
  739. $database_class_name = lib::get_class_name( 'database\database' );
  740. // start by making sure the query list has been generated
  741. if( 0 == count( self::$query_list ) ) self::generate_query_list();
  742. $site_test_sql = is_null( $this->db_site )
  743. ? 'true'
  744. : sprintf( 'IFNULL( service_has_participant_preferred_site_id, '.
  745. 'jurisdiction_site_id ) = %s',
  746. $database_class_name::format_string( $db_site->id ) );
  747. $sql = self::$query_list[ $this->name ];
  748. $sql = preg_replace( '/\<SELECT_PARTICIPANT\>/', $select_participant_sql, $sql, 1 );
  749. $sql = str_replace( '<SELECT_PARTICIPANT>', 'participant_for_queue.id', $sql );
  750. $sql = str_replace( '<SITE_TEST>', $site_test_sql, $sql );
  751. // fill in the settings
  752. $setting_manager = lib::create( 'business\setting_manager' );
  753. $setting = $setting_manager->get_setting( 'calling', 'start time', $this->db_site );
  754. $sql = str_replace( '<CALLING_START_TIME>', $setting.':00', $sql );
  755. $setting = $setting_manager->get_setting( 'calling', 'end time', $this->db_site );
  756. $sql = str_replace( '<CALLING_END_TIME>', $setting.':00', $sql );
  757. return $sql;
  758. }
  759. /**
  760. * The date (YYYY-MM-DD) with respect to check all queue states.
  761. * @author Patrick Emond <emondpd@mcmaster.ca>
  762. * @param string $date
  763. * @access public
  764. * @static
  765. */
  766. public static function set_viewing_date( $date = NULL )
  767. {
  768. // validate the input
  769. $datetime_obj = util::get_datetime_object( $date );
  770. if( $date != $datetime_obj->format( 'Y-m-d' ) )
  771. log::err( 'The selected viewing date ('.$date.') may not be valid.' );
  772. self::$viewing_date = $datetime_obj->format( 'Y-m-d' );
  773. }
  774. /**
  775. * Creates the participant_for_queue temporary table needed by all queues.
  776. *
  777. * @author Patrick Emond <emondpd@mcmaster.ca>
  778. * @param database\participant $db_participant If provided then only that participant will
  779. * be affected by the operation.
  780. * @access protected
  781. * @static
  782. */
  783. protected static function create_participant_for_queue( $db_participant = NULL )
  784. {
  785. $database_class_name = lib::get_class_name( 'database\database' );
  786. $service_id = lib::create( 'business\session' )->get_service()->id;
  787. if( static::$participant_for_queue_created ) return;
  788. // build participant_for_queue table
  789. $sql = sprintf( 'CREATE TEMPORARY TABLE IF NOT EXISTS participant_for_queue '.
  790. static::$participant_for_queue_sql,
  791. $database_class_name::format_string( $service_id ) );
  792. if( !is_null( $db_participant ) )
  793. $sql .= sprintf( ' AND participant.id = %s ',
  794. $database_class_name::format_string( $db_participant->id ) );
  795. static::db()->execute( 'DROP TABLE IF EXISTS participant_for_queue' );
  796. static::db()->execute( $sql );
  797. if( is_null( $db_participant ) )
  798. static::db()->execute(
  799. 'ALTER TABLE participant_for_queue '.
  800. 'ADD INDEX fk_id ( id ), '.
  801. 'ADD INDEX fk_participant_person_id ( participant_person_id ), '.
  802. 'ADD INDEX fk_participant_gender ( participant_gender ), '.
  803. 'ADD INDEX fk_participant_language ( participant_language ), '.
  804. 'ADD INDEX fk_participant_age_group_id ( participant_age_group_id ), '.
  805. 'ADD INDEX fk_participant_active ( participant_active ), '.
  806. 'ADD INDEX fk_participant_state_id ( participant_state_id ), '.
  807. 'ADD INDEX fk_service_has_participant_preferred_site_id ( service_has_participant_preferred_site_id ), '.
  808. 'ADD INDEX fk_effective_qnaire_id ( effective_qnaire_id ), '.
  809. 'ADD INDEX fk_last_consent_accept ( last_consent_accept ), '.
  810. 'ADD INDEX fk_last_assignment_id ( last_assignment_id )' );
  811. // build participant_for_queue_phone_count table
  812. $sql = sprintf(
  813. 'CREATE TEMPORARY TABLE IF NOT EXISTS participant_for_queue_phone_count '.
  814. 'SELECT participant.person_id, COUNT(*) phone_count '.
  815. 'FROM participant '.
  816. 'JOIN service_has_participant ON participant.id = service_has_participant.participant_id '.
  817. 'AND service_has_participant.service_id = %s '.
  818. 'LEFT JOIN phone ON participant.person_id = phone.person_id '.
  819. 'AND phone.active AND phone.number IS NOT NULL ',
  820. $database_class_name::format_string( $service_id ) );
  821. if( !is_null( $db_participant ) )
  822. $sql .= sprintf( 'WHERE participant.id = %s ',
  823. $database_class_name::format_string( $db_participant->id ) );
  824. $sql .= 'GROUP BY participant.person_id ';
  825. static::db()->execute( 'DROP TABLE IF EXISTS participant_for_queue_phone_count' );
  826. static::db()->execute( $sql );
  827. if( is_null( $db_participant ) )
  828. static::db()->execute(
  829. 'ALTER TABLE participant_for_queue_phone_count '.
  830. 'ADD INDEX dk_person_id ( person_id ), '.
  831. 'ADD INDEX dk_phone_count ( phone_count )' );
  832. static::$participant_for_queue_created = true;
  833. }
  834. /**
  835. * Creates the queue_list_cache needed by all queues.
  836. *
  837. * @author Patrick Emond <emondpd@mcmaster.ca>
  838. * @access protected
  839. * @static
  840. */
  841. protected static function create_queue_list_cache()
  842. {
  843. if( 0 == count( self::$queue_list_cache ) )
  844. {
  845. $queue_mod = lib::create( 'database\modifier' );
  846. $queue_mod->order( 'id' );
  847. foreach( static::select( $queue_mod ) as $db_queue )
  848. {
  849. self::$queue_list_cache[$db_queue->name] =
  850. array( 'object' => $db_queue,
  851. 'parent' => NULL );
  852. if( !is_null( $db_queue->parent_queue_id ) )
  853. { // this queue has a parent, find and index it
  854. foreach( array_reverse( self::$queue_list_cache ) as $queue_cache )
  855. { // search in reverse order, most parent's aren't far from their child
  856. if( $db_queue->parent_queue_id == $queue_cache['object']->id )
  857. {
  858. self::$queue_list_cache[$db_queue->name]['parent'] = $queue_cache['object'];
  859. break;
  860. }
  861. }
  862. }
  863. }
  864. }
  865. }
  866. /**
  867. * Whether the participant_for_queue temporary table has been created.
  868. * @var boolean
  869. * @access protected
  870. * @static
  871. */
  872. protected static $participant_for_queue_created = false;
  873. /**
  874. * The site to restrict the queue to.
  875. * @var site
  876. * @access protected
  877. */
  878. protected $db_site = NULL;
  879. /**
  880. * The date (YYYY-MM-DD) with respect to check all queue states.
  881. * @var string
  882. * @access protected
  883. * @static
  884. */
  885. protected static $viewing_date = NULL;
  886. /**
  887. * Whether or not calling times are enabled.
  888. * @var boolean
  889. * @access protected
  890. * @static
  891. */
  892. protected static $calling_times_enabled = NULL;
  893. /**
  894. * The queries for each queue
  895. * @var associative array of strings
  896. * @access protected
  897. * @static
  898. */
  899. protected static $query_list = array();
  900. /**
  901. * A cache of all queues and their parents used by get_query_parts()
  902. * @var array
  903. * @access private
  904. * @static
  905. */
  906. private static $queue_list_cache = array();
  907. /**
  908. * A cache of participant counts for each queue and each qnaire
  909. * @var associative array of integers
  910. * @access protected
  911. * @static
  912. */
  913. protected static $participant_count_cache = array();
  914. /**
  915. * A string containing the SQL used to create the participant_for_queue data
  916. * @var string
  917. * @access protected
  918. * @static
  919. */
  920. protected static $participant_for_queue_sql = <<<'SQL'
  921. SELECT participant.id,
  922. participant.person_id AS participant_person_id,
  923. participant.active AS participant_active,
  924. participant.gender AS participant_gender,
  925. participant.age_group_id AS participant_age_group_id,
  926. participant.state_id AS participant_state_id,
  927. participant.language AS participant_language,
  928. participant.override_quota AS participant_override_quota,
  929. service_has_participant.preferred_site_id AS service_has_participant_preferred_site_id,
  930. first_address.id AS first_address_id,
  931. first_address.timezone_offset AS first_address_timezone_offset,
  932. first_address.daylight_savings AS first_address_daylight_savings,
  933. primary_region.id AS primary_region_id,
  934. jurisdiction.site_id AS jurisdiction_site_id,
  935. last_consent.accept AS last_consent_accept,
  936. current_interview.qnaire_id AS current_interview_qnaire_id,
  937. last_assignment.id AS last_assignment_id,
  938. last_assignment.end_datetime AS last_assignment_end_datetime,
  939. IF
  940. (
  941. current_interview.id IS NULL,
  942. ( SELECT id FROM qnaire WHERE rank = 1 ),
  943. IF( current_interview.completed, next_qnaire.id, current_qnaire.id )
  944. ) as effective_qnaire_id,
  945. IF
  946. (
  947. current_qnaire.type IS NULL,
  948. ( SELECT id FROM qnaire WHERE rank = 1 ),
  949. IF( current_interview.completed, next_qnaire.type, current_qnaire.type )
  950. ) as effective_qnaire_type,
  951. IF
  952. (
  953. current_interview.id IS NULL,
  954. NULL,
  955. IF
  956. (
  957. current_interview.completed,
  958. next_prev_assignment.end_datetime + INTERVAL next_qnaire.delay WEEK,
  959. NULL
  960. )
  961. ) as start_qnaire_date
  962. FROM participant
  963. JOIN service_has_participant
  964. ON participant.id = service_has_participant.participant_id
  965. AND service_has_participant.datetime IS NOT NULL
  966. AND service_id = %s
  967. LEFT JOIN person_first_address
  968. ON participant.person_id = person_first_address.person_id
  969. LEFT JOIN address first_address
  970. ON person_first_address.address_id = first_address.id
  971. LEFT JOIN person_primary_address
  972. ON participant.person_id = person_primary_address.person_id
  973. LEFT JOIN address AS primary_address
  974. ON person_primary_address.address_id = primary_address.id
  975. LEFT JOIN region AS primary_region
  976. ON primary_address.region_id = primary_region.id
  977. LEFT JOIN jurisdiction
  978. ON jurisdiction.postcode = primary_address.postcode
  979. AND jurisdiction.service_id = service_has_participant.service_id
  980. JOIN participant_last_consent
  981. ON participant.id = participant_last_consent.participant_id
  982. LEFT JOIN consent AS last_consent
  983. ON last_consent.id = participant_last_consent.consent_id
  984. LEFT JOIN interview AS current_interview
  985. ON current_interview.participant_id = participant.id
  986. LEFT JOIN interview_last_assignment
  987. ON current_interview.id = interview_last_assignment.interview_id
  988. LEFT JOIN assignment AS last_assignment
  989. ON interview_last_assignment.assignment_id = last_assignment.id
  990. LEFT JOIN qnaire AS current_qnaire
  991. ON current_qnaire.id = current_interview.qnaire_id
  992. LEFT JOIN qnaire AS next_qnaire
  993. ON next_qnaire.rank = ( current_qnaire.rank + 1 )
  994. LEFT JOIN qnaire AS next_prev_qnaire
  995. ON next_prev_qnaire.id = next_qnaire.prev_qnaire_id
  996. LEFT JOIN interview AS next_prev_interview
  997. ON next_prev_interview.qnaire_id = next_prev_qnaire.id
  998. AND next_prev_interview.participant_id = participant.id
  999. LEFT JOIN interview_last_assignment next_prev_interview_last_assignment
  1000. ON next_prev_interview.id = next_prev_interview_last_assignment.interview_id
  1001. LEFT JOIN assignment AS next_prev_assignment
  1002. ON next_prev_assignment.id = next_prev_interview_last_assignment.assignment_id
  1003. WHERE
  1004. (
  1005. current_qnaire.rank IS NULL
  1006. OR current_qnaire.rank =
  1007. (
  1008. SELECT MAX( qnaire.rank )
  1009. FROM interview
  1010. JOIN qnaire ON qnaire.id = interview.qnaire_id
  1011. WHERE interview.participant_id = current_interview.participant_id
  1012. GROUP BY current_interview.participant_id
  1013. )
  1014. )
  1015. AND
  1016. (
  1017. next_prev_assignment.end_datetime IS NULL
  1018. OR next_prev_assignment.end_datetime =
  1019. (
  1020. SELECT MAX( assignment.end_datetime )
  1021. FROM interview
  1022. JOIN assignment ON assignment.interview_id = interview.id
  1023. WHERE interview.qnaire_id = next_prev_qnaire.id
  1024. AND assignment.id = next_prev_assignment.id
  1025. GROUP BY next_prev_assignment.interview_id
  1026. )
  1027. )
  1028. SQL;
  1029. }