<?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(); }