Adiciona submódulo ldap_courses_sync para sincronização de cursos via LDAP

Cria o módulo modules/ldap_courses_sync/ seguindo o mesmo padrão do
ldap_research_groups_sync, com bundle `course`, campos field_course_*
e field_user_courses, sincronização de membros via atributo LDAP, e
aba "Courses Sync" no painel unificado.

Também registra `courses` no módulo pai (routing, UnifiedAccessRulesForm
e GlobalAccessRuleForm) para suporte a access rules unificadas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 13:49:47 -03:00
parent 7e338677a3
commit 4a65619c27
40 changed files with 3597 additions and 1 deletions

View File

@@ -23,4 +23,4 @@ ldap_groups_sync.access_rule_form:
requirements:
_permission: 'administer ldap groups sync'
rule_index: 'new|\d+'
group_type: 'departments|research_groups'
group_type: 'departments|research_groups|courses'

View File

@@ -0,0 +1,102 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.course.field_course_code
- field.field.group.course.field_course_coord
- field.field.group.course.field_course_coord_assoc
- field.field.group.course.field_course_mail
- field.field.group.course.field_course_phone
- field.field.group.course.field_course_department
- group.type.course
module:
- path
id: group.course.default
targetEntityType: group
bundle: course
mode: default
content:
field_course_code:
type: string_textfield
weight: 122
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_course_mail:
type: email_default
weight: 125
region: content
settings:
placeholder: ''
size: 60
third_party_settings: { }
field_course_phone:
type: string_textfield
weight: 124
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
field_course_coord:
type: entity_reference_autocomplete
weight: 126
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_course_coord_assoc:
type: entity_reference_autocomplete
weight: 127
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
field_course_department:
type: entity_reference_autocomplete
weight: 25
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
label:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
langcode:
type: language_select
weight: 2
region: content
settings:
include_locked: true
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
weight: 120
region: content
settings:
display_label: true
third_party_settings: { }
hidden:
uid: true

View File

@@ -0,0 +1,81 @@
langcode: pt-br
status: true
dependencies:
config:
- field.field.group.course.field_course_code
- field.field.group.course.field_course_coord
- field.field.group.course.field_course_coord_assoc
- field.field.group.course.field_course_mail
- field.field.group.course.field_course_phone
- field.field.group.course.field_course_department
- group.type.course
module:
- options
id: group.course.default
targetEntityType: group
bundle: course
mode: default
content:
field_course_code:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -3
region: content
field_course_mail:
type: basic_string
label: above
settings: { }
third_party_settings: { }
weight: 0
region: content
field_course_phone:
type: string
label: above
settings:
link_to_entity: false
third_party_settings: { }
weight: -1
region: content
field_course_coord:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 1
region: content
field_course_coord_assoc:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 2
region: content
field_course_department:
type: entity_reference_label
label: above
settings:
link: true
third_party_settings: { }
weight: 25
region: content
label:
type: string
label: hidden
settings:
link_to_entity: false
third_party_settings: { }
weight: -5
region: content
hidden:
changed: true
created: true
entity_print_view_epub: true
entity_print_view_pdf: true
entity_print_view_word_docx: true
langcode: true
uid: true

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_course_code
- group.type.course
id: group.course.field_course_code
field_name: field_course_code
entity_type: group
bundle: course
label: 'Código'
description: 'Código do curso (chave de sincronização LDAP)'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_course_coord
- group.type.course
module:
- user
id: group.course.field_course_coord
field_name: field_course_coord
entity_type: group
bundle: course
label: Coordenador
description: 'Usuário responsável pela coordenação do curso'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:user'
handler_settings:
target_bundles: null
sort:
field: _none
direction: ASC
auto_create: false
filter:
type: _none
include_anonymous: false
field_type: entity_reference

View File

