diff --git a/app/Resources/public/css/base.css b/app/Resources/public/css/base.css index 067da48fb4..d13b67f35f 100644 --- a/app/Resources/public/css/base.css +++ b/app/Resources/public/css/base.css @@ -8142,7 +8142,7 @@ input + .progress { width: 120px; } -footer { +footer.footer { font-size: 12px; padding-top: 10px; } @@ -9824,7 +9824,7 @@ ul.dropdown-menu.inner > li > a { } @media (min-width: 320px) and (max-width: 479px) { - footer { + footer.footer { padding-top: 15px; } diff --git a/main/course_info/infocours.php b/main/course_info/infocours.php index 1bdc981ef6..385660b940 100755 --- a/main/course_info/infocours.php +++ b/main/course_info/infocours.php @@ -517,6 +517,23 @@ if ($allowPortfolioTool) { 2 ); $form->addGroup($group, '', [get_lang("EmailToTeachersWhenNewPost")]); + + $group = []; + $group[] = $form->createElement( + 'radio', + 'email_alert_teachers_student_new_comment', + get_lang('EmailToTeachersAndStudentWhenNewComment'), + get_lang('Yes'), + 1 + ); + $group[] = $form->createElement( + 'radio', + 'email_alert_teachers_student_new_comment', + null, + get_lang('No'), + 2 + ); + $form->addGroup($group, '', [get_lang("EmailToTeachersAndStudentWhenNewComment")]); } $form->addButtonSave(get_lang('SaveSettings'), 'submit_save'); @@ -1015,6 +1032,12 @@ if ($allowPortfolioTool) { get_lang('MaxScore') => [ $form->createElement('number', 'portfolio_max_score', get_lang('MaxScore'), ['step' => 'any', 'min' => 0]), ], + get_lang('RequiredNumberOfItems') => [ + $form->createElement('number', 'portfolio_number_items', '', ['step' => '1', 'min' => 0]), + ], + get_lang('RequiredNumberOfComments') => [ + $form->createElement('number', 'portfolio_number_comments', '', ['step' => '1', 'min' => 0]), + ], $form->addButtonSave(get_lang('SaveSettings'), 'submit_save', true), ]; diff --git a/main/img/icons/128/eye-slash.png b/main/img/icons/128/eye-slash.png new file mode 100644 index 0000000000..89fca12950 Binary files /dev/null and b/main/img/icons/128/eye-slash.png differ diff --git a/main/img/icons/16/eye-slash.png b/main/img/icons/16/eye-slash.png new file mode 100644 index 0000000000..2e4e5cc9eb Binary files /dev/null and b/main/img/icons/16/eye-slash.png differ diff --git a/main/img/icons/22/eye-slash.png b/main/img/icons/22/eye-slash.png new file mode 100644 index 0000000000..b1b5181be4 Binary files /dev/null and b/main/img/icons/22/eye-slash.png differ diff --git a/main/img/icons/32/eye-slash.png b/main/img/icons/32/eye-slash.png new file mode 100644 index 0000000000..b958ffae6a Binary files /dev/null and b/main/img/icons/32/eye-slash.png differ diff --git a/main/img/icons/32/wizard_na.png b/main/img/icons/32/wizard_na.png new file mode 100644 index 0000000000..ec59014edb Binary files /dev/null and b/main/img/icons/32/wizard_na.png differ diff --git a/main/img/icons/48/eye-slash.png b/main/img/icons/48/eye-slash.png new file mode 100644 index 0000000000..9a2e396689 Binary files /dev/null and b/main/img/icons/48/eye-slash.png differ diff --git a/main/img/icons/64/eye-slash.png b/main/img/icons/64/eye-slash.png new file mode 100644 index 0000000000..449f3751a6 Binary files /dev/null and b/main/img/icons/64/eye-slash.png differ diff --git a/main/img/icons/svg/eye-slash.svg b/main/img/icons/svg/eye-slash.svg new file mode 100644 index 0000000000..a1a08ac849 --- /dev/null +++ b/main/img/icons/svg/eye-slash.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/inc/ajax/course.ajax.php b/main/inc/ajax/course.ajax.php index a814f57adf..bc094d46a3 100755 --- a/main/inc/ajax/course.ajax.php +++ b/main/inc/ajax/course.ajax.php @@ -303,6 +303,9 @@ switch ($action) { $json['items'][] = [ 'id' => $user['user_id'], 'text' => "{$user['username']} ($userCompleteName)", + 'avatarUrl' => UserManager::getUserPicture($user['id']), + 'username' => $user['username'], + 'completeName' => $userCompleteName, ]; } diff --git a/main/inc/ajax/extra_field.ajax.php b/main/inc/ajax/extra_field.ajax.php index 7ca504c719..cdfa281d02 100755 --- a/main/inc/ajax/extra_field.ajax.php +++ b/main/inc/ajax/extra_field.ajax.php @@ -36,7 +36,9 @@ switch ($action) { break; case 'search_tags': header('Content-Type: application/json'); - $tag = isset($_REQUEST['q']) ? $_REQUEST['q'] : null; + $tag = $_REQUEST['q'] ?? null; + $pageLimit = isset($_REQUEST['page_limit']) ? (int) $_REQUEST['page_limit'] : 10; + $byId = !empty($_REQUEST['byid']); $result = []; if (empty($tag)) { @@ -44,22 +46,15 @@ switch ($action) { exit; } - $extraFieldOption = new ExtraFieldOption($type); - $tags = Database::getManager() - ->getRepository('ChamiloCoreBundle:Tag') - ->createQueryBuilder('t') - ->where("t.tag LIKE :tag") - ->andWhere('t.fieldId = :field') - ->setParameter('field', $fieldId) - ->setParameter('tag', "$tag%") - ->getQuery() - ->getResult(); + ->getRepository(Tag::class) + ->findByFieldIdAndText($fieldId, $tag, $pageLimit) + ; /** @var Tag $tag */ foreach ($tags as $tag) { $result[] = [ - 'id' => $tag->getTag(), + 'id' => $byId ? $tag->getId() : $tag->getTag(), 'text' => $tag->getTag(), ]; } diff --git a/main/inc/ajax/portfolio.ajax.php b/main/inc/ajax/portfolio.ajax.php new file mode 100644 index 0000000000..04e14e1a61 --- /dev/null +++ b/main/inc/ajax/portfolio.ajax.php @@ -0,0 +1,79 @@ +query->has('a') ? $httpRequest->query->get('a') : $httpRequest->request->get('a'); +$currentUserId = api_get_user_id(); +$currentUser = api_get_user_entity($currentUserId); + +$em = Database::getManager(); + +$item = null; +$comment = null; + +if ($httpRequest->query->has('item')) { + /** @var Portfolio $item */ + $item = $em->find( + Portfolio::class, + $httpRequest->query->getInt('item') + ); +} + +if ($httpRequest->query->has('comment')) { + $comment = $em->find( + PortfolioComment::class, + $httpRequest->query->getInt('comment') + ); +} + +$httpResponse = Response::create(); + +switch ($action) { + case 'find_template': + if (!$item) { + $httpResponse->setStatusCode(Response::HTTP_NOT_FOUND); + break; + } + + if (!$item->isTemplate() || $item->getUser() !== $currentUser) { + $httpResponse->setStatusCode(Response::HTTP_FORBIDDEN); + break; + } + + $httpResponse = JsonResponse::create( + [ + 'title' => $item->getTitle(), + 'content' => $item->getContent(), + ] + ); + break; + case 'find_template_comment': + if (!$comment) { + $httpResponse->setStatusCode(Response::HTTP_NOT_FOUND); + break; + } + + if (!$comment->isTemplate() || $comment->getAuthor() !== $currentUser) { + $httpResponse->setStatusCode(Response::HTTP_FORBIDDEN); + break; + } + + $httpResponse = JsonResponse::create( + [ + 'content' => $comment->getContent(), + ] + ); + break; +} + +$httpResponse->send(); diff --git a/main/inc/lib/PortfolioController.php b/main/inc/lib/PortfolioController.php index 48539f7670..38fe04c7ac 100644 --- a/main/inc/lib/PortfolioController.php +++ b/main/inc/lib/PortfolioController.php @@ -12,6 +12,7 @@ use Chamilo\CoreBundle\Entity\PortfolioCategory; use Chamilo\CoreBundle\Entity\PortfolioComment; use Chamilo\UserBundle\Entity\User; use Doctrine\ORM\Query\Expr\Join; +use Mpdf\MpdfException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request as HttpRequest; @@ -459,7 +460,31 @@ class PortfolioController { global $interbreadcrumb; + $templates = $this->em + ->getRepository(Portfolio::class) + ->findBy( + [ + 'isTemplate' => true, + 'course' => $this->course, + 'session' => $this->session, + 'user' => $this->owner, + ] + ); + $form = new FormValidator('add_portfolio', 'post', $this->baseUrl.'action=add_item'); + $form->addSelectFromCollection( + 'template', + [ + get_lang('Template'), + null, + '', + ], + $templates, + [], + true, + 'getTitle' + ); if (api_get_configuration_value('save_titles_as_html')) { $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']); @@ -555,16 +580,27 @@ class PortfolioController } $messageSubject = sprintf(get_lang('PortfolioAlertNewPostSubject'), $messageCourseTitle); + $messageContent = sprintf( + get_lang('PortfolioAlertNewPostContent'), + $this->owner->getCompleteName(), + $messageCourseTitle, + $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()]) + ); + $messageContent .= '

' + .'
'.Security::remove_XSS($portfolio->getTitle()).'
' + .'
'.$portfolio->getExcerpt().'
'.'
'; foreach ($userIdListToSend as $userIdToSend) { - $messageContent = sprintf( - get_lang('PortfolioAlertNewPostContent'), - $this->owner->getCompleteName(), - $messageCourseTitle, - $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()]) + MessageManager::send_message_simple( + $userIdToSend, + $messageSubject, + $messageContent, + 0, + false, + false, + [], + false ); - - MessageManager::send_message_simple($userIdToSend, $messageSubject, $messageContent, 0, false, false, [], false); } } @@ -607,6 +643,32 @@ class PortfolioController $(window).on("load", function () { $("input[name=\'title\']").focus(); }); + $(\'#add_portfolio_template\').on(\'change\', function () { + $(\'#portfolio-spinner\').show(); + + $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template&item=\' + this.value) + .done(function(response) { + if (CKEDITOR.instances.title) { + CKEDITOR.instances.title.setData(response.title); + } else { + document.getElementById(\'add_portfolio_title\').value = response.title; + } + + CKEDITOR.instances.content.setData(response.content); + }) + .fail(function () { + if (CKEDITOR.instances.title) { + CKEDITOR.instances.title.setData(\'\'); + } else { + document.getElementById(\'add_portfolio_title\').value = \'\'; + } + + CKEDITOR.instances.content.setData(\'\'); + }) + .always(function() { + $(\'#portfolio-spinner\').hide(); + }); + }); '.$extra['jquery_ready_content'].' }); '; @@ -641,13 +703,12 @@ class PortfolioController { global $interbreadcrumb; - if (!$this->itemBelongToOwner($item)) { + if (!api_is_allowed_to_edit() && !$this->itemBelongToOwner($item)) { api_not_allowed(true); } - $categories = $this->em - ->getRepository('ChamiloCoreBundle:PortfolioCategory') - ->findBy(['user' => $this->owner]); + $itemCourse = $item->getCourse(); + $itemSession = $item->getSession(); $form = new FormValidator('edit_portfolio', 'post', $this->baseUrl."action=edit_item&id={$item->getId()}"); @@ -723,6 +784,21 @@ class PortfolioController ); if ($form->validate()) { + if ($itemCourse) { + api_item_property_update( + api_get_course_info($itemCourse->getCode()), + TOOL_PORTFOLIO, + $item->getId(), + 'PortfolioUpdated', + api_get_user_id(), + [], + null, + '', + '', + $itemSession ? $itemSession->getId() : 0 + ); + } + $values = $form->exportValues(); $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC')); @@ -821,9 +897,18 @@ class PortfolioController api_not_allowed(true); } - $item->setIsVisible( - !$item->isVisible() - ); + switch ($item->getVisibility()) { + case Portfolio::VISIBILITY_HIDDEN: + $item->setVisibility(Portfolio::VISIBILITY_VISIBLE); + break; + case Portfolio::VISIBILITY_VISIBLE: + $item->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER); + break; + case Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER: + default: + $item->setVisibility(Portfolio::VISIBILITY_HIDDEN); + break; + } $this->em->persist($item); $this->em->flush(); @@ -863,6 +948,7 @@ class PortfolioController public function index(HttpRequest $httpRequest) { $listByUser = false; + $listHighlighted = $httpRequest->query->has('list_highlighted'); if ($httpRequest->query->has('user')) { $this->owner = api_get_user_entity($httpRequest->query->getInt('user')); @@ -916,7 +1002,7 @@ class PortfolioController $portfolio = []; if ($this->course) { $frmTagList = $this->createFormTagFilter($listByUser); - $frmStudentList = $this->createFormStudentFilter($listByUser); + $frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted); $frmStudentList->setDefaults(['user' => $this->owner->getId()]); // it translates the category title with the current user language $categories = $this->getCategoriesForIndex(null, 0); @@ -931,7 +1017,12 @@ class PortfolioController $portfolio = $this->getCategoriesForIndex(); } - $items = $this->getItemsForIndex($listByUser, $frmTagList); + if ($listHighlighted) { + $items = $this->getHighlightedItems(); + } else { + $items = $this->getItemsForIndex($listByUser, $frmTagList); + } + // it gets and translate the sub-categories $categoryId = $httpRequest->query->getInt('categoryId'); $subCategoryIdsReq = isset($_REQUEST['subCategoryIds']) ? Security::remove_XSS($_REQUEST['subCategoryIds']) : ''; @@ -1001,6 +1092,22 @@ class PortfolioController { global $interbreadcrumb; + if (!$this->itemBelongToOwner($item)) { + if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN + || ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER && !api_is_allowed_to_edit()) + ) { + api_not_allowed(true); + } + } + + HookPortfolioItemViewed::create() + ->setEventData(['portfolio' => $item]) + ->notifyItemViewed() + ; + + $itemCourse = $item->getCourse(); + $itemSession = $item->getSession(); + $form = $this->createCommentForm($item); $commentsRepo = $this->em->getRepository(PortfolioComment::class); @@ -1017,8 +1124,8 @@ class PortfolioController $query->getArrayResult(), [ 'decorate' => true, - 'rootOpen' => '', + 'rootOpen' => '
', + 'rootClose' => '
', 'childOpen' => function ($node) use ($commentsRepo) { /** @var PortfolioComment $comment */ $comment = $commentsRepo->find($node['id']); @@ -1034,16 +1141,27 @@ class PortfolioController ] ); - return '
  • + return '
    '
                         .$author->getCompleteName().'
    '; }, - 'childClose' => '
  • ', + 'childClose' => '', 'nodeDecorator' => function ($node) use ($commentsRepo, $clockIcon, $item) { + $commentActions = []; /** @var PortfolioComment $comment */ $comment = $commentsRepo->find($node['id']); + if ($this->commentBelongsToOwner($comment)) { + $commentActions[] = Display::url( + Display::return_icon( + $comment->isTemplate() ? 'wizard.png' : 'wizard_na.png', + $comment->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate') + ), + $this->baseUrl.http_build_query(['action' => 'template_comment', 'id' => $comment->getId()]) + ); + } + $commentActions[] = Display::url( Display::return_icon('discuss.png', get_lang('ReplyToThisComment')), '#', @@ -1117,22 +1235,23 @@ class PortfolioController } } - $nodeHtml = '

    '.PHP_EOL - .$comment->getAuthor()->getCompleteName().PHP_EOL.''.$clockIcon.PHP_EOL - .Display::dateToStringAgoAndLongDate($comment->getDate()).''.PHP_EOL; + $nodeHtml = '

    '.implode(PHP_EOL, $commentActions).'
    '.PHP_EOL + .''.PHP_EOL + .Security::remove_XSS($comment->getContent()).PHP_EOL; $nodeHtml .= $this->generateAttachmentList($comment); @@ -1149,18 +1268,114 @@ class PortfolioController $template->assign('form', $form); $template->assign('attachment_list', $this->generateAttachmentList($item)); + if ($itemCourse) { + $propertyInfo = api_get_item_property_info( + $itemCourse->getId(), + TOOL_PORTFOLIO, + $item->getId(), + $itemSession ? $itemSession->getId() : 0 + ); + + if ($propertyInfo) { + $template->assign( + 'last_edit', + [ + 'date' => $propertyInfo['lastedit_date'], + 'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName(), + ] + ); + } + } + $layout = $template->get_template('portfolio/view.html.twig'); $content = $template->fetch($layout); $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl]; + $editLink = Display::url( + Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM), + $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()]) + ); + $actions = []; $actions[] = Display::url( Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), $this->baseUrl ); - $this->renderView($content, Security::remove_XSS($item->getTitle()), $actions, false); + if ($this->itemBelongToOwner($item)) { + $actions[] = $editLink; + + $actions[] = Display::url( + Display::return_icon( + $item->isTemplate() ? 'wizard.png' : 'wizard_na.png', + $item->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate'), + [], + ICON_SIZE_MEDIUM + ), + $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()]) + ); + + $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]); + + if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) { + $actions[] = Display::url( + Display::return_icon('invisible.png', get_lang('MakeVisible'), [], ICON_SIZE_MEDIUM), + $visibilityUrl + ); + } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) { + $actions[] = Display::url( + Display::return_icon('visible.png', get_lang('MakeVisibleForTeachers'), [], ICON_SIZE_MEDIUM), + $visibilityUrl + ); + } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) { + $actions[] = Display::url( + Display::return_icon('eye-slash.png', get_lang('MakeInvisible'), [], ICON_SIZE_MEDIUM), + $visibilityUrl + ); + } + + $actions[] = Display::url( + Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM), + $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()]) + ); + } else { + $actions[] = Display::url( + Display::return_icon('copy.png', get_lang('CopyToMyPortfolio'), [], ICON_SIZE_MEDIUM), + $this->baseUrl.http_build_query(['action' => 'copy', 'id' => $item->getId()]) + ); + } + + if (api_is_allowed_to_edit()) { + $actions[] = Display::url( + Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio'), [], ICON_SIZE_MEDIUM), + $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()]) + ); + $actions[] = $editLink; + + $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]); + + if ($item->isHighlighted()) { + $actions[] = Display::url( + Display::return_icon('award_red.png', get_lang('UnmarkAsHighlighted'), [], ICON_SIZE_MEDIUM), + $highlightedUrl + ); + } else { + $actions[] = Display::url( + Display::return_icon('award_red_na.png', get_lang('MarkAsHighlighted'), [], ICON_SIZE_MEDIUM), + $highlightedUrl + ); + } + + if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) { + $actions[] = Display::url( + Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioItem'), [], ICON_SIZE_MEDIUM), + $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]) + ); + } + } + + $this->renderView($content, $item->getTitle(true), $actions, false); } /** @@ -1173,7 +1388,7 @@ class PortfolioController $portfolio = new Portfolio(); $portfolio - ->setIsVisible(false) + ->setVisibility(Portfolio::VISIBILITY_HIDDEN) ->setTitle( sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName()) ) @@ -1207,7 +1422,7 @@ class PortfolioController $portfolio = new Portfolio(); $portfolio - ->setIsVisible(false) + ->setVisibility(Portfolio::VISIBILITY_HIDDEN) ->setTitle( sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName()) ) @@ -1289,7 +1504,7 @@ class PortfolioController $portfolio = new Portfolio(); $portfolio - ->setIsVisible(false) + ->setVisibility(Portfolio::VISIBILITY_HIDDEN) ->setTitle($values['title']) ->setContent($values['content']) ->setUser($owner) @@ -1380,7 +1595,7 @@ class PortfolioController $portfolio = new Portfolio(); $portfolio - ->setIsVisible(false) + ->setVisibility(Portfolio::VISIBILITY_HIDDEN) ->setTitle($values['title']) ->setContent($values['content']) ->setUser($owner) @@ -1437,6 +1652,7 @@ class PortfolioController */ public function details(HttpRequest $httpRequest) { + $currentUserId = api_get_user_id(); $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit(); $actions = []; @@ -1474,8 +1690,6 @@ class PortfolioController } $frmStudent = new FormValidator('frm_student_list', 'get'); - $slctStudentOptions = []; - $slctStudentOptions[$this->owner->getId()] = $this->owner->getCompleteName(); $urlParams = http_build_query( [ @@ -1485,15 +1699,27 @@ class PortfolioController ] ); - $frmStudent->addSelectAjax( - 'user', - get_lang('SelectLearnerPortfolio'), - $slctStudentOptions, - [ - 'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams", - 'placeholder' => get_lang('SearchStudent'), - ] - ); + $frmStudent + ->addSelectAjax( + 'user', + get_lang('SelectLearnerPortfolio'), + [], + [ + 'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams", + 'placeholder' => get_lang('SearchStudent'), + 'formatResult' => SelectAjax::templateResultForUsersInCourse(), + 'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(), + ] + ) + ->addOption( + $this->owner->getCompleteName(), + $this->owner->getId(), + [ + 'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()), + 'data-username' => $this->owner->getUsername(), + ] + ) + ; $frmStudent->setDefaults(['user' => $this->owner->getId()]); $frmStudent->addHidden('action', 'details'); $frmStudent->addHidden('cidReq', $this->course->getCode()); @@ -1504,7 +1730,7 @@ class PortfolioController $itemsRepo = $this->em->getRepository(Portfolio::class); $commentsRepo = $this->em->getRepository(PortfolioComment::class); - $getItemsTotalNumber = function () use ($itemsRepo) { + $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) { $qb = $itemsRepo->createQueryBuilder('i'); $qb ->select('COUNT(i)') @@ -1525,9 +1751,20 @@ class PortfolioController } } + if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) { + $visibilityCriteria = [ + Portfolio::VISIBILITY_VISIBLE, + Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER, + ]; + + $qb->andWhere( + $qb->expr()->in('i.visibility', $visibilityCriteria) + ); + } + return $qb->getQuery()->getSingleScalarResult(); }; - $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo) { + $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) { $qb = $itemsRepo->createQueryBuilder('item') ->where('item.user = :user') ->leftJoin('item.category', 'category') @@ -1549,6 +1786,17 @@ class PortfolioController } } + if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) { + $visibilityCriteria = [ + Portfolio::VISIBILITY_VISIBLE, + Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER, + ]; + + $qb->andWhere( + $qb->expr()->in('item.visibility', $visibilityCriteria) + ); + } + if (0 == $columnNo) { $qb->orderBy('item.title', $orderDirection); } elseif (1 == $columnNo) { @@ -1592,7 +1840,7 @@ class PortfolioController $portfolioItemColumnFilter = function (Portfolio $item) { return Display::url( - Security::remove_XSS($item->getTitle()), + $item->getTitle(true), $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]) ); }; @@ -1713,17 +1961,48 @@ class PortfolioController $content .= $frmStudent->returnForm(); } - $content .= Display::page_subheader2(get_lang('PortfolioItems')).PHP_EOL; + $totalNumberOfItems = $tblItems->get_total_number_of_items(); + $totalNumberOfComments = $tblComments->get_total_number_of_items(); + $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items'); + $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments'); - if ($tblItems->get_total_number_of_items() > 0) { + $itemsSubtitle = ''; + + if ($requiredNumberOfItems > 0) { + $itemsSubtitle = sprintf( + get_lang('XAddedYRequired'), + $totalNumberOfItems, + $requiredNumberOfItems + ); + } + + $content .= Display::page_subheader2( + get_lang('PortfolioItems'), + $itemsSubtitle + ).PHP_EOL; + + if ($totalNumberOfItems > 0) { $content .= $tblItems->return_table().PHP_EOL; } else { $content .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning'); } - $content .= Display::page_subheader2(get_lang('PortfolioCommentsMade')).PHP_EOL; + $commentsSubtitle = ''; + + if ($requiredNumberOfComments > 0) { + $commentsSubtitle = sprintf( + get_lang('XAddedYRequired'), + $totalNumberOfComments, + $requiredNumberOfComments + ); + } + + $content .= Display::page_subheader2( + get_lang('PortfolioCommentsMade'), + $commentsSubtitle + ).PHP_EOL; - if ($tblComments->get_total_number_of_items() > 0) { + if ($totalNumberOfComments > 0) { $content .= $tblComments->return_table().PHP_EOL; } else { $content .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning'); @@ -1733,10 +2012,11 @@ class PortfolioController } /** - * @throws \MpdfException + * @throws MpdfException */ public function exportPdf(HttpRequest $httpRequest) { + $currentUserId = api_get_user_id(); $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit(); if ($isAllowedToFilterStudent) { @@ -1763,9 +2043,22 @@ class PortfolioController $pdfContent .= '

    '; } + $visibility = []; + + if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) { + $visibility[] = Portfolio::VISIBILITY_VISIBLE; + $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER; + } + $items = $this->em ->getRepository(Portfolio::class) - ->findItemsByUser($this->owner, $this->course, $this->session); + ->findItemsByUser( + $this->owner, + $this->course, + $this->session, + null, + $visibility + ); $comments = $this->em ->getRepository(PortfolioComment::class) ->findCommentsByUser($this->owner, $this->course, $this->session); @@ -1773,17 +2066,47 @@ class PortfolioController $itemsHtml = $this->getItemsInHtmlFormatted($items); $commentsHtml = $this->getCommentsInHtmlFormatted($comments); - $pdfContent .= Display::page_subheader2(get_lang('PortfolioItems')); + $totalNumberOfItems = count($itemsHtml); + $totalNumberOfComments = count($commentsHtml); + $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items'); + $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments'); - if (count($itemsHtml) > 0) { + $itemsSubtitle = ''; + $commentsSubtitle = ''; + + if ($requiredNumberOfItems > 0) { + $itemsSubtitle = sprintf( + get_lang('XAddedYRequired'), + $totalNumberOfItems, + $requiredNumberOfItems + ); + } + + if ($requiredNumberOfComments > 0) { + $commentsSubtitle = sprintf( + get_lang('XAddedYRequired'), + $totalNumberOfComments, + $requiredNumberOfComments + ); + } + + $pdfContent .= Display::page_subheader2( + get_lang('PortfolioItems'), + $itemsSubtitle + ); + + if ($totalNumberOfItems > 0) { $pdfContent .= implode(PHP_EOL, $itemsHtml); } else { $pdfContent .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning'); } - $pdfContent .= Display::page_subheader2(get_lang('PortfolioCommentsMade')); + $pdfContent .= Display::page_subheader2( + get_lang('PortfolioCommentsMade'), + $commentsSubtitle + ); - if (count($commentsHtml) > 0) { + if ($totalNumberOfComments > 0) { $pdfContent .= implode(PHP_EOL, $commentsHtml); } else { $pdfContent .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning'); @@ -1809,6 +2132,7 @@ class PortfolioController public function exportZip(HttpRequest $httpRequest) { + $currentUserId = api_get_user_id(); $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit(); if ($isAllowedToFilterStudent) { @@ -1825,7 +2149,20 @@ class PortfolioController $commentsRepo = $this->em->getRepository(PortfolioComment::class); $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class); - $items = $itemsRepo->findItemsByUser($this->owner, $this->course, $this->session); + $visibility = []; + + if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) { + $visibility[] = Portfolio::VISIBILITY_VISIBLE; + $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER; + } + + $items = $itemsRepo->findItemsByUser( + $this->owner, + $this->course, + $this->session, + null, + $visibility + ); $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session); $itemsHtml = $this->getItemsInHtmlFormatted($items); @@ -2000,9 +2337,7 @@ class PortfolioController $form->addUserAvatar('user', get_lang('Author')); $form->addLabel(get_lang('Title'), $item->getTitle()); - $itemContent = Security::remove_XSS( - $this->generateItemContent($item) - ); + $itemContent = $this->generateItemContent($item); $form->addLabel(get_lang('Content'), $itemContent); $form->addNumeric( @@ -2039,7 +2374,7 @@ class PortfolioController 'url' => $this->baseUrl, ]; $interbreadcrumb[] = [ - 'name' => Security::remove_XSS($item->getTitle()), + 'name' => $item->getTitle(true), 'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]), ]; @@ -2108,7 +2443,7 @@ class PortfolioController 'url' => $this->baseUrl, ]; $interbreadcrumb[] = [ - 'name' => Security::remove_XSS($item->getTitle()), + 'name' => $item->getTitle(true), 'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]), ]; @@ -2230,6 +2565,79 @@ class PortfolioController exit; } + /** + * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Doctrine\ORM\ORMException + */ + public function markAsHighlighted(Portfolio $item) + { + if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) { + api_not_allowed(true); + } + + $item->setIsHighlighted( + !$item->isHighlighted() + ); + + Database::getManager()->flush(); + + Display::addFlash( + Display::return_message( + $item->isHighlighted() ? get_lang('MarkedAsHighlighted') : get_lang('UnmarkedAsHighlighted'), + 'success' + ) + ); + + header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()])); + exit; + } + + public function markAsTemplate(Portfolio $item) + { + if (!$this->itemBelongToOwner($item)) { + api_not_allowed(true); + } + + $item->setIsTemplate( + !$item->isTemplate() + ); + + Database::getManager()->flush($item); + + Display::addFlash( + Display::return_message( + $item->isTemplate() ? get_lang('PortfolioItemSetAsTemplate') : get_lang('PortfolioItemUnsetAsTemplate'), + 'success' + ) + ); + + header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()])); + exit; + } + + public function markAsTemplateComment(PortfolioComment $comment) + { + if (!$this->commentBelongsToOwner($comment)) { + api_not_allowed(true); + } + + $comment->setIsTemplate( + !$comment->isTemplate() + ); + + Database::getManager()->flush(); + + Display::addFlash( + Display::return_message( + $comment->isTemplate() ? get_lang('PortfolioCommentSetAsTemplate') : get_lang('PortfolioCommentUnsetAsTemplate'), + 'success' + ) + ); + + header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])); + exit; + } + /** * @param bool $showHeader */ @@ -2252,7 +2660,7 @@ class PortfolioController } if ($actions) { - $actions = implode(PHP_EOL, $actions); + $actions = implode('', $actions); $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]); } @@ -2371,12 +2779,17 @@ class PortfolioController return true; } + private function commentBelongsToOwner(PortfolioComment $comment): bool + { + return $comment->getAuthor() === $this->owner; + } + private function createFormTagFilter(bool $listByUser = false): FormValidator { $extraField = new ExtraField('portfolio'); $tagFieldInfo = $extraField->get_handler_field_info_by_tags('tags'); - $chbxTagOptions = array_map( + $selectTagOptions = array_map( function (array $tagOption) { return $tagOption['tag']; }, @@ -2392,9 +2805,14 @@ class PortfolioController FormValidator::LAYOUT_BOX ); - if (!empty($chbxTagOptions)) { - $frmTagList->addCheckBoxGroup('tags', $tagFieldInfo['display_text'], $chbxTagOptions); - } + $frmTagList->addDatePicker('date', get_lang('CreationDate')); + + $frmTagList->addSelect( + 'tags', + get_lang('Tags'), + $selectTagOptions, + ['multiple' => 'multiple'] + ); $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search'); $frmTagList->applyFilter('text', 'trim'); @@ -2419,11 +2837,9 @@ class PortfolioController } /** - * @throws \Exception - * - * @return \FormValidator + * @throws Exception */ - private function createFormStudentFilter(bool $listByUser = false): FormValidator + private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false): FormValidator { $frmStudentList = new FormValidator( 'frm_student_list', @@ -2433,11 +2849,6 @@ class PortfolioController [], FormValidator::LAYOUT_BOX ); - $slctStudentOptions = []; - - if ($listByUser) { - $slctStudentOptions[$this->owner->getId()] = $this->owner->getCompleteName(); - } $urlParams = http_build_query( [ @@ -2447,17 +2858,29 @@ class PortfolioController ] ); - $frmStudentList->addSelectAjax( + /** @var SelectAjax $slctUser */ + $slctUser = $frmStudentList->addSelectAjax( 'user', get_lang('SelectLearnerPortfolio'), - $slctStudentOptions, + [], [ 'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams", 'placeholder' => get_lang('SearchStudent'), + 'formatResult' => SelectAjax::templateResultForUsersInCourse(), + 'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(), ] ); if ($listByUser) { + $slctUser->addOption( + $this->owner->getCompleteName(), + $this->owner->getId(), + [ + 'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()), + 'data-username' => $this->owner->getUsername(), + ] + ); + $link = Display::url( get_lang('BackToMainPortfolio'), $this->baseUrl @@ -2469,7 +2892,21 @@ class PortfolioController ); } - $frmStudentList->addHtml($link); + $frmStudentList->addHtml("

    $link

    "); + + if ($listHighlighted) { + $link = Display::url( + get_lang('BackToMainPortfolio'), + $this->baseUrl + ); + } else { + $link = Display::url( + get_lang('SeeHighlights'), + $this->baseUrl.http_build_query(['list_highlighted' => true]) + ); + } + + $frmStudentList->addHtml("

    $link

    "); return $frmStudentList; } @@ -2492,6 +2929,46 @@ class PortfolioController ->findBy($categoriesCriteria); } + private function getHighlightedItems() + { + $queryBuilder = $this->em->createQueryBuilder(); + $queryBuilder + ->select('pi') + ->from(Portfolio::class, 'pi') + ->where('pi.course = :course') + ->andWhere('pi.isHighlighted = TRUE') + ->setParameter('course', $this->course); + + if ($this->session) { + $queryBuilder->andWhere('pi.session = :session'); + $queryBuilder->setParameter('session', $this->session); + } else { + $queryBuilder->andWhere('pi.session IS NULL'); + } + + $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE]; + + if (api_is_allowed_to_edit()) { + $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER; + } + + $queryBuilder + ->andWhere( + $queryBuilder->expr()->orX( + 'pi.user = :current_user', + $queryBuilder->expr()->andX( + 'pi.user != :current_user', + $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria) + ) + ) + ) + ->setParameter('current_user', api_get_user_id()); + + $queryBuilder->orderBy('pi.creationDate', 'DESC'); + + return $queryBuilder->getQuery()->getResult(); + } + private function getItemsForIndex( bool $listByUser = false, FormValidator $frmFilterList = null @@ -2517,6 +2994,13 @@ class PortfolioController if ($frmFilterList && $frmFilterList->validate()) { $values = $frmFilterList->exportValues(); + if (!empty($values['date'])) { + $queryBuilder + ->andWhere('pi.creationDate >= :date') + ->setParameter(':date', api_get_utc_datetime($values['date'], false, true)) + ; + } + if (!empty($values['tags'])) { $queryBuilder ->innerJoin(ExtraFieldRelTag::class, 'efrt', Join::WITH, 'efrt.itemId = pi.id') @@ -2578,11 +3062,20 @@ class PortfolioController ->setParameter('user', $this->owner); } + $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE]; + + if (api_is_allowed_to_edit()) { + $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER; + } + $queryBuilder ->andWhere( $queryBuilder->expr()->orX( - 'pi.user = :current_user AND (pi.isVisible = TRUE OR pi.isVisible = FALSE)', - 'pi.user != :current_user AND pi.isVisible = TRUE' + 'pi.user = :current_user', + $queryBuilder->expr()->andX( + 'pi.user != :current_user', + $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria) + ) ) ) ->setParameter('current_user', $currentUserId); @@ -2596,7 +3089,7 @@ class PortfolioController $itemsCriteria['user'] = $this->owner; if ($currentUserId !== $this->owner->getId()) { - $itemsCriteria['isVisible'] = true; + $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE; } $items = $this->em @@ -2616,7 +3109,30 @@ class PortfolioController { $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]); + $templates = $this->em + ->getRepository(PortfolioComment::class) + ->findBy( + [ + 'isTemplate' => true, + 'author' => $this->owner, + ] + ); + $form = new FormValidator('frm_comment', 'post', $formAction); + $form->addHeader(get_lang('AddNewComment')); + $form->addSelectFromCollection( + 'template', + [ + get_lang('Template'), + null, + '', + ], + $templates, + [], + true, + 'getExcerpt' + ); $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']); $form->addHidden('item', $item->getId()); $form->addHidden('parent', 0); @@ -2653,6 +3169,8 @@ class PortfolioController $hook->setEventData(['comment' => $comment]); $hook->notifyItemCommented(); + PortfolioNotifier::notifyTeachersAndAuthor($comment); + Display::addFlash( Display::return_message(get_lang('CommentAdded'), 'success') ); @@ -2661,7 +3179,26 @@ class PortfolioController exit; } - return $form->returnForm(); + $js = ''; + + return $form->returnForm().$js; } private function generateAttachmentList($post, bool $includeHeader = true): string @@ -2713,10 +3250,7 @@ class PortfolioController } if ($attachment->getComment()) { - $listItems .= PHP_EOL.Display::span( - Security::remove_XSS($attachment->getComment()), - ['class' => 'text-muted'] - ); + $listItems .= '

    '.Security::remove_XSS($attachment->getComment()).'

    '; } $listItems .= ''; @@ -2725,7 +3259,7 @@ class PortfolioController $listItems .= ''; if ($includeHeader) { - $listItems = Display::page_subheader(get_lang('AttachmentFiles'), null, 'h5', ['class' => 'h4']) + $listItems = '

    '.get_lang('AttachmentFiles').'

    ' .$listItems; } @@ -2752,7 +3286,10 @@ class PortfolioController $originContent = $origin->getContent(); $originContentFooter = vsprintf( get_lang('OriginallyPublishedAsXTitleByYUser'), - [$origin->getTitle(), $origin->getUser()->getCompleteName()] + [ + "{$origin->getTitle(true)}", + $origin->getUser()->getCompleteName(), + ] ); } } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) { @@ -2762,17 +3299,24 @@ class PortfolioController $originContent = $origin->getContent(); $originContentFooter = vsprintf( get_lang('OriginallyCommentedByXUserInYItem'), - [$origin->getAuthor()->getCompleteName(), $origin->getItem()->getTitle()] + [ + $origin->getAuthor()->getCompleteName(), + "{$origin->getItem()->getTitle(true)}", + ] ); } } if ($originContent) { - return "
    $originContent
    " - .'
    '.$item->getContent().'
    '; + return "
    +
    $originContent
    +
    $originContentFooter
    +
    +
    ".Security::remove_XSS($item->getContent()).'
    ' + ; } - return $item->getContent(); + return Security::remove_XSS($item->getContent()); } private function getItemsInHtmlFormatted(array $items): array @@ -2781,20 +3325,43 @@ class PortfolioController /** @var Portfolio $item */ foreach ($items as $item) { + $itemCourse = $item->getCourse(); + $itemSession = $item->getSession(); + $creationDate = api_convert_and_format_date($item->getCreationDate()); $updateDate = api_convert_and_format_date($item->getUpdateDate()); $metadata = ''; - $itemContent = Security::remove_XSS( - $this->generateItemContent($item) - ); + $itemContent = $this->generateItemContent($item); $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata); } diff --git a/main/inc/lib/PortfolioNotifier.php b/main/inc/lib/PortfolioNotifier.php new file mode 100644 index 0000000000..576fb14ac6 --- /dev/null +++ b/main/inc/lib/PortfolioNotifier.php @@ -0,0 +1,102 @@ +getItem(); + $course = $item->getCourse(); + $session = $item->getSession(); + + $messageSubject = sprintf( + get_lang('PortfolioAlertNewCommentSubject'), + $item->getTitle(true) + ); + $userIdListToSend = []; + $userIdListToSend[] = $comment->getItem()->getUser()->getId(); + + $cidreq = api_get_cidreq_params( + $course ? $course->getCode() : '', + $session ? $session->getId() : 0 + ); + $commentUrl = api_get_path(WEB_CODE_PATH).'portfolio/index.php?' + .($course ? $cidreq.'&' : '') + .http_build_query(['action' => 'view', 'id' => $item->getId()])."#comment-{$comment->getId()}"; + + if ($course) { + $courseInfo = api_get_course_info($course->getCode()); + + if (1 !== (int) api_get_course_setting('email_alert_teachers_student_new_comment', $courseInfo)) { + return; + } + + $courseTitle = self::getCourseTitle($course, $session); + $userIdListToSend = array_merge( + $userIdListToSend, + self::getTeacherList($course, $session) + ); + + $messageContent = sprintf( + get_lang('CoursePortfolioAlertNewCommentContent'), + $item->getTitle(), + $courseTitle, + $commentUrl + ); + } else { + $messageContent = sprintf( + get_lang('PortfolioAlertNewCommentContent'), + $item->getTitle(), + $commentUrl + ); + } + + $messageContent .= '

    ' + .'
    '.$comment->getExcerpt().'
    ' + .'
    '.$comment->getAuthor()->getCompleteName().'
    ' + .'
    '; + + foreach ($userIdListToSend as $userIdToSend) { + MessageManager::send_message_simple( + $userIdToSend, + $messageSubject, + $messageContent, + 0, + false, + false, + [], + false + ); + } + } + + private static function getCourseTitle(CourseEntity $course, ?SessionEntity $session = null): string + { + if ($session) { + return "{$course->getTitle()} ({$session->getName()})"; + } + + return $course->getTitle(); + } + + private static function getTeacherList(CourseEntity $course, ?SessionEntity $session = null): array + { + if ($session) { + $teachers = SessionManager::getCoachesByCourseSession( + $session->getId(), + $course->getId() + ); + + return array_values($teachers); + } + + $teachers = CourseManager::get_teacher_list_from_course_code($course->getCode()); + + return array_keys($teachers); + } +} diff --git a/main/inc/lib/add_course.lib.inc.php b/main/inc/lib/add_course.lib.inc.php index 5410b0b700..9a68fdebac 100755 --- a/main/inc/lib/add_course.lib.inc.php +++ b/main/inc/lib/add_course.lib.inc.php @@ -662,7 +662,8 @@ class AddCourse 'documents_default_visibility' => ['default' => 'visible', 'category' => 'document'], 'show_course_in_user_language' => ['default' => 2, 'category' => null], 'email_to_teachers_on_new_work_feedback' => ['default' => 1, 'category' => null], - 'email_alert_teachers_new_post' => ['default' => 2, 'category' => 'portfolio'], + 'email_alert_teachers_new_post' => ['default' => 1, 'category' => 'portfolio'], + 'email_alert_teachers_student_new_comment' => ['default' => 1, 'category' => 'portfolio'], 'agenda_share_events_in_sessions' => ['default' => 0, 'category' => 'agenda'], ]; diff --git a/main/inc/lib/course.lib.php b/main/inc/lib/course.lib.php index dd126fc299..6a71bed002 100755 --- a/main/inc/lib/course.lib.php +++ b/main/inc/lib/course.lib.php @@ -5840,9 +5840,12 @@ class CourseManager if (api_get_configuration_value('allow_portfolio_tool')) { $courseSettings[] = 'email_alert_teachers_new_post'; + $courseSettings[] = 'email_alert_teachers_student_new_comment'; $courseSettings[] = 'qualify_portfolio_item'; $courseSettings[] = 'qualify_portfolio_comment'; $courseSettings[] = 'portfolio_max_score'; + $courseSettings[] = 'portfolio_number_items'; + $courseSettings[] = 'portfolio_number_comments'; } if (api_get_configuration_value('lp_show_max_progress_or_average_enable_course_level_redefinition')) { diff --git a/main/inc/lib/display.lib.php b/main/inc/lib/display.lib.php index 0dc47942a8..ce205340ae 100755 --- a/main/inc/lib/display.lib.php +++ b/main/inc/lib/display.lib.php @@ -2769,7 +2769,7 @@ HTML; /** * Returns the string "1 day ago" with a link showing the exact date time. * - * @param string $dateTime in UTC or a DateTime in UTC + * @param string|DateTime $dateTime in UTC or a DateTime in UTC * * @return string */ diff --git a/main/inc/lib/extra_field.lib.php b/main/inc/lib/extra_field.lib.php index ebdc5ce446..b76e6d691f 100755 --- a/main/inc/lib/extra_field.lib.php +++ b/main/inc/lib/extra_field.lib.php @@ -656,6 +656,7 @@ class ExtraField extends Model $row['variable'], $row['display_text'] ); + $row['options'] = []; // All the tags of the field $sql = "SELECT * FROM $this->table_field_tag diff --git a/main/inc/lib/formvalidator/Element/DatePicker.php b/main/inc/lib/formvalidator/Element/DatePicker.php index 881ebff398..55666df5d4 100644 --- a/main/inc/lib/formvalidator/Element/DatePicker.php +++ b/main/inc/lib/formvalidator/Element/DatePicker.php @@ -122,6 +122,12 @@ class DatePicker extends HTML_QuickForm_text case FormValidator::LAYOUT_BOX_NO_LABEL: return '{element}'; } + + return '
    + + {element} +
    ' + ; } /** diff --git a/main/inc/lib/formvalidator/Element/SelectAjax.php b/main/inc/lib/formvalidator/Element/SelectAjax.php index c172219226..f7d7e5974c 100644 --- a/main/inc/lib/formvalidator/Element/SelectAjax.php +++ b/main/inc/lib/formvalidator/Element/SelectAjax.php @@ -24,12 +24,17 @@ class SelectAjax extends HTML_QuickForm_select { $iso = api_get_language_isocode(api_get_interface_language()); $formatResult = $this->getAttribute('formatResult'); + $formatSelection = $this->getAttribute('formatSelection'); $formatCondition = ''; if (!empty($formatResult)) { - $formatCondition = ', - templateResult : '.$formatResult.', - templateSelection : '.$formatResult; + $formatCondition .= ', + templateResult : '.$formatResult; + } + + if (!empty($formatSelection)) { + $formatCondition .= ', + templateSelection : '.$formatSelection; } $width = 'element'; @@ -109,14 +114,15 @@ class SelectAjax extends HTML_QuickForm_select results: '' }; } - $formatCondition } + $formatCondition }); }); JS; $this->removeAttribute('formatResult'); + $this->removeAttribute('formatSelection'); $this->removeAttribute('minimumInputLength'); $this->removeAttribute('maximumSelectionLength'); $this->removeAttribute('tags'); @@ -143,4 +149,60 @@ JS; return $this->_prepareValue($value, $assoc); } + + public static function templateResultForUsersInCourse(): string + { + return "function (state) { + if (state.loading) { + return state.text; + } + + var \$container = \$( + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ); + + \$container.find('.select2-result-user__avatar img') + .prop({ 'src': state.avatarUrl, 'alt': state.username }) + .css({ 'width': '40px', 'height': '40px' }); + \$container.find('.select2-result-user__info').css({ 'paddingLeft': '6px' }); + \$container.find('.select2-result-user__name').text(state.completeName); + \$container.find('.select2-result-user__username').text(state.username); + + return \$container; + }"; + } + + public static function templateSelectionForUsersInCourse(): string + { + return "function (state) { + if (!state.id) { + return state.text; + } + + if (!state.avatarUrl) { + var avatarUrl = $(state.element).data('avatarurl'); + var username = $(state.element).data('username'); + + state.avatarUrl = avatarUrl; + state.username = username; + state.completeName = state.text; + } + + var \$container = \$(' ' + state.completeName + ''); + + \$container.find('img') + .prop({ 'src': state.avatarUrl, 'alt': state.username }) + .css({ 'width': '20px', 'height': '20px' }); + + return \$container; + }"; + } } diff --git a/main/inc/lib/hook/HookPortfolioItemViewed.php b/main/inc/lib/hook/HookPortfolioItemViewed.php new file mode 100644 index 0000000000..6a3122530a --- /dev/null +++ b/main/inc/lib/hook/HookPortfolioItemViewed.php @@ -0,0 +1,19 @@ +observers as $observer) { + $observer->hookItemViewed($this); + } + } +} diff --git a/main/inc/lib/hook/interfaces/HookPortfolioItemViewedEventInterface.php b/main/inc/lib/hook/interfaces/HookPortfolioItemViewedEventInterface.php new file mode 100644 index 0000000000..acaed500a1 --- /dev/null +++ b/main/inc/lib/hook/interfaces/HookPortfolioItemViewedEventInterface.php @@ -0,0 +1,8 @@ +editItem($item); return; - case 'hide_item': - case 'show_item': + case 'visibility': $id = $httpRequest->query->getInt('id'); /** @var Portfolio $item */ @@ -271,6 +270,43 @@ switch ($action) { case 'delete_attachment': $controller->deleteAttachment($httpRequest); break; + case 'highlighted': + api_protect_teacher_script(); + + $id = $httpRequest->query->getInt('id'); + + /** @var Portfolio $item */ + $item = $em->find('ChamiloCoreBundle:Portfolio', $id); + + if (empty($item)) { + break; + } + + $controller->markAsHighlighted($item); + break; + case 'template': + $id = $httpRequest->query->getInt('id'); + + /** @var Portfolio $item */ + $item = $em->find('ChamiloCoreBundle:Portfolio', $id); + + if (empty($item)) { + break; + } + + $controller->markAsTemplate($item); + break; + case 'template_comment': + $id = $httpRequest->query->getInt('id'); + + $comment = $em->find(PortfolioComment::class, $id); + + if (empty($comment)) { + break; + } + + $controller->markAsTemplateComment($comment); + break; case 'list': default: $controller->index($httpRequest); diff --git a/main/template/default/portfolio/items.html.twig b/main/template/default/portfolio/items.html.twig index f3479a5790..5d20e6dcff 100644 --- a/main/template/default/portfolio/items.html.twig +++ b/main/template/default/portfolio/items.html.twig @@ -5,34 +5,61 @@ {% set delete_img = 'delete.png'|img(22, 'Delete'|get_lang) %} {% set baseurl = _p.web_self ~ '?' ~ (_p.web_cid_query ? _p.web_cid_query ~ '&' : '') %} -
    +
    {% for item in items %} {% set item_url = baseurl ~ {'action':'view', 'id':item.id}|url_encode %} {% set comments = item.lastComments %} -
    -
    -
    +
    +
    + + + {% if item.isHighlighted %} + + + {{ 'Highlighted'|get_lang }} + + {% endif %} + +

    {{ item.title|remove_xss }}

    @@ -62,20 +89,22 @@ {{ 'CreationDate'|get_lang ~ ': ' ~ item.creationDate|date_to_time_ago }} - {% if _u.id == item.user.id and item.creationDate != item.updateDate %} + {% if item.creationDate != item.updateDate %}
  • {{ 'UpdateDate'|get_lang ~ ': ' ~ item.updateDate|date_to_time_ago }}
  • {% endif %} +
    -
    +
    -

    {{ item.excerpt }}

    +

    {{ item.excerpt }}

    -
    +
    +
    {% if comments|length > 0 %}

    @@ -84,27 +113,25 @@ · {{ 'AddNewComment'|get_lang }}

    -
      - {% for comment in comments %} -
    • - -
      - {{ comment.author.completeName }} - - {{ comment.date|date_to_time_ago }} -
      -

      {{ comment.excerpt }}

      -
    • - {% endfor %} -
    + {% for comment in comments %} +
    +
    + + {{ comment.author.completeName }} + + {{ comment.date|date_to_time_ago }} +
    +

    {{ comment.excerpt }}

    +
    + {% endfor %} {% else %} {{ 'AddNewComment'|get_lang }} {% endif %} -
    -
    + +
    {% endfor %}
    diff --git a/main/template/default/portfolio/list.html.twig b/main/template/default/portfolio/list.html.twig index 20fb45f550..c0b7e18716 100644 --- a/main/template/default/portfolio/list.html.twig +++ b/main/template/default/portfolio/list.html.twig @@ -19,20 +19,21 @@
    {% if (categories) %} -
    - {% for category in categories %} +
    +
    + {% for category in categories %} + + {% endfor %} - {% endfor %} - +
    {% if (subcategories) %} -
    -
    +
    {% for subcategory in subcategories %}