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 @@
+
+
+
+
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 '',
+ '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 = '';
- if ($item->getSession()) {
- $metadata .= '- '.get_lang('Course').': '.$item->getSession()->getName().' ('
- .$item->getCourse()->getTitle().')
';
- } elseif (!$item->getSession() && $item->getCourse()) {
- $metadata .= '- '.get_lang('Course').': '.$item->getCourse()->getTitle().'
';
+ if ($itemSession) {
+ $metadata .= '- '.get_lang('Course').': '.$itemSession->getName().' ('
+ .$itemCourse->getTitle().')
';
+ } elseif ($itemCourse) {
+ $metadata .= '- '.get_lang('Course').': '.$itemCourse->getTitle().'
';
}
$metadata .= '- '.sprintf(get_lang('CreationDateXDate'), $creationDate).'
';
- $metadata .= '- '.sprintf(get_lang('UpdateDateXDate'), $updateDate).'
';
+
+ if ($itemCourse) {
+ $propertyInfo = api_get_item_property_info(
+ $itemCourse->getId(),
+ TOOL_PORTFOLIO,
+ $item->getId(),
+ $itemSession ? $itemSession->getId() : 0
+ );
+
+ if ($propertyInfo) {
+ $metadata .= '- '
+ .sprintf(
+ get_lang('UpdatedOnDateXByUserY'),
+ api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
+ api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName()
+ )
+ .'
';
+ }
+ } else {
+ $metadata .= '- '.sprintf(get_lang('UpdateDateXDate'), $updateDate).'
';
+ }
if ($item->getCategory()) {
$metadata .= '- '.sprintf(get_lang('CategoryXName'), $item->getCategory()->getTitle()).'
';
@@ -2802,9 +3369,7 @@ class PortfolioController
$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 %}
+
+
@@ -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 %}
-
+ {% for comment in comments %}
+
+
+ {{ 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 %}
+
{% if (subcategories) %}
-
-