Skip to content
Snippets Groups Projects
election.module 13.9 KiB
Newer Older
<?php

/**
 * @file
 * Contains election.module.
 */

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\election\Entity\Election;
use Drupal\election\Entity\ElectionInterface;
use Drupal\election\Entity\ElectionPost;
use Drupal\election\Entity\ElectionPostInterface;
use Drupal\election\Entity\ElectionPostType;

/**
 * Implements hook_theme().
 * Provides a theme definition for custom content entity.
 */
function election_theme($existing, $type, $theme, $path) {
  $theme = [
    'election' => [
      'render element' => 'elements',
      'file' => 'election.page.inc',
    ],
    'election_post' => [
      'render element' => 'elements',
      'file' => 'election_post.page.inc',
    ],
    'election_ballot' => [
      'render element' => 'elements',
      'file' => 'election_ballot.page.inc',
    ],
    'election_candidate' => [
      'render element' => 'elements',
      'file' => 'election_candidate.page.inc',
    ],
    'election_status_summary' => [
      'variables' => [
        'phases' => [],
      ],
    ],
    'election_actions' => [
      'variables' => [
        'actions' => [],
      ],
    ],
    'election_post_actions' => [
      'variables' => [
        'actions' => [],
      ],
    ],
  ];
  return $theme;
}

/**
 * Implements hook_help().
 */
function election_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    // Main module help for the election module.
    case 'help.page.election':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Run democratic elections through nomination, voting and results counting.') . '</p>';
      return $output;

    default:
  }
}

/**
 * Helper function to extract the entity for the supplied route.
 *
 * @return null|ContentEntityInterface
 */
function election_get_route_entity() {
  $route_match = \Drupal::routeMatch();
  // Entity will be found in the route parameters.
  if (($route = $route_match->getRouteObject()) && ($parameters = $route->getOption('parameters'))) {
    // Determine if the current route represents an entity.
    foreach ($parameters as $name => $options) {
      if (isset($options['type']) && strpos($options['type'], 'entity:') === 0) {
        $entity = $route_match->getParameter($name);
        if ($entity instanceof ContentEntityInterface && $entity->hasLinkTemplate('canonical')) {
          return $entity;
        }

        // Since entity was found, no need to iterate further.
        return NULL;
      }
    }
  }
}

/**
 * Sort posts on election page by preferred eligibility order.
 *
 * @param array $variables
 *   Views preprocess array.
 */
function election_preprocess_views_view(&$variables) {
  $view = $variables['view'];
  if ($view->id() == 'election_posts_for_election_page' && $variables['display_id'] == 'embed') {
    // Sort posts on election page by preferred eligibility order.
    $rows = $variables['rows'];
    $order = Election::getEligibilityOrder();
    usort($rows, function ($a, $b) use ($order) {
      $aTitle = trim(preg_replace('/<!--(.|\s)*?-->/', '', $a['#title']->__toString()));
      $bTitle = trim(preg_replace('/<!--(.|\s)*?-->/', '', $b['#title']->__toString()));

      // https://stackoverflow.com/questions/11145393/sorting-a-php-array-of-arrays-by-custom-order
      $a = array_search($aTitle, $order);
      $b = array_search($bTitle, $order);
      // Both items are dont cares.
      if ($a === FALSE && $b === FALSE) {
        // A == b.
        return 0;
      }
      // $a is a dont care
      elseif ($a === FALSE) {
        // $a > $b
        return 1;
      }
      // $b is a dont care
      elseif ($b === FALSE) {
        // $a < $b
        return -1;
      }
      else {
        // Sort $a and $b ascending.
        return $a - $b;
      }
    });
    $variables['rows'] = $rows;
  }
}

/**
 * Implements hook_entity_extra_field_info().
 */
function election_entity_extra_field_info() {
  $extra = [];

  foreach (ElectionPostType::loadMultiple() as $bundle) {
    $extra['election_post'][$bundle->id()]['display']['field_post_actions'] = [
      'label' => t('Action links for post'),
      'description' => t('e.g. Vote, Nominate, check eligibility, depending on user, phases open, and eligibility'),
      'weight' => -1,
      'visible' => TRUE,
    ];
    $extra['election_post'][$bundle->id()]['display']['field_status_summary'] = [
      'label' => t('Status and eligibility summary'),
      'description' => t('Shows the open/close status for each phase, and the current user\'s eligibility.'),
      'weight' => 0,
      'visible' => TRUE,
  }

  return $extra;
}

/**
 * Implements hook_ENTITY_TYPE_view().
 */
function election_election_post_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  if ($display->getComponent('field_status_summary')) {
    election_election_post_view_field_status_summary($build, $entity, $display, $view_mode);
  }
  if ($display->getComponent('field_post_actions')) {
    election_election_post_view_field_post_actions($build, $entity, $display, $view_mode);
  }
}