@@ -0,0 +1,30 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_course_coord_assoc
- group.type.course
module:
- user
id: group.course.field_course_coord_assoc
field_name: field_course_coord_assoc
entity_type: group
bundle: course
label: 'Coordenador Associado'
description: 'Usuário responsável pela coordenação associada do curso'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:user'
handler_settings:
target_bundles: null
sort:
field: _none
direction: ASC
auto_create: false
filter:
type: _none
include_anonymous: false
field_type: entity_reference

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_course_department
- group.type.department
- group.type.course
id: group.course.field_course_department
field_name: field_course_department
entity_type: group
bundle: course
label: Departamento
description: 'Departamento ao qual este curso está vinculado'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:group'
handler_settings:
target_bundles:
department: department
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_course_mail
- group.type.course
id: group.course.field_course_mail
field_name: field_course_mail
entity_type: group
bundle: course
label: 'E-mail'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: email

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.group.field_course_phone
- group.type.course
module:
- telephone
id: group.course.field_course_phone
field_name: field_course_phone
entity_type: group
bundle: course
label: 'Telefone'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: telephone

View File

@@ -0,0 +1,28 @@
langcode: pt-br
status: true
dependencies:
config:
- field.storage.user.field_user_courses
module:
- user
id: user.user.field_user_courses
field_name: field_user_courses
entity_type: user
bundle: user
label: 'Cursos'
description: 'Cursos do usuário sincronizados do LDAP'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:group'
handler_settings:
target_bundles:
course: course
sort:
field: _none
direction: ASC
auto_create: false
auto_create_bundle: ''
field_type: entity_reference

View File

@@ -0,0 +1,20 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_course_code
field_name: field_course_code
entity_type: group
type: string
settings:
max_length: 255
case_sensitive: false
is_ascii: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_course_coord
field_name: field_course_coord
entity_type: group
type: entity_reference
settings:
target_type: user
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: group.field_course_coord_assoc
field_name: field_course_coord_assoc
entity_type: group
type: entity_reference
settings:
target_type: user
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_course_department
field_name: field_course_department
entity_type: group
type: entity_reference
settings:
target_type: group
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
module:
- group
id: group.field_course_mail
field_name: field_course_mail
entity_type: group
type: email
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,18 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- telephone
id: group.field_course_phone
field_name: field_course_phone
entity_type: group
type: telephone
settings: { }
module: telephone
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,19 @@
langcode: pt-br
status: true
dependencies:
module:
- group
- user
id: user.field_user_courses
field_name: field_user_courses
entity_type: user
type: entity_reference
settings:
target_type: group
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,37 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.course
id: course-admin
label: Admin
weight: 100
admin: true
scope: individual
global_role: null
group_type: course
permissions:
- administer members
- delete group
- edit group
- leave group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update any group_node:article entity
- update any group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity

View File

@@ -0,0 +1,38 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.course
- user.role.administrator
id: course-admin_in
label: Administrador
weight: 102
admin: true
scope: insider
global_role: administrator
group_type: course
permissions:
- administer members
- delete group
- edit group
- leave group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update any group_node:article entity
- update any group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity

View File

@@ -0,0 +1,38 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.course
- user.role.administrator
id: course-admin_out
label: Administrador
weight: 101
admin: true
scope: outsider
global_role: administrator
group_type: course
permissions:
- administer members
- delete group
- edit group
- join group
- view group
- view unpublished group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete any group_node:article entity
- delete any group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update any group_node:article entity
- update any group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity
- view unpublished group_node:article entity
- view unpublished group_node:page entity

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.course
- user.role.anonymous
id: course-anonymous
label: 'Anônimo'
weight: -102
admin: false
scope: outsider
global_role: anonymous
group_type: course
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,27 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.course
- user.role.authenticated
id: course-member
label: Member
weight: -100
admin: false
scope: insider
global_role: authenticated
group_type: course
permissions:
- view group
- access group_media overview
- access group_node overview
- create group_node:article entity
- create group_node:page entity
- delete own group_node:article entity
- delete own group_node:page entity
- update own group_node:article entity
- update own group_node:page entity
- view group_node:article entity
- view group_node:page entity
- view own unpublished group_node:article entity
- view own unpublished group_node:page entity

