Verified Commit b912df7f authored by jake_milburn's avatar jake_milburn Committed by Doug Green
Browse files

Issue #3029267 by jake_milburn, peximo: CPS Scheduler

parent 7765faf4
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Hooks provided by the CPS scheduler module.
 */

/**
 * @addtogroup hooks
 * @{
 */

/**
 * Alter the errors when checking if a changeset can be scheduled.
 *
 * @param $errors
 *  An array of errors keyed by type
 *
 * @param $entities
 *  The list of tracked entitites
 */
function hook_cps_scheduler_not_allowed_alter(&$errors, $entities) {
  unset($errors['entity_type:taxonomy_term']);
  foreach ($entities as $entity_type => $entities) {
    foreach ($entities as $entity_id) {
      if ($entity_type == 'node' && $entity_id == 1) {
        $errors[$entity_type . ':' . $entity_id] = FALSE;
      }
    }
  }
}

/**
 * @} End of "addtogroup hooks".
 */
+25 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * CPS Scheduler drush commands.
 */

/**
 * Implements hook_drush_command().
 */
function cps_scheduler_drush_command() {
  $items['cps-scheduler-run'] = array(
    'description' => t('Run scheduled publishing.'),
    'aliases' => array('cpssr'),
  );

  return $items;
}

/**
 * Drush command callback
 */
function drush_cps_scheduler_run() {
  _cps_scheduler_scheduled_publish();
}
+5 −0
Original line number Diff line number Diff line
name = CPS Scheduler
description = This module allows a CPS site version to be published on specified date and time.
core = 7.x

dependencies[] = cps
+25 −0
Original line number Diff line number Diff line
<?php

function cps_scheduler_schema() {
  $schema['cps_scheduler'] = [
    'description' => 'CPS Scheduler table.',
    'fields' => [
      'changeset_id' => [
        'description' => 'A machine name identifying the changeset.',
        'type' => 'varchar',
        'length' => 64,
        'not null' => TRUE,
        'default' => '',
      ],
      'publish_on' => [
        'description' => 'The UNIX UTC timestamp when to publish',
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
      ],
    ],
    'primary key' => ['changeset_id'],
  ];
  return $schema;
}
+444 −0
Original line number Diff line number Diff line
<?php

define('CPS_SCHEDULER_DATE_FORMAT', 'Y-m-d H:i');

/**
 * Implements hook_menu().
 */
function cps_scheduler_menu() {
  $items = [];

  $items['admin/structure/changesets/scheduled'] = array(
    'title' => 'Scheduled',
    'page callback' => 'cps_changeset_list_page',
    'page arguments' => array('scheduled_site_versions'),
    'file' =>  'includes/admin.inc',
    'file path' => drupal_get_path('module', 'cps'),
    'access arguments' => array('administer changesets'),
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
  );

  return $items;
}

/**
 * Implements hook_permission().
 */
function cps_scheduler_permission() {
  return array(
    'schedule cps changesets' => array(
      'title' => t('Schedule CPS changesets to be published on specified date and time'),
    ),
  );
}

/**
 * Implements hook_cps_changeset_state_types_alter().
 */
function cps_scheduler_cps_changeset_states_alter(&$states) {
  // Add a state type for changesets that are scheduled to be published.
  $states['scheduled'] = array(
    'label' => t('Scheduled'),
    'weight' => 0,
    'type' => 'closed',
  );
}

/**
 * Implements hook_cps_transitions().
 *
 * Add "schedule" and "cancel scheduling" transitions.
 */