function election_election_post_view_field_status_summary(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  $summary = [
    '#theme' => 'election_status_summary',
    '#phases' => $entity->getUserEligibilityInformation(\Drupal::currentUser(), $entity->getEnabledPhases()),
  ];

  $build['field_status_summary'] = [
    '#type' => 'markup',
    '#markup' => \Drupal::service('renderer')->render($summary),
    '#cache' => [
      // 'max-age' => 0,
    ],
  ];
}

function election_election_post_view_field_post_actions(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  if (method_exists($entity, 'getActionLinks')) {
    $summary = [
      '#theme' => 'election_post_actions',
      '#actions' => $entity->getActionLinks(\Drupal::currentUser()),
    ];

    $build['field_post_actions'] = [
      '#type' => 'markup',
      '#markup' => \Drupal::service('renderer')->render($summary),
      '#cache' => [
        // 'max-age' => 0,
      ],
    ];
  }
}

function election_election_update(ElectionInterface $election) {
  $phases = election_check_phase_status_changed($election);
  foreach ($phases as $phase) {
    $election->onPhaseOpenOrClose($phase);
  }

  // @todo check if conditions have changed
}

function election_election_post_update(ElectionPostInterface $election_post) {
  $phases = election_check_phase_status_changed($election_post);
  foreach ($phases as $phase) {
    $election_post->onPhaseOpenOrClose($phase);
  }

  // @todo check if conditions have changed
}