View File

@@ -0,0 +1,17 @@
langcode: pt-br
status: true
dependencies:
config:
- group.type.course
- user.role.authenticated
id: course-outsider
label: Outsider
weight: -101
admin: false
scope: outsider
global_role: authenticated
group_type: course
permissions:
- view group
- view group_node:article entity
- view group_node:page entity

View File

@@ -0,0 +1,10 @@
langcode: pt-br
status: true
dependencies: { }
id: course
label: 'Curso'
description: ''
new_revision: true
creator_membership: true
creator_wizard: true
creator_roles: { }

View File

@@ -0,0 +1,73 @@
/**
* Role mapping table styles
*/
/* Limita a largura da tabela e permite scroll horizontal se necessário */
.role-mappings-table {
max-width: 100%;
table-layout: fixed;
}
/* Define larguras fixas para cada coluna */
.role-mappings-table th:nth-child(1),
.role-mappings-table td:nth-child(1) {
width: 20%;
min-width: 150px;
}
.role-mappings-table th:nth-child(2),
.role-mappings-table td:nth-child(2) {
width: 15%;
min-width: 130px;
}
.role-mappings-table th:nth-child(3),
.role-mappings-table td:nth-child(3) {
width: 25%;
min-width: 180px;
}
.role-mappings-table th:nth-child(4),
.role-mappings-table td:nth-child(4) {
width: 30%;
min-width: 200px;
}
.role-mappings-table th:nth-child(5),
.role-mappings-table td:nth-child(5) {
width: 10%;
min-width: 80px;
text-align: center;
}
/* Estilo dos selects para limitar largura e truncar texto */
.role-mapping-field-select {
max-width: 100%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Estilo para options dentro dos selects - mostra texto completo no dropdown */
.role-mapping-field-select option {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
/* Estilo do textarea */
.role-mapping-values-textarea {
max-width: 100%;
width: 100%;
min-height: 50px;
}
/* Responsividade: em telas menores, permite scroll horizontal */
@media (max-width: 1200px) {
.role-mappings-table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}

View File

@@ -0,0 +1,13 @@
name: LDAP Courses Sync
type: module
description: 'Sincroniza cursos do servidor LDAP com grupos via cron'
version: '1.0.0'
core_version_requirement: ^11
package: Custom
dependencies:
- ldap_groups_sync:ldap_groups_sync
- drupal:options
- drupal:telephone
- group:group
- ldap:ldap_servers
- site_tools

View File

@@ -0,0 +1,23 @@
<?php
/**
* @file
* Arquivo de instalação para ldap_courses_sync.
*/
/**
* Implements hook_install().
*/
function ldap_courses_sync_install() {
\Drupal::messenger()->addWarning(t('Please ensure your group type has the required fields: field_course_code, field_course_phone, field_course_mail, and field_course_department (for department reference).'));
}
/**
* Implements hook_uninstall().
*/
function ldap_courses_sync_uninstall() {
// Remove configurações
\Drupal::configFactory()->getEditable('ldap_courses_sync.settings')->delete();
\Drupal::messenger()->addStatus(t('LDAP Courses Sync module uninstalled.'));
}

View File

@@ -0,0 +1,5 @@
role_mapping_styles:
version: 1.x
css:
theme:
css/role-mapping.css: {}

View File

@@ -0,0 +1 @@
# Menu entry removed: the parent module ldap_groups_sync provides the unified menu entry.

View File

@@ -0,0 +1,5 @@
ldap_courses_sync.tab.config:
title: 'Courses Sync'
route_name: ldap_courses_sync.config
base_route: ldap_groups_sync.config
weight: 30

View File

@@ -0,0 +1,236 @@
<?php
/**
* @file
* Module to synchronize courses from LDAP to groups.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\ldap_courses_sync\LdapCoursesSync;
/**
* Implements hook_help().
*/
function ldap_courses_sync_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.ldap_courses_sync':
return '<p>' . t('This module synchronizes courses from an LDAP server to groups through cron.') . '</p>';
}
}
/**
* Implements hook_cron().
*/
function ldap_courses_sync_cron() {
// Check if module is properly configured
$config = \Drupal::config('ldap_courses_sync.settings');
$ldap_server_id = $config->get('ldap_server_id');
$ldap_query_id = $config->get('ldap_query_id');
// Validate LDAP server
try {
$entity_type_manager = \Drupal::entityTypeManager();
$server_storage = $entity_type_manager->getStorage('ldap_server');
$server = $server_storage->load($ldap_server_id);
if (!$server || !$server->get('status')) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: LDAP server "@server_id" not found or inactive. Configure at /admin/config/local-modules/ldap-courses-sync', [
'@server_id' => $ldap_server_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking LDAP server: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Validate LDAP query
try {
if ($entity_type_manager->hasDefinition('ldap_query_entity')) {
$query_storage = $entity_type_manager->getStorage('ldap_query_entity');
$query = $query_storage->load($ldap_query_id);
if (!$query || !$query->get('status')) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: LDAP query "@query_id" not found or inactive. Configure at /admin/config/local-modules/ldap-courses-sync', [
'@query_id' => $ldap_query_id,
]);
return;
}
}
else {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: ldap_query module not available.');
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking LDAP query: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Validate group type
try {
$group_type_id = $config->get('group_type_id');
$group_type_storage = $entity_type_manager->getStorage('group_type');
$group_type = $group_type_storage->load($group_type_id);
if (!$group_type) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: group type "@type_id" not found. Configure at /admin/config/local-modules/ldap-courses-sync', [
'@type_id' => $group_type_id,
]);
return;
}
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->warning('Synchronization cancelled: error checking group type: @message', [
'@message' => $e->getMessage(),
]);
return;
}
// Get LDAP synchronization service
$ldap_sync = \Drupal::service('ldap_courses_sync.sync');
// Execute courses synchronization
try {
$ldap_sync->syncCourses();
\Drupal::logger('ldap_courses_sync')->info('Courses synchronization executed successfully.');
}
catch (\Exception $e) {
\Drupal::logger('ldap_courses_sync')->error('Error in courses synchronization: @message', [
'@message' => $e->getMessage(),
]);
}
}
/**
* Implements hook_entity_access().
*
* Evaluates access rules configured in the module settings for view, update
* and delete operations on existing entities.
*/
function ldap_courses_sync_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
// Skip group-related entities to avoid conflicts with the group module.
$skip_types = ['group', 'group_content', 'group_relationship', 'group_role', 'group_type'];
if (in_array($entity->getEntityTypeId(), $skip_types, TRUE)) {
return AccessResult::neutral();
}
return \Drupal::service('ldap_courses_sync.access_rules')
->checkAccess($entity, $operation, $account);
}
/**
* Implements hook_entity_create_access().
*
* Evaluates access rules configured in the module settings for create
* operations on new entities.
*/
function ldap_courses_sync_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
$entity_type_id = $context['entity_type_id'] ?? '';
// Skip group-related entities.
$skip_types = ['group', 'group_content', 'group_relationship', 'group_role', 'group_type'];
if (in_array($entity_type_id, $skip_types, TRUE)) {
return AccessResult::neutral();
}
return \Drupal::service('ldap_courses_sync.access_rules')
->checkCreateAccess($account, $entity_type_id, $entity_bundle ?? '');
}
/**
* Implements hook_entity_field_access().
*
* Denies edit access to fields managed by the LDAP sync in forms and APIs
* (REST, JSON:API). Programmatic saves are not affected.
*
* Protected user fields:
* - field_user_courses: multi-value reference to the courses the user belongs
* to, populated by our sync. Protecting this field prevents the validation
* error "This entity cannot be referenced" that occurs when the widget
* tries to validate the referenced group against the current user's access.
*/
function ldap_courses_sync_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
$protected_user_fields = [
'field_user_courses',
];
if (
$field_definition->getTargetEntityTypeId() === 'user' &&
in_array($field_definition->getName(), $protected_user_fields, TRUE) &&
$operation === 'edit'
) {
// Sempre nega edição via UI/API, inclusive para o uid 1.
// Saves programáticos (ldap_user, drush) não passam por este hook.
return AccessResult::forbidden('This field is managed exclusively by LDAP synchronization.');
}
return AccessResult::neutral();
}
/**
* Implements hook_user_presave().
*
* Protects fields managed by the LDAP sync from unauthorized programmatic
* changes on existing users. Allows:
* - Saves during authorized LDAP sync (LdapCoursesSync::$syncing TRUE)
* - New user creation (initial provisioning via ldap_user)
* - Saves by users with the 'edit ldap managed user course fields' permission
*/
function ldap_courses_sync_user_presave(\Drupal\user\UserInterface $user) {
// Permite durante qualquer sync LDAP autorizado.
if (LdapCoursesSync::isSyncing()) {
return;
}
// Permite na criação inicial do usuário (primeiro login LDAP).
if ($user->isNew()) {
return;
}
// Permite se o usuário atual tem a permissão de bypass.
if (\Drupal::currentUser()->hasPermission('edit ldap managed user course fields')) {
return;
}
$original = $user->original;
if ($original === NULL) {
return;
}
// Fields managed by LDAP that cannot be changed externally.
$protected_fields = [
'field_user_courses',
];
foreach ($protected_fields as $field_name) {
if (!$user->hasField($field_name)) {
continue;
}
$original_value = $original->get($field_name)->getValue();
$new_value = $user->get($field_name)->getValue();
if ($original_value !== $new_value) {
$user->set($field_name, $original_value);
\Drupal::logger('ldap_courses_sync')->warning(
'Unauthorized attempt to change @field on user @username was blocked. Use LDAP synchronization to update this field.',
[
'@field' => $field_name,
'@username' => $user->getAccountName(),
]
);
}
}
}