function cps_scheduler_cps_transitions() {
  return [
    'schedule' => [
      'label' => t('Schedule'),
      'valid states' => ['unpublished'],
      'state' => 'scheduled',
      'menu' => [
        'title' => t('Schedule'),
        'weight' => -1,
      ],
      'access callback' => [
        'cps_scheduler_schedule_access_callback' => [],
      ],
      'transition callback' => [
        'cps_scheduler_schedule_transition_callback' => [],
      ],
      'message' => [
        'subject' => t('@site-name: "@site-version-name" submitted for schedule'),
        'body' => '<p>@user-name submitted a site version for schedule.</p><blockquote>!message</blockquote><hr><p>!site-version</p>',
      ],
      'transition message' => t('This site version will be submitted for schedule.'),
      'transition success' => t('The site version has been submitted for schedule.'),
    ],
    'cancel_scheduling' => [
      'label' => t('Cancel scheduling'),
      'valid states' => ['scheduled'],
      'state' => 'unpublished',
      'menu' => [
        'title' => t('Cancel scheduling'),
        'weight' => -1,
      ],
      'access callback' => [
        'cps_transition_default_transition_user_access_callback' => ['schedule cps changesets'],
      ],
      'transition callback' => [
        'cps_scheduler_cancel_schedule_transition_callback' => [],
      ],
      'message' => [
        'subject' => t('@site-name: "@site-version-name" submitted for remove from scheduling'),
        'body' => '<p>@user-name submitted a site version for unscheduling.</p><blockquote>!message</blockquote><hr><p>!site-version</p>',
      ],
      'transition message' => t('This site version will be submitted for remove from scheduling.'),
      'transition success' => t('The site version has been submitted for remove from scheduling.'),
    ],
  ];
}

/**
 * Schedule transition access callback
 *
 * @param \CPSChangeset $changeset
 *  The changeset
 *
 * @param string $transition
 *  The transition name
 *
 * @param array $transition_info
 *  The transition info
 * @param null $account
 *  The user account
 *
 * @return bool
 */
