webform.report.inc 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058
  1. <?php
  2. /**
  3. * @file
  4. * This file includes helper functions for creating reports for webform.module.
  5. *
  6. * @author Nathan Haug <nate@lullabot.com>
  7. */
  8. // All functions within this file need the webform.submissions.inc.
  9. module_load_include('inc', 'webform', 'includes/webform.submissions');
  10. /**
  11. * Retrieve lists of submissions for a given webform.
  12. */
  13. function webform_results_submissions($node, $user_filter, $pager_count) {
  14. global $user;
  15. // Determine whether views or hard-coded tables should be used for the
  16. // submissions table.
  17. if (!webform_variable_get('webform_table')) {
  18. // Load the submissions view.
  19. $view = webform_get_view($node, 'webform_submissions');
  20. if ($user_filter) {
  21. if ($user->uid) {
  22. drupal_set_title(t('Submissions for %user', array('%user' => $user->name)), PASS_THROUGH);
  23. }
  24. else {
  25. drupal_set_title(t('Your submissions'));
  26. webform_disable_page_cache();
  27. }
  28. return $view->preview('default', array($node->nid, $user->uid));
  29. }
  30. else {
  31. return $view->preview('default', array($node->nid));
  32. }
  33. }
  34. if (isset($_GET['results']) && is_numeric($_GET['results'])) {
  35. $pager_count = $_GET['results'];
  36. }
  37. $header = theme('webform_results_submissions_header', array('node' => $node));
  38. if ($user_filter) {
  39. if ($user->uid) {
  40. drupal_set_title(t('Submissions for %user', array('%user' => $user->name)), PASS_THROUGH);
  41. }
  42. else {
  43. drupal_set_title(t('Your submissions'));
  44. webform_disable_page_cache();
  45. }
  46. $submissions = webform_get_submissions(array('nid' => $node->nid, 'uid' => $user->uid), $header, $pager_count);
  47. $count = webform_get_submission_count($node->nid, $user->uid, NULL);
  48. }
  49. else {
  50. $submissions = webform_get_submissions($node->nid, $header, $pager_count);
  51. $count = webform_get_submission_count($node->nid, NULL, NULL);
  52. }
  53. $operation_column = end($header);
  54. $operation_total = $operation_column['colspan'];
  55. $rows = array();
  56. foreach ($submissions as $sid => $submission) {
  57. $row = array(
  58. $submission->is_draft ? t('@serial (draft)', array('@serial' => $submission->serial)) : $submission->serial,
  59. format_date($submission->submitted, 'short'),
  60. );
  61. if (webform_results_access($node, $user)) {
  62. $row[] = theme('username', array('account' => $submission));
  63. $row[] = $submission->remote_addr;
  64. }
  65. $row[] = l(t('View'), "node/$node->nid/submission/$sid");
  66. $operation_count = 1;
  67. // No need to call this multiple times, just reference this in a variable.
  68. $destination = drupal_get_destination();
  69. if (webform_submission_access($node, $submission, 'edit', $user)) {
  70. $row[] = l(t('Edit'), "node/$node->nid/submission/$sid/edit", array('query' => $destination));
  71. $operation_count++;
  72. }
  73. if (webform_submission_access($node, $submission, 'delete', $user)) {
  74. $row[] = l(t('Delete'), "node/$node->nid/submission/$sid/delete", array('query' => $destination));
  75. $operation_count++;
  76. }
  77. if ($operation_count < $operation_total) {
  78. $row[count($row) - 1] = array('data' => $row[count($row) - 1], 'colspan' => $operation_total - $operation_count + 1);
  79. }
  80. $rows[] = $row;
  81. }
  82. $element['#theme'] = 'webform_results_submissions';
  83. $element['#node'] = $node;
  84. $element['#submissions'] = $submissions;
  85. $element['#total_count'] = $count;
  86. $element['#pager_count'] = $pager_count;
  87. $element['#attached']['library'][] = array('webform', 'admin');
  88. $element['table']['#theme'] = 'table';
  89. $element['table']['#header'] = $header;
  90. $element['table']['#rows'] = $rows;
  91. $element['table']['#operation_total'] = $operation_total;
  92. return $element;
  93. }
  94. /**
  95. * Returns the most appropriate view for this webform node.
  96. *
  97. * Site builders can customize the view that webform uses by webform node id or
  98. * by webform content type. For example, use webform_results_123 to for node
  99. * id 123 or webform_results_my_content_type for a node of type my_content_type.
  100. *
  101. * @param object $node
  102. * Loaded webform node.
  103. * @param string $view_id
  104. * The machine_id of the view, such as webform_results or webform_submissions.
  105. *
  106. * @return object|null
  107. * The loaded view.
  108. */
  109. function webform_get_view($node, $view_id) {
  110. foreach (array("{$view_id}_{$node->nid}", "{$view_id}_{$node->type}", $view_id) as $id) {
  111. $view = views_get_view($id);
  112. if ($view) {
  113. return $view;
  114. }
  115. }
  116. }
  117. /**
  118. * Theme the list of links for selecting the number of results per page.
  119. *
  120. * @param array $variables
  121. * Array with keys:
  122. * - "total_count": The total number of results available.
  123. * - "pager_count": The current number of results displayed per page.
  124. *
  125. * @return string
  126. * Pager.
  127. */
  128. function theme_webform_results_per_page(array $variables) {
  129. $total_count = $variables['total_count'];
  130. $pager_count = $variables['pager_count'];
  131. $output = '';
  132. // Create a list of results-per-page options.
  133. $counts = array(
  134. '20' => '20',
  135. '50' => '50',
  136. '100' => '100',
  137. '200' => '200',
  138. '500' => '500',
  139. '1000' => '1000',
  140. '0' => t('All'),
  141. );
  142. $count_links = array();
  143. foreach ($counts as $number => $text) {
  144. if ($number < $total_count) {
  145. $count_links[] = l($text, $_GET['q'], array('query' => array('results' => $number), 'attributes' => array('class' => array($pager_count == $number ? 'selected' : ''))));
  146. }
  147. }
  148. $output .= '<div class="webform-results-per-page">';
  149. if (count($count_links) > 1) {
  150. $output .= t('Show !count results per page.', array('!count' => implode(' | ', $count_links)));
  151. }
  152. else {
  153. $output .= t('Showing all results.');
  154. }
  155. if ($total_count > 1) {
  156. $output .= ' ' . t('@total results total.', array('@total' => $total_count));
  157. }
  158. $output .= '</div>';
  159. return $output;
  160. }
  161. /**
  162. * Theme the header of the submissions table.
  163. *
  164. * This is done in it's own function so that webform can retrieve the header and
  165. * use it for sorting the results.
  166. */
  167. function theme_webform_results_submissions_header($variables) {
  168. $node = $variables['node'];
  169. $columns = array(
  170. array('data' => t('#'), 'field' => 'sid', 'sort' => 'desc'),
  171. array('data' => t('Submitted'), 'field' => 'submitted'),
  172. );
  173. if (webform_results_access($node)) {
  174. $columns[] = array('data' => t('User'), 'field' => 'name');
  175. $columns[] = array('data' => t('IP Address'), 'field' => 'remote_addr');
  176. }
  177. $columns[] = array('data' => t('Operations'), 'colspan' => module_exists('print') ? 5 : 3);
  178. return $columns;
  179. }
  180. /**
  181. * Preprocess function for webform-results-submissions.tpl.php.
  182. */
  183. function template_preprocess_webform_results_submissions(&$vars) {
  184. $vars['node'] = $vars['element']['#node'];
  185. $vars['submissions'] = $vars['element']['#submissions'];
  186. $vars['table'] = $vars['element']['table'];
  187. $vars['total_count'] = $vars['element']['#total_count'];
  188. $vars['pager_count'] = $vars['element']['#pager_count'];
  189. $vars['is_submissions'] = (arg(2) == 'submissions') ? 1 : 0;
  190. unset($vars['element']);
  191. }
  192. /**
  193. * Create a table containing all submitted values for a webform node.
  194. */
  195. function webform_results_table($node, $pager_count = 0) {
  196. // Determine whether views or hard-coded tables should be used for the
  197. // submissions table.
  198. if (!webform_variable_get('webform_table')) {
  199. // Load and preview the results view with a node id argument.
  200. $view = webform_get_view($node, 'webform_results');
  201. return $view->preview('default', array($node->nid));
  202. }
  203. // Get all the submissions for the node.
  204. if (isset($_GET['results']) && is_numeric($_GET['results'])) {
  205. $pager_count = $_GET['results'];
  206. }
  207. // Get all the submissions for the node.
  208. $header = theme('webform_results_table_header', array('node' => $node));
  209. $submissions = webform_get_submissions($node->nid, $header, $pager_count);
  210. $total_count = webform_get_submission_count($node->nid, NULL, NULL);
  211. $output[] = array(
  212. '#theme' => 'webform_results_table',
  213. '#node' => $node,
  214. '#components' => $node->webform['components'],
  215. '#submissions' => $submissions,
  216. '#total_count' => $total_count,
  217. '#pager_count' => $pager_count,
  218. );
  219. if ($pager_count) {
  220. $output[] = array('#theme' => 'pager');
  221. }
  222. return $output;
  223. }
  224. /**
  225. * Theme function for the Webform results table header.
  226. */
  227. function theme_webform_results_table_header($variables) {
  228. return array(
  229. array('data' => t('#'), 'field' => 'sid', 'sort' => 'desc'),
  230. array('data' => t('Submitted'), 'field' => 'submitted'),
  231. array('data' => t('User'), 'field' => 'name'),
  232. array('data' => t('IP Address'), 'field' => 'remote_addr'),
  233. );
  234. }
  235. /**
  236. * Theme the results table displaying all the submissions for a particular node.
  237. *
  238. * @param $node
  239. * The node whose results are being displayed.
  240. * @param $components
  241. * An associative array of the components for this webform.
  242. * @param $submissions
  243. * An array of all submissions for this webform.
  244. * @param $total_count
  245. * The total number of submissions to this webform.
  246. * @param $pager_count
  247. * The number of results to be shown per page.
  248. *
  249. * @return string
  250. * HTML string with result data.
  251. */
  252. function theme_webform_results_table($variables) {
  253. drupal_add_library('webform', 'admin');
  254. $node = $variables['node'];
  255. $submissions = $variables['submissions'];
  256. $total_count = $variables['total_count'];
  257. $pager_count = $variables['pager_count'];
  258. $rows = array();
  259. $cell = array();
  260. // This header has to be generated separately so we can add the SQL necessary.
  261. // to sort the results.
  262. $header = theme('webform_results_table_header', array('node' => $node));
  263. // Generate a row for each submission.
  264. foreach ($submissions as $sid => $submission) {
  265. $link_text = $submission->is_draft ? t('@serial (draft)', array('@serial' => $submission->serial)) : $submission->serial;
  266. $cell[] = l($link_text, 'node/' . $node->nid . '/submission/' . $sid);
  267. $cell[] = format_date($submission->submitted, 'short');
  268. $cell[] = theme('username', array('account' => $submission));
  269. $cell[] = $submission->remote_addr;
  270. $component_headers = array();
  271. // Generate a cell for each component.
  272. foreach ($node->webform['components'] as $component) {
  273. $data = isset($submission->data[$component['cid']]) ? $submission->data[$component['cid']] : NULL;
  274. $submission_output = webform_component_invoke($component['type'], 'table', $component, $data);
  275. if ($submission_output !== NULL) {
  276. $component_headers[] = array('data' => check_plain($component['name']), 'field' => $component['cid']);
  277. $cell[] = $submission_output;
  278. }
  279. }
  280. $rows[] = $cell;
  281. unset($cell);
  282. }
  283. if (!empty($component_headers)) {
  284. $header = array_merge($header, $component_headers);
  285. }
  286. if (count($rows) == 0) {
  287. $rows[] = array(array('data' => t('There are no submissions for this form. <a href="!url">View this form</a>.', array('!url' => url('node/' . $node->nid))), 'colspan' => 4));
  288. }
  289. $output = '';
  290. $output .= theme('webform_results_per_page', array('total_count' => $total_count, 'pager_count' => $pager_count));
  291. $output .= theme('table', array('header' => $header, 'rows' => $rows));
  292. return $output;
  293. }
  294. /**
  295. * Delete all submissions for a node.
  296. *
  297. * @param $nid
  298. * The node id whose submissions will be deleted.
  299. * @param $batch_size
  300. * The number of submissions to be processed. NULL means all submissions.
  301. *
  302. * @return int
  303. * The number of submissions processed.
  304. */
  305. function webform_results_clear($nid, $batch_size = NULL) {
  306. $node = node_load($nid);
  307. $submissions = webform_get_submissions($nid, NULL, $batch_size);
  308. $count = 0;
  309. foreach ($submissions as $submission) {
  310. webform_submission_delete($node, $submission);
  311. $count++;
  312. }
  313. return $count;
  314. }
  315. /**
  316. * Confirmation form to delete all submissions for a node.
  317. *
  318. * @param $nid
  319. * ID of node for which to clear submissions.
  320. */
  321. function webform_results_clear_form($form, $form_state, $node) {
  322. drupal_set_title(t('Clear Form Submissions'));
  323. $form = array();
  324. $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
  325. $question = t('Are you sure you want to delete all submissions for this form?');
  326. return confirm_form($form, $question, 'node/' . $node->nid . '/webform-results', NULL, t('Clear'), t('Cancel'));
  327. }
  328. /**
  329. * Form submit handler.
  330. */
  331. function webform_results_clear_form_submit($form, &$form_state) {
  332. $nid = $form_state['values']['nid'];
  333. $node = node_load($nid);
  334. // Set a modest batch size, due to the inefficiency of the hooks invoked when
  335. // submissions are deleted.
  336. $batch_size = min(webform_export_batch_size($node), 500);
  337. // Set up a batch to clear the results.
  338. $batch = array(
  339. 'operations' => array(
  340. array('webform_clear_batch_rows', array($node, $batch_size)),
  341. ),
  342. 'finished' => 'webform_clear_batch_finished',
  343. 'title' => t('Clear submissions'),
  344. 'init_message' => t('Clearing submission data'),
  345. 'error_message' => t('The submissions could not be cleared because an error occurred.'),
  346. 'file' => drupal_get_path('module', 'webform') . '/includes/webform.report.inc',
  347. );
  348. batch_set($batch);
  349. $form_state['redirect'] = 'node/' . $nid . '/webform-results';
  350. }
  351. /**
  352. * Batch API callback; Write the rows of the export to the export file.
  353. */
  354. function webform_clear_batch_rows($node, $batch_size, &$context) {
  355. // Initialize the results if this is the first execution of the batch
  356. // operation.
  357. if (!isset($context['results']['count'])) {
  358. $context['results'] = array(
  359. 'count' => 0,
  360. 'total' => webform_get_submission_count($node->nid),
  361. 'node' => $node,
  362. );
  363. }
  364. // Clear a batch of submissions.
  365. $count = webform_results_clear($node->nid, $batch_size);
  366. $context['results']['count'] += $count;
  367. // Display status message.
  368. $context['message'] = t('Cleared @count of @total submissions...', array('@count' => $context['results']['count'], '@total' => $context['results']['total']));
  369. $context['finished'] = $count > 0
  370. ? $context['results']['count'] / $context['results']['total']
  371. : 1.0;
  372. }
  373. /**
  374. * Batch API completion callback; Finish clearing submissions.
  375. */
  376. function webform_clear_batch_finished($success, $results, $operations) {
  377. if ($success) {
  378. $title = $results['node']->title;
  379. drupal_set_message(t('Webform %title entries cleared.', array('%title' => $title)));
  380. watchdog('webform', 'Webform %title entries cleared.', array('%title' => $title));
  381. }
  382. }
  383. /**
  384. * Form to configure the download of CSV files.
  385. */
  386. function webform_results_download_form($form, &$form_state, $node) {
  387. module_load_include('inc', 'webform', 'includes/webform.export');
  388. module_load_include('inc', 'webform', 'includes/webform.components');
  389. $form['#attached']['js'][] = drupal_get_path('module', 'webform') . '/js/webform-admin.js';
  390. // If an export is waiting to be downloaded, redirect the user there after
  391. // the page has finished loading.
  392. if (isset($_SESSION['webform_export_info'])) {
  393. $download_url = url('node/' . $node->nid . '/webform-results/download-file', array('absolute' => TRUE));
  394. $form['#attached']['js'][] = array('data' => array('webformExport' => $download_url), 'type' => 'setting');
  395. }
  396. $form['node'] = array(
  397. '#type' => 'value',
  398. '#value' => $node,
  399. );
  400. $form['format'] = array(
  401. '#type' => 'radios',
  402. '#title' => t('Export format'),
  403. '#options' => webform_export_list(),
  404. '#default_value' => webform_variable_get('webform_export_format'),
  405. );
  406. $form['delimited_options'] = array(
  407. '#type' => 'container',
  408. 'warning' => array(
  409. '#markup' => '<p>' .
  410. t('<strong>Warning:</strong> Opening delimited text files with spreadsheet applications may expose you to <a href="!link">formula injection</a> or other security vulnerabilities. When the submissions contain data from untrusted users and the downloaded file will be used with spreadsheets, use Microsoft Excel format.',
  411. array('!link' => url('https://www.google.com/search?q=spreadsheet+formula+injection'))) .
  412. '</p>',
  413. ),
  414. 'delimiter' => array(
  415. '#type' => 'select',
  416. '#title' => t('Delimited text format'),
  417. '#description' => t('This is the delimiter used in the CSV/TSV file when downloading Webform results. Using tabs in the export is the most reliable method for preserving non-latin characters. You may want to change this to another character depending on the program with which you anticipate importing results.'),
  418. '#default_value' => webform_variable_get('webform_csv_delimiter'),
  419. '#options' => array(
  420. ',' => t('Comma (,)'),
  421. '\t' => t('Tab (\t)'),
  422. ';' => t('Semicolon (;)'),
  423. ':' => t('Colon (:)'),
  424. '|' => t('Pipe (|)'),
  425. '.' => t('Period (.)'),
  426. ' ' => t('Space ( )'),
  427. ),
  428. ),
  429. '#states' => array(
  430. 'visible' => array(
  431. ':input[name=format]' => array('value' => 'delimited'),
  432. ),
  433. ),
  434. );
  435. $form['header_keys'] = array(
  436. '#type' => 'radios',
  437. '#title' => t('Column header format'),
  438. '#options' => array(
  439. -1 => t('None'),
  440. 0 => t('Label'),
  441. 1 => t('Form Key'),
  442. ),
  443. '#default_value' => 0,
  444. '#description' => t('Choose whether to show the label or form key in each column header.'),
  445. );
  446. $form['select_options'] = array(
  447. '#type' => 'fieldset',
  448. '#title' => t('Select list options'),
  449. '#collapsible' => TRUE,
  450. '#collapsed' => TRUE,
  451. );
  452. $form['select_options']['select_keys'] = array(
  453. '#type' => 'radios',
  454. '#title' => t('Select keys'),
  455. '#options' => array(
  456. 0 => t('Full, human-readable options (values)'),
  457. 1 => t('Short, raw options (keys)'),
  458. ),
  459. '#default_value' => 0,
  460. '#description' => t('Choose which part of options should be displayed from key|value pairs.'),
  461. );
  462. $form['select_options']['select_format'] = array(
  463. '#type' => 'radios',
  464. '#title' => t('Select list format'),
  465. '#options' => array(
  466. 'separate' => t('Separate'),
  467. 'compact' => t('Compact'),
  468. ),
  469. '#default_value' => 'separate',
  470. '#attributes' => array('class' => array('webform-select-list-format')),
  471. '#theme' => 'webform_results_download_select_format',
  472. );
  473. $csv_components = array('info' => t('Submission information'));
  474. // Prepend information fields with "-" to indent.
  475. foreach (webform_results_download_submission_information($node) as $key => $title) {
  476. $csv_components[$key] = '-' . $title;
  477. }
  478. $csv_components += webform_component_list($node, 'csv', TRUE);
  479. $form['components'] = array(
  480. '#type' => 'select',
  481. '#title' => t('Included export components'),
  482. '#options' => $csv_components,
  483. '#default_value' => array_keys($csv_components),
  484. '#multiple' => TRUE,
  485. '#size' => 10,
  486. '#description' => t('The selected components will be included in the export.'),
  487. '#process' => array('webform_component_select'),
  488. );
  489. $form['range'] = array(
  490. '#type' => 'fieldset',
  491. '#title' => t('Download range options'),
  492. '#collapsible' => TRUE,
  493. '#collapsed' => TRUE,
  494. '#tree' => TRUE,
  495. '#theme' => 'webform_results_download_range',
  496. '#element_validate' => array('webform_results_download_range_validate'),
  497. '#after_build' => array('webform_results_download_range_after_build'),
  498. );
  499. $form['range']['range_type'] = array(
  500. '#type' => 'radios',
  501. '#options' => array(
  502. 'all' => t('All submissions'),
  503. 'new' => t('Only new submissions since your last download'),
  504. 'latest' => t('Only the latest'),
  505. 'range_serial' => t('All submissions starting from'),
  506. 'range_date' => t('All submissions by date'),
  507. ),
  508. '#default_value' => 'all',
  509. );
  510. $form['range']['latest'] = array(
  511. '#type' => 'textfield',
  512. '#size' => 5,
  513. '#maxlength' => 8,
  514. );
  515. $form['range']['start'] = array(
  516. '#type' => 'textfield',
  517. '#size' => 5,
  518. '#maxlength' => 8,
  519. );
  520. $form['range']['end'] = array(
  521. '#type' => 'textfield',
  522. '#size' => 5,
  523. '#maxlength' => 8,
  524. );
  525. $date_attributes = array('placeholder' => format_date(REQUEST_TIME, 'custom', webform_date_format('short')));
  526. $form['range']['start_date'] = array(
  527. '#type' => 'textfield',
  528. '#size' => 20,
  529. '#attributes' => $date_attributes,
  530. );
  531. $form['range']['end_date'] = array(
  532. '#type' => 'textfield',
  533. '#size' => 20,
  534. '#attributes' => $date_attributes,
  535. );
  536. // If drafts are allowed, provide options to filter download based on draft
  537. // status.
  538. $form['range']['completion_type'] = array(
  539. '#type' => 'radios',
  540. '#title' => t('Included submissions'),
  541. '#default_value' => 'all',
  542. '#options' => array(
  543. 'all' => t('Finished and draft submissions'),
  544. 'finished' => t('Finished submissions only'),
  545. 'draft' => t('Drafts only'),
  546. ),
  547. '#access' => ($node->webform['allow_draft'] || $node->webform['auto_save']),
  548. );
  549. // By default results are downloaded. User can override this value if
  550. // programmatically submitting this form.
  551. $form['download'] = array(
  552. '#type' => 'value',
  553. '#default_value' => TRUE,
  554. );
  555. $form['actions'] = array('#type' => 'actions');
  556. $form['actions']['submit'] = array(
  557. '#type' => 'submit',
  558. '#value' => t('Download'),
  559. );
  560. return $form;
  561. }
  562. /**
  563. * FormAPI element validate function for the range fieldset.
  564. */
  565. function webform_results_download_range_validate($element, $form_state) {
  566. switch ($element['range_type']['#value']) {
  567. case 'latest':
  568. // Download latest x submissions.
  569. if ($element['latest']['#value'] == '') {
  570. form_error($element['latest'], t('Latest number of submissions field is required.'));
  571. }
  572. else {
  573. if (!is_numeric($element['latest']['#value'])) {
  574. form_error($element['latest'], t('Latest number of submissions must be numeric.'));
  575. }
  576. else {
  577. if ($element['latest']['#value'] <= 0) {
  578. form_error($element['latest'], t('Latest number of submissions must be greater than 0.'));
  579. }
  580. }
  581. }
  582. break;
  583. case 'range_serial':
  584. // Download Start-End range of submissions.
  585. // Start submission number.
  586. if ($element['start']['#value'] == '') {
  587. form_error($element['start'], t('Start submission number is required.'));
  588. }
  589. else {
  590. if (!is_numeric($element['start']['#value'])) {
  591. form_error($element['start'], t('Start submission number must be numeric.'));
  592. }
  593. else {
  594. if ($element['start']['#value'] <= 0) {
  595. form_error($element['start'], t('Start submission number must be greater than 0.'));
  596. }
  597. }
  598. }
  599. // End submission number.
  600. if ($element['end']['#value'] != '') {
  601. if (!is_numeric($element['end']['#value'])) {
  602. form_error($element['end'], t('End submission number must be numeric.'));
  603. }
  604. else {
  605. if ($element['end']['#value'] <= 0) {
  606. form_error($element['end'], t('End submission number must be greater than 0.'));
  607. }
  608. else {
  609. if ($element['end']['#value'] < $element['start']['#value']) {
  610. form_error($element['end'], t('End submission number must not be less than Start submission number.'));
  611. }
  612. }
  613. }
  614. }
  615. break;
  616. case 'range_date':
  617. // Download Start-end range of submissions.
  618. // Start submission time.
  619. $format = webform_date_format('short');
  620. $start_date = DateTime::createFromFormat($format, $element['start_date']['#value']);
  621. if ($element['start_date']['#value'] == '') {
  622. form_error($element['start_date'], t('Start date range is required.'));
  623. }
  624. elseif ($start_date === FALSE) {
  625. form_error($element['start_date'], t('Start date range is not in a valid format.'));
  626. }
  627. // End submission time.
  628. $end_date = DateTime::createFromFormat($format, $element['end_date']['#value']);
  629. if ($element['end_date']['#value'] != '') {
  630. if ($end_date === FALSE) {
  631. form_error($element['end_date'], t('End date range is not in a valid format.'));
  632. }
  633. elseif ($start_date !== FALSE && $start_date > $end_date) {
  634. form_error($element['end_date'], t('End date range must not be before the Start date.'));
  635. }
  636. }
  637. break;
  638. }
  639. // Check that the range will return something at all.
  640. $range_options = array(
  641. 'range_type' => $element['range_type']['#value'],
  642. 'start' => $element['start']['#value'],
  643. 'end' => $element['end']['#value'],
  644. 'latest' => $element['latest']['#value'],
  645. 'start_date' => $element['start_date']['#value'],
  646. 'end_date' => $element['end_date']['#value'],
  647. 'completion_type' => $element['completion_type']['#value'],
  648. 'batch_size' => 1,
  649. 'batch_number' => 0,
  650. );
  651. if (!form_get_errors() && !webform_download_sids_count($form_state['values']['node']->nid, $range_options)) {
  652. form_error($element['range_type'], t('The specified range will not return any results.'));
  653. }
  654. }
  655. /**
  656. * FormAPI after build function for the download range fieldset.
  657. */
  658. function webform_results_download_range_after_build($element, &$form_state) {
  659. $node = $form_state['values']['node'];
  660. // Build a list of counts of new and total submissions.
  661. $last_download = webform_download_last_download_info($node->nid);
  662. $element['#webform_download_info']['sid'] = $last_download ? $last_download['sid'] : 0;
  663. $element['#webform_download_info']['serial'] = $last_download ? $last_download['serial'] : NULL;
  664. $element['#webform_download_info']['requested'] = $last_download ? $last_download['requested'] : $node->created;
  665. $element['#webform_download_info']['total'] = webform_get_submission_count($node->nid, NULL, NULL);
  666. $element['#webform_download_info']['new'] = webform_download_sids_count($node->nid, array('range_type' => 'new'));
  667. return $element;
  668. }
  669. /**
  670. * Theme the output of the export range fieldset.
  671. */
  672. function theme_webform_results_download_range($variables) {
  673. drupal_add_library('webform', 'admin');
  674. $element = $variables['element'];
  675. $download_info = $element['#webform_download_info'];
  676. // Set description for total of all submissions.
  677. $element['range_type']['all']['#theme_wrappers'] = array('webform_inline_radio');
  678. $element['range_type']['all']['#title'] .= ' (' . t('@count total', array('@count' => $download_info['total'])) . ')';
  679. // Set description for "New submissions since last download".
  680. $format = webform_date_format('short');
  681. $requested_date = format_date($download_info['requested'], 'custom', $format);
  682. $element['range_type']['new']['#theme_wrappers'] = array('webform_inline_radio');
  683. $element['range_type']['new']['#title'] .= ' (' . t('@count new since @date', array('@count' => $download_info['new'], '@date' => $requested_date)) . ')';
  684. // Disable option if there are no new submissions.
  685. if ($download_info['new'] == 0) {
  686. $element['range_type']['new']['#attributes']['disabled'] = 'disabled';
  687. }
  688. // Render latest x submissions option.
  689. $element['latest']['#attributes']['class'][] = 'webform-set-active';
  690. $element['latest']['#theme_wrappers'] = array();
  691. $element['range_type']['latest']['#theme_wrappers'] = array('webform_inline_radio');
  692. $element['range_type']['latest']['#title'] = t('Only the latest !number submissions', array('!number' => drupal_render($element['latest'])));
  693. // Render Start-End submissions option.
  694. $element['start']['#attributes']['class'][] = 'webform-set-active';
  695. $element['end']['#attributes']['class'][] = 'webform-set-active';
  696. $element['start']['#theme_wrappers'] = array();
  697. $element['end']['#theme_wrappers'] = array();
  698. $element['start_date']['#attributes']['class'] = array('webform-set-active');
  699. $element['end_date']['#attributes']['class'] = array('webform-set-active');
  700. $element['start_date']['#theme_wrappers'] = array();
  701. $element['end_date']['#theme_wrappers'] = array();
  702. $element['range_type']['range_serial']['#theme_wrappers'] = array('webform_inline_radio');
  703. $last_serial = $download_info['serial'] ? $download_info['serial'] : drupal_placeholder(t('none'));
  704. $element['range_type']['range_serial']['#title'] = t('Submissions by number from !start and optionally to: !end &nbsp; (Last downloaded: !serial)',
  705. array('!start' => drupal_render($element['start']), '!end' => drupal_render($element['end']), '!serial' => $last_serial));
  706. // Date range.
  707. $element['range_type']['range_date']['#theme_wrappers'] = array('webform_inline_radio');
  708. $element['range_type']['range_date']['#title'] = t('Submissions by date from !start_date and optionally to: !end_date', array('!start_date' => drupal_render($element['start_date']), '!end_date' => drupal_render($element['end_date'])));
  709. return drupal_render_children($element);
  710. }
  711. /**
  712. * Theme the output of the select list format radio buttons.
  713. */
  714. function theme_webform_results_download_select_format($variables) {
  715. drupal_add_library('webform', 'admin');
  716. $element = $variables['element'];
  717. $output = '';
  718. // Build an example table for the separate option.
  719. $header = array(t('Option A'), t('Option B'), t('Option C'));
  720. $rows = array(
  721. array('X', '', ''),
  722. array('X', '', 'X'),
  723. array('', 'X', 'X'),
  724. );
  725. $element['separate']['#attributes']['class'] = array();
  726. $element['separate']['#description'] = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE));
  727. $element['separate']['#description'] .= t('Separate options are more suitable for building reports, graphs, and statistics in a spreadsheet application.');
  728. $output .= drupal_render($element['separate']);
  729. // Build an example table for the compact option.
  730. $header = array(t('My select list'));
  731. $rows = array(
  732. array('Option A'),
  733. array('Option A,Option C'),
  734. array('Option B,Option C'),
  735. );
  736. $element['compact']['#attributes']['class'] = array();
  737. $element['compact']['#description'] = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE));
  738. $element['compact']['#description'] .= t('Compact options are more suitable for importing data into other systems.');
  739. $output .= drupal_render($element['compact']);
  740. return $output;
  741. }
  742. /**
  743. * Submit handler for webform_results_download_form().
  744. */
  745. function webform_results_download_form_submit(&$form, &$form_state) {
  746. $node = $form_state['values']['node'];
  747. $format = $form_state['values']['format'];
  748. $options = array(
  749. 'delimiter' => $form_state['values']['delimiter'],
  750. 'components' => array_keys(array_filter($form_state['values']['components'])),
  751. 'header_keys' => $form_state['values']['header_keys'],
  752. 'select_keys' => $form_state['values']['select_keys'],
  753. 'select_format' => $form_state['values']['select_format'],
  754. 'range' => $form_state['values']['range'],
  755. 'download' => $form_state['values']['download'],
  756. );
  757. $defaults = webform_results_download_default_options($node, $format);
  758. $options += $defaults;
  759. $options['range'] += $defaults['range'];
  760. // Determine an appropriate batch size based on the form and server specs.
  761. if (!isset($options['range']['batch_size'])) {
  762. $options['range']['batch_size'] = webform_export_batch_size($node);
  763. }
  764. $options['file_name'] = _webform_export_tempname();
  765. // Set up a batch to export the results.
  766. $batch = webform_results_export_batch($node, $format, $options);
  767. batch_set($batch);
  768. }
  769. /**
  770. * Calculate an appropriate batch size for bulk submission operations.
  771. *
  772. * @param object $node
  773. * The webform node.
  774. *
  775. * @return int
  776. * The number of submissions to be processed at once.
  777. */
  778. function webform_export_batch_size($node) {
  779. // Start the batch size at 50,000 per batch, but divide by number of
  780. // components in the form. For example, if a form had 10 components, it would
  781. // export 5,000 submissions at a time.
  782. $batch_size = ceil(50000 / max(1, count($node->webform['components'])));
  783. // Every 32MB of additional memory after 64MB adds a multiplier in size.
  784. $memory_limit = parse_size(ini_get('memory_limit'));
  785. $mb = 1048576;
  786. $memory_modifier = max(1, ($memory_limit - (64 * $mb)) / (32 * $mb));
  787. $batch_size = ceil($batch_size * $memory_modifier);
  788. // For time reasons, limit the batch size to 5,000.
  789. $batch_size = min($batch_size, 5000);
  790. // Allow a non-UI configuration to override the batch size.
  791. $batch_size = variable_get('webform_export_batch_size', $batch_size);
  792. return $batch_size;
  793. }
  794. /**
  795. * Returns a temporary export filename.
  796. */
  797. function _webform_export_tempname() {
  798. $webform_export_path = variable_get('webform_export_path', 'temporary://');
  799. // If the directory does not exist, create it.
  800. file_prepare_directory($webform_export_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  801. return drupal_tempnam($webform_export_path, 'webform_');
  802. }
  803. /**
  804. * Generate a Excel-readable CSV file containing all submissions for a Webform.
  805. *
  806. * @deprecated in webform:7.x-4.8 and is removed from webform:7.x-5.0. Use
  807. * webform_results_export_batch().
  808. * @see https://www.drupal.org/project/webform/issues/2465291
  809. *
  810. * @return array|null
  811. * The array of export info or null if the file could not be opened.
  812. */
  813. function webform_results_export($node, $format = 'delimited', $options = array()) {
  814. module_load_include('inc', 'webform', 'includes/webform.export');
  815. module_load_include('inc', 'webform', 'includes/webform.components');
  816. $defaults = webform_results_download_default_options($node, $format);
  817. $options += $defaults;
  818. $options['range'] += $defaults['range'];
  819. // Open a new Webform exporter object.
  820. $exporter = webform_export_create_handler($format, $options);
  821. $file_name = _webform_export_tempname();
  822. $handle = fopen($file_name, 'w');
  823. if (!$handle) {
  824. return;
  825. }
  826. // Add the beginning of file marker (little-endian usually for MS Excel).
  827. $exporter->bof($handle);
  828. // Add headers to the file.
  829. $row_count = 0;
  830. $col_count = 0;
  831. $headers = webform_results_download_headers($node, $options);
  832. foreach ($headers as $row) {
  833. // Output header if header_keys is non-negative. -1 means no headers.
  834. if ($options['header_keys'] >= 0) {
  835. $exporter->add_row($handle, $row, $row_count);
  836. $row_count++;
  837. }
  838. $col_count = count($row) > $col_count ? count($row) : $col_count;
  839. }
  840. // Write data from submissions. $last_is is non_NULL to trigger returning it
  841. // by reference.
  842. $last_sid = TRUE;
  843. $rows = webform_results_download_rows($node, $options, 0, $last_sid);
  844. foreach ($rows as $row) {
  845. $exporter->add_row($handle, $row, $row_count);
  846. $row_count++;
  847. }
  848. // Add the closing bytes.
  849. $exporter->eof($handle, $row_count, $col_count);
  850. // Close the file.
  851. @fclose($handle);
  852. $export_info['format'] = $format;
  853. $export_info['options'] = $options;
  854. $export_info['file_name'] = $file_name;
  855. $export_info['row_count'] = $row_count;
  856. $export_info['col_count'] = $col_count;
  857. $export_info['last_sid'] = $last_sid;
  858. $export_info['exporter'] = $exporter;
  859. return $export_info;
  860. }
  861. /**
  862. * Return a Batch API array of commands that will generate an export.
  863. *
  864. * @param $node
  865. * The webform node on which to generate the analysis.
  866. * @param string $format
  867. * (optional) Delimiter of the exported file.
  868. * @param array $options
  869. * (optional) An associative array of options that define the output format.
  870. * These are generally passed through from the GUI interface. Possible options
  871. * include:
  872. * - sids: An array of submission IDs to which this export may be filtered.
  873. * May be used to generate exports that are per-user or other groups of
  874. * submissions.
  875. *
  876. * @return array
  877. * A Batch API array suitable to pass to batch_set().
  878. */
  879. function webform_results_export_batch($node, $format = 'delimited', array $options = array()) {
  880. $defaults = webform_results_download_default_options($node, $format);
  881. $options += $defaults;
  882. $options['range'] += $defaults['range'];
  883. return array(
  884. 'operations' => array(
  885. array('webform_results_batch_bof', array($node, $format, $options)),
  886. array('webform_results_batch_headers', array($node, $format, $options)),
  887. array('webform_results_batch_rows', array($node, $format, $options)),
  888. array('webform_results_batch_eof', array($node, $format, $options)),
  889. array('webform_results_batch_post_process', array($node, $format, $options)),
  890. array('webform_results_batch_results', array($node, $format, $options)),
  891. ),
  892. 'finished' => 'webform_results_batch_finished',
  893. 'title' => t('Exporting submissions'),
  894. 'init_message' => t('Creating export file'),
  895. 'error_message' => t('The export file could not be created because an error occurred.'),
  896. 'file' => drupal_get_path('module', 'webform') . '/includes/webform.report.inc',
  897. );
  898. }
  899. /**
  900. * Print the header rows for the downloadable webform data.
  901. *
  902. * @param $node
  903. * The webform node on which to generate the analysis.
  904. * @param array $options
  905. * A list of options that define the output format. These are generally passed
  906. * through from the GUI interface.
  907. */
  908. function webform_results_download_headers($node, array $options) {
  909. module_load_include('inc', 'webform', 'includes/webform.components');
  910. $submission_information = webform_results_download_submission_information($node, $options);
  911. // Fill in the header for the submission information (if any).
  912. $header[2] = $header[1] = $header[0] = count($submission_information) ? array_fill(0, count($submission_information), '') : array();
  913. if (count($submission_information)) {
  914. $header[0][0] = $node->title;
  915. if ($options['header_keys']) {
  916. $header[1][0] = t('submission_details');
  917. $submission_information_headers = array_keys($submission_information);
  918. }
  919. else {
  920. $header[1][0] = t('Submission Details');
  921. $submission_information_headers = array_values($submission_information);
  922. }
  923. foreach ($submission_information_headers as $column => $label) {
  924. $header[2][$column] = $label;
  925. }
  926. }
  927. // Compile header information for components.
  928. foreach ($options['components'] as $cid) {
  929. if (isset($node->webform['components'][$cid])) {
  930. $component = $node->webform['components'][$cid];
  931. // Let each component determine its headers.
  932. if (webform_component_feature($component['type'], 'csv')) {
  933. $component_header = (array) webform_component_invoke($component['type'], 'csv_headers', $component, $options);
  934. // Allow modules to modify the component CSV header.
  935. drupal_alter('webform_csv_header', $component_header, $component);
  936. // Merge component CSV header to overall CSV header.
  937. $header[0] = array_merge($header[0], (array) $component_header[0]);
  938. $header[1] = array_merge($header[1], (array) $component_header[1]);
  939. $header[2] = array_merge($header[2], (array) $component_header[2]);
  940. }
  941. }
  942. }
  943. return $header;
  944. }
  945. /**
  946. * Returns rows of downloadable webform data.
  947. *
  948. * @deprecated in webform:7.x-4.8 and is removed from webform:7.x-5.0. See
  949. * webform_results_download_rows_process().
  950. * @see https://www.drupal.org/project/webform/issues/2465291
  951. *
  952. * @param $node
  953. * The webform node on which to generate the analysis.
  954. * @param array $options
  955. * A list of options that define the output format. These are generally passed
  956. * through from the GUI interface.
  957. * @param int $serial_start
  958. * The starting position for the Serial column in the output.
  959. * @param $last_sid
  960. * If set to a non-NULL value, the last sid will be returned.
  961. *
  962. * @return array
  963. * An array of rows built according to the provided $serial_start and
  964. * $pager_count variables. Note that the current page number is determined
  965. * by the super-global $_GET['page'] variable.
  966. */
  967. function webform_results_download_rows($node, array $options, $serial_start = 0, &$last_sid = NULL) {
  968. // Get all the required submissions for the download.
  969. $filters['nid'] = $node->nid;
  970. if (isset($options['sids'])) {
  971. $filters['sid'] = $options['sids'];
  972. }
  973. elseif (!empty($options['completion_type']) && $options['completion_type'] !== 'all') {
  974. $filters['is_draft'] = (int) ($options['completion_type'] === 'draft');
  975. }
  976. $submissions = webform_get_submissions($filters, NULL);
  977. if (isset($last_sid)) {
  978. $last_sid = end($submissions) ? key($submissions) : NULL;
  979. }
  980. return webform_results_download_rows_process($node, $options, $serial_start, $submissions);
  981. }
  982. /**
  983. * Processes the submissions to be downloaded into exported rows.
  984. *
  985. * This is an internal routine and not intended for use by other modules.
  986. *
  987. * @param $node
  988. * The webform node on which to generate the analysis.
  989. * @param array $options
  990. * A list of options that define the output format. These are generally passed
  991. * through from the GUI interface.
  992. * @param $serial_start
  993. * The starting position for the Serial column in the output.
  994. * @param array $submissions
  995. * An associative array of loaded submissions, indexed by sid.
  996. *
  997. * @return array
  998. * An array of rows built according to the provided $serial_start and
  999. * $pager_count variables. Note that the current page number is determined
  1000. * by the super-global $_GET['page'] variable.
  1001. */
  1002. function webform_results_download_rows_process($node, array $options, $serial_start, array $submissions) {
  1003. module_load_include('inc', 'webform', 'includes/webform.components');
  1004. $submission_information = webform_results_download_submission_information($node, $options);
  1005. // Generate a row for each submission.
  1006. $row_count = 0;
  1007. $rows = array();
  1008. foreach ($submissions as $sid => $submission) {
  1009. $row_count++;
  1010. $row = array();
  1011. // Add submission information.
  1012. foreach (array_keys($submission_information) as $token) {
  1013. $cell = module_invoke_all('webform_results_download_submission_information_data', $token, $submission, $options, $serial_start, $row_count);
  1014. $context = array('token' => $token, 'submission' => $submission, 'options' => $options, 'serial_start' => $serial_start, 'row_count' => $row_count);
  1015. drupal_alter('webform_results_download_submission_information_data', $cell, $context);
  1016. // implode() to ensure everything from a single value goes into one
  1017. // column, even if more than one module responds to this item.
  1018. $row[] = implode(', ', $cell);
  1019. }
  1020. foreach ($options['components'] as $cid) {
  1021. if (isset($node->webform['components'][$cid])) {
  1022. $component = $node->webform['components'][$cid];
  1023. // Let each component add its data.
  1024. $raw_data = isset($submission->data[$cid]) ? $submission->data[$cid] : NULL;
  1025. if (webform_component_feature($component['type'], 'csv')) {
  1026. $data = webform_component_invoke($component['type'], 'csv_data', $component, $options, $raw_data);
  1027. // Allow modules to modify the CSV data.
  1028. drupal_alter('webform_csv_data', $data, $component, $submission);
  1029. if (is_array($data)) {
  1030. $row = array_merge($row, array_values($data));
  1031. }
  1032. else {
  1033. $row[] = isset($data) ? $data : '';
  1034. }
  1035. }
  1036. }
  1037. }
  1038. $rows[$serial_start + $row_count] = $row;
  1039. }
  1040. return $rows;
  1041. }
  1042. /**
  1043. * Default columns for submission information.
  1044. *
  1045. * By default all exports have several columns of generic information that
  1046. * applies to all submissions. This function returns the list of generic columns
  1047. * plus columns added by other modules.
  1048. *
  1049. * @param $options
  1050. * Filter down the list of columns based on a provided column list.
  1051. *
  1052. * @return array
  1053. * List of generic columns plus columns added by other modules
  1054. */
  1055. function webform_results_download_submission_information($node, $options = array()) {
  1056. $submission_information = module_invoke_all('webform_results_download_submission_information_info');
  1057. drupal_alter('webform_results_download_submission_information_info', $submission_information);
  1058. if (isset($options['components'])) {
  1059. foreach ($submission_information as $key => $label) {
  1060. if (!in_array($key, $options['components'])) {
  1061. unset($submission_information[$key]);
  1062. }
  1063. }
  1064. }
  1065. return $submission_information;
  1066. }
  1067. /**
  1068. * Implements hook_webform_results_download_submission_information_info().
  1069. */
  1070. function webform_webform_results_download_submission_information_info() {
  1071. return array(
  1072. 'webform_serial' => t('Serial'),
  1073. 'webform_sid' => t('SID'),
  1074. 'webform_time' => t('Submitted Time'),
  1075. 'webform_completed_time' => t('Completed Time'),
  1076. 'webform_modified_time' => t('Modified Time'),
  1077. 'webform_draft' => t('Draft'),
  1078. 'webform_ip_address' => t('IP Address'),
  1079. 'webform_uid' => t('UID'),
  1080. 'webform_username' => t('Username'),
  1081. );
  1082. }
  1083. /**
  1084. * Implements hook_webform_results_download_submission_information_data().
  1085. */
  1086. function webform_webform_results_download_submission_information_data($token, $submission, array $options, $serial_start, $row_count) {
  1087. switch ($token) {
  1088. case 'webform_serial':
  1089. return $submission->serial;
  1090. case 'webform_sid':
  1091. return $submission->sid;
  1092. case 'webform_time':
  1093. // Return timestamp in local time (not UTC).
  1094. if (!empty($options['iso8601_date'])) {
  1095. return format_date($submission->submitted, 'custom', 'Y-m-d\TH:i:s');
  1096. }
  1097. else {
  1098. return format_date($submission->submitted, 'short');
  1099. }
  1100. case 'webform_completed_time':
  1101. if (!$submission->completed) {
  1102. return '';
  1103. }
  1104. // Return timestamp in local time (not UTC).
  1105. elseif (!empty($options['iso8601_date'])) {
  1106. return format_date($submission->completed, 'custom', 'Y-m-d\TH:i:s');
  1107. }
  1108. else {
  1109. return format_date($submission->completed, 'short');
  1110. }
  1111. case 'webform_modified_time':
  1112. // Return timestamp in local time (not UTC).
  1113. if (!empty($options['iso8601_date'])) {
  1114. return format_date($submission->modified, 'custom', 'Y-m-d\TH:i:s');
  1115. }
  1116. else {
  1117. return format_date($submission->modified, 'short');
  1118. }
  1119. case 'webform_draft':
  1120. return $submission->is_draft;
  1121. case 'webform_ip_address':
  1122. return $submission->remote_addr;
  1123. case 'webform_uid':
  1124. return $submission->uid;
  1125. case 'webform_username':
  1126. return $submission->name;
  1127. }
  1128. }
  1129. /**
  1130. * Get options for creating downloadable versions of the webform data.
  1131. *
  1132. * @param $node
  1133. * The webform node on which to generate the analysis.
  1134. * @param string $format
  1135. * The export format being used.
  1136. *
  1137. * @return array
  1138. * Option for creating downloadable version of the webform data.
  1139. */
  1140. function webform_results_download_default_options($node, $format) {
  1141. $submission_information = webform_results_download_submission_information($node);
  1142. $options = array(
  1143. 'delimiter' => webform_variable_get('webform_csv_delimiter'),
  1144. 'components' => array_merge(array_keys($submission_information), array_keys(webform_component_list($node, 'csv', TRUE))),
  1145. 'header_keys' => 0,
  1146. 'select_keys' => 0,
  1147. 'select_format' => 'separate',
  1148. 'range' => array(
  1149. 'range_type' => 'all',
  1150. 'completion_type' => 'all',
  1151. ),
  1152. );
  1153. // Allow exporters to merge in additional options.
  1154. $exporter_information = webform_export_fetch_definition($format);
  1155. if (isset($exporter_information['options'])) {
  1156. $options = array_merge($options, $exporter_information['options']);
  1157. }
  1158. return $options;
  1159. }
  1160. /**
  1161. * Send a generated webform results file to the user's browser.
  1162. *
  1163. * @param $node
  1164. * The webform node.
  1165. * @param $export_info
  1166. * Export information array retrieved from webform_results_export().
  1167. */
  1168. function webform_results_download($node, $export_info) {
  1169. // If the exporter provides a custom download method, use that.
  1170. if (method_exists($export_info['exporter'], 'download')) {
  1171. $export_info['exporter']->download($node, $export_info);
  1172. }
  1173. // Otherwise use the set_headers() method to set headers and then read in the
  1174. // file directly. Delete it when complete.
  1175. else {
  1176. $export_name = _webform_safe_name($node->title);
  1177. if (!strlen($export_name)) {
  1178. $export_name = t('Untitled');
  1179. }
  1180. $export_info['exporter']->set_headers($export_name);
  1181. ob_clean();
  1182. // The @ makes it silent.
  1183. @readfile($export_info['file_name']);
  1184. // Clean up, the @ makes it silent.
  1185. @unlink($export_info['file_name']);
  1186. }
  1187. // Save the last exported sid for new-only exports.
  1188. webform_results_export_success($node, $export_info);
  1189. exit();
  1190. }
  1191. /**
  1192. * Save the last-exported sid for new-only exports.
  1193. *
  1194. * @param $node
  1195. * The webform node.
  1196. * @param $export_info
  1197. * Export information array retrieved from webform_results_export().
  1198. */
  1199. function webform_results_export_success($node, $export_info) {
  1200. if (!in_array($export_info['options']['range']['range_type'], array('range', 'range_serial', 'range_date')) && !empty($export_info['last_sid'])) {
  1201. // Insert a new record or update an existing record.
  1202. db_merge('webform_last_download')
  1203. ->key(array(
  1204. 'nid' => $node->nid,
  1205. 'uid' => $GLOBALS['user']->uid,
  1206. ))
  1207. ->fields(array(
  1208. 'sid' => $export_info['last_sid'],
  1209. 'requested' => REQUEST_TIME,
  1210. ))
  1211. ->execute();
  1212. }
  1213. }
  1214. /**
  1215. * Menu callback; Download an exported file.
  1216. *
  1217. * This callabck requires that an export file be already generated by a batch
  1218. * process. The $_SESSION settings are usually put in place by the
  1219. * webform_results_export_results() function.
  1220. *
  1221. * @param $node
  1222. * The webform $node whose export file is being downloaded.
  1223. *
  1224. * @return null|string
  1225. * Either an export file is downloaded with the appropriate HTTP headers set,
  1226. * or an error page is shown if now export is available to download.
  1227. */
  1228. function webform_results_download_callback($node) {
  1229. if (isset($_SESSION['webform_export_info'])) {
  1230. module_load_include('inc', 'webform', 'includes/webform.export');
  1231. $export_info = $_SESSION['webform_export_info'];
  1232. $export_info['exporter'] = webform_export_create_handler($export_info['format'], $export_info['options']);
  1233. unset($_SESSION['webform_export_info']);
  1234. if (isset($_COOKIE['webform_export_info'])) {
  1235. unset($_COOKIE['webform_export_info']);
  1236. $params = session_get_cookie_params();
  1237. setcookie('webform_export_info', '', -1, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  1238. }
  1239. webform_results_download($node, $export_info);
  1240. }
  1241. else {
  1242. return t('No export file ready for download. The file may have already been downloaded by your browser. Visit the <a href="!href">download export form</a> to create a new export.', array('!href' => url('node/' . $node->nid . '/webform-results/download')));
  1243. }
  1244. }
  1245. /**
  1246. * Batch API callback; Write the opening byte in the export file.
  1247. */
  1248. function webform_results_batch_bof($node, $format = 'delimited', $options = array(), &$context = NULL) {
  1249. module_load_include('inc', 'webform', 'includes/webform.export');
  1250. $exporter = webform_export_create_handler($format, $options);
  1251. $handle = fopen($options['file_name'], 'w');
  1252. if (!$handle) {
  1253. return;
  1254. }
  1255. $exporter->bof($handle);
  1256. @fclose($handle);
  1257. }
  1258. /**
  1259. * Batch API callback; Write the headers of the export to the export file.
  1260. */
  1261. function webform_results_batch_headers($node, $format = 'delimited', $options = array(), &$context = NULL) {
  1262. module_load_include('inc', 'webform', 'includes/webform.export');
  1263. $exporter = webform_export_create_handler($format, $options);
  1264. $handle = fopen($options['file_name'], 'a');
  1265. if (!$handle) {
  1266. return;
  1267. }
  1268. $headers = webform_results_download_headers($node, $options);
  1269. $row_count = 0;
  1270. $col_count = 0;
  1271. foreach ($headers as $row) {
  1272. // Output header if header_keys is non-negative. -1 means no headers.
  1273. if ($options['header_keys'] >= 0) {
  1274. $exporter->add_row($handle, $row, $row_count);
  1275. $row_count++;
  1276. }
  1277. $col_count = count($row) > $col_count ? count($row) : $col_count;
  1278. }
  1279. $context['results']['row_count'] = $row_count;
  1280. $context['results']['col_count'] = $col_count;
  1281. @fclose($handle);
  1282. }
  1283. /**
  1284. * Batch API callback; Write the rows of the export to the export file.
  1285. */
  1286. function webform_results_batch_rows($node, $format = 'delimited', $options = array(), &$context = NULL) {
  1287. module_load_include('inc', 'webform', 'includes/webform.export');
  1288. // Initialize the sandbox if this is the first execution of the batch
  1289. // operation.
  1290. if (!isset($context['sandbox']['batch_number'])) {
  1291. $context['sandbox']['batch_number'] = 0;
  1292. $context['sandbox']['sid_count'] = webform_download_sids_count($node->nid, $options['range']);
  1293. $context['sandbox']['batch_max'] = max(1, ceil($context['sandbox']['sid_count'] / $options['range']['batch_size']));
  1294. $context['sandbox']['serial'] = 0;
  1295. $context['sandbox']['last_sid'] = 0;
  1296. }
  1297. // Retrieve the submissions for this batch process.
  1298. $options['range']['batch_number'] = $context['sandbox']['batch_number'];
  1299. $query = webform_download_sids_query($node->nid, $options['range']);
  1300. // Join to the users table to include user name in results, as required by
  1301. // webform_results_download_rows_process.
  1302. $query->leftJoin('users', 'u', 'u.uid = ws.uid');
  1303. $query->fields('u', array('name'));
  1304. $query->fields('ws');
  1305. if (!empty($options['sids'])) {
  1306. $query->condition('ws.sid', $options['sids'], 'IN');
  1307. }
  1308. $submissions = webform_get_submissions_load($query);
  1309. $rows = webform_results_download_rows_process($node, $options, $context['sandbox']['serial'], $submissions);
  1310. // Write these submissions to the file.
  1311. $exporter = webform_export_create_handler($format, $options);
  1312. $handle = fopen($options['file_name'], 'a');
  1313. if (!$handle) {
  1314. return;
  1315. }
  1316. foreach ($rows as $row) {
  1317. $exporter->add_row($handle, $row, $context['results']['row_count']);
  1318. $context['results']['row_count']++;
  1319. }
  1320. $context['sandbox']['serial'] += count($submissions);
  1321. $context['sandbox']['last_sid'] = end($submissions) ? key($submissions) : NULL;
  1322. $context['sandbox']['batch_number']++;
  1323. @fclose($handle);
  1324. // Display status message.
  1325. $context['message'] = t('Exported @count of @total submissions...', array('@count' => $context['sandbox']['serial'], '@total' => $context['sandbox']['sid_count']));
  1326. $context['finished'] = $context['sandbox']['batch_number'] < $context['sandbox']['batch_max']
  1327. ? $context['sandbox']['batch_number'] / $context['sandbox']['batch_max']
  1328. : 1.0;
  1329. $context['results']['last_sid'] = $context['sandbox']['last_sid'];
  1330. }
  1331. /**
  1332. * Batch API callback; Write the closing bytes in the export file.
  1333. */
  1334. function webform_results_batch_eof($node, $format = 'delimited', $options = array(), &$context = NULL) {
  1335. module_load_include('inc', 'webform', 'includes/webform.export');
  1336. $exporter = webform_export_create_handler($format, $options);
  1337. // We open the file for reading and writing, rather than just appending for
  1338. // exporters that need to adjust the beginning of the files as well as the
  1339. // end, i.e. webform_exporter_excel_xlsx::eof().
  1340. $handle = fopen($options['file_name'], 'r+');
  1341. if (!$handle) {
  1342. return;
  1343. }
  1344. // Move pointer to the end of the file.
  1345. fseek($handle, 0, SEEK_END);
  1346. $exporter->eof($handle, $context['results']['row_count'], $context['results']['col_count']);
  1347. @fclose($handle);
  1348. }
  1349. /**
  1350. * Batch API callback; Do any last processing on the finished export.
  1351. */
  1352. function webform_results_batch_post_process($node, $format = 'delimited', $options = array(), &$context = NULL) {
  1353. module_load_include('inc', 'webform', 'includes/webform.export');
  1354. $context['results']['node'] = $node;
  1355. $context['results']['file_name'] = $options['file_name'];
  1356. $exporter = webform_export_create_handler($format, $options);
  1357. $exporter->post_process($context['results']);
  1358. }
  1359. /**
  1360. * Batch API callback; Set the $_SESSION variables used to download the file.
  1361. *
  1362. * Because we want the user to be returned to the main form first, we have to
  1363. * temporarily store information about the created file, send the user to the
  1364. * form, then use JavaScript to request node/x/webform-results/download-file,
  1365. * which will execute webform_results_download_file().
  1366. */
  1367. function webform_results_batch_results($node, $format, $options, &$context) {
  1368. $export_info = array(
  1369. 'format' => $format,
  1370. 'options' => $options,
  1371. 'file_name' => $context['results']['file_name'],
  1372. 'row_count' => $context['results']['row_count'],
  1373. 'last_sid' => $context['results']['last_sid'],
  1374. );
  1375. if (isset($_SESSION)) {
  1376. // UI exection. Defer resetting last-downloaded sid until download page. Set
  1377. // a session variable containing the information referencing the exported
  1378. // file. A cookie is also set to allow the browser to ensure the redirect to
  1379. // the file only happens one time.
  1380. $_SESSION['webform_export_info'] = $export_info;
  1381. $params = session_get_cookie_params();
  1382. setcookie('webform_export_info', '1', REQUEST_TIME + 120, $params['path'], $params['domain'], $params['secure'], FALSE);
  1383. }
  1384. else {
  1385. // Drush execution of wfx command. Reset last-downloaded sid now. $_SESSION
  1386. // is not supported for command line execution.
  1387. webform_results_export_success($node, $export_info);
  1388. }
  1389. }
  1390. /**
  1391. * Batch API completion callback; Display completion message and cleanup.
  1392. */
  1393. function webform_results_batch_finished($success, $results, $operations) {
  1394. if ($success) {
  1395. $download_url = url('node/' . $results['node']->nid . '/webform-results/download-file');
  1396. drupal_set_message(t('Export creation complete. Your download should begin now. If it does not start, <a href="!href">download the file here</a>. This file may only be downloaded once.', array('!href' => $download_url)));
  1397. }
  1398. else {
  1399. drupal_set_message(t('An error occurred while generating the export file.'));
  1400. if (isset($results['file_name']) && is_file($results['file_name'])) {
  1401. @unlink($results['file_name']);
  1402. }
  1403. }
  1404. }
  1405. /**
  1406. * Provides a simple analysis of all submissions to a webform.
  1407. *
  1408. * @param $node
  1409. * The webform node on which to generate the analysis.
  1410. * @param $sids
  1411. * An array of submission IDs to which this analysis may be filtered. May be
  1412. * used to generate results that are per-user or other groups of submissions.
  1413. * @param $analysis_component
  1414. * A webform component. If passed in, additional information may be returned
  1415. * relating specifically to that component's analysis, such as a list of
  1416. * "Other" values within a select list.
  1417. *
  1418. * @return array
  1419. * Renderable array: A simple analysis of all submissions to a webform.
  1420. */
  1421. function webform_results_analysis($node, $sids = array(), $analysis_component = NULL) {
  1422. if (!is_array($sids)) {
  1423. $sids = array();
  1424. }
  1425. // Build a renderable for the content of this page.
  1426. $analysis = array(
  1427. '#theme' => array('webform_analysis__' . $node->nid, 'webform_analysis'),
  1428. '#node' => $node,
  1429. '#component' => $analysis_component,
  1430. );
  1431. // See if a query (possibly with exposed filter) needs to restrict the
  1432. // submissions that are being analyzed.
  1433. $query = NULL;
  1434. if (empty($sids)) {
  1435. $view = webform_get_view($node, 'webform_analysis');
  1436. if ($view->type != t('Default') || $view->name != 'webform_analysis') {
  1437. // The view has been customized from the no-op built-in view. Use it.
  1438. $view->set_display();
  1439. $view->init_handlers();
  1440. $view->override_url = $_GET['q'];
  1441. $view->preview = TRUE;
  1442. $view->pre_execute(array($node->nid));
  1443. $view->build();
  1444. // Let modules modify the view just prior to executing it.
  1445. foreach (module_implements('views_pre_execute') as $module) {
  1446. $function = $module . '_views_pre_execute';
  1447. $function($view);
  1448. }
  1449. // If the view is already executed, there was an error in generating it.
  1450. $query = $view->executed ? NULL : $view->query->query();
  1451. $view->post_execute();
  1452. if (isset($view->exposed_widgets)) {
  1453. $analysis['exposed_filter']['#markup'] = $view->exposed_widgets;
  1454. }
  1455. }
  1456. }
  1457. // If showing all components, display selection form.
  1458. if (!$analysis_component) {
  1459. $analysis['form'] = drupal_get_form('webform_analysis_components_form', $node);
  1460. }
  1461. // Add all the components to the analysis renderable array.
  1462. $components = isset($analysis_component) ? array($analysis_component['cid']) : webform_analysis_enabled_components($node);
  1463. foreach ($components as $cid) {
  1464. // Do component specific call.
  1465. $component = $node->webform['components'][$cid];
  1466. if ($data = webform_component_invoke($component['type'], 'analysis', $component, $sids, isset($analysis_component), $query)) {
  1467. drupal_alter('webform_analysis_component_data', $data, $node, $component);
  1468. $analysis['data'][$cid] = array(
  1469. '#theme' => array('webform_analysis_component__' . $node->nid . '__' . $cid, 'webform_analysis_component__' . $node->nid, 'webform_analysis_component'),
  1470. '#node' => $node,
  1471. '#component' => $component,
  1472. '#data' => $data,
  1473. );
  1474. $analysis['data'][$cid]['basic'] = array(
  1475. '#theme' => array('webform_analysis_component_basic__' . $node->nid . '__' . $cid, 'webform_analysis_component_basic__' . $node->nid, 'webform_analysis_component_basic'),
  1476. '#component' => $component,
  1477. '#data' => $data,
  1478. );
  1479. }
  1480. }
  1481. drupal_alter('webform_analysis', $analysis);
  1482. return $analysis;
  1483. }
  1484. /**
  1485. * Prerender function for webform-analysis.tpl.php.
  1486. */
  1487. function template_preprocess_webform_analysis(&$variables) {
  1488. $analysis = $variables['analysis'];
  1489. $variables['node'] = $analysis['#node'];
  1490. $variables['component'] = $analysis['#component'];
  1491. }
  1492. /**
  1493. * Prerender function for webform-analysis-component.tpl.php.
  1494. */
  1495. function template_preprocess_webform_analysis_component(&$variables) {
  1496. $component_analysis = $variables['component_analysis'];
  1497. $variables['node'] = $component_analysis['#node'];
  1498. $variables['component'] = $component_analysis['#component'];
  1499. // Ensure defaults.
  1500. $variables['component_analysis']['#data'] += array(
  1501. 'table_header' => NULL,
  1502. 'table_rows' => array(),
  1503. 'other_data' => array(),
  1504. );
  1505. $variables['classes_array'][] = 'webform-analysis-component-' . $variables['component']['type'];
  1506. $variables['classes_array'][] = 'webform-analysis-component--' . str_replace('_', '-', implode('--', webform_component_parent_keys($variables['node'], $variables['component'])));
  1507. }
  1508. /**
  1509. * Render an individual component's analysis data in a table.
  1510. *
  1511. * @param $variables
  1512. * An array of theming variables for this theme function. Included keys:
  1513. * - $component: The component whose analysis is being rendered.
  1514. * - $data: An array of array containing the analysis data. Contains the keys:
  1515. * - table_header: If this table has more than a single column, an array
  1516. * of header labels.
  1517. * - table_rows: If this component has a table that should be rendered, an
  1518. * array of values
  1519. *
  1520. * @return string
  1521. * The rendered table.
  1522. */
  1523. function theme_webform_analysis_component_basic($variables) {
  1524. $data = $variables['data'];
  1525. // Ensure defaults.
  1526. $data += array(
  1527. 'table_header' => NULL,
  1528. 'table_rows' => array(),
  1529. 'other_data' => array(),
  1530. );
  1531. // Combine the "other data" into the table rows by default.
  1532. if (is_array($data['other_data'])) {
  1533. foreach ($data['other_data'] as $other_data) {
  1534. if (is_array($other_data)) {
  1535. $data['table_rows'][] = $other_data;
  1536. }
  1537. else {
  1538. $data['table_rows'][] = array(
  1539. array(
  1540. 'colspan' => 2,
  1541. 'data' => $other_data,
  1542. ),
  1543. );
  1544. }
  1545. }
  1546. }
  1547. elseif (strlen($data['other_data'])) {
  1548. $data['table_rows'][] = array(
  1549. 'colspan' => 2,
  1550. 'data' => $data['other_data'],
  1551. );
  1552. }
  1553. return theme('table', array(
  1554. 'header' => $data['table_header'],
  1555. 'rows' => $data['table_rows'],
  1556. 'sticky' => FALSE,
  1557. 'attributes' => array('class' => array('webform-analysis-table')),
  1558. ));
  1559. }
  1560. /**
  1561. * Return a list of components that should be displayed for analysis.
  1562. *
  1563. * @param $node
  1564. * The node whose components' data is being analyzed.
  1565. *
  1566. * @return array
  1567. * An array of component IDs.
  1568. */
  1569. function webform_analysis_enabled_components($node) {
  1570. $cids = array();
  1571. foreach ($node->webform['components'] as $cid => $component) {
  1572. if (!empty($component['extra']['analysis'])) {
  1573. $cids[] = $cid;
  1574. }
  1575. }
  1576. return $cids;
  1577. }
  1578. /**
  1579. * Form for selecting which components should be shown on the analysis page.
  1580. */
  1581. function webform_analysis_components_form($form, &$form_state, $node) {
  1582. form_load_include($form_state, 'inc', 'webform', 'includes/webform.components');
  1583. $form['#node'] = $node;
  1584. $component_list = webform_component_list($node, 'analysis', TRUE);
  1585. $enabled_components = webform_analysis_enabled_components($node);
  1586. if (empty($component_list)) {
  1587. $help = t('No components have added that support analysis. <a href="!url">Add components to your form</a> to see calculated data.', array('!url' => url('node/' . $node->nid . '/webform')));
  1588. }
  1589. elseif (empty($enabled_components)) {
  1590. $help = t('No components have analysis enabled in this form. Enable analysis under the "Add analysis components" fieldset.');
  1591. }
  1592. else {
  1593. $help = t('This page shows analysis of submitted data, such as the number of submissions per component value, calculations, and averages. Additional components may be added under the "Add analysis components" fieldset.');
  1594. }
  1595. $form['help'] = array(
  1596. '#markup' => '<p>' . $help . '</p>',
  1597. '#access' => !empty($help),
  1598. '#weight' => -100,
  1599. );
  1600. $form['components'] = array(
  1601. '#type' => 'select',
  1602. '#title' => t('Add analysis components'),
  1603. '#options' => $component_list,
  1604. '#default_value' => $enabled_components,
  1605. '#multiple' => TRUE,
  1606. '#size' => 10,
  1607. '#description' => t('The selected components will be included on the analysis page.'),
  1608. '#process' => array('webform_component_select'),
  1609. '#access' => count($component_list),
  1610. );
  1611. $form['actions'] = array(
  1612. '#type' => 'actions',
  1613. '#access' => count($component_list),
  1614. );
  1615. $form['actions']['submit'] = array(
  1616. '#type' => 'submit',
  1617. '#value' => t('Update analysis display'),
  1618. );
  1619. return $form;
  1620. }
  1621. /**
  1622. * Submit handler for webform_analysis_components_form().
  1623. */
  1624. function webform_analysis_components_form_submit($form, $form_state) {
  1625. $node = $form['#node'];
  1626. foreach ($form_state['values']['components'] as $cid => $enabled) {
  1627. $node->webform['components'][$cid]['extra']['analysis'] = (bool) $enabled;
  1628. }
  1629. node_save($node);
  1630. }
  1631. /**
  1632. * Output the content of the Analysis page.
  1633. *
  1634. * @see webform_results_analysis()
  1635. */
  1636. function theme_webform_results_analysis($variables) {
  1637. $node = $variables['node'];
  1638. $data = $variables['data'];
  1639. $analysis_component = $variables['component'];
  1640. $rows = array();
  1641. $question_number = 0;
  1642. $single = isset($analysis_component);
  1643. $header = array(
  1644. $single ? $analysis_component['name'] : t('Q'),
  1645. array('data' => $single ? '&nbsp;' : t('responses'), 'colspan' => '10'),
  1646. );
  1647. foreach ($data as $cid => $row_data) {
  1648. $question_number++;
  1649. if (is_array($row_data)) {
  1650. $row = array();
  1651. if (!$single) {
  1652. $row['data'][] = array('data' => '<strong>' . $question_number . '</strong>', 'rowspan' => count($row_data) + 1, 'valign' => 'top');
  1653. $row['data'][] = array('data' => '<strong>' . check_plain($node->webform['components'][$cid]['name']) . '</strong>', 'colspan' => '10');
  1654. $row['class'][] = 'webform-results-question';
  1655. }
  1656. $rows = array_merge($rows, array_merge(array($row), $row_data));
  1657. }
  1658. }
  1659. if (count($rows) == 0) {
  1660. $rows[] = array(array('data' => t('There are no submissions for this form. <a href="!url">View this form</a>.', array('!url' => url('node/' . $node->nid))), 'colspan' => 20));
  1661. }
  1662. return theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE, 'attributes' => array('class' => array('webform-results-analysis'))));
  1663. }
  1664. /**
  1665. * Given a set of range options, retrieve a set of SIDs for a webform node.
  1666. *
  1667. * @deprecated in webform:7.x-4.8 and is removed from webform:7.x-5.0. Use
  1668. * webform_download_sids_query().
  1669. * @see https://www.drupal.org/project/webform/issues/2465291
  1670. */
  1671. function webform_download_sids($nid, $range_options, $uid = NULL) {
  1672. return webform_download_sids_query($nid, $range_options, $uid)
  1673. ->fields('ws', array('sid'))
  1674. ->execute()
  1675. ->fetchCol();
  1676. }
  1677. /**
  1678. * Retrieves a count the number of matching submissions.
  1679. */
  1680. function webform_download_sids_count($nid, $range_options, $uid = NULL) {
  1681. return webform_download_sids_query($nid, $range_options, $uid)
  1682. ->countQuery()
  1683. ->execute()
  1684. ->fetchField();
  1685. }
  1686. /**
  1687. * Given a set of range options, return an unexecuted select query.
  1688. *
  1689. * The query will have no fields as they should be added by the caller as
  1690. * desired.
  1691. *
  1692. * @param int $nid
  1693. * The node id of the webform.
  1694. * @param array $range_options
  1695. * Associate array of range options.
  1696. * @param int $uid
  1697. * The user id of the user whose last download information should be used,
  1698. * or the current user if NULL. This is unrelated to which user submitted
  1699. * the submissions.
  1700. *
  1701. * @return QueryAlterableInterface
  1702. * The query object.
  1703. */
  1704. function webform_download_sids_query($nid, array $range_options, $uid = NULL) {
  1705. $query = db_select('webform_submissions', 'ws')
  1706. ->condition('ws.nid', $nid)
  1707. ->addTag('webform_download_sids');
  1708. switch ($range_options['range_type']) {
  1709. case 'all':
  1710. // All Submissions.
  1711. $query->orderBy('ws.sid', 'ASC');
  1712. break;
  1713. case 'new':
  1714. // All Since Last Download.
  1715. $download_info = webform_download_last_download_info($nid, $uid);
  1716. $last_sid = $download_info ? $download_info['sid'] : 0;
  1717. $query
  1718. ->condition('ws.sid', $last_sid, '>')
  1719. ->orderBy('ws.sid', 'ASC');
  1720. break;
  1721. case 'latest':
  1722. // Last x Submissions.
  1723. $start_sid = webform_download_latest_start_sid($nid, $range_options['latest'], $range_options['completion_type']);
  1724. $query->condition('ws.sid', $start_sid, '>=');
  1725. break;
  1726. case 'range':
  1727. // Submissions Start-End.
  1728. $query->condition('ws.sid', $range_options['start'], '>=');
  1729. if ($range_options['end']) {
  1730. $query->condition('ws.sid', $range_options['end'], '<=');
  1731. }
  1732. $query->orderBy('ws.sid', 'ASC');
  1733. break;
  1734. case 'range_serial':
  1735. // Submissions Start-End, using serial numbers.
  1736. $query->condition('ws.serial', $range_options['start'], '>=');
  1737. if ($range_options['end']) {
  1738. $query->condition('ws.serial', $range_options['end'], '<=');
  1739. }
  1740. $query->orderBy('ws.serial', 'ASC');
  1741. break;
  1742. case 'range_date':
  1743. $date_field = $range_options['completion_type'] == 'finished' ? 'ws.completed' : 'ws.submitted';
  1744. $format = webform_date_format('short');
  1745. $start_date = DateTime::createFromFormat($format, $range_options['start_date']);
  1746. $start_date->setTime(0, 0, 0);
  1747. $query->condition($date_field, $start_date->getTimestamp(), '>=');
  1748. $end_time = DateTime::createFromFormat($format, $range_options['end_date']);
  1749. if ($range_options['end_date'] != '' && ($end_time !== FALSE)) {
  1750. // Check for the full day's submissions.
  1751. $end_time->setTime(23, 59, 59);
  1752. $query->condition($date_field, $end_time->getTimestamp(), '<=');
  1753. }
  1754. $query->orderBy($date_field, 'ASC');
  1755. break;
  1756. }
  1757. // Filter down to draft or finished submissions.
  1758. if (!empty($range_options['completion_type']) && $range_options['completion_type'] !== 'all') {
  1759. $query->condition('is_draft', (int) ($range_options['completion_type'] === 'draft'));
  1760. }
  1761. if (isset($range_options['batch_number']) && !empty($range_options['batch_size'])) {
  1762. $query->range($range_options['batch_number'] * $range_options['batch_size'], $range_options['batch_size']);
  1763. }
  1764. drupal_alter('webform_download_sids_query', $query);
  1765. return $query;
  1766. }
  1767. /**
  1768. * Get this user's last download information, including the SID and timestamp.
  1769. *
  1770. * This function provides an array of information about the last download that
  1771. * a user had for a particular Webform node. Currently it only returns an array
  1772. * with two keys:
  1773. * - sid: The last submission ID that was downloaded.
  1774. * - requested: The timestamp of the last download request.
  1775. *
  1776. * @param $nid
  1777. * The Webform NID.
  1778. * @param $uid
  1779. * The user account ID for which to retrieve download information.
  1780. *
  1781. * @return array|false
  1782. * An array of download information or FALSE if this user has never downloaded
  1783. * results for this particular node.
  1784. */
  1785. function webform_download_last_download_info($nid, $uid = NULL) {
  1786. $uid = isset($uid) ? $uid : $GLOBALS['user']->uid;
  1787. $query = db_select('webform_last_download', 'wld');
  1788. $query->leftJoin('webform_submissions', 'wfs', 'wld.sid = wfs.sid');
  1789. $info = $query
  1790. ->fields('wld')
  1791. ->fields('wfs', array('serial'))
  1792. ->condition('wld.nid', $nid)
  1793. ->condition('wld.uid', $uid)
  1794. ->execute()
  1795. ->fetchAssoc();
  1796. return $info;
  1797. }
  1798. /**
  1799. * Get an SID based a requested latest count.
  1800. *
  1801. * @param int $nid
  1802. * The webform NID.
  1803. * @param int $latest_count
  1804. * The latest count on which the SID will be retrieved.
  1805. * @param string $completion_type
  1806. * The completion type, either "finished", "draft", or "all".
  1807. *
  1808. * @return int
  1809. * The submission ID that starts the latest sequence of submissions.
  1810. */
  1811. function webform_download_latest_start_sid($nid, $latest_count, $completion_type = 'all') {
  1812. // @todo: Find a more efficient DBTNG query to retrieve this number.
  1813. $query = db_select('webform_submissions', 'ws')
  1814. ->fields('ws', array('sid'))
  1815. ->condition('nid', $nid)
  1816. ->orderBy('ws.sid', 'DESC')
  1817. ->range(0, $latest_count)
  1818. ->addTag('webform_download_latest_start_sid');
  1819. if ($completion_type !== 'all') {
  1820. $query->condition('is_draft', (int) ($completion_type === 'draft'));
  1821. }
  1822. $latest_sids = $query->execute()->fetchCol();
  1823. return $latest_sids ? min($latest_sids) : 1;
  1824. }