View File

@@ -0,0 +1,9 @@
administer ldap courses sync:
title: 'Administer LDAP Courses Sync'
description: 'Configure the LDAP courses synchronization.'
restrict access: true
edit ldap managed user course fields:
title: 'Edit LDAP-managed course fields on users'
description: 'Allows manually editing fields such as field_user_courses that are normally managed by LDAP synchronization. Use only in emergencies.'
restrict access: true

View File

@@ -0,0 +1,7 @@
ldap_courses_sync.config:
path: '/admin/config/local-modules/ldap-courses-sync'
defaults:
_form: '\Drupal\ldap_courses_sync\Form\LdapCoursesSyncConfigForm'
_title: 'LDAP Courses Sync'
requirements:
_permission: 'administer ldap courses sync'

View File

@@ -0,0 +1,8 @@
services:
ldap_courses_sync.sync:
class: Drupal\ldap_courses_sync\LdapCoursesSync
arguments: ['@entity_type.manager', '@config.factory', '@logger.factory', '@ldap.bridge']
ldap_courses_sync.access_rules:
class: Drupal\ldap_groups_sync\GroupAccessRulesService
arguments: ['@config.factory', '@entity_type.manager', 'ldap_courses_sync.settings']

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\ldap_courses_sync\Plugin\EntityReferenceSelection;
use Drupal\group\Plugin\EntityReferenceSelection\GroupSelection;
/**
* Plugin for course entity reference selection.
*
* Restricts selection to the 'course' group type.
*
* @EntityReferenceSelection(
* id = "default:course",
* label = @Translation("Course selection"),
* entity_types = {"group"},
* group = "default",
* weight = 1
* )
*/
class CourseSelection extends GroupSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$query->condition('type', 'course');
return $query;
}
}

