groups/modules/groups/groups_reports/groups_reports.module

721 lines
24 KiB
Plaintext

<?php
define('REPORT_PERIOD_MONTHLY', 'monthly');
define('REPORT_PERIOD_3MONTHS', '3months');
define('REPORT_PERIOD_YEARLY', 'year');
/**
* Implements hook_menu().
*/
function groups_reports_menu() {
$items['reports/groups-membership-report'] = array(
'title' => 'User group membership report',
'description' => 'View membership statistic aggregated by continents.',
'page callback' => 'drupal_get_form',
'page arguments' => array('groups_reports_groups_membership_report_form'),
'access callback' => array('groups_reports_access'),
'weight' => -1,
);
$items['reports/groups-membership-history-report'] = array(
'title' => 'Membership history report',
'description' => 'View membership history.',
'page callback' => 'groups_reports_groups_membership_history_report',
'access callback' => array('groups_reports_access'),
'weight' => -1,
);
$items['reports/group-status-report'] = array(
'title' => 'Group status report',
'description' => 'View the completeness of user groups.',
'page callback' => 'drupal_get_form',
'page arguments' => array('groups_reports_groups_status_report_form'),
'access callback' => array('groups_reports_access'),
'weight' => -1,
);
$items['reports/group-contact-report/csv'] = array(
'title' => 'Group contact report CSV export',
'description' => 'Export group organizers in CSV format',
'page callback' => 'groups_reports_groups_contact_report_csv_export',
'access callback' => TRUE,
'weight' => -1,
);
$items['admin/config/system/reports'] = array(
'title' => 'Groups report Settings',
'description' => 'Groups report settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('groups_reports_admin_settings'),
'access arguments' => array('administer site configuration'),
'file' => 'groups_reports.admin.inc',
);
return $items;
}
/**
* Implements a hook_access() callback.
*
* Allow access for ambassador and community_manager roles.
*/
function groups_reports_access() {
global $user;
return (
(in_array("administrator", $user->roles)) ||
(in_array("ambassador", $user->roles)) ||
(in_array("community_manager", $user->roles))
);
}
/**
* Update the daily membership statistics
*/
function groups_reports_update_membership_stat($timestamp = null) {
$timestr = gmdate('Ymd', $timestamp ? $timestamp : time());
$result = db_select('node', 'n')
->fields('n')->condition('status', 1)
->condition('type', 'group', '=')
->execute();
foreach ($result as $record) {
$node = node_load($record->nid);
// update meetup.com membership data
$membercount = 0;
if (isset($node->field_meetup_members[LANGUAGE_NONE])) {
$membercount = (int)$node->field_meetup_members[LANGUAGE_NONE][0]['value'];
}
db_merge('groups_membership_stat')
->key(array('nid' => $node->nid, 'timestamp' => $timestr, 'datasrc' => 'M'))
->fields(array(
'nid' => $node->nid,
'timestamp' => $timestr,
'membercount' => $membercount,
'datasrc' => 'M',
))->execute();
// update alternate membership data
$membercount = 0;
if (isset($node->field_group_members[LANGUAGE_NONE])) {
$membercount = (int)$node->field_group_members[LANGUAGE_NONE][0]['value'];
}
db_merge('groups_membership_stat')
->key(array('nid' => $node->nid, 'timestamp' => $timestr, 'datasrc' => 'C'))
->fields(array(
'nid' => $node->nid,
'timestamp' => $timestr,
'membercount' => $membercount,
'datasrc' => 'C',
))->execute();
}
watchdog('Groups Reports', 'Groups membership statistics had been updated.', array(), WATCHDOG_INFO);
}
/**
* Implements hook_cronapi()
*
* Define groups_reports module's scheduled jobs.
*/
function groups_reports_cronapi($op, $job = NULL) {
$items['groups_update_membership_stat'] = array(
'description' => 'Update group membership daily statistcs.',
'rule' => '0 4 * * *',
'callback' => 'groups_reports_update_membership_stat',
'arguments' => array(),
);
return $items;
}
/**
* Return the default date of the membership report.
*
* @return string
* return the current date if now groups_membership_stat record exists,
* otherwise return the most recent date.
*/
function _groups_reports_get_default_membership_date() {
$query = db_select('groups_membership_stat');
$query->addExpression('MAX(timestamp)');
$recent_date = $query->execute()->fetchField();
if (isset($recent_date)) {
$recent_ts = DateTime::createFromFormat( 'Ymd', $recent_date, new DateTimeZone('UTC'))
->getTimestamp();
} else {
$recent_ts = time();
}
return gmdate('Y-m-d', $recent_ts);
}
/**
* Form constructor of groups membership report.
*/
function groups_reports_groups_membership_report_form($form = array(), &$form_state) {
$form['filter'] = array(
'#type' => 'fieldset',
'#title' => t('Report filters'),
'#attributes' => array('class' => array('container-inline')),
);
$form['filter']['date'] = array(
'#type' => 'date_popup',
'#title' => t('Report date'),
'#title_display' => 'invisible',
'#date_format' => 'Y-m-d',
'#default_value' => _groups_reports_get_default_membership_date(),
);
$form['filter']['submit'] = array(
'#type' => 'submit',
'#value' => 'Apply',
);
// get filter values
if (isset($form_state['values'])) {
$filter_date = $form_state['values']['date'];
} else {
$filter_date = $form['filter']['date']['#default_value'];
}
$report_date = DateTime::createFromFormat( 'Y-m-d', $filter_date, new DateTimeZone('UTC'))->getTimestamp();
// build report
module_load_include('inc', 'field_group_location', 'field_group_lookup');
$chart_data = array(
'labels' => array(),
'datasets' => array(chartjs_create_dataset()),
);
$continents = _continent_get_predefined_list();
$report = groups_report_get_regional_membership_report($report_date);
$total = 0;
foreach ($continents as $key => $value) {
$data[$key] = array();
$summary[$key] = array(
'title' => t('<a href="!url">@title</a>', array('!url' => '#'.$key, '@title' => $value)),
'count' => isset($report['totals'][$key]) ? $report['totals'][$key] : 0,
);
$chart_data['labels'][] = $value;
$chart_data['datasets'][0]->data[] = $summary[$key]['count'];
$total += $summary[$key]['count'];
}
$form['chart'] = array(
'#type' => 'chart',
'#labels' => $chart_data['labels'],
'#datasets' => $chart_data['datasets'],
'#options' => array('responsive' => true, 'maintainAspectRatio' => false),
);
$form['totals'] = array(
'#prefix' => '<div class="totals-container">',
'#suffix' => '</div>',
'#markup' => t('<span class="totals_label">Community members worldwide</span> <span class="total">@total</span> <span class="suffix">people</span>', array('@total' => $total)),
);
foreach ($report['rows'] as $row) {
$data[$row['field_group_location_continent']][] = array(
'group' => $row['title'],
'count' => $row['membercount'],
);
}
$header = array(t('User Group'), t('Members'));
foreach ($continents as $key => $value) {
$data[$key][] = array(
'group' => '<b>Total</b>',
'count' => '<b>'.$summary[$key]['count'].'</b>',
);
$form['report_table_'.$key] = array(
'#prefix' => '<a name="'.$key.'"></a><h3>'.check_plain($value).'</h3>',
'#theme' => 'table',
'#header' => $header,
'#rows' => $data[$key],
'#sticky' => FALSE,
'#attributes' => array('id' => 'group-report-continent'),
'#empty' => t('No membership data available.'),
);
}
return $form;
}
/**
* Form submit callback of groups membership report
*/
function groups_reports_groups_membership_report_form_submit($form = array(), &$form_state) {
$form_state['rebuild'] = TRUE;
}
/**
* Implements group membership history report menu callback.
*/
function groups_reports_groups_membership_history_report() {
$report = groups_report_get_members_report(time());
$chart_data = array(
'labels' => array(),
'datasets' => array(chartjs_create_dataset()),
);
$chart_data['datasets'][0]->strokeColor = '#0099da';
$chart_data['datasets'][0]->fillColor = '#d7edfb';
$chart_data['datasets'][0]->pointColor = '#0099da';
$chart_data['datasets'][0]->pointStrokeColor = '#0099da';
$chart_data['datasets'][0]->pointHighlightFill = '#000000';
$chart_data['datasets'][0]->pointHighlightStroke = '#000000';
foreach ($report as &$item) {
$item['timestamp'] = format_date($item['timestamp'], 'custom', 'm/d/Y', 'GMT');
$chart_data['labels'][] = $item['timestamp'];
$chart_data['datasets'][0]->data[] = $item['count'];
}
$report = array_reverse($report);
$build['chart'] = array(
'#type' => 'chart',
'#chart_type' => 'line',
'#name' => 'membership-history-chart',
'#height' => '300px',
'#labels' => $chart_data['labels'],
'#datasets' => $chart_data['datasets'],
'#options' => array(
'responsive' => true,
'maintainAspectRatio' => false,
'datasetFill' => true,
'bezierCurve' => false,
'datasetStrokeWidth' => 3,
),
);
$header = array(t('Date'), t('Members'));
$build['summary_table'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $report,
'#sticky' => FALSE,
'#attributes' => array('id' => 'group-report-summary'),
'#empty' => t('No membership data available.'),
);
return $build;
}
/**
* Construct the date interval array based on a date
* argument and report period type.
*
* @param $date
* last date of the interval
* @param $report_period
* type of report period like monthly, last 3 months, yearly
* @return
* array of timestamps.
*/
function groups_reports_get_axis_dates($date, $report_period, $timestamp_result = TRUE) {
$result = array();
switch ($report_period) {
case REPORT_PERIOD_MONTHLY:
for ($i = 0; $i < 30; $i++) {
$result[] = $date;
$date -= (24*60*60); // 1day
}
break;
case REPORT_PERIOD_3MONTHS:
for ($i = 0; $i < 15; $i++) {
$result[] = $date;
$date -= (24*60*60)*6; // 6days
}
break;
case REPORT_PERIOD_YEARLY:
// take 1st day of every month
$year = (int)gmdate('Y', $date);
$month = (int)gmdate('n', $date);
echo "year = $year\n";
echo "month = $month\n";
for ($i = 0; $i < 12; $i++) {
$result[] = gmmktime(0, 0, 0, $month, 1, $year);
$month--;
if ($month == 0) {
$month = 12;
$year--;
}
}
break;
default:
throw new Exeception('Invalid report period');
}
if ($timestamp_result == FALSE) {
array_walk($result, 'groups_reports_transform_gmdate');
}
return $result;
}
/**
* Transform a timestamp to a date string, used as
* an array_walk() callback.
*/
function groups_reports_transform_gmdate(&$item, $key) {
$item = gmdate('Ymd', $item);
}
/**
* Returns an aggregated membership report by date.
*/
function groups_report_get_members_report($date, $report_period = REPORT_PERIOD_MONTHLY) {
$date_interval = groups_reports_get_axis_dates($date, $report_period);
$date_interval = array_reverse($date_interval);
$interval_filter = $date_interval;
array_walk($interval_filter, 'groups_reports_transform_gmdate');
$query = db_select('groups_membership_stat', 's');
$query->fields('s', array('timestamp'));
$query->condition('s.timestamp', $interval_filter, 'in');
$query->addExpression('sum(s.membercount)', 'membercount');
$query->groupBy('s.timestamp');
$result = $query->execute();
$rows = array();
foreach ($date_interval as $ts) {
$key = gmdate('Ymd', $ts);
$rows[$key] = array(
'timestamp' => $ts,
'count' => 0,
);
}
foreach ($result as $membership) {
$rows[$membership->timestamp]['count'] = $membership->membercount;
}
return $rows;
}
/**
* Create a regional membership report for a given date.
*/
function groups_report_get_regional_membership_report($date) {
$query = db_select('node', 'n');
$query->join('field_data_field_group_location', 'l', 'n.nid = l.entity_id');
$query->leftJoin('groups_membership_stat', 's', 'n.nid = s.nid');
$query->fields('n', array('nid', 'title'));
$query->fields('l', array('field_group_location_continent', 'field_group_location_country'));
$query->addExpression('sum(s.membercount)', 'membercount');
$query->condition('n.type', 'group', '=');
$query->condition('n.status', 1, '=');
$query->condition('s.timestamp', gmdate('Ymd', $date), '=');
$query->groupBy('n.title');
$query->groupBy('n.nid');
$query->groupBy('l.field_group_location_continent');
$query->groupBy('l.field_group_location_country');
$result = $query->execute();
$rows = array();
$totals = array();
foreach ($result as $row) {
$rows[] = (array)$row;
if (!isset($totals[$row->field_group_location_continent])) {
$totals[$row->field_group_location_continent] = 0;
}
$totals[$row->field_group_location_continent] += $row->membercount;
}
return array(
'rows' => $rows,
'totals' => $totals,
);
}
/**
* Return organic group members filtered by group role.
* @param $gid Group Id
* @param $role_name Role name
* @param $remove_admin_user Remove admin user (defaults to TRUE)
* @return Array of user entities.
*/
function _groups_reports_og_members_by_role($gid, $role_name, $remove_admin_user = TRUE) {
$query = db_select('og_users_roles', 'ogur');
$query->innerJoin('og_role', 'ogr', 'ogur.rid = ogr.rid');
$uids = $query
->fields('ogur', array('uid'))
->condition('ogr.gid', $gid, '=')
->condition('ogr.name', $role_name, '=')
->execute()
->fetchCol();
// remove administrator
if (($key = array_search(1, $uids)) !== false) {
unset($uids[$key]);
}
$users = user_load_multiple($uids);
return $users;
}
/**
* Map uid, name, email from a user account.
*/
function _groups_reports_map_users($users) {
$items = array();
foreach ($users as $user) {
$items[] = array(
'uid' => $user->uid,
'name' => $user->data['oauth2_fullname'],
'email' => $user->mail);
}
return $items;
}
/**
* Create a group profile completeness status report.
*/
function groups_reports_group_status_report($filter_status = FALSE) {
$status_point_max = 3;
$items = array();
$query = db_select('node', 'n');
$query->join('field_data_field_group_location', 'l', 'n.nid = l.entity_id');
$query->join('field_data_field_group_status', 'gs', 'n.nid = gs.entity_id');
$query->fields('n', array('nid', 'title'));
$query->fields('l', array('field_group_location_continent', 'field_group_location_country'));
$query->fields('gs', array('field_group_status_value'));
if ($filter_status != FALSE) {
$status = $filter_status == 'official' ? 1 : 0;
$query->condition('gs.field_group_status_value', $status, '=');
}
$query->condition('n.type', 'group', '=');
$query->condition('n.status', 1, '=');
$query->orderBy('l.field_group_location_continent', 'ASC');
$query->orderBy('n.title', 'ASC');
$result = $query->execute();
$rows = array();
$totals = array();
foreach ($result as $row) {
$node = node_load($row->nid);
$item = new stdClass;
$item->gid = $node->nid;
$item->title = $node->title;
$item->continent = $row->field_group_location_continent;
$item->group_status = $row->field_group_status_value;
$item->organizers = _groups_reports_map_users(
_groups_reports_og_members_by_role($row->nid, 'administrator member'));
// assign imported organizers
$v = _groups_reports_extract_resource($node, 19);
if (isset($v)) {
$item->organizers_orig = _groups_reports_parse_organizers($v);
} else {
$item->organizers_orig = array();
}
$uids = array();
if (isset($node->field_ambassadors[LANGUAGE_NONE][0])) {
foreach ($node->field_ambassadors[LANGUAGE_NONE] as $user_ref) {
$uids[] = $user_ref['target_id'];
}
}
$item->ambassadors = _groups_reports_map_users(user_load_multiple($uids));
// assign social links
$item->social_links = array();
if (isset($node->field_resource_links[LANGUAGE_NONE][0])) {
foreach ($node->field_resource_links[LANGUAGE_NONE] as $resource_link) {
// except Facebook, Google Plus, Meetup.com, Linkedin here
if (($resource_link['key'] == 18) || ($resource_link['key'] == 9) ||
($resource_link['key'] == 7) || ($resource_link['key'] == 5)) {
$item->social_links[] = $resource_link['value'];
}
}
}
$item->status_messages = array();
$item->status_point = 0;
$item->status_point_max = $status_point_max;
groups_reports_group_status_validate_organizers($item);
groups_reports_group_status_validate_ambassadors($item);
groups_reports_group_status_validate_social_links($item);
$items[] = $item;
}
return $items;
}
/**
* Group status report validation: check organizers
* +1 if organizers assigned to the group.
*/
function groups_reports_group_status_validate_organizers(&$item) {
if (count($item->organizers) > 0) {
$item->status_point++;
} else {
$item->status_messages[] = t('No organizers assigned to this group');
}
}
/**
* Group status report validation: check ambassadors
* +1 if ambassadors assigned to the group.
*/
function groups_reports_group_status_validate_ambassadors(&$item) {
if (count($item->ambassadors) > 0) {
$item->status_point++;
} else {
$item->status_messages[] = t('No Ambassadors assigned to this group');
}
}
/**
* Group status report validation: check social links
* +1 if a Facebook page, Meetup.com group, Linkedin group assigned to group.
*/
function groups_reports_group_status_validate_social_links(&$item) {
if (count($item->social_links) > 0) {
$item->status_point++;
} else {
$item->status_messages[] = t('No social link assigned to this group');
}
}
/**
* Form constructor of group status report
*/
function groups_reports_groups_status_report_form($form = array(), &$form_state) {
module_load_include('inc', 'field_group_location', 'field_group_lookup');
$continents = _continent_get_predefined_list();
$status = (isset($_GET['status'])) ? $_GET['status'] : FALSE;
$rows = groups_reports_group_status_report($status);
$totals = new stdClass();
$totals->points = 0;
$totals->max = 0;
foreach ($rows as $row) {
if ($row->group_status == 1) {
$row->title = sprintf('%s (official)', $row->title);
}
$totals->points += $row->status_point;
$totals->max += $row->status_point_max;
$organizers = '';
foreach ($row->organizers as $organizer) {
$organizers .= l(sprintf('%s <%s>', $organizer['name'], $organizer['email']), 'user/'.$organizer['uid']).'<br/>';
}
$ambassadors = '';
foreach ($row->ambassadors as $ambassador) {
$ambassadors .= l(sprintf('%s <%s>', $ambassador['name'], $ambassador['email']), 'user/'.$ambassador['uid']).'<br/>';
}
$status_messages = '';
foreach ($row->status_messages as $msg) {
$status_messages .= '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>'.$msg.'<br/>';
}
$completeness_class = 'col-status-new';
if ($row->status_point > 0) {
$completeness_class = 'col-status-missing';
}
if ($row->status_point == $row->status_point_max) {
$completeness_class = 'col-status-complete';
}
$group_url = url(drupal_get_path_alias('node/'.$row->gid), array('absolute' => TRUE));
$data[$row->continent][] = array(
'completeness' => array(
'data' => '',
'class' => array('col-completeness', $completeness_class)),
'title' => array(
'data' => sprintf('<a href="%s">%s</a>', $group_url, $row->title),
'class' => array('col-user-group')),
'status' => array(
'data' => sprintf('<span class="status-point">%d</span><span class="status-max">/%d</span>', $row->status_point, $row->status_point_max),
'class' => array('col-status')),
'organizers' => array(
'data' => $organizers,
'class' => array('col-organizers')),
'ambassadors' => array(
'data' => $ambassadors,
'class' => array('col-ambassadors')),
'status_messages' => array(
'data' => $status_messages,
'class' => array('col-messages')),
);
}
$total_percents = (int)(($totals->points / $totals->max) * 100+.5);;
$form['totals'] = array(
'#prefix' => '<div class="totals-container">',
'#suffix' => '</div>',
'#markup' => t('<span class="totals_label">Process level</span> <span class="total">@total</span> <span class="suffix">%</span><br/>Total @points points from maximum @max', array('@total' => $total_percents, '@points' => $totals->points, '@max' => $totals->max)),
);
$header = array(
array('data' => '', 'class' => array('col-completeness')),
array('data' => t('User Group'), 'class' => array('col-user-group')),
array('data' => t('Status'), 'class' => array('col-status')),
array('data' => t('Organizers'), 'class' => array('col-organizers')),
array('data' => t('Ambassadors'), 'class' => array('col-ambassadors')),
array('data' => t('Status messages'), 'class' => array('col-messages')),
);
foreach ($continents as $key => $value) {
if (isset($data[$key])) {
$form['report_table_'.$key] = array(
'#prefix' => '<a name="'.$key.'"></a><h3>'.check_plain($value).'</h3>',
'#theme' => 'table',
'#header' => $header,
'#rows' => $data[$key],
'#sticky' => FALSE,
'#attributes' => array('id' => 'group-report-continent'),
'#empty' => t('No membership data available.'),
);
}
}
return $form;
}
/**
* Display a row of contact data as a csv line.
*/
function _groups_report_contact_csv_row($row, $organizer, $source, &$seen) {
if (!isset($organizer['email'])) {
return;
}
if (in_array($organizer['email'], $seen) == false) {
printf("'%s','%s','%s','%s'\n", $row->title, $organizer['name'], $organizer['email'], $source);
$seen[] = $organizer['email'];
}
}
/**
* Export group organizer contacts in CSV format.
*
* The groups_contact_report_key variable contains the token required for
* public access.
*/
function groups_reports_groups_contact_report_csv_export() {
$access = groups_reports_access();
// check for token if not authenticated
if (($access == FALSE) && (isset($_GET['token']))) {
$access = (variable_get('groups_contact_report_key', 'dummytoken') == $_GET['token']);
}
if ($access == FALSE) {
drupal_access_denied();
return;
}
drupal_add_http_header('Content-Type', 'text/csv; utf-8');
drupal_add_http_header('Content-Disposition', 'attachment; filename="groups-contacts.csv"');
$status = (isset($_GET['status'])) ? $_GET['status'] : FALSE;
$rows = groups_reports_group_status_report($status);
printf("'%s','%s','%s'\n", 'User group', 'Full name', 'Email', 'Source');
foreach ($rows as $row) {
$seen = array();
foreach ($row->organizers as $organizer) {
_groups_report_contact_csv_row($row, $organizer, 'db', $seen);
}
foreach ($row->organizers_orig as $organizer) {
_groups_report_contact_csv_row($row, $organizer, 'json', $seen);
}
}
exit;
}
/**
* Extract resource field value based on key from a group
* node.
* @param $node
* source node
* @param $resource_id
* id of resource to extract from resource field
* @return string|boolean
* string representation of the resource value, or
* returns false if not found.
*/
function _groups_reports_extract_resource($node, $resource_id) {
if (isset($node->field_resource_links[LANGUAGE_NONE])) {
foreach ($node->field_resource_links[LANGUAGE_NONE] as $resource) {
if ($resource['key'] == $resource_id) {
return $resource['value'];
}
}
}
return FALSE;
}
/**
* Parse delimiter parsed organizer names into array
* format.
* @param $str
* input string
* @return
* parsed output as an array.
*/
function _groups_reports_parse_organizers($str) {
$organizers = array();
$elements = explode(';', $str);
if (isset($elements)) {
foreach ($elements as $element) {
$organizer['name'] = trim(strip_tags($element));
preg_match('#\<(.*?)\>#', $element, $match);
if (isset($match[1])) {
$organizer['email'] = $match[1];
}
if (!empty($organizer['name'])) {
$organizers[] = $organizer;
}
}
}
return $organizers;
}