social_course-8.x-2.11/social_course.module
social_course.module
<?php
/**
* @file
* The Social course module.
*/
use Drupal\block\Entity\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\group\Entity\GroupInterface;
use Drupal\group\Entity\GroupRelationship;
use Drupal\group\Entity\GroupTypeInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\search_api\Query\QueryInterface;
use Drupal\social_course\Entity\CourseEnrollmentInterface;
use Drupal\social_course\Plugin\Group\RelationHandler\SocialCourseNodePermissionProvider;
use Drupal\social_course\Plugin\Join\SocialCourseDirectJoin;
use Drupal\social_course\Plugin\Join\SocialCourseInviteJoin;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ViewExecutable;
/**
* Implements hook_theme().
*/
function social_course_theme(): array {
return [
'course_add_list' => [
'variables' => [
'content' => NULL,
],
],
'group__course_basic__teaser' => [
'base hook' => 'group',
],
'group__course_basic__featured' => [
'base hook' => 'group',
],
'group__course_basic__hero' => [
'base hook' => 'group',
],
'group__course_basic__hero__sky' => [
'base hook' => 'group',
],
'group__course_basic__statistic__sky' => [
'base hook' => 'group',
],
'group__course_advanced__teaser' => [
'base hook' => 'group',
],
'group__course_advanced__featured' => [
'base hook' => 'group',
],
'group__course_advanced__hero' => [
'base hook' => 'group',
],
'group__course_advanced__hero__sky' => [
'base hook' => 'group',
],
'group__course_advanced__statistic__sky' => [
'base hook' => 'group',
],
'course_material_hero' => [
'variables' => [
'title' => NULL,
'hero_node' => NULL,
'node' => NULL,
'node_type' => NULL,
'section_class' => NULL,
'parent_course' => NULL,
'parent_course_type' => NULL,
'parent_section' => NULL,
'material' => NULL,
],
],
'course_material_pager' => [
'variables' => [
'material' => NULL,
],
],
'course_material_navigation' => [
'variables' => [
'items' => [],
'parent_course' => NULL,
'parent_section' => NULL,
],
],
'course_navigation' => [
'variables' => [
'items' => [],
'course_sections' => [],
'parent_course' => NULL,
'parent_section' => NULL,
],
],
'course_section_navigation' => [
'variables' => [
'items' => [],
],
],
'course_related_courses' => [
'variables' => [
'items' => [],
],
],
'node__course_section__teaser' => [
'variables' => [
'parts_count' => NULL,
],
'base hook' => 'node',
],
'node__course_article__full' => [
'variables' => [
'parent_course' => NULL,
],
'base hook' => 'node',
],
'node__course_video__full' => [
'variables' => [
'parent_course' => NULL,
],
'base hook' => 'node',
],
'block__course_material_navigation' => [
'base hook' => 'block',
],
'block__course_navigation' => [
'base hook' => 'block',
],
'field__group__field_course_opening_date' => [
'base hook' => 'field',
],
'paragraph__images__default' => [
'base hook' => 'paragraph',
],
'paragraph__image_text__default' => [
'base hook' => 'paragraph',
],
'paragraph__text_image__default' => [
'base hook' => 'paragraph',
],
];
}
/**
* Retrieves the course material types that are available.
*
* @return array
* An array of course materials
*/
function social_course_get_material_types() {
// Ensure we only calculate this once per request.
$material_types = &drupal_static(__FUNCTION__);
if (!is_null($material_types)) {
return $material_types;
}
// By default we have an article and video course type.
$material_types = [
'course_article',
'course_video',
];
// Allow other modules to register their own types.
\Drupal::moduleHandler()
->alter('social_course_material_types', $material_types);
return $material_types;
}
/**
* Prepares variables for the social page hero data.
*
* Default template: page-hero-data.html.twig.
*
* @param array $variables
* An associative array containing:
* - title: Page title as a string
* - node_type: Type of node
* - section_class: Class based on type of node
* - parent_section: Information about parent section
* - material: Information about materials parent section
* - parent_course: Information about parent course.
*/
function template_preprocess_course_material_hero(array &$variables): void {
$material_types = social_course_get_material_types();
// Get current user.
$account = \Drupal::currentUser();
// Get current node object or node id.
$node = \Drupal::routeMatch()->getParameter('node');
if (!is_object($node) && !is_null($node)) {
$node = \Drupal::service('entity_type.manager')
->getStorage('node')
->load($node);
}
if ($node instanceof NodeInterface && in_array($node->getType(), $material_types)) {
// Get the current route name to check if the user is on the edit page.
$route = \Drupal::routeMatch()->getRouteName();
if (!in_array($route, [
'entity.node.edit_form',
'entity.node.delete_form',
])) {
if ($node->access('update', $account)) {
$variables['node_edit_url'] = $node->toUrl('edit-form')->toString();
}
}
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$course_wrapper->setCourseFromMaterial($node);
$section = $course_wrapper->getSectionFromMaterial($node);
$variables['node_type'] = $node->getType();
if ($course = $course_wrapper->getCourse()) {
$variables['parent_course'] = [
'label' => $course->label(),
'url' => $course->toUrl(),
];
}
$variables['parent_section'] = [
'number' => $course_wrapper->getSectionNumber($section) + 1,
'title' => $section->label(),
'url' => $section->toUrl(),
];
$variables['material'] = [
'number' => $course_wrapper->getMaterialNumber($node) + 1,
'count' => count($course_wrapper->getMaterials($section)),
];
}
}
/**
* Prepares variables for the material page navigation data.
*
* @param array $variables
* An associative array containing:
* - material: Information about materials in parent section.
*/
function template_preprocess_course_material_pager(array &$variables): void {
$material_types = social_course_get_material_types();
// Get current node object or node id.
$node = \Drupal::routeMatch()->getParameter('node');
if (is_numeric($node)) {
$node = \Drupal::entityTypeManager()
->getStorage('node')
->load($node);
}
if ($node instanceof NodeInterface && in_array($node->getType(), $material_types)) {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$course_wrapper->setCourseFromMaterial($node);
if ($course = $course_wrapper->getCourse()) {
$section = $course_wrapper->getSectionFromMaterial($node);
$materials = $course_wrapper->getMaterials($section);
$material_number = $course_wrapper->getMaterialNumber($node) + 1;
$variables['material']['number'] = $material_number;
$variables['material']['count'] = count($materials);
// Create link to previous material.
if ($material_number > 1) {
$previous_url = Url::fromRoute('entity.node.canonical', [
'node' => $course_wrapper->getMaterial($node, -1)->id(),
], [
'attributes' => [
'class' => [
'btn',
'btn-raised',
'btn-default',
'waves-effect',
],
],
]);
$variables['material']['previous'] = Link::fromTextAndUrl(t('Previous'), $previous_url)
->toRenderable();
}
// Create link to next material.
if ($material_number < count($materials)) {
$account = \Drupal::currentUser();
$storage = \Drupal::entityTypeManager()
->getStorage('course_enrollment');
$next_material_id = $course_wrapper->getMaterial($node, 1)->id();
$next_material_enrollment = $storage->loadByProperties([
'gid' => $course->id(),
'sid' => $section->id(),
'mid' => $next_material_id,
'uid' => $account->id(),
]);
// If a user is not yet enrolled in the next section then we link to the
// section start page. Otherwise we just link there directly.
if (!$next_material_enrollment) {
$next_url = Url::fromRoute('social_course.next_material', [
'group' => $course->id(),
'node' => $section->id(),
], [
'attributes' => [
'class' => [
'btn',
'btn-raised',
'waves-effect',
'next-material',
],
],
]);
}
else {
$next_url = Url::fromRoute('entity.node.canonical', [
'node' => $course_wrapper->getMaterial($node, 1)->id(),
], [
'attributes' => [
'class' => [
'btn',
'btn-raised',
'waves-effect',
'next-material',
],
],
]);
}
$variables['material']['next'] = Link::fromTextAndUrl(t('Continue'), $next_url)
->toRenderable();
}
else {
$next_url = Url::fromRoute('social_course.next_material', [
'group' => $course->id(),
'node' => $section->id(),
], [
'attributes' => [
'class' => [
'btn',
'btn-raised',
'btn-primary',
'waves-effect',
],
],
]);
$section_number = $course_wrapper->getSectionNumber($section) + 1;
if ($section_number < count($course_wrapper->getSections())) {
// Set next button title from section url redirect if it exists.
$title = empty($section->get('field_course_section_redirect')->title) ? t('Finish section') : $section->get('field_course_section_redirect')->title;
}
else {
$title = t('Finish course');
}
$variables['material']['next'] = Link::fromTextAndUrl($title, $next_url)
->toRenderable();
}
}
}
}
/**
* Implements hook_social_user_account_header_create_links().
*/
function social_course_social_user_account_header_create_links(array $context): array {
return [
'add_course' => [
'#type' => 'link',
'#attributes' => [
'title' => t('Create New Course'),
],
'#title' => t('New Course'),
// Put this after 'Create Topic'.
'#weight' => 250,
] + Url::fromRoute('social_course.course_add')->toRenderArray(),
];
}
/**
* Implements hook_social_user_account_header_account_links().
*/
function social_course_social_user_account_header_account_links(array $context): array {
// We require a user and user access for these links.
if (empty($context['user']) || !($context['user'] instanceof AccountInterface)) {
return [];
}
return [
'my_courses' => [
'#type' => 'link',
'#attributes' => [
'title' => t('View my courses'),
],
'#title' => t('My courses'),
// Put this after "My Topics".
'#weight' => 750,
] + Url::fromRoute('view.courses_user.page', [
'user' => $context['user']->id(),
])->toRenderArray(),
];
}
/**
* Prepares variables for list of available node type templates.
*
* Default template: node-add-list.html.twig.
*
* @param array $variables
* An associative array containing:
* - content: An array of content types.
*
* @see node_add_page()
*/
function template_preprocess_course_add_list(array &$variables): void {
$variables['types'] = [];
if (!empty($variables['content'])) {
foreach ($variables['content'] as $type) {
$group_type = $type->id();
$variables['types'][$group_type] = [
'type' => $group_type,
'add_link' => Link::fromTextAndUrl($type->label(), Url::fromRoute('entity.group.add_form', [
'group_type' => $type->id(),
]))->toString(),
'description' => [
'#markup' => $type->getDescription(),
],
];
}
}
}
/**
* Implements hook_form_alter().
*/
function social_course_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
// Add and edit forms.
$section_forms = [
'node_course_section_edit_form',
'node_course_section_form',
];
if (in_array($form_id, $section_forms)) {
$form['#attached']['library'][] = 'social_course/admin';
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
$group = _social_group_get_current_group();
if ($group instanceof GroupInterface && in_array($group->bundle(), $bundles)) {
$course_wrapper->setCourse($group);
if ($course_wrapper->courseIsSequential()) {
$form['field_course_section_redirect']['#disabled'] = TRUE;
}
}
}
$course_forms = [
'group_course_basic_add_form',
'group_course_basic_edit_form',
'group_course_advanced_add_form',
'group_course_advanced_edit_form',
];
if (in_array($form_id, $course_forms)) {
$form['uid']['widget'][0]['target_id']['#title'] = t('Author');
$form['actions']['submit']['#submit'][] = '_social_course_group_form_submit';
// Add widget to sort sections.
/** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
$form_object = $form_state->getFormObject();
/** @var \Drupal\group\Entity\GroupInterface $group */
$group = $form_object->getEntity();
if ($group->id()) {
/** @var \Drupal\group\Entity\Storage\GroupRelationshipStorageInterface $group_relationship_storage */
$group_relationship_storage = \Drupal::entityTypeManager()
->getStorage('group_content');
$content = $group_relationship_storage->loadByGroup($group, 'group_node:course_section');
$form['sections'] = [
'#type' => 'fieldset',
'#title' => t('Sections'),
'#access' => !empty($content),
'#tree' => TRUE,
];
$form['sections']['help'] = [
'#type' => 'details',
'#title' => t('Drag and drop to change the order'),
];
$form['sections']['list'] = [
'#type' => 'table',
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'section-item-weight',
],
],
'#header' => [
t('Title'),
t('Weight'),
],
];
foreach ($content as $item) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $item->getEntity();
$weight = $entity->get('field_course_section_weight')->value;
$form['sections']['list'][$entity->id()]['#weight'] = $weight;
$form['sections']['list'][$entity->id()]['#attributes']['class'][] = 'draggable';
$form['sections']['list'][$entity->id()]['label'] = [
'#markup' => '<span>' . $entity->label() . '</span>',
];
$form['sections']['list'][$entity->id()]['weight'] = [
'#type' => 'textfield',
'#title' => t('Weight for @title', [
'@title' => $entity->label(),
]),
'#title_display' => 'invisible',
'#default_value' => $weight,
'#attributes' => [
'class' => [
'section-item-weight',
],
],
];
}
uasort($form['sections']['list'], function ($a, $b) {
if (!isset($a['#weight']) || !isset($b['#weight']) || $a['#weight'] == $b['#weight']) {
return 0;
}
return $a['#weight'] < $b['#weight'] ? -1 : 1;
});
}
$widget = &$form['field_course_redirect_url']['widget'][0];
$widget['uri']['#description'] = $widget['#description'];
$widget['uri']['#type'] = 'textfield';
// Add a checkbox for users to easily enable/disable course date and time.
if (isset($form['field_course_opening_date'])) {
$form['field_course_opening_date_status'] = [
'#type' => 'checkbox',
'#weight' => -1,
'#title' => t('Set starting date and time'),
'#description' => t("When enabled, course sections and other information can only be accessed by members after the course starts. Enrolment can only take place before the course has started.
<br>When not enabled, course sections and other information can be accessed right after enrolment."),
'#default_value' => !empty($form['field_course_opening_date']['widget'][0]['value']['#default_value']),
];
$form['field_course_opening_date']['widget'][0]['value']['#states'] = [
'visible' => [
'input[name="field_course_opening_date_status"]' => ['checked' => TRUE],
],
];
// Set correct field group 'date_and_time'.
$form['field_course_opening_date_status']['#group'] = 'group_date_and_time';
$form['#group_children']['field_course_opening_date_status'] = $form['field_course_opening_date_status'];
// Remove extra fieldset from datetime field.
unset($form['field_course_opening_date']['widget'][0]['#theme_wrappers']);
}
$form['actions']['submit']['#value'] = t('Save');
// Remove field 'group_type' for courses forms.
if (isset($form['group_type'])) {
unset($form['group_type']);
}
// Modify field labels and descriptions.
social_course_change_form_field_texts($form);
if (isset($form['tab_settings']['default_route'])) {
$form['tab_settings']['default_route']['#title'] =
t(str_replace('Group', 'Course', $form['tab_settings']['default_route']['#title']));
}
}
if (method_exists($form_state->getFormObject(), 'getEntity')) {
$entity = $form_state->getFormObject()->getEntity();
if ($entity instanceof NodeInterface) {
$group = _social_group_get_current_group();
if ($group instanceof GroupInterface && str_contains($group->bundle(), 'course')) {
// Modify field labels and descriptions.
social_course_change_form_field_texts($form);
}
}
}
// Action forms.
$action_forms = [
'group_content_course_basic-group_membership_group-join_form',
'group_content_course_basic-group_membership_group-leave_form',
'group_content_course_basic-group_membership_add_form',
'group_content_course_advanced-group_membership_group-join_form',
'group_content_course_advanced-group_membership_group-leave_form',
'group_content_course_advanced-group_membership_add_form',
];
// Perform alterations on joining / leaving groups.
if (in_array($form_id, $action_forms)) {
$form['#attributes']['class'][] = 'form--default';
if (isset($form['actions']['submit'])) {
$form['actions']['submit']['#button_type'] = 'primary';
$form['actions']['submit']['#button_level'] = 'raised';
}
if (isset($form['actions']['cancel'])) {
// Some `cancel` buttons are not inputs but links.
if (isset($form['actions']['cancel']['#type']) && $form['actions']['cancel']['#type'] == 'link') {
$form['actions']['cancel']['#attributes']['class'][] = 'btn btn-flat';
}
else {
$form['actions']['cancel']['#button_type'] = 'flat';
}
}
switch ($form_id) {
case 'group_content_course_basic-group_membership_add_form':
case 'group_content_course_advanced-group_membership_add_form':
// Make the form a card and exclude the title.
$form['#attributes']['class'][] = 'card';
$form['actions']['#prefix'] = '</div></div>';
break;
case 'group_content_course_basic-group_membership_group-join_form':
case 'group_content_course_advanced-group_membership_group-join_form':
$form['actions']['submit']['#value'] = t('Enroll in course');
if (isset($form['path'])) {
$form['path']['#access'] = FALSE;
}
// Make the form a card and exclude the title.
$form['#attributes']['class'][] = 'card';
$form['actions']['#prefix'] = '</div></div>';
$form['help'] = [
'#type' => 'item',
'#markup' => t('You will be able to follow this course by enrolling.'),
];
$form['actions']['submit']['#submit'][] = '_social_course_redirect_users_on_join';
break;
case 'group_content_course_basic-group_membership_group-leave_form':
case 'group_content_course_advanced-group_membership_group-leave_form':
// Extract the actions form the card.
$form['description']['#prefix'] = '<div class="clearfix">';
$form['description']['#suffix'] = '</div></div></div>';
$form['actions']['submit']['#value'] = t('Cancel enrollment');
break;
}
}
// Memberhsip forms.
$membership_forms = [
'group_content_course_basic-group_membership_add_form',
'group_content_course_basic-group_membership_edit_form',
'group_content_course_advanced-group_membership_add_form',
'group_content_course_advanced-group_membership_edit_form',
];
if (in_array($form_id, $membership_forms)) {
// Change titles on membership forms.
$form['entity_id']['widget'][0]['target_id']['#title'] = t('Select a member');
$form['group_roles']['widget']['#title'] = t('Course roles');
if ('group_content_course_basic-group_membership_edit_form' == $form_id) {
// Remove the user selection autocomplete on editing group membership.
$form['entity_id']['#access'] = FALSE;
// Add redirect to Group Membership page.
$form['actions']['submit']['#submit'][] = '_social_group_membership_edit_form_submit';
}
}
// Remove Courses from Social Group add form.
if ($form_id == 'social_group_add') {
$excluded_groups = ['course_advanced', 'course_basic'];
foreach ($excluded_groups as $excluded_group) {
if (isset($form['group_settings']['group_type']['#options'][$excluded_group])) {
unset($form['group_settings']['group_type']['#options'][$excluded_group]);
}
}
}
}
/**
* Redirects users after joining to the course.
*/
function _social_course_redirect_users_on_join(array $form, FormStateInterface $form_state): void {
$group = \Drupal::routeMatch()->getParameter('group');
// Redirect user back to the group they joined.
if ($group instanceof GroupInterface) {
$form_state->setRedirectUrl($group->toUrl());
}
}
/**
* Replace word "Group" with "Course" in form fields text.
*
* @param array $form
* Form array.
*/
function social_course_change_form_field_texts(array &$form): void {
foreach ($form as &$field) {
if (is_array($field) && isset($field['widget'])) {
// Translation strings for replacing.
[$Group_string, $group_string, $Course_string, $course_string] = [
t('Group')->render(),
t('group')->render(),
t('Course')->render(),
t('course')->render(),
];
// Modify all fields titles.
if (isset($field['widget']['#title'])) {
$title =& $field['widget']['#title'];
$title = t(str_replace([$Group_string, $group_string], [$Course_string, $course_string], $title));
}
// Modify all fields descriptions.
if (isset($field['widget']['#description'])) {
$description =& $field['widget']['#description'];
$description = t(str_replace([$Group_string, $group_string], [$Course_string, $course_string], $description));
}
// Modify checkbox titles.
if (isset($field['widget']['value']['#title'])) {
$title =& $field['widget']['value']['#title'];
$title = t(str_replace([$Group_string, $group_string], [$Course_string, $course_string], $title));
}
// Modify checkbox descriptions.
if (isset($field['widget']['value']['#description'])) {
$description =& $field['widget']['value']['#description'];
$description = t(str_replace([$Group_string, $group_string], [$Course_string, $course_string], $description));
}
}
}
}
/**
* Implements hook_social_group_join_info_alter().
*/
function social_course_social_group_join_info_alter(array &$info): void {
if (isset($info['social_group_direct_join'])) {
$info['social_group_direct_join']['class'] = SocialCourseDirectJoin::class;
}
/** @var \Drupal\social_course\CourseWrapperInterface $wrapper */
$wrapper = \Drupal::service('social_course.course_wrapper');
foreach ($wrapper->getAvailableBundles() as $type) {
if (\Drupal::moduleHandler()->moduleExists("social_{$type}_invite")) {
$info['social_group_invite_join']['class'] = SocialCourseInviteJoin::class;
break;
}
}
}
/**
* Implements hook_social_group_join_method_info_alter().
*/
function social_course_social_group_join_method_info_alter(
array &$items,
?FieldableEntityInterface $entity,
): void {
if ($entity !== NULL && $entity->getEntityTypeId() === 'group') {
/** @var \Drupal\social_course\CourseWrapperInterface $wrapper */
$wrapper = \Drupal::service('social_course.course_wrapper');
if (
in_array($type = $entity->bundle(), $wrapper->getAvailableBundles()) &&
\Drupal::moduleHandler()->moduleExists("social_{$type}_request")
) {
$weight = $items['added']['weight'];
$items['added']['weight'] = $items['request']['weight'];
$items['request']['weight'] = $weight;
}
}
}
/**
* Implements hook_social_group_join_method_usage().
*/
function social_course_social_group_join_method_usage(): array {
$items = [];
/** @var \Drupal\social_course\CourseWrapperInterface $wrapper */
$wrapper = \Drupal::service('social_course.course_wrapper');
$types = array_filter(
$wrapper->getAvailableBundles(),
fn (string $type): bool =>
\Drupal::moduleHandler()->moduleExists("social_{$type}_request"),
);
if (!empty($types)) {
$items[] = [
'entity_type' => 'group',
'bundle' => $types,
'field' => 'field_group_allowed_join_method',
];
}
return $items;
}
/**
* Implements hook_social_group_types_alter().
*/
function social_course_social_group_types_alter(array &$social_group_types): void {
/** @var \Drupal\social_course\CourseWrapperInterface $wrapper */
$wrapper = \Drupal::service('social_course.course_wrapper');
$social_group_types = array_merge(
$social_group_types,
$wrapper->getAvailableBundles(),
);
}
/**
* Implements hook_social_group_hide_types_alter().
*/
function social_course_social_group_hide_types_alter(array &$hidden_types): void {
/** @var \Drupal\social_course\CourseWrapperInterface $wrapper */
$wrapper = \Drupal::service('social_course.course_wrapper');
$hidden_types = array_merge($hidden_types, $wrapper->getAvailableBundles());
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function social_course_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state): void {
$view_id = $form_state->get('view')->id();
if ($view_id === 'search_content') {
foreach (['course_article', 'course_section', 'course_video'] as $type) {
unset($form['type']['#options'][$type]);
}
return;
}
if ($view_id === 'newest_groups') {
foreach (['course_advanced', 'course_basic'] as $type) {
unset($form['type']['#options'][$type]);
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Remove not Course Groups from Group field on Course section forms.
*/
function social_course_form_node_form_alter(array &$form, FormStateInterface $form_state): void {
// @todo Fix it in social_group module.
if (isset($form['groups']['widget']['#options'])) {
$group = _social_group_get_current_group();
$course_types = ['course_basic' , 'course_advanced'];
if ($group instanceof GroupInterface && in_array($group->bundle(), $course_types)) {
// Filter the options by Course type.
$form['groups']['widget']['#options'] = array_filter(
$form['groups']['widget']['#options'],
function ($options, $key) use ($course_types) {
// When don't have different group, isn't be multi-array, it will treat it.
$group_id = is_array($options) ? array_key_first($options) : $key;
// The options are split by group type, we'll use the first one to filter.
$first_option = \Drupal::entityTypeManager()
->getStorage('group')
->load($group_id);
// The group machine-name to filter the options.
if (!is_null($first_option) && in_array($first_option->bundle(), $course_types)) {
return TRUE;
}
return FALSE;
},
ARRAY_FILTER_USE_BOTH
);
// Change field groups title for course section node.
if (isset($form['groups']['widget']['#options'])) {
$form['groups']['widget']['#title'] = t('Course');
}
}
}
}
/**
* Implements template_preprocess_form_element() for "fieldset".
*/
function social_course_preprocess_fieldset(array &$variables): void {
// We modify tooltips here using hook function from
// "social_group_flexible_group" module as it returns what we need.
if (
empty(($element = $variables['element'])['#field_name']) ||
!in_array($element['#field_name'], [
'field_flexible_group_visibility',
'field_group_allowed_visibility',
'field_group_allowed_join_method',
'field_content_visibility',
])
) {
return;
}
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
if (($group_type = \Drupal::routeMatch()
->getRawParameter('group_type')) && !in_array($group_type, $bundles)) {
return;
}
if (($group = _social_group_get_current_group()) && !in_array($group->bundle(), $bundles)) {
return;
}
// Only proceed if we have valid course context.
if ($group_type === NULL && $group === NULL) {
return;
}
social_group_flexible_group_preprocess_fieldset($variables);
if (!isset($variables['popover'])) {
return;
}
$old = [
t('Group')->render(),
t('group')->render(),
];
$new = [
t('Course')->render(),
t('course')->render(),
];
$title = &$variables['popover']['toggle']['#attributes']['data-title'];
$title = str_replace($old[0], $new[0], $title);
$description = &$variables['popover']['requirements']['descriptions']['#markup'];
$description = str_replace($old, $new, $description);
}
/**
* Implements hook_preprocess_HOOK().
*/
function social_course_preprocess_group(array &$variables): void {
$group = $variables['group'];
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
if (in_array($group->bundle(), $course_wrapper->getAvailableBundles())) {
$course_wrapper->setCourse($group);
// If we are on user profile page we should get user id from route.
$account = \Drupal::routeMatch()->getParameter('user');
if (is_numeric($account)) {
$account = \Drupal::entityTypeManager()->getStorage('user')->load($account);
}
if (!$account instanceof AccountInterface) {
// If user isn't exists in route get current.
$account = \Drupal::currentUser();
}
// Attach library.
$variables['#attached']['library'][] = 'social_course/social_course';
// Count number of course sections.
$sections = $course_wrapper->getSections();
$variables['course_sections'] = count($sections);
$variables['finished_sections'] = 0;
foreach ($sections as $section) {
$status = $course_wrapper->getSectionStatus($section, $account);
if ($status === CourseEnrollmentInterface::FINISHED) {
$variables['finished_sections'] += 1;
}
}
// Get Course status.
$variables['course_status'] = FALSE;
// Get Course published status.
if ($course_wrapper->getCoursePublishedStatus() == 0) {
$variables['status_label'] = t('Unpublished');
}
if ($group->getMember($account)) {
$course_wrapper->setCourse($group);
switch ($course_wrapper->getCourseStatus($account)) {
case CourseEnrollmentInterface::NOT_STARTED:
$variables['course_status'] = 'enrolled';
break;
case CourseEnrollmentInterface::IN_PROGRESS:
$variables['course_status'] = 'started';
break;
case CourseEnrollmentInterface::FINISHED:
$variables['course_status'] = 'finished';
break;
}
}
// Add teaser tag variable.
if (in_array($variables['view_mode'], ['teaser', 'featured'])) {
$variables['title_prefix']['teaser_tag'] = [
'#markup' => '<div class="teaser__tag">' . t('Course') . '</div>',
];
}
if (isset($variables['group_settings_help'])) {
$help_text_markup = &$variables['group_settings_help'];
$help_text_markup = t(
str_replace(
[t('Group')->render(), t('group')->render()],
[t('Course')->render(), t('course')->render()],
$help_text_markup,
),
);
}
}
}
/**
* Implements hook_preprocess_HOOK() for "social_group_invitations".
*/
function social_course_preprocess_views_view__social_group_invitations(array &$variables): void {
// Change button text.
if ($group = _social_group_get_current_group()) {
/** @var \Drupal\social_course\CourseWrapper $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
if (in_array($group->bundle(), $bundles)) {
if (isset($variables['more']['#title'])) {
$variables['more']['#title'] = str_replace(t('group')->render(), t('course')->render(), $variables['more']['#title']);
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for group_content entities.
*/
function social_course_group_content_insert(GroupRelationship $group_content): void {
/** @var \Drupal\social_course\CourseWrapper $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
$group = $group_content->getGroup();
if (!in_array($group->bundle(), $bundles)) {
return;
}
$course_wrapper->setCourse($group);
$sections = $course_wrapper->getSections();
switch ($group_content->getPlugin()->getPluginId()) {
case 'group_node:course_section':
// Invalidate cache tags.
$cache_tags = _social_group_cache_tags($group);
Cache::invalidateTags($cache_tags);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $group_content->getEntity();
if ($entity->getEntityTypeId() == 'node' && $entity->bundle() == 'course_section') {
if (isset($sections[$entity->id()])) {
unset($sections[$entity->id()]);
}
if ($section = end($sections)) {
$weight = (int) $section->get('field_course_section_weight')->getString() + 1;
$entity->get('field_course_section_weight')->setValue($weight);
$entity->save();
}
}
break;
case 'group_membership':
foreach ($sections as $section) {
Cache::invalidateTags($section->getCacheTags());
}
$cache_tags = _social_group_cache_tags($group);
Cache::invalidateTags($cache_tags);
break;
}
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
function social_course_group_content_delete(GroupRelationship $group_content): void {
/** @var \Drupal\social_course\CourseWrapper $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
$group = $group_content->getGroup();
if (!in_array($group->bundle(), $bundles)) {
return;
}
$course_wrapper->setCourse($group);
$sections = $course_wrapper->getSections();
switch ($group_content->getPlugin()->getPluginId()) {
case 'group_membership':
foreach ($sections as $section) {
Cache::invalidateTags($section->getCacheTags());
}
$cache_tags = _social_group_cache_tags($group);
Cache::invalidateTags($cache_tags);
break;
}
}
/**
* Implements hook_menu_local_tasks_alter().
*/
function social_course_menu_local_tasks_alter(array &$data, string $route_name): void {
$group = _social_group_get_current_group();
if ($group instanceof GroupInterface) {
/** @var \Drupal\social_course\CourseWrapper $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
if (!in_array($group->bundle(), $bundles)) {
return;
}
if ($group->bundle() == 'course_basic') {
$group_tabs = [
'social_group.events',
'social_group.topics',
'social_group.stream',
'social_group.members',
];
foreach ($group_tabs as $key) {
if (isset($data['tabs'][0][$key])) {
$data['tabs'][0][$key]['#access'] = AccessResult::forbidden();
}
}
}
if (isset($data['tabs'][0]['social_group.about'])) {
$data['tabs'][0]['social_group.about']['#link']['title'] = t('Sections');
}
if (isset($data['tabs'][0]['group.view'])) {
$data['tabs'][0]['group.view']['#access'] = AccessResult::forbidden();
}
}
}
/**
* Form submit for group form.
*/
function _social_course_group_form_submit(array $form, FormStateInterface $form_state): void {
/** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
$form_object = $form_state->getFormObject();
/** @var \Drupal\group\Entity\GroupInterface $group */
$group = $form_object->getEntity();
$sections_list = $form_state->getValue(['sections', 'list'], []);
if (is_array($sections_list)) {
foreach ($sections_list as $nid => $value) {
if ($node = Node::load($nid)) {
$node->get('field_course_section_weight')->setValue($value['weight']);
$node->save();
}
}
}
if (empty($form_state->getValue('field_course_opening_date_status'))) {
$group->get('field_course_opening_date')->setValue(NULL);
$group->save();
}
}
/**
* Implements hook_preprocess_html().
*/
function social_course_preprocess_html(array &$variables): void {
$group = _social_group_get_current_group();
if (!$group || !in_array($group->bundle(), [
'course_basic',
'course_advanced',
])) {
return;
}
switch (\Drupal::routeMatch()->getRouteName()) {
case 'entity.group.join':
$variables['head_title']['title'] = t('Enroll in course @label', [
'@label' => $group->label(),
]);
break;
case 'view.group_members.page_group_members':
$variables['head_title']['title'] = t('Course members');
break;
case 'view.group_topics.page_group_topics':
$variables['head_title']['title'] = t('Course Topics');
break;
case 'view.group_events.page_group_events':
$variables['head_title']['title'] = t('Course Events');
break;
}
}
/**
* Implements hook_preprocess_page().
*/
function social_course_preprocess_page(array &$variables): void {
if (!isset($variables['node']) || !($variables['node'] instanceof NodeInterface)) {
return;
}
/** @var \Drupal\node\NodeInterface $node */
$node = $variables['node'];
// Display navigation block on the left side of course pages.
$material_types = social_course_get_material_types();
if (in_array($node->bundle(), $material_types)) {
if (!$variables['content_attributes'] instanceof Attribute) {
$variables['content_attributes'] = new Attribute();
}
$variables['content_attributes']->addClass('sidebar-left');
}
}
/**
* Implements hook_preprocess_page_title().
*/
function social_course_preprocess_page_title(array &$variables): void {
$group = _social_group_get_current_group();
if (!$group || !in_array($group->bundle(), [
'course_basic',
'course_advanced',
])) {
return;
}
switch (\Drupal::routeMatch()->getRouteName()) {
case 'view.group_information.page_group_about':
$variables['title'] = t('About');
break;
case 'entity.group.join':
$variables['title'] = t('Enroll in course %label', [
'%label' => $group->label(),
]);
break;
case 'view.group_topics.page_group_topics':
$variables['title'] = t('Course Topics');
break;
case 'view.group_events.page_group_events':
$variables['title'] = t('Course Events');
break;
}
}
/**
* Implements hook_preprocess_block().
*/
function social_course_preprocess_block(array &$variables): void {
switch ($variables['elements']['#plugin_id']) {
case 'views_block:upcoming_events-upcoming_events_group':
case 'views_block:latest_topics-group_topics_block':
case 'views_block:group_members-block_newest_members':
$group = _social_group_get_current_group();
if (!$group || !in_array($group->bundle(), [
'course_basic',
'course_advanced',
])) {
return;
}
$variables['subtitle'] = t('in the course');
break;
case 'course_material_navigation':
case 'course_navigation':
$nid = \Drupal::routeMatch()->getRawParameter('node');
$storage = \Drupal::entityTypeManager()->getStorage('node');
if (is_numeric($nid) && ($node = $storage->load($nid))) {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$course_wrapper->setCourseFromMaterial($node);
if ($course_wrapper->getCourse()) {
$section = $course_wrapper->getSectionFromMaterial($node);
$variables['course_url'] = $course_wrapper->getCourse()->toUrl();
$variables['section_number'] = $course_wrapper->getSectionNumber($section) + 1;
}
}
break;
case 'views_block:group_managers-block_list_managers':
$group = _social_group_get_current_group();
if ($group instanceof GroupInterface && in_array($group->bundle(), [
'course_basic',
'course_advanced',
])) {
$variables['label']['#markup'] = t('Course Leaders');
}
break;
}
}
/**
* Implements hook_views_pre_render().
*/
function social_course_views_pre_render(ViewExecutable $view): void {
$key = $view->id() . ':' . $view->current_display;
$group = _social_group_get_current_group();
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$bundles = $course_wrapper->getAvailableBundles();
if (!$group instanceof GroupInterface || !in_array($group->bundle(), $bundles)) {
return;
}
switch ($key) {
case 'upcoming_events:upcoming_events_group':
$view->empty['area_text_custom']->options['content'] = t('No upcoming events in this course');
break;
case 'latest_topics:group_topics_block':
$view->empty['area_text_custom']->options['content'] = t('No topics in this course');
break;
case 'group_members:block_newest_members':
$view->empty['area_text_custom']->options['content'] = t('No members in this course');
break;
case 'group_members:page_group_members':
$view->empty['area']->options['content']['value'] = t('No members in this course');
break;
case 'group_topics:page_group_topics':
$view->empty['area_text_custom']->options['content'] = t('No topics in this course');
break;
case 'group_events:page_group_events':
$view->empty['area_text_custom']->options['content'] = t('No events in this course');
break;
}
}
/**
* Implements hook_block_access().
*/
function social_course_block_access(Block $block, string $operation, AccountInterface $account): AccessResultInterface {
// There are some blocks that this module handles, we don't care about others.
switch ($block->getPluginId()) {
case 'social_page_title_block':
return social_course_social_page_title_block_access($block, $operation, $account);
case 'course_material_hero':
case 'course_material_navigation':
case 'course_navigation':
case 'course_material_pager':
return social_course_course_navigation_block_access($block, $operation, $account);
case 'system_breadcrumb_block':
return social_course_system_breadcrumb_block_access($block, $operation, $account);
case 'profile_hero_block':
case 'profile_statistic_block':
if ($operation === 'view') {
$request_path = $block->getVisibility()['request_path'];
$request_path['pages'] .= "\r\n/user/*/courses";
$block->setVisibilityConfig('request_path', $request_path);
return AccessResult::neutral();
}
break;
default:
return AccessResult::neutral();
}
return AccessResult::neutral();
}
/**
* Handles the extra access check for the social_page_title_block.
*/
function social_course_social_page_title_block_access(Block $block, string $operation, AccountInterface $account): AccessResultInterface {
// We only care about the view operation.
// @todo This is possibly redundant because then the route wouldn't match?
if ($operation !== 'view') {
return AccessResult::neutral();
}
$edit_routes = [
'entity.node.edit_form',
'entity.node.delete_form',
];
$current_route = \Drupal::routeMatch()->getRouteName();
// We ignore non-view routes.
if (in_array($current_route, $edit_routes)) {
return AccessResult::neutral();
}
$nid = \Drupal::routeMatch()->getRawParameter('node');
// If this route has no node then we can't handle it.
if (empty($nid) || !($node = Node::load($nid))) {
return AccessResult::neutral();
}
$material_types = social_course_get_material_types();
// We disallow the page title block for course materials.
if (in_array($node->getType(), $material_types)) {
return AccessResult::forbidden();
}
// We don't have an opinion on other node types.
return AccessResult::neutral();
}
/**
* Handles access restrictions for the course navigation blocks.
*/
function social_course_course_navigation_block_access(Block $block, string $operation, AccountInterface $account): AccessResultInterface {
// We only care about the view operation.
// @todo This is possibly redundant because then the route wouldn't match?
if ($operation !== 'view') {
return AccessResult::neutral();
}
$edit_routes = [
'entity.node.edit_form',
'entity.node.delete_form',
];
$current_route = \Drupal::routeMatch()->getRouteName();
// Hide the block on edit forms.
if (in_array($current_route, $edit_routes)) {
return AccessResult::forbidden();
}
// We don't care about these blocks on other routes.
return AccessResult::neutral();
}
/**
* Handles access restrictions for the system_breadcrumb_block.
*/
function social_course_system_breadcrumb_block_access(Block $block, string $operation, AccountInterface $account): AccessResultInterface {
// We only care about the view operation.
// @todo This is possibly redundant because then the route wouldn't match?
if ($operation !== 'view') {
return AccessResult::neutral();
}
// The breadcrumb block is hidden on course pages.
$course_types = ['course_basic', 'course_advanced'];
$group_type = \Drupal::routeMatch()->getParameter('group_type');
$group = _social_group_get_current_group();
if (($group instanceof GroupInterface && in_array($group->bundle(), $course_types))
|| $group_type instanceof GroupTypeInterface && in_array($group_type->id(), $course_types)) {
return AccessResult::forbidden();
}
// We don't care about other situations.
return AccessResult::neutral();
}
/**
* Implements hook_preprocess_HOOK().
*/
function social_course_preprocess_node(array &$variables): void {
$node = $variables['elements']['#node'];
if ($node->bundle() == 'course_section') {
// Load parent group (course).
$group_content = GroupRelationship::loadByEntity($node);
// Attach library.
$variables['#attached']['library'][] = 'social_course/social_course';
if (!$group_content) {
return;
}
$group = current($group_content)->getGroup();
$variables['parent_group'] = $group;
$account = \Drupal::currentUser();
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$course_wrapper->setCourse($group);
$storage = \Drupal::entityTypeManager()->getStorage('course_enrollment');
/** @var \Drupal\social_course\Entity\CourseEnrollmentInterface[] $entities */
$entities = $storage->loadByProperties([
'uid' => \Drupal::currentUser()->id(),
'gid' => $group->id(),
'sid' => $node->id(),
]);
if (!$entities) {
$variables['section_status'] = 'not-started';
$variables['allowed_start'] = $course_wrapper->sectionAccess($node, \Drupal::currentUser(), 'start')->isAllowed();
if (!$node->get('field_course_section_content')->first()) {
$variables['allowed_start'] = FALSE;
}
}
else {
foreach ($entities as $entity) {
if (!$entity->hasFinishedAttempt() && $entity->getStatus() === CourseEnrollmentInterface::IN_PROGRESS) {
$variables['section_status'] = 'in-progress';
$variables['section_current'] = $entity->get('mid')->target_id;
break;
}
elseif ($entity->getStatus() === CourseEnrollmentInterface::FINISHED) {
$variables['section_status'] = 'finished';
$materials = $course_wrapper->getMaterials($node);
if ($material = current($materials)) {
$variables['section_current'] = $material->id();
}
}
}
}
if (isset($variables['url']) && !$course_wrapper->sectionAccess($node, $account, 'view')->isAllowed()) {
unset($variables['url']);
}
$variables['section_number'] = $course_wrapper->getSectionNumber($node) + 1;
// Add node edit url for management.
if ($variables['view_mode'] === 'teaser') {
if ($node->access('update', $account)) {
$variables['node_edit_url'] = $node->toUrl('edit-form')->toString();
}
$variables['parts_count'] = count($course_wrapper->getMaterials($node));
$variables['parts_finished'] = count($course_wrapper->getFinishedMaterials($node, $account));
}
}
if ($node->bundle() == 'course_article' || $node->bundle() == 'course_video') {
// Load parent group (course).
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$course_wrapper->setCourseFromMaterial($node);
$course = $course_wrapper->getCourse();
if (!$course) {
return;
}
$variables['parent_course']['label'] = $course->label();
$variables['parent_course']['url'] = $course->toUrl();
}
}
/**
* Implements hook_node_access().
*/
function social_course_node_access(NodeInterface $node, string $operation, AccountInterface $account): AccessResultInterface {
if ($operation === 'view' && $node->id()) {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$section = $course_wrapper->getSectionFromMaterial($node);
if ($node->bundle() === 'course_section' && $group = $course_wrapper->setCourseFromSection($node)->getCourse()) {
return $group->getMember($account) ? AccessResult::allowed() : AccessResult::forbidden();
}
if ($section && $group = $course_wrapper->setCourseFromSection($section)->getCourse()) {
return $group->getMember($account) ? AccessResult::allowed() : AccessResult::forbidden();
}
}
if (
in_array($node->bundle(), SocialCourseNodePermissionProvider::COURSE_SECTION_CONTENT_BUNDLES) &&
// Operation "create" is handled by "social_course_node_create_access()".
in_array($operation, ['view', 'update', 'delete'])
) {
/** @var \Drupal\group\Entity\GroupInterface $group */
$group = _social_group_get_current_group();
if ($group instanceof GroupInterface) {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
if (in_array($group->bundle(), $course_wrapper->getAvailableBundles())) {
// Check if a user has a permission to create a content.
return AccessResult::allowedIf($group->hasPermission("$operation " . $node->bundle(), $account))
->cachePerUser()
->addCacheableDependency($node);
}
}
}
return AccessResult::neutral();
}
/**
* Implements hook_ENTITY_TYPE_create_access() for "node".
*/
function social_course_node_create_access(AccountInterface $account, array $context, string $entity_bundle): AccessResultInterface {
if (!in_array($entity_bundle, SocialCourseNodePermissionProvider::COURSE_SECTION_CONTENT_BUNDLES)) {
return AccessResult::neutral();
}
/** @var \Drupal\group\Entity\GroupInterface $group */
$group = _social_group_get_current_group();
if ($group instanceof GroupInterface) {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
if (in_array($group->bundle(), $course_wrapper->getAvailableBundles())) {
// Check if a user has a permission to create a content.
return AccessResult::allowedIf($group->hasPermission("create $entity_bundle", $account))
->cachePerUser();
}
}
return AccessResult::neutral();
}
/**
* Implements hook_entity_delete().
*/
function social_course_entity_delete(EntityInterface $entity): void {
switch ($entity->getEntityTypeId()) {
case 'group':
// Delete all course enrollments for the course that was deleted.
$storage = \Drupal::entityTypeManager()->getStorage('course_enrollment');
$entities = $storage->loadByProperties([
'gid' => $entity->id(),
]);
foreach ($entities as $entity) {
$entity->delete();
}
break;
case 'node':
/** @var \Drupal\node\NodeInterface $entity */
// Delete any enrollment entities for the section or material that was
// deleted.
$storage = \Drupal::entityTypeManager()->getStorage('course_enrollment');
foreach (['sid', 'mid'] as $column) {
$entities = $storage->loadByProperties([
$column => $entity->id(),
]);
foreach ($entities as $enrollment_entity) {
$enrollment_entity->delete();
}
}
// If this was a section then also remove the materials.
$is_section = $entity->bundle() === 'course_section';
// Guard against breaking of the section node type.
$has_materials = $entity->hasField('field_course_section_content');
if ($is_section && $has_materials) {
$section_content = $entity->get('field_course_section_content');
// Guard against the wrong field type being added.
if (!($section_content instanceof EntityReferenceFieldItemListInterface)) {
throw new \RuntimeException('The field used to store course section content should be an entity reference type field.');
}
foreach ($section_content->referencedEntities() as $section_material) {
$section_material->delete();
}
}
// If this was a material then also remove it as value for parts in a
// section.
if (in_array($entity->bundle(), social_course_get_material_types(), TRUE)) {
social_course_remove_material_from_courses((int) $entity->id());
}
break;
case 'user':
// Remove all course enrollments for the deleted user.
$storage = \Drupal::entityTypeManager()->getStorage('course_enrollment');
$entities = $storage->loadByProperties([
'uid' => $entity->id(),
]);
foreach ($entities as $entity) {
$entity->delete();
}
break;
}
}
/**
* Finds the course sections that a material is referenced in.
*
* @param int $material_id
* The id of the material to search for.
*
* @return int[]
* An array of node ids corresponding to the course section nodes that
* reference the material as part of their content.
*/
function social_course_find_sections_with_material(int $material_id): array {
// Find all course sections that reference this material.
// Stolen from: https://drupal.stackexchange.com/a/248054/2166
$sections = \Drupal::entityQueryAggregate('node')
->accessCheck(TRUE)
->condition('type', 'course_section')
->condition('field_course_section_content', $material_id)
->groupBy('nid')
->execute();
return array_column($sections, 'nid');
}
/**
* Ensures that a material is no longer referenced by any course sections.
*
* @param int $material_id
* The id of the material that should be removed from the sections.
*/
function social_course_remove_material_from_courses(int $material_id): void {
$section_ids = social_course_find_sections_with_material($material_id);
$sections = Node::loadMultiple($section_ids);
foreach ($sections as $section) {
$section_content = $section->get('field_course_section_content');
// Guard against the wrong field type being added.
if (!($section_content instanceof EntityReferenceFieldItemListInterface)) {
$bundle = $section->bundle();
throw new \RuntimeException("The field used to store course section content should be an entity reference type field for node type {${$bundle}}.");
}
/** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $section_content */
$materials = $section_content->getValue();
// Find the index of the material in this field.
$idx = array_search($material_id, array_column($materials, 'target_id'));
$section_content->removeItem((int) $idx);
$section->save();
}
}
/**
* Implements hook_entity_view().
*/
function social_course_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, string $view_mode): void {
if (!($entity->id() && $entity->getEntityTypeId() == 'node')) {
return;
}
if ($view_mode == 'full') {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$account = \Drupal::currentUser();
$storage = \Drupal::entityTypeManager()
->getStorage('course_enrollment');
/** @var \Drupal\node\NodeInterface $entity */
if ($entity->bundle() == 'course_section') {
$course_wrapper->setCourseFromSection($entity);
$materials = $course_wrapper->getMaterials($entity);
$material = current($materials);
if ($course_wrapper->getCourse() && !$course_wrapper->courseIsSequential() && $material) {
// Join user to course.
$exists = $storage->getQuery()
->accessCheck(TRUE)
->condition('gid', $course_wrapper->getCourse()->id())
->condition('sid', $entity->id())
->condition('mid', $material->id())
->condition('uid', $account->id())
->execute();
if (!$exists) {
$enrollment = $storage->create([
'gid' => $course_wrapper->getCourse()->id(),
'sid' => $entity->id(),
'mid' => $material->id(),
'status' => CourseEnrollmentInterface::IN_PROGRESS,
]);
$enrollment->save();
}
}
}
else {
$section = $course_wrapper->getSectionFromMaterial($entity);
if ($section) {
$course_wrapper->setCourseFromSection($section);
if ($course_wrapper->getCourse() && !$course_wrapper->courseIsSequential()) {
// Join user to course.
$exists = $storage->getQuery()
->accessCheck(TRUE)
->condition('gid', $course_wrapper->getCourse()->id())
->condition('sid', $section->id())
->condition('mid', $entity->id())
->condition('uid', $account->id())
->execute();
if (!$exists) {
$enrollment = $storage->create([
'gid' => $course_wrapper->getCourse()->id(),
'sid' => $section->id(),
'mid' => $entity->id(),
'status' => CourseEnrollmentInterface::IN_PROGRESS,
]);
$enrollment->save();
}
}
}
}
}
elseif ($view_mode == 'teaser' && $entity->bundle() == 'course_section') {
$build['#cache']['contexts'][] = 'user';
}
}
/**
* Implements hook_entity_base_field_info().
*/
function social_course_entity_base_field_info(EntityTypeInterface $entity_type): array {
$fields = [];
if ($entity_type->id() === 'group') {
$fields['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Published'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE)
->setDisplayOptions('view', [
'region' => 'hidden',
])
->setDisplayOptions('form', [
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
}
return $fields;
}
/**
* Implements hook_query_alter().
*/
function social_course_query_alter(AlterableInterface $query): void {
if (!$query instanceof SelectInterface) {
return;
}
$tables = $query->getTables();
if (!isset($tables['groups_field_data'])) {
return;
}
if (!$account = $query->getMetaData('account')) {
$account = \Drupal::currentUser();
}
else {
$account = \Drupal::currentUser();
}
/** @var \Drupal\social_course\Service\SocialCourseAccessService $access_service */
$access_service = \Drupal::service('social_course.access_service');
if ($access_service->hasFullAccess()) {
return;
}
if (!$account->hasPermission('view unpublished groups')) {
$alias = $tables['groups_field_data']['alias'];
$or = $query->orConditionGroup();
$or->condition("{$alias}.status", '1');
// If user has access to view own unpublished groups.
if ($account->hasPermission('view own unpublished groups')) {
$or->condition("{$alias}.uid", (string) $account->id());
}
$query->condition($or);
}
}
/**
* Implements hook_search_api_query_alter().
*/
function social_course_search_api_query_alter(QueryInterface &$query): void {
$account = \Drupal::currentUser();
// Skip if a user has global access or can manage all unpublished groups.
/** @var \Drupal\social_course\Service\SocialCourseAccessService $access_service */
$access_service = \Drupal::service('social_course.access_service');
if ($access_service->hasFullAccess() || $account->hasPermission('view unpublished groups')) {
return;
}
$index = $query->getIndex();
// Ignore when search index does not support group entity type.
if (!in_array('entity:group', $index->getDatasourceIds())) {
return;
}
$fields = $index->getFields();
$is_field = FALSE;
/** @var string $status_field */
foreach ($fields as $status_field => $field) {
if ($field->getPropertyPath() === 'status' && $field->getDatasourceId() === 'entity:group') {
$is_field = TRUE;
break;
}
}
if (!$is_field) {
return;
}
if ($account->hasPermission('view own unpublished groups')) {
$found = FALSE;
// Search of field for group owner ID.
/** @var string $user_field */
foreach ($fields as $user_field => $field) {
if ($field->getPropertyPath() === 'uid' && $field->getDatasourceId() === 'entity:group') {
$found = TRUE;
break;
}
}
// Allow seeing only the published group when the search index does not
// contain a field for the owner of the group.
if (!$found) {
return;
}
}
$condition_group = $query->createConditionGroup('AND')
->addCondition('search_api_datasource', 'entity:group');
if (isset($status_field)) {
if (isset($user_field)) {
// Add own unpublished groups to search results when the search index
// contains a field for the owner of the group.
$condition_group->addConditionGroup(
$query->createConditionGroup('OR')
->addCondition($status_field, TRUE)
->addCondition($user_field, (string) $account->id())
);
}
else {
// Select only published groups because the search index does not contain
// a field for the owner of the group so own groups can not be linked to
// the current user.
$condition_group->addCondition($status_field, TRUE);
}
}
$query->addConditionGroup(
$query->createConditionGroup('OR')
->addConditionGroup($condition_group)
->addCondition('search_api_datasource', 'entity:group', '<>')
);
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function social_course_group_presave(GroupInterface $entity): void {
if ($entity->hasField('field_course_opening_date') && $entity->hasField('field_course_opening_status')) {
if ($date = $entity->get('field_course_opening_date')->value) {
$status = $date <= gmdate("Y-m-d\TH:i:00");
$entity->set('field_course_opening_status', $status);
}
else {
$entity->set('field_course_opening_status', TRUE);
}
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for "node".
*/
function social_course_node_presave(NodeInterface $node): void {
// Remove all "in progress" attempts with this material,
// otherwise user will be redirected to the orphaned section step.
if ($node->hasField($name = 'field_course_section_content')) {
if ($node->isNew()) {
// Nothing to do for a new node.
return;
}
/** @var \Drupal\node\NodeInterface $original */
$original = $node->original;
$removed_materials = array_diff(
array_column($original->get($name)->getValue(), 'target_id'),
array_column($node->get($name)->getValue(), 'target_id'),
);
if ($removed_materials) {
$eids = \Drupal::entityQuery('course_enrollment')
->accessCheck(FALSE)
->condition('sid', $node->id())
->condition('mid', $removed_materials, 'IN')
->condition('status', CourseEnrollmentInterface::IN_PROGRESS)
->execute();
if ($eids) {
// Load all enrollments and delete.
$storage = \Drupal::entityTypeManager()->getStorage('course_enrollment');
$storage->delete($storage->loadMultiple($eids));
// Clear cache for the current section.
Cache::invalidateTags($node->getCacheTags());
}
}
}
}
/**
* Implements hook_cron().
*/
function social_course_cron(): void {
/** @var \Drupal\social_course\CourseWrapperInterface $course_wrapper */
$course_wrapper = \Drupal::service('social_course.course_wrapper');
$query = \Drupal::entityQuery('group')
->accessCheck(TRUE)
->condition('type', $course_wrapper->getAvailableBundles(), 'IN')
->condition('field_course_opening_status', FALSE)
->condition('field_course_opening_date', gmdate("Y-m-d\TH:i:00"), '<=')
->range(0, 20);
if ($results = $query->execute()) {
$storage = \Drupal::entityTypeManager()->getStorage('group');
if ($entities = $storage->loadMultiple($results)) {
/** @var \Drupal\group\Entity\GroupInterface[] $entities */
foreach ($entities as $entity) {
if (!$entity->get('field_course_opening_status')->value) {
// @todo send notifications.
}
$entity->save();
}
}
}
}
/**
* Implements hook_social_core_compatible_content_forms_alter().
*/
function social_course_social_core_compatible_content_forms_alter(array &$compatible_content_type_forms): void {
$compatible_content_type_forms[] = 'node_course_section_form';
$compatible_content_type_forms[] = 'node_course_section_edit_form';
}
/**
* Implements hook_module_implements_alter().
*/
function social_course_module_implements_alter(array &$implementations, string $hook): void {
if ($hook === 'form_alter') {
$group = $implementations['social_course'];
unset($implementations['social_course']);
$implementations['social_course'] = $group;
}
}
/**
* Implements hook_social_email_broadcast_notifications_alter().
*/
function social_course_social_email_broadcast_notifications_alter(array &$items): void {
$items['community_updates']['bulk_mailing'][] = [
'name' => 'course_bulk_mailing',
'label' => t('Courses: Course managers can update me on courses I belong to'),
'entity_type' => [
'group' => \Drupal::service('social_course.course_wrapper')->getAvailableBundles(),
],
];
}
/**
* Get destination url of the requested course.
*
* @return string
* The destination url which will be used in one-time-login-url.
*/
function _social_course_get_destination() {
$url = '';
$request = \Drupal::request();
if ($request->query->has('destination')) {
$destination = $request->query->get('destination');
if (strpos($destination, 'requested-membership') !== FALSE) {
$options = ['query' => ['destination' => $destination]];
$url = '/login' . Url::fromUri('internal:', $options)
->toString();
}
if (strpos($destination, 'quickjoin') !== FALSE) {
// Get's the last value of the destination.
$destination = explode('?', $destination);
$destination = array_pop($destination);
$options = ['query' => ['destination' => trim($destination)]];
$url = '/login' . Url::fromUri('internal:', $options)
->toString();
}
}
return $url;
}
/**
* Implements hook_views_query_alter().
*/
function social_course_views_query_alter(ViewExecutable $view, QueryPluginBase $query): void {
if ($view->id() === 'newest_groups') {
$query->addWhere(1, 'groups_field_data.type', 'course_%', 'NOT LIKE');
}
}
/**
* Implements hook_mail_alter().
*
* Overrides user emails with proper token replacements.
*/
function social_course_mail_alter(&$message) {
if ($message['module'] == 'user') {
$token_service = \Drupal::token();
$token_options = [
'langcode' => $message['langcode'],
'callback' => '_social_course_user_mail_tokens',
'clear' => TRUE,
];
$mail_config = \Drupal::config('user.mail');
$variables = [
'user' => $message['params']['account'],
];
$message['body'][0] = $token_service
->replace($mail_config
->get($message['key'] . '.body'), $variables, $token_options);
}
}
/**
* The callback to properly add token replacements for user mails.
*
* This overrides user_mail_tokens() from Drupal core
* by appending a custom destination URL to tue one-time-login link.
*
* @param array $replacements
* An associative array variable containing mappings from token names to
* values (for use with strtr()).
* @param array $data
* An associative array of token replacement values. If the 'user' element
* exists, it must contain a user account object with the following
* properties:
* - login: The UNIX timestamp of the user's last login.
* - pass: The hashed account login password.
* @param array $options
* A keyed array of settings and flags to control the token replacement
* process. See \Drupal\Core\Utility\Token::replace().
*/
function _social_course_user_mail_tokens(&$replacements, $data, $options) {
if (isset($data['user'])) {
$replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options) . _social_course_get_destination();
$replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options);
}
}
/**
* Helper callback to replace deprecated group permissions.
*
* @param string $group_name
* A machine name of group type.
*/
function _social_course_update_group_permissions(string $group_name): void {
/** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
$group_type = \Drupal::entityTypeManager()
->getStorage('group_type')
->load($group_name);
/** @var \Drupal\group\Access\GroupPermissionHandler $service */
$service = \Drupal::service('group.permissions');
$available_permissions = array_keys($service->getPermissionsByGroupType($group_type));
$group_roles = $group_type->getRoles();
foreach ($group_roles as $group_role) {
// Group admins don't have permissions list.
if ($group_role->isAdmin()) {
continue;
}
$broken_permissions = array_diff($group_role->getPermissions(), $available_permissions);
foreach ($broken_permissions as $broken_permission) {
// Group permission like "view gnode:event content" should be replaced
// with "view gnode:event relationship".
if (str_ends_with($broken_permission, ' content')) {
$group_role->grantPermission(str_replace('content', 'relationship', $broken_permission));
$group_role->revokePermission($broken_permission);
}
}
$group_role->save();
}
}
/**
* Implements hook_social_core_add_form_title_override().
*
* Override page title for the given routes.
*/
function social_course_social_core_add_form_title_override(): array {
return [
'social_course.course_add' => [
'label' => 'Course',
],
];
}
