--- /dev/null
+<?php
+
+/**
+ * @file
+ * Admin callbacks for the Achievements Recent module.
+ */
+
+/**
+ * Create and seed the limited achievement leaderboards.
+ */
+function achievements_recent_settings() {
+ $form['achievements_recent_leaderboard_init'] = array(
+ '#description' => t("If your site already has achievements and users with unlocks, you can manually initialize and populate the recent leaderboards below. Otherwise, the recent leaderboards will fill with users as they unlock new achievements. <strong>You do not have to initialize the recent leaderboards if your site has just enabled the Achievements module</strong> - the following options exist primarily for previous installations. <strong>This initialization is a database-intensive operation</strong>, so it will only populate 1000 users at a time. You'll be informed of its progress and whether you need to keep clicking. If you prefer to run these imports via the command line, you can use something like <code>drush ev 'achievements_recent_leaderboard_init(<em>DAYS</em>);'</code>, where <em>DAYS</em> is one of the following: @valid_days.", array('@valid_days' => implode(", ", array_keys(achievements_recent_valid_days())))),
+ '#title' => t('Initialize recent leaderboards'),
+ '#type' => 'fieldset',
+ );
+
+ foreach (achievements_recent_valid_days() as $days => $days_human) {
+ $form_key = 'achievements_recent_leaderboard_init_' . $days .'_day';
+ $form['achievements_recent_leaderboard_init'][$form_key] = array(
+ '#achievement_days' => $days, // Stomach sick... what's... oh:
+ '#attributes' => array('style' => 'margin-top: 9px;'),
+ '#type' => 'submit', // Gasp! Inline spacing! Crap CSS!
+ '#submit' => array('achievements_recent_leaderboard_init_submit'),
+ '#value' => t('Initialize @days_human leaderboard', array('@days_human' => $days_human)),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Submit callback; initialize a recent leaderboard.
+ */
+function achievements_recent_leaderboard_init_submit($form, &$form_state) {
+ achievements_recent_leaderboard_init($form_state['clicked_button']['#achievement_days']);
+}
--- /dev/null
+<?php
+
+/**
+ * @file
+ * Install, update, and uninstall functions for the Achievements Recent module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function achievements_recent_schema() {
+ $schema['achievement_totals_days'] = array(
+ 'description' => 'A leaderboard of recent day totals across the entire site.',
+ 'fields' => array(
+ 'days' => array(
+ 'default' => 0,
+ 'description' => "The recent day timeframe this data corresponds to (ex. 7, 30, etc.).",
+ 'not null' => TRUE,
+ 'type' => 'int',
+ ),
+ 'uid' => array(
+ 'default' => 0,
+ 'description' => 'The {users}.uid that is being ranked on the recent day leaderboard.',
+ 'not null' => TRUE,
+ 'type' => 'int',
+ ),
+ 'points' => array(
+ 'default' => 0,
+ 'description' => "The {users}.uid's recent day achievement point total.",
+ 'not null' => TRUE,
+ 'type' => 'int',
+ ),
+ 'unlocks' => array(
+ 'default' => 0,
+ 'description' => "The {users}.uid's recent day total of achievement unlocks.",
+ 'not null' => TRUE,
+ 'type' => 'int',
+ ),
+ 'timestamp_earliest' => array(
+ 'default' => 0,
+ 'description' => 'The Unix timestamp when the {users}.uid first received an achievement in the last recent days.',
+ 'not null' => TRUE,
+ 'type' => 'int',
+ ),
+ 'timestamp_latest' => array(
+ 'default' => 0,
+ 'description' => 'The Unix timestamp when the {users}.uid last received an achievement.',
+ 'not null' => TRUE,
+ 'type' => 'int',
+ ),
+ 'achievement_id' => array(
+ 'default' => '',
+ 'description' => 'The ID of the achievement the {users}.uid has most recently unlocked.',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'type' => 'varchar',
+ ),
+ ),
+ 'indexes' => array(
+ 'days_uid_points' => array('days', 'uid', 'points'),
+ 'days_uid_unlocks' => array('days', 'uid', 'unlocks'),
+ 'days_uid_timestamp_earliest' => array('days', 'uid', 'timestamp_earliest'),
+ 'days_points_timestamp_latest' => array('days', 'points', 'timestamp_latest'),
+ 'days_unlocks_timestamp_latest' => array('days', 'unlocks', 'timestamp_latest'),
+ 'days_uid_points_unlocks' => array('days', 'uid', 'points', 'unlocks'),
+ ),
+ 'primary key' => array('days', 'uid'),
+ );
+
+ return $schema;
+}
--- /dev/null
+<?php
+
+/**
+ * @file
+ * Enables leaderboards limited to recent unlocks and point totals.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function achievements_recent_menu() {
+ $items['admin/config/people/achievements-recent'] = array(
+ 'access arguments' => array('administer achievements'),
+ 'description' => 'Initialize the recent achievement leaderboards.',
+ 'file' => 'achievements_recent.admin.inc',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('achievements_recent_settings'),
+ 'title' => 'Achievements Recent',
+ );
+
+ return $items;
+}
+
+/**
+ * Recalculate a user's stats based on the number of days.
+ *
+ * @param $uid
+ * The user to recalculate achievement info for (defaults to current user).
+ * @params $days
+ * The number of days to constrain the recalculations to.
+ *
+ * @return $totals
+ * An array of results suitable for the recent leaderboard table. If the
+ * user is not currently an achiever or the passed $days are not a valid
+ * frame, FALSE is returned instead. Depending on your needs, check
+ * $totals['unlocks'] to determine if they've unlocked anything in $days.
+ */
+function achievements_recent_totals_recalculate($uid = NULL, $days = NULL) {
+ list($uid, $access) = achievements_user_is_achiever($uid);
+ if (!$access || !achievements_recent_valid_days($days)) {
+ return FALSE;
+ }
+
+ $totals = array(
+ 'days' => $days,
+ 'uid' => $uid,
+ 'points' => 0,
+ 'unlocks' => 0,
+ 'timestamp_earliest' => NULL,
+ 'timestamp_latest' => NULL,
+ 'achievement_id' => NULL,
+ );
+ $achievements = achievements_load();
+ $unlocks = achievements_unlocked_already(NULL, $uid);
+ $earliest_allowed = REQUEST_TIME - 60 * 60 * 24 * $days;
+
+ foreach ($unlocks as $unlock) {
+ if ($unlock['timestamp'] >= $earliest_allowed) {
+ $totals['points'] += $achievements[$unlock['achievement_id']]['points'];
+ $totals['unlocks']++; // Parent code should check for 0 unlocks, not 0 points.
+
+ if (!isset($totals['timestamp_earliest']) || $unlock['timestamp'] <= $totals['timestamp_earliest']) {
+ $totals['timestamp_earliest'] = $unlock['timestamp'];
+ }
+
+ if ($unlock['timestamp'] >= $totals['timestamp_latest']) {
+ $totals['timestamp_latest'] = $unlock['timestamp'];
+ $totals['achievement_id'] = $unlock['achievement_id'];
+ }
+ }
+ }
+
+ return $totals;
+}
+
+/**
+ * Initialize a recent leaderboard with legacy data.
+ *
+ * @param $days
+ * The number of days worth of data to go through.
+ */
+function achievements_recent_leaderboard_init($days = NULL) {
+ if (achievements_recent_valid_days($days)) {
+ // Fetch users who have unlocked something in the past $days and are
+ // not already in the $days recent leaderboard, which allows us to
+ // resume filling the table if we timeout on a ton of results.
+ $query = db_select('achievement_unlocks', 'au');
+ $query->leftJoin('achievement_totals_days', 'attd', "au.uid = attd.uid AND attd.days = $days");
+ $accounts = $query->fields('au', array('uid'))->condition('au.timestamp', REQUEST_TIME - 60 * 60 * 24 * $days, '>=')
+ ->isNull('attd.uid')->groupBy('au.uid')->execute(); // 7 more days until Diablo III. Stay awhile and listen!
+
+ // Calculate and save 'em.
+ $limit = 1000; $imported = $skipped = 0;
+ foreach ($accounts as $account) {
+ $totals = achievements_recent_totals_recalculate($account->uid, $days);
+ if ($totals) { // NP: 'Smile Song' from Shannon Chan-Kent's album 'My Little Pony: Friendship is Magic Season 2'.
+ db_merge('achievement_totals_days')->key(array('uid' => $account->uid, 'days' => $days))->fields($totals)->execute();
+ if (++$imported >= $limit) { break; } // This stuff is pretty db intensive, so we'll limit how many we do at once.
+ }
+ else {
+ $skipped++;
+ }
+ }
+
+ $day_counts = achievements_recent_valid_days(); // Try to spit some semblance of statistical sanity for those doing the import.
+ $status = t('@imported of @total users were added to the @labeled leaderboard. @skipped were skipped for not being valid achievers.',
+ array('@labeled' => $day_counts[$days], '@imported' => $imported, '@total' => $accounts->rowCount(), '@skipped' => $skipped));
+ drupal_is_cli() ? print $status : drupal_set_message($status);
+
+ if ($accounts->rowCount() - $imported - $skipped > 0) {
+ $clicky = t('@remaining users still need to be added to the @labeled leaderboard. Keep clicking the button, bub.',
+ array('@labeled' => $day_counts[$days], '@remaining' => $accounts->rowCount() - $imported - $skipped));
+ drupal_is_cli() ? print $clicky : drupal_set_message($clicky, 'warning');
+ }
+ }
+}
+
+/**
+ * Return all valid day counts or check the validity of a passed value.
+ *
+ * @param $days
+ * Optional. If $days is passed, return TRUE if the day count is valid,
+ * FALSE otherwise. If $days is not passed, return an array of all valid
+ * day counts as keys, with the values as human-readable strings.
+ */
+function achievements_recent_valid_days($days = NULL) {
+ $day_counts = array(
+ 7 => t('7 day'),
+ 30 => t('30 day'),
+ );
+
+ return $days ? isset($day_counts[$days]) : $day_counts;
+}