'activity', 'title' => t('Activity'), 'callback' => 'activity_page', 'access' => user_access('view public activity'), 'weight' => 1, ); $items[] = array( 'path' => 'activity/all', 'title' => t('All activity'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'access' => user_access('view public activity'), ); $items[] = array( 'path' => 'activity/mine', 'title' => t('My activity'), 'access' => $user->uid, 'type' => MENU_LOCAL_TASK, 'access' => user_access('view own activity'), ); $items[] = array( 'path' => 'activity/all/feed', 'title' => t('All activity'), 'callback' => 'activity_feed', 'callback arguments' => array(ACTIVITY_ALL), 'access' => user_access('view public activity'), ); $items[] = array( 'path' => 'admin/settings/activity', 'title' => t('Activity Settings'), 'description' => t('Customize what will display on your users activity page.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('activity_admin_settings'), ); } else { if ($user->uid) { $items[] = array( 'path' => 'activity/'. $user->uid. '/feed', 'title' => t('My activity'), 'callback' => 'activity_feed', 'callback arguments' => array($user->uid), 'type' => MENU_CALLBACK, ); } } return $items; } /** * Admin section * TODO: 1) the defaults aren't available at activity render time unless this * this page has been saved. This is potentially confusing since they * show up in the textfields. */ function activity_admin_settings() { if ($activity_info = activity_get_info()) { foreach ($activity_info as $module => $info) { if (!empty($info)) { $tokens = array(); $token_list = array(); foreach (token_get_list($module) as $name => $token_array) { $token_list = array_merge($token_list, $token_array); } ksort($token_list); foreach ($token_list as $token => $desc) { $tokens[] = '['. $token .']: '. $desc; } if (count($info['types'])) { $form[$module] = array( '#type' => 'fieldset', '#title' => t('Tokens for @name', array('@name' => t($module))), '#collapsible' => true, '#collapsed' => true, '#description' => t('Available tokens') . theme('item_list', $tokens), ); if ($ops = $info['ops']) { if ($roles = $info['roles']) { foreach ($roles as $role_name => $role) { $form[$module][$role_name] = array( '#type' => 'fieldset', '#title' => t('Messages visible to the "%role_name" role.', array('%role_name' => $role['#name'])), '#collapsible' => true, '#collapsed' => false, '#description' => $role['#description'] ? $role['#description'] : '', ); } if ($types = $info['types']) { foreach ($ops as $op_name => $op) { foreach ($info['types'] as $type_name => $type) { foreach ($roles as $role_name => $role) { $token_field = "{$module}_{$type_name}_{$op_name}_{$role_name}"; $form[$module][$role_name][$token_field] = array( '#type' => 'textfield', '#title' => $type. ': '. $op, '#default_value' => variable_get($token_field, $role['#default'] ? $role['#default'] : ''), ); } } } } } } } } } return system_settings_form($form); } else { drupal_set_message(t('No supported modules enabled. Check the !activity_section for supported modules.', array('!activity_section' => l(t('Activity section'), 'admin/build/modules')))); } } /** * API function * * @return an array of module names and metadata from those modules that * implement hook_activity_info. */ function activity_get_info() { foreach (module_implements('activity_info') as $module) { $info[$module] = module_invoke($module, 'activity_info'); } return $info; } /** * API function * Insert an activity record. This gets called by modules wishing to record * their activities. * @param $module The name of the module that is doing the recording, eg. 'node' * @param $type Module's can track more than one type of activity. For example, * the nodeactivity module tracks activities for each content type separately. * $type should be an identifier for the calling module to use. */ function activity_insert($module, $type, $operation, $data, $target_users_roles) { $aid = db_next_id('activity'); db_query("INSERT INTO {activity} (aid, module, type, operation, created, data) VALUES (%d, '%s', '%s', '%s', %d, '%s')", $aid, $module, $type, $operation, time(), serialize($data)); foreach ($target_users_roles as $uid => $role) { db_query("INSERT INTO {activity_targets} (aid, target_uid, target_role) VALUES (%d, %d, '%s')", $aid, $uid, $role); } return $aid; } /** * The API supports: * @param $uids * - a single uid * - an array of uids * - can include the special uid ACTIVITY_ALL * @param $filters * - an array where keys are one of module, type, operation, target_role * - values are arrays of possible values for the keys. * For example: * array('target_role' => 'Author', 'type' => 'Delete') * this would find activity where the author had deleted something. * Example 2: * array('target_role' => array('Requester', 'Requestee')) * This shows that the values can be arrays as well. * @param $limit * The number of results desired * @param $tablesort_headers * An array that determines the sorting of the result set. * */ function activity_get_activity($uids = ACTIVITY_ALL, $filters = NULL, $limit = NULL, $tablesort_headers = NULL) { $wheres = array(); // Build the WHERE clause for user id. if (!is_array($uids)) { $wheres[] = "at.target_uid = %d"; $params[] = $uids; } else { foreach ($uids as $uid) { $nums[] = "%d"; $params[] = $uid; } $wheres[] = 'at.target_uid IN ('. implode(',', $nums). ')'; } // Build sql limiting query to on filtered fields if (!empty($filters) && is_array($filters)) { foreach ($filters as $column => $values) { // Of the possible columns, role is in the at table and all others in the // a table. Prefix the column name with the appropriate table. if ($column == 'target_role') { $column = 'at.target_role'; } else { $column = "a.{$column}"; } if (is_array($values) && count($values) > 1) { foreach ($values as $value) { $strings[] = "'%s'"; $params[] = $value; } $wheres[] = $column. ' IN ('. implode(',', $strings). ')'; } else { $wheres[] = $column. " = '%s'"; // $values is a string with the single value. $params[] = $values; } } } if (count($wheres) > 0) { $where = implode(' AND ', $wheres); $where = "WHERE $where"; } // We always include tablesort_sql in the query so that this API is friendly // to sortable tables. If no headers were passed in, use the default headers. if (empty($tablesort_headers)) { $tablesort_headers = activity_get_tablesort_headers(); $tablesort_headers['a.created']['sort'] = 'desc'; } // Build the sql and do the query. Wrapping it in db_rewrite_sql allows other // modules to impose access restrictions on activity listings. $sql = "SELECT a.*, at.target_uid, at.target_role FROM {activity_targets} at INNER JOIN {activity} a ON a.aid = at.aid $where "; $tablesort_sql = tablesort_sql($tablesort_headers); $sql = db_rewrite_sql("$sql $tablesort_sql", 'at', 'aid', array('uids' => $uids)); if (is_numeric($limit)) { $result = pager_query($sql, $limit, 0, NULL, $params); } else { $result = db_query($sql, $params); } $activity = array(); while ($row = db_fetch_array($result)) { $row['data'] = unserialize($row['data']); $activity[] = $row; } return $activity; } function activity_get_tablesort_headers() { return array( 'at.aid' => array('field' => 'at.aid', 'data' => t('Id')), 'a.module' => array('field' => 'a.module', 'data' => t('Module')), 'a.type' => array('field' => 'a.type', 'data' => t('Type')), 'a.operation' => array('field' => 'a.operation', 'data' => t('Operation')), 'a.created' => array('field' => 'a.created', 'data' => t('Created')), ); } /** * create a block for display * @param op * @param delta * @returns block HTML * * TODO: Add "more activity" link to blocks which goes to activity table page. */ function activity_block($op = 'list', $delta = 0) { global $user, $form_values; if ($op == 'list') { $block['my']['info'] = t("Activity - Mine: show the current user's activity."); $block['all']['info'] = t("Activity - All: show all recent activity"); return $block; } elseif ($op == 'configure') { $form['items'] = array( '#type' => 'select', '#title' => t('Number of items'), '#default_value' => variable_get('activity_block_'. $delta, 5), '#options' =>drupal_map_assoc(range(1, 50)), ); return $form; } elseif ($op == 'save') { variable_set('activity_block_'. $delta, $form_values['items']); } elseif ($op == 'view') { switch ($delta) { case 'my': if (user_access('view own activity')) { // Grab the number of requested activities plus one. We use this one // to determine whether or not to show the "more" link and only display // the correct number of items. $activity = activity_get_activity($user->uid, NULL, variable_get('activity_block_'. $delta, 5) + 1); if ($count = count($activity)) { if ($count > variable_get('activity_block_'. $delta, 5)) { $more_link = ''; array_pop($activity); } return array('title' => t('My activity'), 'content' => theme('activity_block', $activity, $more_link)); } } break; case 'all': if (user_access('view public activity')) { $activity = activity_get_activity(ACTIVITY_ALL, NULL, variable_get('activity_block_'. $delta, 5) + 1); if ($count = count($activity)) { if ($count > variable_get('activity_block_'. $delta, 5)) { $more_link = $more_link = ''; array_pop($activity); } return array('title' => t('Recent activity'), 'content' => theme('activity_block', $activity, $more_link)); } } break; } } } function activity_page($page = 'all') { global $user; if ($page == 'mine') { $activities = activity_get_activity($user->uid, NULL, 20); $table = activity_table($activities); $feed_url = url('activity/'. $user->uid. '/feed'); drupal_add_feed($feed_url); $feed = theme('feed_icon', $feed_url); return theme('activity_page', $activities, $table); } else if ($page == 'all') { $activities = activity_get_activity(ACTIVITY_ALL, NULL, 20); $table = activity_table($activities); $feed_url = url('activity/all/feed'); drupal_add_feed($feed_url); $feed = theme('feed_icon', $feed_url); return theme('activity_page', $activities, $table); } } /** * Menu callback to display the records in a page */ function activity_table($activities, $scope = ACTIVITY_SCOPE_ALL) { $display_headers = array( 'created' => array('field' => 'created', 'data' => t('Date')), //t('Visible to'), t('Message'), ); $rows = array(); foreach ($activities as $activity) { if ($activity['target_uid'] == ACTIVITY_ALL) { $visible_to = t('Everyone'); } else if ($activity['target_uid'] == $user->uid) { $visible_to = t('You'); } else { $visible_to = theme('username', user_load(array('uid' => $activity['target_uid']))); } $rows[] = array( format_date($activity['created'], 'small'), //$visible_to, activity_token_replace($activity, $scope), ); } $output = theme('table', $display_headers, $rows); $output .= theme('pager'); return $output; } /** * menu callback to return a feed of a signed in user's activity page */ function activity_feed($arg) { global $locale; if ($arg == ACTIVITY_ALL) { $activities = activity_get_activity(ACTIVITY_ALL, NULL, 20); $url = url('activity/all', NULL, NULL, TRUE); $feed_title = t('All activity'); } else if (is_numeric($arg)) { $user = user_load(array('uid' => $arg)); if ($user) { $activities = activity_get_activity($arg, NULL, 20); $url = url('activity/'. $user->uid, NULL, NULL, TRUE); $feed_title = t('Activity for @username', array('@username' => theme('username', $user))); } } if (count($activities) > 0) { foreach ($activities as $activity) { $function = $activity['module']. '_format_rss_item'; if (function_exists($function)) { // each module gets a chance to build its own feed item. // They should use the $activity to prepare variables and // call format_rss_item($title, $link, $item_text, $extra); $items .= $function($activity); } else { $message = activity_token_replace($activity); $items .= format_rss_item($message, url('activity/'. $user->uid, NULL, NULL, TRUE), format_date($activity['created']). '

'. $message. '

'); } } } $channel = array( 'version' => '2.0', 'title' => variable_get('site_name', 'Drupal') .' - '. $feed_title, 'link' => $url, 'description' => variable_get('site_mission', ''), 'language' => $locale, ); // TODO: Figure out what the right namespace should be. $namespaces = array('xmlns:dc="http://purl.org/dc/elements/1.1/"'); $output = "\n"; $output .= "\n"; $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']); $output .= "\n"; drupal_set_header('Content-Type: application/rss+xml; charset=utf-8'); print $output; } function activity_default_tokens($activity = FALSE) { if ($activity) { $tokens['time-small'] = format_date($activity['created'], 'small'); $tokens['time-medium'] = format_date($activity['created'], 'medium'); $tokens['time-large'] = format_date($activity['created'], 'large'); } else { $tokens = array(); $tokens['time-small'] = t('Date and time in small format: @example', array('@example' => format_date(time(), 'small'))); $tokens['time-medium'] = t('Date and time in medium format: @example', array('@example' => format_date(time(), 'medium'))); $tokens['time-large'] = t('Date and time in large format: @example', array('@example' => format_date(time(), 'large'))); } return $tokens; } /** * determine what the message should say */ function activity_token_replace($activity) { extract($activity); $data += activity_default_tokens($activity); $var = "{$module}_{$type}_{$operation}_{$target_role}"; if ($pattern = variable_get($var, FALSE)) { $message = token_replace($pattern, $module, $data); return $message; } } /** * date formatter * $timestamp is the unix timestamp() of when an activity occurred. * $end is the human language formatted string expressing how long ago this was. */ function activity_format_offset($timestamp) { $offset = (strftime("%j") + strftime("%Y") * 365) - (strftime("%j", $timestamp) + strftime("%Y", $timestamp) * 365); if ($offset >= 7) { $offset = (strftime("%V") + strftime("%Y") * 52) - (strftime("%V", $timestamp) + strftime("%Y", $timestamp) * 52); $end = $offset != 0 ? format_plural($offset, t("a week ago"), t("@count weeks ago", array("@count" => $offset))) : t("Today"); } else if ($offset > 0) { $end = format_plural($offset, t('Yesterday'), t('@count days ago', array('@count' => $offset))); } else { $end = t('Today'); } return $end; } /** * Implementation of hook_simpletest(). */ function activity_simpletest() { $module_name = 'activity'; $dir = drupal_get_path('module', $module_name). '/tests'; $tests = file_scan_directory($dir, '\.test$'); return array_keys($tests); } /** * theme function for displaying the users activity page */ function theme_activity_page($activities, $table) { return $table; //return theme('item_list', $items, NULL, 'ul', array('class' => 'activity-list')); } /** * theme function for displaying the users activity page */ function theme_activity_block($items, $more_link) { $activites = array(); foreach ($items as $activity) { $activities[] = activity_token_replace($activity); } if ($content = theme('item_list', $activities, NULL, 'ul', array('class' => 'activity-list'))) { $content .= $more_link; return $content; } } if (!function_exists('phptemplate_username')) { /** * Format a username. * * @param $object * The user object to format, usually returned from user_load(). * @return * A string containing an HTML link to the user's page if the passed object * suggests that this is a site user. Otherwise, only the username is returned. */ function phptemplate_username($object) { global $user; if ($object->uid && $object->name) { if ($object->uid == $user->uid) { $name = t('You'); } // Shorten the name when it is too long or it will break many tables. else if (drupal_strlen($object->name) > 20) { $name = drupal_substr($object->name, 0, 15) .'...'; } else { $name = $object->name; } if (user_access('access user profiles') && ($user->uid != $object->uid)) { $output = l($name, 'user/'. $object->uid, array('title' => t('View user profile.'))); } else { $output = check_plain($name); } } else if ($object->name) { // Sometimes modules display content composed by people who are // not registered members of the site (e.g. mailing list or news // aggregator modules). This clause enables modules to display // the true author of the content. if ($object->homepage) { $output = l($object->name, $object->homepage); } else { $output = check_plain($object->name); } $output .= ' ('. t('not verified') .')'; } else { $output = variable_get('anonymous', t('Anonymous')); } return $output; } }