View File

@@ -0,0 +1,54 @@
# Portuguese (Brazil) translation for LDAP Courses Sync module
# Copyright (c) 2026
# This file is distributed under the same license as the LDAP Courses Sync module.
#
msgid ""
msgstr ""
"Project-Id-Version: LDAP Courses Sync 1.0.0\n"
"POT-Creation-Date: 2026-03-02 00:00+0000\n"
"PO-Revision-Date: 2026-03-02 00:00+0000\n"
"Language-Team: Portuguese (Brazil)\n"
"Language: pt-br\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: ldap_courses_sync.routing.yml
msgid "LDAP Courses Sync"
msgstr "Sincronização de Cursos LDAP"
#: ldap_courses_sync.links.task.yml
msgid "Courses Sync"
msgstr "Sincronização de Cursos"
#: ldap_courses_sync.module
msgid "This module synchronizes courses from an LDAP server to groups through cron."
msgstr "Este módulo sincroniza cursos de um servidor LDAP para grupos através do cron."
#: ldap_courses_sync.install
msgid "Please ensure your group type has the required fields: field_course_code, field_course_phone, field_course_mail, and field_course_department (for department reference)."
msgstr "Certifique-se de que o tipo de grupo possui os campos obrigatórios: field_course_code, field_course_phone, field_course_mail e field_course_department (para referência de departamento)."
msgid "LDAP Courses Sync module uninstalled."
msgstr "Módulo LDAP Courses Sync desinstalado."
#: src/Form/LdapCoursesSyncConfigForm.php
msgid "Select the group type where courses will be synchronized. <br><strong>Required fields:</strong> field_course_code, field_course_phone, field_course_mail"
msgstr "Selecione o tipo de grupo onde os cursos serão sincronizados. <br><strong>Campos obrigatórios:</strong> field_course_code, field_course_phone, field_course_mail"
msgid "The default \"course\" group type is not installed."
msgstr "O tipo de grupo padrão \"course\" não está instalado."
msgid "The button above will create:<ul>\n <li>Group type \"course\" with all required fields</li>\n <li>User fields for course synchronization</li>\n <li>Default group roles and displays</li>\n </ul>Or you can <a href=\"@url\">create a custom group type</a> manually."
msgstr "O botão acima irá criar:<ul>\n <li>Tipo de grupo \"course\" com todos os campos necessários</li>\n <li>Campos de usuário para sincronização de cursos</li>\n <li>Papéis de grupo padrão e exibições</li>\n </ul>Ou você pode <a href=\"@url\">criar um tipo de grupo personalizado</a> manualmente."
msgid "Select the LDAP server configured for courses synchronization."
msgstr "Selecione o servidor LDAP configurado para sincronização de cursos."
msgid "Select the LDAP query that will be used to search for courses."
msgstr "Selecione a query LDAP que será usada para buscar cursos."
msgid "Synchronize Courses"
msgstr "Sincronizar Cursos"