function election_check_phase_status_changed($entity) {
  $phases = [];
  foreach (Election::getPhases() as $phase_id => $phase) {
    if ($entity->original->getPhaseStatus($phase_id) != $entity->getPhaseStatus($phase_id)) {
      // Phases will always have unique key as we are using $phase_id as key.
      $phases[$phase_id] = $phase;
    }

    // $fieldsToTriggerChange = [
    //   'status_' . $phase_id,
    //   'status_' . $phase_id . '_open',
    //   'status_' . $phase_id . '_close',
    // ];
    // foreach ($fieldsToTriggerChange as $field) {
    //   if ($entity->original->$field->value != $entity->$field->value) {
    //     $phases[$phase_id] = $phase;
    //   }
    // }
  }
  return $phases;
function election_get_election_from_context() {
  $context_provider = \Drupal::service('election.election_route_context');
  $contexts = $context_provider->getRuntimeContexts(['election']);
  $election = $contexts['election']->getContextValue();

  if (!$election) {
    return NULL;
  }

  if (is_string($election)) {
    $election = ElectionPost::load($election);
  }

  return $election;
}

function election_get_election_post_from_context() {
  $context_provider = \Drupal::service('election.election_route_context');
  $contexts = $context_provider->getRuntimeContexts(['election_post']);
  $election_post = $contexts['election_post']->getContextValue();
  if (!$election_post) {
    return NULL;
  }
  if (is_string($election_post)) {
    $election_post = ElectionPost::load($election_post);
  }
  return $election_post;
}

/**
 * @param mixed $form_display
 * @param mixed $context
 *
 * @return [type]
 */
function election_entity_form_display_alter(&$form_display, $context) {
  // Show nomination form for candidates if not an editor.
  if ($context['entity_type'] == 'election_candidate') {
    $user = \Drupal::currentUser();
    $canEditFull = $user->hasPermission('add election candidate entities without eligibility');
    if (!$canEditFull) {
      $storage = \Drupal::service('entity_type.manager')->getStorage('entity_form_display');
      $nomination_display = $storage->load($context['entity_type'] . '.' . $context['bundle'] . '.nomination');
      if ($nomination_display) {
        $form_display = $nomination_display;
      }
    }
  }
}

/**
 * Adds template possibility for view modes
 * Implements hook_provider_theme_suggestions_hook_alter
 */
function election_theme_suggestions_election_candidate_alter(array &$suggestions, array $vars, $hook) {
  if ($election_candidate = $vars['elements']['#election_candidate']) {
    if (isset($vars['elements']['#view_mode'])) {
      $suggestions[] = 'election_candidate__' . $vars['elements']['#view_mode'];
    }
  }
}
/**
 * Override ballot form error to be a bit friendlier.
 *
 * Implements hook_preprocess_HOOK().
 *
 * @param $variables
 */
function election_preprocess_status_messages(&$variables) {
  if (isset($variables['message_list']['error'])) {
    $error_messages = $variables['message_list']['error'];
    foreach ($error_messages as $delta => $message) {
      if (is_array($message)) {
        continue;
      }
      if (strpos((string) $message, '#edit-rankings') !== FALSE) {
        $variables['message_list']['error'][$delta] = t("You have not completed the ballot correctly, see highlighted areas below.");
      }
    }
  }
}

/**
 * Override format for HTML results field.
 *
 * @todo not sure why can't do this on entity BaseFieldDefinition?
 *
 * @param array $variables
 *   Preprocess variables.
 */
function election_preprocess_field__count_results_html(&$variables) {
  if (isset($variables['items'][0]['content'])) {
    $variables['items'][0]['content']['#format'] = 'full_html';
  }
}

/**
 * Implements hook_mail().
 */
function election_mail($key, &$message, $params) {
  $options = [
    'langcode' => $message['langcode'],

  switch ($key) {
    case 'election.notify_user':
      $message['format'] = 'text/html';
      $message['headers']['Content-Type'] = 'text/html';

      $message['from'] = \Drupal::config('system.site')->get('mail');
      $message['subject'] = $params['subject'];
      $message['body'][] = t($params['body']);
      break;
  }
}

/**
 * Close based on access to status fields.
 *
 * Implements hook_entity_field_access.
 */
function election_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  if ($items && $operation == 'view') {
    $entity = $items->getEntity();

    if ($entity->getEntityTypeId() == 'election_candidate') {
      // If results have been set, don't allow viewing candidate status until they are published.
      if ($field_definition->getName() == 'candidate_status') {
        if (in_array($entity->candidate_status->value, ['defeated', 'elected'])) {
          if (!$account->hasPermission('view published count results')) {
            return AccessResult::forbidden();
          }
          if (!$account->hasPermission('view unpublished count results')) {
            return AccessResult::forbiddenIf(!$entity->getElectionPost()->count_results_published->value);
          }
        }
      }
      return AccessResult::neutral();
    }

    if ($entity->getEntityTypeId() != 'election' && $entity->getEntityTypeId() != 'election_post') {
      return AccessResult::neutral();
    }

    if ($entity->getEntityTypeId() == 'election_post' && stristr($field_definition->getName(), 'count_')) {
      if (!$account->hasPermission('view published count results')) {
        return AccessResult::forbidden();
      }

      if (!$entity->count_results_published->value && !$account->hasPermission('view unpublished count results')) {
        return AccessResult::forbidden();
      }
    }

    $phases = Election::getPhases();
    foreach ($phases as $phase_id => $phase) {
      if (in_array($field_definition->getName(), ['status_' . $phase_id])) {
        // If it's a scheduled election or post, make sure we run a function ONCE.
        if ($entity->get('status_' . $phase_id)->getValue() == 'scheduled') {
          $started = $entity->get('status_' . $phase_id . '_open')->value < strtotime('now');
          $ended = $entity->get('status_' . $phase_id . '_close')->value < strtotime('now');

          // Store the fact we've run it in state so we don't do it again.
          if ($started) {
            $state_key = 'opened_' . $entity->getEntityTypeId() . '_' . $entity->id();
          }
          elseif ($ended) {
            $state_key = 'opened_' . $entity->getEntityTypeId() . '_' . $entity->id();
          }
          if (!\Drupal::state()->get($state_key)) {
            $entity->onPhaseOpenOrClose($phase);
          }
          \Drupal::state()->set($state_key, TRUE);
        }
      }
    }
  }

  // Limit editing count-related fields to a specific permission.
  if ($items && $operation == 'edit') {
    $entity = $items->getEntity();
    if ($entity->getEntityTypeId() == 'election_post' && stristr($field_definition->getName(), 'count_')) {
      return AccessResult::allowedIfHasPermission($account, 'edit count results');
    }
  }

  return AccessResult::neutral();
}