/[drupal]/contributions/modules/journal/journal.module
ViewVC logotype

Contents of /contributions/modules/journal/journal.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.21 - (show annotations) (download) (as text)
Fri Jul 17 00:53:09 2009 UTC (4 months, 1 week ago) by sun
Branch: MAIN
Changes since 1.20: +2 -1 lines
File MIME type: text/x-php
by sun: Added comment_form to default list of forms.
1 <?php
2 // $Id: journal.module,v 1.20 2009/06/21 00:02:13 sun Exp $
3
4 /**
5 * @file
6 * Allows to maintain a custom log of performed setup and configuration actions.
7 *
8 * Journal module adds additional fields to all forms in a Drupal site.
9 * Only users granted the 'access journal' permission are able to add entries
10 * to the journal.
11 *
12 * Journal form fields may be disabled for certain forms, for example forms that
13 * are displayed in blocks.
14 *
15 * @todo Describe Demo module implementation.
16 * @todo Are we able to track enabling/disabling of modules automatically?
17 * @todo Allow to include all form field values in a journal entry.
18 */
19
20 /**
21 * Implementation of hook_help().
22 */
23 function journal_help($path, $arg) {
24 switch ($path) {
25 case 'admin/modules#description':
26 return t('Allows to maintain a custom log of performed setup and configuration actions.');
27 }
28 }
29
30 /**
31 * Implementation of hook_perm().
32 */
33 function journal_perm() {
34 return array('access journal');
35 }
36
37 /**
38 * Implementation of hook_menu().
39 */
40 function journal_menu() {
41 $items = array();
42 $items['admin/reports/journal'] = array(
43 'title' => 'Journal entries',
44 'description' => 'View journal entries.',
45 'page callback' => 'journal_view',
46 'access arguments' => array('access journal'),
47 );
48 $items['admin/reports/journal/list'] = array(
49 'title' => 'List',
50 'type' => MENU_DEFAULT_LOCAL_TASK,
51 'weight' => -10,
52 );
53 $items['admin/reports/journal/patches'] = array(
54 'title' => 'Patches',
55 'description' => 'View list of applied patches and hacks on this Drupal site.',
56 'page callback' => 'journal_patch_view',
57 'access arguments' => array('access journal'),
58 'type' => MENU_LOCAL_TASK,
59 );
60 $items['admin/reports/journal/patches/edit'] = array(
61 'title' => 'Edit patch',
62 'page callback' => 'drupal_get_form',
63 'page arguments' => array('journal_patch_form'),
64 'access arguments' => array('access journal'),
65 'type' => MENU_CALLBACK,
66 );
67 $items['admin/reports/journal/patches/delete'] = array(
68 'page callback' => 'drupal_get_form',
69 'page arguments' => array('journal_patch_delete_confirm'),
70 'access arguments' => array('access journal'),
71 'type' => MENU_CALLBACK,
72 );
73 return $items;
74 }
75
76 /**
77 * Add Journal fields to all forms.
78 *
79 * Any form, except user-defined form_ids, will be extended by a fieldset
80 * to enter a journal entry. All journal form ids are stored in one variable
81 * array; having form_ids as keys and a boolean value whether to skip a form id
82 * (0) or force/require a journal entry for it (1).
83 *
84 * @see journal_form_ids_default()
85 */
86 function journal_form_alter(&$form, &$form_state, $form_id) {
87 if (!user_access('access journal')) {
88 return;
89 }
90 $entry_required = FALSE;
91 if (!isset($form['#submit'])) {
92 $form['#submit'] = array();
93 }
94 // Check whether form has to/must not be extended.
95 $journal_ids = variable_get('journal_form_ids', journal_form_ids_default());
96 if (isset($journal_ids[$form_id]) || in_array('system_settings_form_submit', $form['#submit'])) {
97 if (isset($journal_ids[$form_id]) && !$journal_ids[$form_id]) {
98 // No journal entry for 'form_id' => 0.
99 return;
100 }
101 else {
102 // Require journal entry for 'form_id' => 1 or system settings forms.
103 $entry_required = TRUE;
104 }
105 }
106
107 // Shift system_settings_form buttons.
108 if (in_array('system_settings_form_submit', $form['#submit'])) {
109 $weight = isset($form['buttons']['#weight']) ? $form['buttons']['#weight'] : 0;
110 $form['buttons']['#weight'] = $weight + 2;
111 $journal_weight = $weight + 1;
112 $entry_required = TRUE;
113 }
114 else {
115 $journal_weight = 100;
116 }
117
118 // Prepend our journal submit handler, so we can eliminate the form value of
119 // journal_entry, which would be saved as a variable in system_settings_form()
120 // otherwise.
121 array_unshift($form['#submit'], 'journal_form_submit');
122
123 $form['journal'] = array(
124 '#weight' => $journal_weight,
125 '#tree' => FALSE,
126 );
127
128 // Store the path on which this form was initially displayed.
129 // We need to store this in a hidden field, since forms with custom '#action's
130 // (like admin/build/modules) will reset our value to $_GET['q'] otherwise.
131 $form['journal']['journal_location'] = array(
132 '#type' => 'hidden',
133 '#value' => (!empty($_REQUEST['journal_location']) ? $_REQUEST['journal_location'] : $_GET['q']),
134 );
135
136 // Add journal entry field.
137 $form['journal']['journal_entry'] = array(
138 '#type' => 'textarea',
139 '#title' => t('Journal entry'),
140 '#description' => t('If not empty, contents of this field will be logged to the system journal.'),
141 '#required' => $entry_required,
142 '#wysiwyg' => FALSE,
143 );
144 if ($entry_required && user_access('access devel information')) {
145 $form['journal']['journal_entry']['#required'] = FALSE;
146 $form['journal']['journal_omit'] = array(
147 '#type' => 'checkbox',
148 '#title' => t('Skip journal entry'),
149 '#return_value' => 1,
150 '#default_value' => 0,
151 '#description' => t('The journal entry for this form is required. If enabled, the form can be submitted without a journal entry.'),
152 );
153 $form['#validate'][] = 'journal_form_validate';
154 }
155 }
156
157 /**
158 * Validate optional journal entry for privileged users.
159 */
160 function journal_form_validate($form, &$form_state) {
161 if (empty($form_state['values']['journal_omit']) && empty($form_state['values']['journal_entry'])) {
162 form_set_error('journal', t('!name field is required.', array('!name' => t('Journal entry'))));
163 }
164 }
165
166 /**
167 * Save a new journal entry and clean out form values.
168 */
169 function journal_form_submit($form, &$form_state) {
170 if (!empty($form_state['values']['journal_entry'])) {
171 journal_add_entry($form_state['values']['journal_entry'], $form_state['values']['journal_location']);
172 }
173 unset($form_state['values']['journal_entry'], $form_state['values']['journal_location']);
174 }
175
176 /**
177 * Indicate if a form must not extended.
178 *
179 * @param string $form_id
180 * A form_id to check against.
181 *
182 * @return bool
183 * True if form should be skipped, false if form can be extended.
184 *
185 * @todo Introduce a new FAPI attribute #journal = TRUE to require a journal
186 * entry if Journal module is enabled - OR - introduce a new hook_journal?
187 */
188 function journal_form_ids_default() {
189 return array(
190 'comment_form' => 0,
191 'dblog_filter_form' => 0,
192 'devel_admin_settings' => 0,
193 'devel_execute_form' => 0,
194 'devel_switch_user_form' => 0,
195 'journal_patch_form' => 0,
196 'node_filter_form' => 0,
197 'poll_view_voting' => 0,
198 'search_block_form' => 0,
199 'search_box' => 0,
200 'search_form' => 0,
201 'search_theme_form' => 0,
202 'system_modules' => 1,
203 'user_admin_perm' => 1,
204 'user_filter_form' => 0,
205 'user_login_block' => 0,
206 'views_exposed_form' => 0,
207 'views_filters' => 0,
208 'views_ui_add_display_form' => 0,
209 'views_ui_add_item_form' => 0,
210 'views_ui_analyze_view_button' => 0,
211 'views_ui_config_item_form' => 0,
212 'views_ui_config_type_form' => 0,
213 'views_ui_edit_details_form' => 0,
214 'views_ui_edit_display_form' => 0,
215 'views_ui_export_page' => 0,
216 'views_ui_list_views_form' => 0,
217 'views_ui_preview_form' => 0,
218 'views_ui_rearrange_form' => 0,
219 'views_ui_remove_display_form' => 0,
220 );
221 }
222
223 /**
224 * Implementation of hook_user().
225 */
226 function journal_user($op, &$edit, &$user) {
227 if ($op == 'delete') {
228 db_query('UPDATE {journal} SET uid = 0 WHERE uid = %d', $user->uid);
229 }
230 }
231
232 /**
233 * Implementation of hook_block().
234 */
235 function journal_block($op = 'list', $delta = 0, $edit = array()) {
236 if ($op == 'list') {
237 $blocks = array();
238 $blocks['backlog'] = array(
239 'info' => t('Journal entries'),
240 'weight' => -10,
241 'enabled' => 1,
242 'region' => 'right',
243 );
244 return $blocks;
245 }
246 else if ($op == 'view' && user_access('access journal')) {
247 $block = array();
248 switch ($delta) {
249 case 'backlog':
250 $result = db_query("SELECT j.*, u.name FROM {journal} j INNER JOIN {users} u ON j.uid = u.uid WHERE j.location = '%s' ORDER BY j.timestamp DESC", $_GET['q']);
251 if ($output = journal_output($result, 'list')) {
252 drupal_add_css(drupal_get_path('module', 'journal') .'/journal.css', 'module', 'all', FALSE);
253 $block = array(
254 'subject' => t('Journal entries'),
255 'content' => $output,
256 );
257 }
258 break;
259 }
260 return $block;
261 }
262 }
263
264 /**
265 * Output a sortable table containing all journal entries.
266 */
267 function journal_view() {
268 $sql = "SELECT j.*, u.name FROM {journal} j INNER JOIN {users} u ON j.uid = u.uid";
269
270 $header = array(
271 array('data' => t('Date'), 'field' => 'j.timestamp', 'sort' => 'desc'),
272 array('data' => t('User'), 'field' => 'u.name'),
273 t('Message'),
274 t('Location'),
275 );
276 $tablesort = tablesort_sql($header);
277 $result = pager_query($sql . $tablesort, 50);
278
279 return journal_output($result, 'table', $header);
280 }
281
282 /**
283 * Render journal entries.
284 *
285 * Use this function to render and return
286 * - a journal provided as a database query result resource or
287 * - a custom journal provided as an array containing journal entry objects.
288 *
289 * This function may look insane to some, but it ensures that implementation of
290 * journal module into other modules is as easy as possible.
291 *
292 * @param array $journal
293 * A database query result resource or an array containing journal entry
294 * objects to output.
295 * @param string $format
296 * Whether to output all log entries as 'table', 'list' or plain 'text'.
297 * @param array $header
298 * An array containing header data for 'table' output.
299 *
300 * @todo Add XML output format.
301 */
302 function journal_output($journal, $format = 'table', $header = array()) {
303 switch ($format) {
304 case 'text':
305 // Output delimiter in first line, since this may change.
306 $output = '\t' . "\n";
307
308 while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) {
309 $row = array(
310 $entry->timestamp,
311 $entry->uid,
312 $entry->message,
313 $entry->location,
314 );
315 $output .= implode("\t", $row) ."\n";
316 }
317 break;
318
319 case 'list':
320 $output = '';
321 while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) {
322 $output .= '<li>';
323 $output .= '<span class="journal-info">'. theme('username', $entry) .' '. format_date($entry->timestamp, 'small') .':</span>';
324 $output .= '<span class="journal-entry">'. filter_xss_admin($entry->message) .'</span>';
325 $output .= '</li>';
326 }
327 if ($output) {
328 $output = '<ul id="journal-backlog">'. $output .'</ul>';
329 }
330 break;
331
332 case 'table':
333 default:
334 $rows = array();
335 while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) {
336 $rows[] = array(
337 format_date($entry->timestamp, 'small'),
338 theme('username', $entry),
339 filter_xss_admin($entry->message),
340 l(truncate_utf8($entry->location, 32, FALSE, TRUE), $entry->location),
341 );
342 }
343
344 if (empty($rows)) {
345 $rows[] = array(array('data' => t('No journal entries available.'), 'colspan' => 4));
346 }
347
348 $output = theme('table', $header, $rows);
349 $output .= theme('pager', NULL, 50, 0);
350 break;
351 }
352
353 return $output;
354 }
355
356 /**
357 * Convert a journal into an array.
358 *
359 * @param mixed $data
360 * Journal data.
361 * @param string $type
362 * The type of passed-in journal data.
363 *
364 * @return array $journal
365 * An array containing one or more journal entry objects.
366 */
367 function journal_convert($data, $type = 'text') {
368 $journal = array();
369
370 switch ($type) {
371 case 'text':
372 default:
373 $data = explode('\n', $data);
374
375 // Determine delimiter string.
376 $delimiter = array_shift($data);
377
378 while ($row = array_shift($data)) {
379 $row = explode($delimiter, $row);
380 $journal[] = (object)$row;
381 }
382 break;
383 }
384
385 return $journal;
386 }
387
388 /**
389 * Store a new journal entry in the database.
390 *
391 * @param string $description
392 * A journal entry text entered by an user.
393 * @param string $location
394 * The path on which the journal entry has been entered.
395 */
396 function journal_add_entry($description, $location) {
397 global $user;
398
399 db_query("INSERT INTO {journal} (uid, message, location, timestamp) VALUES (%d, '%s', '%s', %d)", $user->uid, $description, $location, time());
400 }
401
402 /**
403 * Output a sortable table containing all recorded patches.
404 */
405 function journal_patch_view() {
406 $header = array(
407 array('data' => t('Date'), 'field' => 'j.timestamp', 'sort' => 'desc'),
408 array('data' => t('Module'), 'field' => 'j.module'),
409 array('data' => t('User'), 'field' => 'u.name'),
410 t('Description'),
411 t('Issue'),
412 t('Status'),
413 t('Operations'),
414 );
415 $sql = "SELECT j.*, u.name FROM {journal_patch} j INNER JOIN {users} u ON j.uid = u.uid";
416 $result = pager_query($sql . tablesort_sql($header), 25);
417 $module_list = module_list(FALSE, FALSE);
418 $rows = array();
419 while ($entry = db_fetch_object($result)) {
420 $modules = array();
421 foreach (explode(',', $entry->module) as $module) {
422 $info = drupal_parse_info_file(drupal_get_path('module', $module) .'/'. $module .'.info');
423 if (isset($info['project'])) {
424 $url = 'http://drupal.org/project/issues/'. $info['project'];
425 $modules[] = l($info['name'], $url);
426 }
427 else {
428 $modules[] = $info['name'];
429 }
430 }
431 if ($entry->url != '') {
432 if (preg_match('@drupal.org/node/(\d+)@', $entry->url, $issue_title)) {
433 $issue_link = l('#'. $issue_title[1], $entry->url);
434 }
435 else {
436 $issue_link = l(t('View'), $entry->url);
437 }
438 }
439 else {
440 $issue_link = '';
441 }
442 $rows[] = array(
443 format_date($entry->timestamp, 'small'),
444 implode(', ', $modules),
445 theme('username', $entry),
446 filter_xss_admin($entry->description),
447 $issue_link,
448 t($entry->status),
449 l(t('edit'), "admin/reports/journal/patches/edit/$entry->pid"),
450 );
451 }
452
453 if (empty($rows)) {
454 $rows[] = array(array('data' => t('No patch entries available.'), 'colspan' => 7));
455 }
456
457 $output = drupal_get_form('journal_patch_form');
458 $output .= theme('table', $header, $rows);
459 $output .= theme('pager', NULL, 50, 0);
460 return $output;
461 }
462
463 /**
464 * Form builder function for patches.
465 */
466 function journal_patch_form($form_state, $pid = NULL) {
467 drupal_add_css(drupal_get_path('module', 'journal') .'/journal_patch.css', 'module', 'all', FALSE);
468 $patch = array();
469 if (isset($pid)) {
470 $patch = db_fetch_array(db_query("SELECT j.* FROM {journal_patch} j WHERE j.pid = %d", $pid));
471 }
472 $patch += array(
473 'module' => '',
474 'description' => '',
475 'url' => '',
476 'status' => 'open',
477 );
478
479 $form = array();
480 $form['patch'] = array(
481 '#type' => 'fieldset',
482 '#title' => t('Add patch record'),
483 '#tree' => TRUE,
484 );
485 $form['patch']['module'] = array(
486 '#type' => 'select',
487 '#title' => t('Affected modules'),
488 '#options' => module_list(FALSE, FALSE, TRUE),
489 '#multiple' => TRUE,
490 '#default_value' => explode(',', $patch['module']),
491 '#size' => 8,
492 '#required' => TRUE,
493 '#prefix' => '<div class="journal-patch-module-select">',
494 '#suffix' => '</div>',
495 );
496 $form['patch']['description'] = array(
497 '#type' => 'textarea',
498 '#title' => t('Description'),
499 '#default_value' => $patch['description'],
500 '#required' => TRUE,
501 '#prefix' => '<div class="clear-block">',
502 '#suffix' => '</div>',
503 );
504 $form['patch']['url'] = array(
505 '#type' => 'textfield',
506 '#title' => t('Issue URL'),
507 '#default_value' => $patch['url'],
508 '#prefix' => '<div class="journal-patch-issue clear-block">',
509 );
510 $form['patch']['status'] = array(
511 '#type' => 'select',
512 '#title' => t('Status'),
513 '#options' => array('open' => t('open'), 'fixed' => t('fixed'), "won't fix" => t("won't fix")),
514 '#default_value' => $patch['status'],
515 '#suffix' => '</div>',
516 );
517 if (!empty($patch['pid'])) {
518 $form['patch']['pid'] = array(
519 '#type' => 'value',
520 '#value' => $patch['pid'],
521 );
522 }
523 $form['patch']['submit'] = array(
524 '#type' => 'submit',
525 '#value' => isset($patch['pid']) ? t('Save') : t('Add'),
526 );
527 if (!empty($patch['pid'])) {
528 $form['patch']['delete'] = array(
529 '#type' => 'submit',
530 '#value' => t('Delete'),
531 );
532 }
533 return $form;
534 }
535
536 /**
537 * Submit handler for journal patch form.
538 */
539 function journal_patch_form_submit($form, &$form_state) {
540 global $user;
541
542 $patch = $form_state['values']['patch'];
543
544 if (preg_match('@^#\d+@', $patch['url'])) {
545 $patch['url'] = 'http://drupal.org/node/'. substr($patch['url'], 1);
546 }
547
548 if ($form_state['values']['op'] == t('Add')) {
549 db_query("INSERT INTO {journal_patch} (uid, module, description, url, status, timestamp) VALUES (%d, '%s', '%s', '%s', '%s', %d)", $user->uid, implode(',', $patch['module']), $patch['description'], $patch['url'], $patch['status'], time());
550 }
551 else if ($form_state['values']['op'] == t('Save')) {
552 db_query("UPDATE {journal_patch} SET uid = %d, module = '%s', description = '%s', url = '%s', status = '%s' WHERE pid = %d", $user->uid, implode(',', $patch['module']), $patch['description'], $patch['url'], $patch['status'], $patch['pid']);
553 $form_state['redirect'] = 'admin/reports/journal/patches';
554 }
555 else if ($form_state['values']['op'] == t('Delete')) {
556 $form_state['redirect'] = 'admin/reports/journal/patches/delete/'. $patch['pid'];
557 }
558 }
559
560 /**
561 * Confirmation form to delete a patch record.
562 */
563 function journal_patch_delete_confirm($form_state, $pid) {
564 $form = array();
565 $form['pid'] = array('#type' => 'value', '#value' => $pid);
566 $description = db_result(db_query("SELECT description FROM {journal_patch} WHERE pid = %d", $pid));
567 $form['patch_description'] = array(
568 '#type' => 'item',
569 '#title' => t('Description'),
570 '#value' => filter_xss_admin($description),
571 );
572 return confirm_form($form,
573 t('Are you sure you want to delete this patch record?'),
574 'admin/reports/journal/patches',
575 t('This action cannot be undone.'), t('Delete'), t('Cancel')
576 );
577 }
578
579 /**
580 * Form submit callback for patch record delete confirm form.
581 */
582 function journal_patch_delete_confirm_submit($form, &$form_state) {
583 db_query("DELETE FROM {journal_patch} WHERE pid = %d", $form_state['values']['pid']);
584 $form_state['redirect'] = 'admin/reports/journal/patches';
585 }
586

  ViewVC Help
Powered by ViewVC 1.1.2