View File

@@ -102,6 +102,9 @@ class GlobalAccessRuleForm extends AccessRuleFormBase {
if ($this->moduleHandler->moduleExists('ldap_research_groups_sync')) {
$options['research_groups'] = $this->t('Research Groups');
}
if ($this->moduleHandler->moduleExists('ldap_courses_sync')) {
$options['courses'] = $this->t('Courses');
}
$form['group_type'] = [
'#type' => 'select',
@@ -131,6 +134,7 @@ class GlobalAccessRuleForm extends AccessRuleFormBase {
protected function getConfigName(): string {
return match($this->groupType) {
'research_groups' => 'ldap_research_groups_sync.settings',
'courses' => 'ldap_courses_sync.settings',
default => 'ldap_departments_sync.settings',
};
}
@@ -141,6 +145,7 @@ class GlobalAccessRuleForm extends AccessRuleFormBase {
protected function getDefaultGroupTypeId(): string {
return match($this->groupType) {
'research_groups' => 'research_group',
'courses' => 'course',
default => 'departments',
};
}

View File

@@ -33,6 +33,11 @@ class UnifiedAccessRulesForm extends FormBase {
'module' => 'ldap_research_groups_sync',
'label' => 'Research Groups',
],
'courses' => [
'config' => 'ldap_courses_sync.settings',
'module' => 'ldap_courses_sync',
'label' => 'Courses',
],
];
/**