From ce73a7ebd8fa76345fba3be57dea04b8a050fcf5 Mon Sep 17 00:00:00 2001 From: christianbeeznst Date: Wed, 6 Mar 2024 21:09:16 -0500 Subject: [PATCH 1/3] User: Add deleting users in 2 steps - refs #5097 --- assets/css/app.scss | 37 +++ .../admin/access_url_add_users_to_url.php | 4 +- .../admin/access_url_check_user_session.php | 2 +- .../admin/dashboard_add_users_to_user.php | 10 +- public/main/admin/user_edit.php | 6 +- public/main/admin/user_list.php | 262 ++++++++++++++---- public/main/admin/user_list_consent.php | 2 + public/main/cron/request_removal_reminder.php | 8 +- public/main/inc/ajax/exercise.ajax.php | 1 + public/main/inc/lib/SkillModel.php | 2 +- public/main/inc/lib/TicketManager.php | 3 +- ...ss_url_edit_users_to_url_functions.lib.php | 2 +- public/main/inc/lib/course.lib.php | 9 +- public/main/inc/lib/exercise.lib.php | 2 + public/main/inc/lib/groupmanager.lib.php | 2 +- public/main/inc/lib/myspace.lib.php | 6 +- public/main/inc/lib/online.inc.php | 10 +- public/main/inc/lib/sessionmanager.lib.php | 13 +- public/main/inc/lib/statistics.lib.php | 23 +- public/main/inc/lib/sub_language.class.php | 4 +- public/main/inc/lib/tracking.lib.php | 2 +- public/main/inc/lib/urlmanager.lib.php | 4 +- public/main/inc/lib/usergroup.lib.php | 5 +- public/main/inc/lib/usermanager.lib.php | 70 ++--- .../inc/lib/zombie/zombie_manager.class.php | 1 + public/main/user/add_users_to_session.php | 23 +- public/main/user/resume_session.php | 4 +- public/main/user/subscribe_user.php | 10 +- public/main/user/user.php | 3 +- public/main/user/user_export.php | 7 +- .../Controller/SecurityController.php | 17 +- src/CoreBundle/Entity/User.php | 16 +- .../Schema/V200/Version20240306204200.php | 34 +++ .../Repository/Node/UserRepository.php | 33 ++- 34 files changed, 456 insertions(+), 181 deletions(-) create mode 100644 src/CoreBundle/Migrations/Schema/V200/Version20240306204200.php diff --git a/assets/css/app.scss b/assets/css/app.scss index 8094606fb8..fd608ef8a6 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -385,6 +385,43 @@ cursor: pointer; } +.users-list { + .nav-tabs { + display: flex; + list-style-type: none; + padding: 0; + border-bottom: 1px solid #ddd; + margin: 0 0 20px; + } + + .nav-tabs .nav-item { + margin-bottom: -1px; + } + + .nav-tabs .nav-link { + display: block; + padding: 0.5rem 1rem; + margin-right: 0.1rem; + background: #f8f8f8; + border: 1px solid #ddd; + border-radius: 0.25rem 0.25rem 0 0; + text-decoration: none; + color: #555; + } + + .nav-tabs .nav-link:hover { + background-color: #e9ecef; + } + + .nav-tabs .nav-link.active { + color: #495057; + background-color: #fff; + border-color: #ddd #ddd #fff; + border-bottom-color: transparent; + font-weight: bolder; + } +} + //@import 'primevue-md-light-indigo/theme.css'; //@import '~primevue/resources/primevue.min.css'; //@import '~primeflex/primeflex.css'; diff --git a/public/main/admin/access_url_add_users_to_url.php b/public/main/admin/access_url_add_users_to_url.php index fafce1528b..4883c6eb17 100644 --- a/public/main/admin/access_url_add_users_to_url.php +++ b/public/main/admin/access_url_add_users_to_url.php @@ -66,7 +66,7 @@ if ($_POST['form_sent']) { /* Display GUI */ if (empty($first_letter_user)) { - $sql = "SELECT count(*) as nb_users FROM $tbl_user"; + $sql = "SELECT count(*) as nb_users FROM $tbl_user WHERE active <> -1"; $result = Database::query($sql); $num_row = Database::fetch_array($result); if ($num_row['nb_users'] > 1000) { @@ -81,7 +81,7 @@ $first_letter_user_lower = Database::escape_string(api_strtolower($first_letter_ $target_name = api_sort_by_first_name() ? 'firstname' : 'lastname'; $target_name = 'lastname'; $sql = "SELECT user_id,lastname,firstname,username FROM $tbl_user - WHERE ".$target_name." LIKE '".$first_letter_user_lower."%' OR ".$target_name." LIKE '".$first_letter_user_lower."%' + WHERE active <> -1 AND ".$target_name." LIKE '".$first_letter_user_lower."%' OR ".$target_name." LIKE '".$first_letter_user_lower."%' ORDER BY ".(count($users) > 0 ? '(user_id IN('.implode(',', $users).')) DESC,' : '').' '.$target_name; $result = Database::query($sql); $db_users = Database::store_result($result); diff --git a/public/main/admin/access_url_check_user_session.php b/public/main/admin/access_url_check_user_session.php index 7ae15ed988..15dad9860d 100644 --- a/public/main/admin/access_url_check_user_session.php +++ b/public/main/admin/access_url_check_user_session.php @@ -78,7 +78,7 @@ foreach ($session_list as $session_item) { ON u.id = su.user_id AND su.relation_type <> ".Session::DRH." LEFT OUTER JOIN $table_access_url_user uu ON (uu.user_id = u.id) - WHERE su.session_id = $session_id AND $access_where + WHERE su.session_id = $session_id AND $access_where AND u.active <> -1 $order_clause"; $result = Database::query($sql); diff --git a/public/main/admin/dashboard_add_users_to_user.php b/public/main/admin/dashboard_add_users_to_user.php index 3218f4182e..e1a1996c77 100644 --- a/public/main/admin/dashboard_add_users_to_user.php +++ b/public/main/admin/dashboard_add_users_to_user.php @@ -103,7 +103,7 @@ function search_users($needle, $type = 'multiple') $sql = "SELECT user.id as user_id, username, lastname, firstname FROM $tbl_user user LEFT JOIN $tbl_access_url_rel_user au ON (au.user_id = user.id) - WHERE + WHERE user.active <> -1 AND ".(api_sort_by_first_name() ? 'firstname' : 'lastname')." LIKE '$needle%' AND status NOT IN(".DRH.', '.SESSIONADMIN.', '.STUDENT_BOSS.") AND user.id NOT IN ($user_anonymous, $current_user_id, $user_id) @@ -114,7 +114,7 @@ function search_users($needle, $type = 'multiple') } else { $sql = "SELECT id as user_id, username, lastname, firstname FROM $tbl_user user - WHERE + WHERE user.active <> -1 AND ".(api_sort_by_first_name() ? 'firstname' : 'lastname')." LIKE '$needle%' AND status NOT IN(".DRH.', '.SESSIONADMIN.', '.STUDENT_BOSS.") AND id NOT IN ($user_anonymous, $current_user_id, $user_id) @@ -132,7 +132,7 @@ function search_users($needle, $type = 'multiple') $sql = 'SELECT user.id as user_id, username, lastname, firstname FROM '.$tbl_user.' user INNER JOIN '.$tbl_user_rel_access_url.' url_user ON (url_user.user_id=user.id) - WHERE + WHERE user.active <> -1 AND access_url_id = '.$access_url_id.' AND ( username LIKE "'.$needle.'%" OR @@ -400,7 +400,7 @@ if (api_is_multiple_url_enabled()) { FROM $tbl_user user LEFT JOIN $tbl_access_url_rel_user au ON (au.user_id = user.id) - WHERE + WHERE user.active <> -1 AND $without_assigned_users user.id NOT IN ($user_anonymous, $current_user_id, $user_id) AND status NOT IN(".DRH.', '.SESSIONADMIN.', '.ANONYMOUS.") $search_user AND @@ -410,7 +410,7 @@ if (api_is_multiple_url_enabled()) { } else { $sql = "SELECT id as user_id, username, lastname, firstname FROM $tbl_user user - WHERE + WHERE user.active <> -1 AND $without_assigned_users id NOT IN ($user_anonymous, $current_user_id, $user_id) AND status NOT IN(".DRH.', '.SESSIONADMIN.', '.ANONYMOUS.") diff --git a/public/main/admin/user_edit.php b/public/main/admin/user_edit.php index 94f17dc4d3..683d5ca34a 100644 --- a/public/main/admin/user_edit.php +++ b/public/main/admin/user_edit.php @@ -435,7 +435,11 @@ if ($form->validate()) { $expiration_date = $user['expiration_date']; } - $active = $user_data['platform_admin'] ? 1 : intval($user['active']); + if (isset($user['active'])) { + $active = $user_data['platform_admin'] ? 1 : intval($user['active']); + } else { + $active = -1; + } //If the user is set to admin the status will be overwrite by COURSEMANAGER = 1 if (1 == $platform_admin) { diff --git a/public/main/admin/user_list.php b/public/main/admin/user_list.php index 20a49fac60..4244ac368f 100644 --- a/public/main/admin/user_list.php +++ b/public/main/admin/user_list.php @@ -18,6 +18,8 @@ $urlId = api_get_current_access_url_id(); $currentUserId = api_get_user_id(); $action = $_REQUEST['action'] ?? ''; +$view = $_GET['view'] ?? 'all'; +$showDeletedUsers = 'deleted' === $view; // Login as can be used by different roles if (isset($_GET['user_id']) && 'login_as' === $action) { @@ -73,37 +75,27 @@ if ($variables) { Session::write('variables_to_show', $variablesToShow); $htmlHeadXtra[] = ' - - diff --git a/assets/vue/components/basecomponents/ChamiloIcons.js b/assets/vue/components/basecomponents/ChamiloIcons.js index 311a6c112d..bf46b0eb1a 100644 --- a/assets/vue/components/basecomponents/ChamiloIcons.js +++ b/assets/vue/components/basecomponents/ChamiloIcons.js @@ -118,5 +118,4 @@ export const chamiloIconToClass = { "map-search": "mdi mdi-map-search-outline", "join-group": "mdi mdi-account-multiple-plus", "add-topic": "mdi mdi-forum-outline", - 'badge-account': "mdi mdi-badge-account-horizontal", }; diff --git a/assets/vue/router/index.js b/assets/vue/router/index.js index 3e89eab785..a64fe418ab 100644 --- a/assets/vue/router/index.js +++ b/assets/vue/router/index.js @@ -12,7 +12,6 @@ import toolIntroRoutes from "./ctoolintro" import pageRoutes from "./page" import socialNetworkRoutes from "./social" import termsRoutes from "./terms" -import skillsRoutes from "./skill" //import courseCategoryRoutes from './coursecategory'; import documents from "./documents" @@ -141,7 +140,6 @@ const router = createRouter({ component: MySessionListUpcoming, meta: { requiresAuth: true }, }, - skillsRoutes, termsRoutes, socialNetworkRoutes, catalogueCourses, diff --git a/public/documentation/changelog.html b/public/documentation/changelog.html index 6148f12898..5696f78251 100644 --- a/public/documentation/changelog.html +++ b/public/documentation/changelog.html @@ -90,7 +90,6 @@
  • Blogs: The blogs tool is not available. It might be included again in future versions, but there were too many changes required to include it in v2.0.
  • Dropbox: The dropbox tool is not available. It might be included again in future versions, but there were too many changes required to include it in v2.0.
  • Global: Sub-folder mode: Chamilo 2 will not support being installed in a sub-folder. Nowadays, defining a subdomain has become very easy and provides higher security and more configuration flexibility, and supporting sub-folders is a complex task.
  • -
  • WYSIWYG: New online editor with different implementation as Chamilo 1. Will take time to re-integrate all toolbar options into v2, so advanced features (math formula, easy integration of YT video, etc) temporarily disabled.
  • Known issues

    diff --git a/public/main/template/default/skill/skill_wheel.html.twig b/public/main/template/default/skill/skill_wheel.html.twig index e69de29bb2..14f4257c5b 100644 --- a/public/main/template/default/skill/skill_wheel.html.twig +++ b/public/main/template/default/skill/skill_wheel.html.twig @@ -0,0 +1,648 @@ +{% import '@ChamiloCore/Macros/box.html.twig' as display %} + +{% autoescape false %} + +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    + + + + +
    +
    +

    + + {{ "View skills wheel"|trans }} + +

    +
    +
    +
    +
    + +
    +
    +
      +
    • + {{ "Basic skills"|trans }} +
    • +
    • + {{ "Skills you can learn"|trans }} +
    • +
    • + {{ "Skills searched for"|trans }} +
    • +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + + + + +{% endautoescape %} diff --git a/src/CoreBundle/Entity/Skill.php b/src/CoreBundle/Entity/Skill.php index 235ee622e8..0dc6177174 100644 --- a/src/CoreBundle/Entity/Skill.php +++ b/src/CoreBundle/Entity/Skill.php @@ -9,15 +9,8 @@ namespace Chamilo\CoreBundle\Entity; use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; -use Chamilo\CoreBundle\DataProvider\SkillDataProvider; -use Chamilo\CoreBundle\Dto\SkillInputDto; use Chamilo\CoreBundle\Repository\SkillRepository; -use Chamilo\CoreBundle\State\SkillPostProcessor; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -27,55 +20,7 @@ use Stringable; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; -#[ApiResource( - operations: [ - new GetCollection( - uriTemplate: '/skills/options', - normalizationContext: ['groups' => ['skill:read']], - security: "is_granted('ROLE_USER')", - name: 'get_skill_options', - provider: SkillDataProvider::class - ), - new GetCollection( - uriTemplate: '/skills/gradebook/options', - normalizationContext: ['groups' => ['skill:read']], - security: "is_granted('ROLE_USER')", - name: 'get_skill_gradebook_options', - provider: SkillDataProvider::class - ), - new GetCollection( - uriTemplate: '/skills/all', - normalizationContext: ['groups' => ['skill:read']], - security: "is_granted('ROLE_USER')", - name: 'get_all_skills', - provider: SkillDataProvider::class - ), - new GetCollection( - uriTemplate: '/skills/user/{user_id}', - normalizationContext: ['groups' => ['skill:read']], - security: "is_granted('ROLE_USER')", - name: 'get_skills_by_user', - provider: SkillDataProvider::class - ), - new Post( - securityPostDenormalize: "is_granted('ROLE_USER')", - input: SkillInputDto::class, - processor: SkillPostProcessor::class - ), - new Get( - security: "is_granted('ROLE_USER')" - ), - new Delete(security: "is_granted('DELETE', object)"), - new Put(security: "is_granted('EDIT', object)"), - ], - normalizationContext: [ - 'groups' => ['skill:read'], - ], - denormalizationContext: [ - 'groups' => ['skill:write'], - ], - security: "is_granted('ROLE_USER')", -)] +#[ApiResource(security: 'is_granted(\'ROLE_ADMIN\')', normalizationContext: ['groups' => ['skill:read']])] #[ApiFilter(SearchFilter::class, properties: ['issuedSkills.user' => 'exact'])] #[ORM\Table(name: 'skill')] #[ORM\Entity(repositoryClass: SkillRepository::class)] @@ -134,7 +79,6 @@ class Skill implements Stringable #[ORM\Column(name: 'description', type: 'text', nullable: false)] protected string $description; #[Assert\NotNull] - #[Groups(['skill:read', 'skill:write'])] #[ORM\Column(name: 'access_url_id', type: 'integer', nullable: false)] protected int $accessUrlId; #[Groups(['skill:read', 'skill_rel_user:read'])] @@ -143,7 +87,6 @@ class Skill implements Stringable #[ORM\ManyToOne(targetEntity: Asset::class, cascade: ['persist', 'remove'])] #[ORM\JoinColumn(name: 'asset_id', referencedColumnName: 'id')] protected ?Asset $asset = null; - #[Groups(['skill:read', 'skill:write'])] #[ORM\Column(name: 'criteria', type: 'text', nullable: true)] protected ?string $criteria = null; #[ORM\Column(name: 'status', type: 'integer', nullable: false, options: ['default' => 1])] diff --git a/src/CoreBundle/Repository/SkillRepository.php b/src/CoreBundle/Repository/SkillRepository.php index deda5207f1..6b509944eb 100644 --- a/src/CoreBundle/Repository/SkillRepository.php +++ b/src/CoreBundle/Repository/SkillRepository.php @@ -6,14 +6,9 @@ declare(strict_types=1); namespace Chamilo\CoreBundle\Repository; -use Chamilo\CoreBundle\Component\Utils\ChamiloApi; -use Chamilo\CoreBundle\Entity\Asset; use Chamilo\CoreBundle\Entity\Course; -use Chamilo\CoreBundle\Entity\GradebookCategory; use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\Skill; -use Chamilo\CoreBundle\Entity\SkillRelGradebook; -use Chamilo\CoreBundle\Entity\SkillRelSkill; use Chamilo\CoreBundle\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Collections\Criteria; @@ -25,12 +20,9 @@ use Doctrine\Persistence\ManagerRegistry; */ class SkillRepository extends ServiceEntityRepository { - private $assetRepository; - - public function __construct(ManagerRegistry $registry, AssetRepository $assetRepository) + public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Skill::class); - $this->assetRepository = $assetRepository; } public function deleteAsset(Skill $skill): void @@ -95,195 +87,4 @@ class SkillRepository extends ServiceEntityRepository return $qb->getQuery()->getOneOrNullResult(); } - - public function getAllSkills($loadUserData = false, $userId = null, $id = null, $parentId = null): array - { - $qb = $this->createQueryBuilder('s'); - - // Filtrar por ID específico si se proporciona - if (null !== $id) { - $qb->andWhere('s.id = :id') - ->setParameter('id', $id); - } - - // Filtrar por habilidades relacionadas con un usuario específico si se proporciona - if ($loadUserData && null !== $userId) { - $qb->innerJoin('s.issuedSkills', 'isu') - ->andWhere('isu.user = :userId') - ->setParameter('userId', $userId); - } - - // Filtrar por padre si se proporciona - if (null !== $parentId) { - $qb->innerJoin('s.skills', 'ss', 'WITH', 'ss.parent = :parentId') - ->setParameter('parentId', $parentId); - } - - // Ordenar por algún criterio si es necesario - $qb->orderBy('s.id', 'ASC'); - - $query = $qb->getQuery(); - $skills = $query->getResult(); - - // Convertir cada Skill en un array de detalles - $skillsWithDetails = array_map(function ($skill) { - return $this->getSkillDetails($skill); // Asume que getSkillDetails está definido y funciona como se espera - }, $skills); - - return $skillsWithDetails; - } - - public function getSkillDetails(Skill $skill): array - { - $assetUrl = '/img/icons/64/badges-default.png'; - if ($skill->getAsset()) { - $assetUrl = $this->assetRepository->getAssetUrl($skill->getAsset()); - } - - $skillDetails = [ - 'id' => $skill->getId(), - 'title' => $this->translateName($skill->getTitle()), - 'shortCode' => $this->translateCode($skill->getShortCode()), - 'description' => $skill->getDescription(), - 'icon' => $assetUrl, - ]; - $skillDetails['parents'] = $this->getSkillParents($skill); - $skillDetails['gradebooks'] = $this->getGradebooksBySkill($skill); - - return $skillDetails; - } - - public function getParentOptions(): array - { - $qb = $this->createQueryBuilder('s'); - $qb->select('s.id', 's.title') - ->where('s.status = :status') - ->setParameter('status', Skill::STATUS_ENABLED); - - return $qb->getQuery()->getArrayResult(); - } - - public function getGradebookOptions(): array - { - $gradebookRepo = $this->getEntityManager()->getRepository(GradebookCategory::class); - $qb = $gradebookRepo->createQueryBuilder('g'); - $qb->select('g.id', 'g.title'); - - return $qb->getQuery()->getArrayResult(); - } - - - public function addSkill(array $params): ?Skill - { - $em = $this->getEntityManager(); - - $skill = new Skill(); - $skill->setTitle($params['title']) - ->setShortCode($params['short_code'] ?? '') - ->setDescription($params['description'] ?? '') - ->setCriteria($params['criteria'] ?? '') - ->setAccessUrlId($params['access_url_id']) - ->setIcon($params['icon'] ?? ''); - - $em->persist($skill); - - // Relate with parent skills - if (!empty($params['parent_id'])) { - foreach ($params['parent_id'] as $parentId) { - $parentSkill = $this->find($parentId); - if ($parentSkill) { - $skillRelSkill = new SkillRelSkill(); - $skillRelSkill->setSkill($skill) - ->setParent($parentSkill) - ->setLevel($params['level'] ?? 0) - ->setRelationType($params['relation_type'] ?? 0); - - $em->persist($skillRelSkill); - } - } - } - - // Relate with Gradebooks - if (!empty($params['gradebook_id'])) { - foreach ($params['gradebook_id'] as $gradebookId) { - $gradebook = $em->getRepository(GradebookCategory::class)->find($gradebookId); - if ($gradebook) { - $skillRelGradebook = new SkillRelGradebook(); - $skillRelGradebook->setGradeBookCategory($gradebook) - ->setSkill($skill); - - $em->persist($skillRelGradebook); - } - } - } - - $em->flush(); - - return $skill; - } - - /** - * Translates a skill name into the current user's language, if a translation is available. - */ - private function translateName(string $name): string - { - $variable = ChamiloApi::getLanguageVar($name, 'Skill'); - return $GLOBALS[$variable] ?? $name; - } - - /** - * Translates a skill code into the current user's language, if a translation is available. - * - * This is useful for displaying skill codes in a user-friendly manner, especially when they have specific meanings or abbreviations that may not be immediately clear to all users. - */ - private function translateCode(string $code): string - { - if (empty($code)) { - return ''; - } - - $variable = ChamiloApi::getLanguageVar($code, 'SkillCode'); - return $GLOBALS[$variable] ?? $code; - } - - - /** - * Retrieves the parent skills of a specific skill. - */ - private function getSkillParents(Skill $skill): array - { - $parents = []; - $currentSkill = $skill; - - // While the current skill has a parent, keep looking up the hierarchy - while ($currentSkillRelSkill = $this->getEntityManager()->getRepository(SkillRelSkill::class)->findOneBy(['skill' => $currentSkill])) { - $parentSkill = $currentSkillRelSkill->getParent(); - if ($parentSkill) { - $parents[] = $parentSkill; - $currentSkill = $parentSkill; // Move to the next level in the hierarchy - } else { - break; // No more parents - } - } - - return $parents; - } - - - /** - * Fetches gradebook categories associated with a given skill. - */ - private function getGradebooksBySkill(Skill $skill): array - { - $qb = $this->getEntityManager()->createQueryBuilder(); - - $qb->select('g') - ->from(GradebookCategory::class, 'g') - ->innerJoin(SkillRelGradebook::class, 'sg', 'WITH', 'g.id = sg.gradeBookCategory') - ->where('sg.skill = :skill') - ->setParameter('skill', $skill); - - return $qb->getQuery()->getResult(); - } - } diff --git a/src/CoreBundle/Resources/views/Layout/skill_layout.html.twig b/src/CoreBundle/Resources/views/Layout/skill_layout.html.twig index 93c7c51543..05ed35e34f 100644 --- a/src/CoreBundle/Resources/views/Layout/skill_layout.html.twig +++ b/src/CoreBundle/Resources/views/Layout/skill_layout.html.twig @@ -4,7 +4,7 @@ {# topbar #} {# {% include "@ChamiloCore/Layout/topbar.html.twig" %}#} {% include 'default/skill/skill_wheel.js.html.twig' %} - {% include 'default/skill/skill_wheel2.html.twig' %} + {% include 'default/skill/skill_wheel.html.twig' %} {% autoescape false %}
    {{ content }}