function cps_scheduler_schedule_access_callback(CPSChangeset $changeset, $transition, $transition_info, $account = NULL) {
  if (user_access('schedule cps changesets')) {
    return _cps_scheduler_can_be_scheduled($changeset);
  }

  return FALSE;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add publish settings on transition form.
 */
function cps_scheduler_form_cps_transition_transition_form_alter(&$form, &$form_state, $form_id) {
  if ($form_state['transition'] == 'schedule') {
    $changeset = $form_state['changeset'];

    $publish_on = db_select('cps_scheduler', 'cs')
      ->fields('cs', ['publish_on'])
      ->condition('changeset_id', $changeset->changeset_id)
      ->execute()
      ->fetchField();

    $form['scheduler_settings'] = [
      '#type' => 'fieldset',
      '#title' => t('Scheduling options'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#weight' => -100,
    ];

    $form['scheduler_settings']['publish_on'] = [
      '#type' => 'textfield',
      '#title' => t('Publish on'),
      '#maxlength' => 30,
      '#required' => FALSE,
      '#default_value' => !empty($publish_on) ? date(CPS_SCHEDULER_DATE_FORMAT, $publish_on) : '',
      '#description' => t('Leave the date blank for no scheduled publishing.'),
      '#disabled' => $changeset->status == 'scheduled',
    ];

    if (module_exists('date_popup')) {
      // Make this a popup calendar widget.
      $form['scheduler_settings']['publish_on']['#type'] = 'date_popup';
      $form['scheduler_settings']['publish_on']['#date_format'] = CPS_SCHEDULER_DATE_FORMAT;
      $form['scheduler_settings']['publish_on']['#date_year_range'] = '0:+10';
      $form['scheduler_settings']['publish_on']['#date_increment'] = 1;
      unset($form['scheduler_settings']['publish_on']['#maxlength']);
    }

    $form['#validate'][] = 'cps_scheduler_form_cps_transition_transition_form_validate';
    array_unshift($form['#submit'], 'cps_scheduler_form_cps_transition_transition_form_submit');
  }
}

/**
 * CPS Scheduler form validate handler
 */
function cps_scheduler_form_cps_transition_transition_form_validate($form, $form_state) {
  if (empty($form_state['values']['publish_on'])) {
    form_set_error('publish_on', t("The 'publish on' date cannot be empty"));
  }

  if (strtotime($form_state['values']['publish_on']) <= REQUEST_TIME) {
    form_set_error('publish_on', t("The 'publish on' date must be in the future"));
  }
}

/**
 * CPS Scheduler form submit handler
 */
function cps_scheduler_form_cps_transition_transition_form_submit($form, &$form_state) {
  // Set publish on attribute to changeset to use it on transition callback.
  if (!empty($form_state['values']['publish_on'])) {
    $form_state['changeset']->publish_on = $form_state['values']['publish_on'];
  }
}

/**
 * Schedule transition callback
 *
 * Add changeset to scheduling and set its status to "scheduled".
 *
 * @param \CPSChangeset $changeset
 *  The changeset
 *
 * @param string $transition
 *  The transition name
 *
 * @param array $transition_info
 *  The transition info
 *
 * @return bool
 * @throws \InvalidMergeQueryException
 */
function cps_scheduler_schedule_transition_callback(CPSChangeset $changeset, $transition, $transition_info) {
  if (!empty($changeset->publish_on)) {
    $publish_on = strtotime($changeset->publish_on);
    $message = t('Scheduled for %date', ['%date' => date(CPS_SCHEDULER_DATE_FORMAT, $publish_on)]);

    db_merge('cps_scheduler')
      ->key(['changeset_id' => $changeset->changeset_id])
      ->fields(['publish_on' => $publish_on])
      ->execute();

    $changeset->setStatus($transition_info['state'], $message);
    $changeset->save();

    return TRUE;
  }

  return FALSE;
}

/**
 * Cancel scheduling transition callback
 *
 * Delete the scheduling and revert the changeset status to unpublished.
 *
 * @param \CPSChangeset $changeset
 *  The changeset
 *
 * @param string $transition
 *  The transition name
 *
 * @param array $transition_info
 *  The transition info
 *
 * @return bool
 */
function cps_scheduler_cancel_schedule_transition_callback(CPSChangeset $changeset, $transition, $transition_info) {
  db_delete('cps_scheduler')
    ->condition('changeset_id', $changeset->changeset_id)
    ->execute();

  $message = t('Site version removed form scheduling.');
  $changeset->setStatus($transition_info['state'], $message);
  $changeset->save();
  return TRUE;
}

/**
 * Implements hook_cps_changeset_operations_alter()
 *
 * For scheduled changeset only view, status and cancel scheduling operations
 * are available
 *
 * @param $operations
 * @param $changeset
 */
function cps_scheduler_cps_changeset_operations_alter(&$operations, $changeset) {
  $allowed_states = ['site', 'view', 'cancel_scheduling'];
  if ($changeset->status == 'scheduled') {
    foreach (element_children($operations) as $state) {
      if (!in_array($state, $allowed_states)) {
        $operations[$state]['#access'] = FALSE;
      }
    }
  }
}

/**
 * Implements hook_cps_changeset_access_alter().
 *
 * For scheduled changeset only view and cancel scheduling actions are available
 */
function cps_scheduler_cps_changeset_access_alter(&$access, $op, $changeset, $account) {
  if ($changeset && $changeset->status == 'scheduled') {
    // Allow bulk copy from a scheduled changeset.
    if ($op == 'update' && arg(4) == 'bulk' && arg(5) == 'copy') {
      return $access;
    }

    if (!in_array($op, ['view', 'cancel_scheduling'])) {
      $access = FALSE;
    }
  }

  return $access;
}

/**
 * Implements hook_cron().
 */
function cps_scheduler_cron() {
  _cps_scheduler_scheduled_publish();
}

/**
 * Get all the scheduled site versions and publish them.
 */
function _cps_scheduler_scheduled_publish() {
  $changeset_ids = db_select('cps_scheduler', 'cs')
    ->fields('cs', ['changeset_id', 'changeset_id'])
    ->condition('cs.publish_on', 0, '>')
    ->condition('cs.publish_on', REQUEST_TIME, '<=')
    ->execute()
    ->fetchAllKeyed();

  if (!empty($changeset_ids)) {
    foreach ($changeset_ids as $changeset_id) {
      $changeset = cps_changeset_load($changeset_id);

      // If changeset_id is not valid abort the publish operation.
      if (!$changeset) {
        watchdog('cps_scheduler', t('There was an error loading %changeset.'), ['%changeset' => $changeset_id], WATCHDOG_ERROR);
        break;
      }

      // If publish process is locked, log the error and stop the flow.
      $lock = cps_acquire_processing_lock();
      if (!$lock) {
        watchdog('cps_scheduler', t('CPS is processing another site version, please try again later.'), [], WATCHDOG_ERROR);
        break;
      }

      // Change the changeset status to publish it: only open changeset could be
      // published.
      $changeset->status = 'unpublished';

      if (cps_publish_changeset($changeset)) {
        // Remove changeset's scheduling.
        db_delete('cps_scheduler')
          ->condition('changeset_id', $changeset_id)
          ->execute();

        cps_release_processing_lock();
        watchdog('cps_scheduler', t('%changeset successfully published.'), ['%changeset' => $changeset_id], WATCHDOG_INFO);
      }
      else {
        watchdog('cps_scheduler', t('There was an error processing %changeset.'), ['%changeset' => $changeset_id], WATCHDOG_ERROR);
        cps_release_processing_lock();
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function cps_scheduler_form_cps_changeset_preview_form_alter(&$form, &$form_state) {
  $changeset_id = cps_get_current_changeset(TRUE);

  if ($changeset_id != CPS_PUBLISHED_CHANGESET) {
    // Add publish and schedule/unschedule links.
    $changeset = cps_changeset_load($changeset_id);

    if ($changeset->status == 'scheduled') {
      $unschedule_path = 'admin/structure/changesets/' . $changeset_id . '/transition-cancel_scheduling';
      $unschedule_text = t('Unschedule Site Version');

      $form['unschedule'] = [
        '#markup' => l(
          $unschedule_text,
          $unschedule_path,
          ['attributes' => ['title' => $unschedule_text, 'class' => ['cps-changeset--cancel-scheduling']]]
        ),
        '#access' => drupal_valid_path($unschedule_path),
      ];
    }
    else {
      $publish_text = t('Publish Site Version');
      $publish_path = 'admin/structure/changesets/' . $changeset_id . '/publish';

      $schedule_text = t('Schedule Site Version');
      $schedule_path = 'admin/structure/changesets/' . $changeset_id . '/transition-schedule';

      $form['publish'] = [
        '#markup' => l(
          $publish_text,
          $publish_path,
          ['attributes' => ['title' => $publish_text, 'class' => ['cps-changeset--publish']]]
        ),
        '#access' => drupal_valid_path($publish_path),
      ];

      $form['schedule'] = [
        '#markup' => l(
          $schedule_text,
          $schedule_path,
          ['attributes' => ['title' => $schedule_text, 'class' => ['cps-changeset--schedule']]]
        ),
        '#access' => drupal_valid_path($schedule_path),
      ];
    }
  }
}

/**
 * Implements hook_preprocess_page().
 */
function cps_scheduler_preprocess_page(&$variables) {
  $path_to_module = drupal_get_path('module', 'cps_scheduler');
  drupal_add_css($path_to_module . '/css/cps_scheduler.css');
}

/**
 * Implements hook_views_api().
 */
function cps_scheduler_views_api() {
  return array(
    'path' => drupal_get_path('module', 'cps_scheduler') . '/views',
    'api' => 3,
  );
}

/**
 * Check if changeset has changed entities that are not allowed for scheduling.
 *
 * @param $changeset_id
 *  The changeset ID
 *
 * @return array|bool
 */
function _cps_scheduler_can_be_scheduled($changeset) {
  $errors = [];
  $entities = cps_get_tracked_entities($changeset->changeset_id);
  $not_allowed = variable_get('cps_scheduler_disallowed_entity_types', []);

  foreach ($not_allowed as $entity_type) {
    if (isset($entities[$entity_type])) {
      $errors['entity_type:' . $entity_type] = FALSE;
    }
  }

  drupal_alter('cps_scheduler_not_allowed', $errors, $entities);

  return count($errors) == 0;
}
Loading