PortfolioController   F
last analyzed

Complexity

Total Complexity 418

Size/Duplication

Total Lines 4498
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2621
dl 0
loc 4498
rs 0.8
c 1
b 0
f 0
wmc 418

58 Methods

Rating   Name   Duplication   Size   Complexity  
B generateAttachmentList() 0 63 8
C commentVisibilityChooser() 0 131 10
A copyComment() 0 28 1
B processAttachments() 0 63 8
A getCommentsInHtmlFormatted() 0 25 2
A copyItem() 0 30 1
A showHideCategory() 0 17 2
F editItem() 0 194 16
A createFormTagFilter() 0 49 5
A addCategory() 0 71 4
A deleteCategory() 0 15 2
C addItem() 0 241 10
A translateDisplayName() 0 5 2
C itemVisibilityChooser() 0 132 10
F getItemsForIndex() 0 173 27
A qualifyComment() 0 73 2
A markAsTemplate() 0 21 3
A translateCategory() 0 76 3
C fixMediaSourcesToHtml() 0 65 14
B generateItemContent() 0 51 7
A getHighlightedItems() 0 63 4
A markImportantCommentInItem() 0 19 2
B getCommentsForIndex() 0 66 8
A __construct() 0 13 3
A deleteTag() 0 22 2
F listCategories() 0 93 13
B deleteAttachment() 0 63 9
C listTags() 0 150 10
F view() 0 373 42
B editCategory() 0 84 5
B teacherCopyComment() 0 84 5
B editComment() 0 100 5
B teacherCopyItem() 0 80 5
A markAsHighlighted() 0 28 4
B downloadAttachment() 0 47 7
F exportZip() 0 228 21
A getLabelForCommentDate() 0 26 4
A renderView() 0 29 5
A deleteItem() 0 20 2
A showHideItem() 0 28 5
A formatZipIndexFile() 0 41 3
F exportPdf() 0 118 15
B createCommentForm() 0 109 5
A addAttachmentsFieldToForm() 0 21 1
A getCategoriesForIndex() 0 14 4
A categoryBelongToOwner() 0 7 2
A blockIsNotAllowed() 0 4 2
B createFormStudentFilter() 0 86 6
F index() 0 153 19
A getLanguageVariable() 0 10 2
A commentBelongsToOwner() 0 3 1
A itemBelongToOwner() 0 7 2
B getItemsInHtmlFormatted() 0 56 8
A deleteComment() 0 20 2
A isAllowed() 0 18 5
F details() 0 361 38
A markAsTemplateComment() 0 21 3
A qualifyItem() 0 65 2

How to fix   Complexity   

Complex Class

Complex classes like PortfolioController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PortfolioController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\ExtraField as ExtraFieldEntity;
7
use Chamilo\CoreBundle\Entity\ExtraFieldRelTag;
8
use Chamilo\CoreBundle\Entity\Portfolio;
9
use Chamilo\CoreBundle\Entity\PortfolioAttachment;
10
use Chamilo\CoreBundle\Entity\PortfolioCategory;
11
use Chamilo\CoreBundle\Entity\PortfolioComment;
12
use Chamilo\CoreBundle\Entity\PortfolioRelTag;
13
use Chamilo\CoreBundle\Entity\Tag;
14
use Chamilo\CoreBundle\Entity\User;
15
use Chamilo\CoreBundle\Event\Events;
16
use Chamilo\CoreBundle\Event\PortfolioCommentEditedEvent;
17
use Chamilo\CoreBundle\Event\PortfolioCommentScoredEvent;
18
use Chamilo\CoreBundle\Event\PortfolioItemAddedEvent;
19
use Chamilo\CoreBundle\Event\PortfolioItemCommentedEvent;
20
use Chamilo\CoreBundle\Event\PortfolioItemDeletedEvent;
21
use Chamilo\CoreBundle\Event\PortfolioItemDownloadedEvent;
22
use Chamilo\CoreBundle\Event\PortfolioItemEditedEvent;
23
use Chamilo\CoreBundle\Event\PortfolioItemScoredEvent;
24
use Chamilo\CoreBundle\Event\PortfolioItemViewedEvent;
25
use Chamilo\CoreBundle\Event\PortfolioItemVisibilityChangedEvent;
26
use Chamilo\CoreBundle\Framework\Container;
27
use Chamilo\CourseBundle\Entity\CItemProperty;
28
use Doctrine\ORM\Query\Expr\Join;
29
use Mpdf\MpdfException;
30
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
31
use Symfony\Component\Filesystem\Filesystem;
32
use Symfony\Component\HttpFoundation\Request as HttpRequest;
33
34
/**
35
 * Class PortfolioController.
36
 */
37
class PortfolioController
38
{
39
    public string $baseUrl;
40
    private ?\Chamilo\CoreBundle\Entity\Course $course;
41
    private ?\Chamilo\CoreBundle\Entity\Session $session;
42
    private \Chamilo\CoreBundle\Entity\User $owner;
43
    private \Doctrine\ORM\EntityManagerInterface $em;
44
    private bool $advancedSharingEnabled;
45
46
    /**
47
     * PortfolioController constructor.
48
     */
49
    public function __construct()
50
    {
51
        $this->em = Database::getManager();
52
53
        $this->owner = api_get_user_entity();
54
        $this->course = api_get_course_entity();
55
        $this->session = api_get_session_entity();
56
57
        $cidreq = api_get_cidreq();
58
        $this->baseUrl = api_get_self().'?'.($cidreq ? $cidreq.'&' : '');
59
60
        $this->advancedSharingEnabled = true === api_get_configuration_value('portfolio_advanced_sharing')
61
            && $this->course;
62
    }
63
64
    /**
65
     * @throws Exception
66
     */
67
    public function translateCategory($category, $languages, $languageId): void
68
    {
69
        global $interbreadcrumb;
70
71
        $originalName = $category->getTitle();
72
        $variableLanguage = '$'.$this->getLanguageVariable($originalName);
73
74
        $translateUrl = api_get_path(WEB_AJAX_PATH).'lang.ajax.php?a=translate_portfolio_category&sec_token='.Security::get_token();
75
        $form = new FormValidator('new_lang_variable', 'POST', $translateUrl);
76
        $form->addHeader(get_lang('Add terms to the sub-language'));
77
        $form->addText('variable_language', get_lang('Language variable'), false);
78
        $form->addText('original_name', get_lang('Original name'), false);
79
80
        $languagesOptions = [0 => get_lang('None')];
81
        foreach ($languages as $language) {
82
            $languagesOptions[$language->getId()] = $language->getOriginalName();
83
        }
84
85
        $form->addSelect(
86
            'sub_language',
87
            [get_lang('Sub-language'), get_lang('Only active sub-languages appear in this list')],
88
            $languagesOptions
89
        );
90
91
        if ($languageId) {
92
            $languageInfo = api_get_language_info($languageId);
93
            $form->addText(
94
                'new_language',
95
                [get_lang('Translation'), get_lang('If this term has already been translated, this operation will replace its translation for this sub-language.')]
96
            );
97
98
            $form->addHidden('category_id', $category->getId());
99
            $form->addHidden('id', $languageInfo['parent_id']);
100
            $form->addHidden('sub', $languageInfo['id']);
101
            $form->addHidden('sub_language_id', $languageInfo['id']);
102
            $form->addHidden('redirect', true);
103
            $form->addButtonSave(get_lang('Save'));
104
        }
105
106
        $form->setDefaults([
107
            'variable_language' => $variableLanguage,
108
            'original_name' => $originalName,
109
            'sub_language' => $languageId,
110
        ]);
111
        $form->addRule('sub_language', get_lang('Required'), 'required');
112
        $form->freeze(['variable_language', 'original_name']);
113
114
        $interbreadcrumb[] = [
115
            'name' => get_lang('Portfolio'),
116
            'url' => $this->baseUrl,
117
        ];
118
        $interbreadcrumb[] = [
119
            'name' => get_lang('Categories'),
120
            'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
121
        ];
122
        $interbreadcrumb[] = [
123
            'name' => Security::remove_XSS($category->getTitle()),
124
            'url' => $this->baseUrl.'action=edit_category&id='.$category->getId(),
125
        ];
126
127
        $actions = [];
128
        $actions[] = Display::url(
129
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
130
            $this->baseUrl.'action=edit_category&id='.$category->getId()
131
        );
132
133
        $js = '<script>
134
            $(function() {
135
              $("select[name=\'sub_language\']").on("change", function () {
136
                    location.href += "&sub_language=" + this.value;
137
                });
138
            });
139
        </script>';
140
        $content = $form->returnForm();
141
142
        $this->renderView($content.$js, get_lang('Translate category'), $actions);
143
    }
144
145
    public function listCategories(): void
146
    {
147
        global $interbreadcrumb;
148
149
        $parentId = isset($_REQUEST['parent_id']) ? (int) $_REQUEST['parent_id'] : 0;
150
        $table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
151
        $headers = [
152
            get_lang('Title'),
153
            get_lang('Description'),
154
        ];
155
        if ($parentId === 0) {
156
            $headers[] = get_lang('Sub-categories');
157
        }
158
        $headers[] = get_lang('Actions');
159
160
        $column = 0;
161
        foreach ($headers as $header) {
162
            $table->setHeaderContents(0, $column, $header);
163
            $column++;
164
        }
165
        $currentUserId = api_get_user_id();
166
        $row = 1;
167
        $categories = $this->getCategoriesForIndex($parentId);
168
169
        foreach ($categories as $category) {
170
            $column = 0;
171
            $subcategories = $this->getCategoriesForIndex($category->getId());
172
            $linkSubCategories = $category->getTitle();
173
            if (count($subcategories) > 0) {
174
                $linkSubCategories = Display::url(
175
                    $category->getTitle(),
176
                    $this->baseUrl.'action=list_categories&parent_id='.$category->getId()
177
                );
178
            }
179
            $table->setCellContents($row, $column++, $linkSubCategories);
180
            $table->setCellContents($row, $column++, strip_tags($category->getDescription()));
181
            if ($parentId === 0) {
182
                $table->setCellContents($row, $column++, count($subcategories));
183
            }
184
185
            // Actions
186
            $links = null;
187
            // Edit action
188
            $url = $this->baseUrl.'action=edit_category&id='.$category->getId();
189
            $links .= Display::url(Display::return_icon('edit.png', get_lang('Edit')), $url).'&nbsp;';
190
            // Visible action: if active
191
            if ($category->isVisible() != 0) {
192
                $url = $this->baseUrl.'action=hide_category&id='.$category->getId();
193
                $links .= Display::url(Display::return_icon('visible.png', get_lang('Hide')), $url).'&nbsp;';
194
            } else { // else if not active
195
                $url = $this->baseUrl.'action=show_category&id='.$category->getId();
196
                $links .= Display::url(Display::return_icon('invisible.png', get_lang('Show')), $url).'&nbsp;';
197
            }
198
            // Delete action
199
            $url = $this->baseUrl.'action=delete_category&id='.$category->getId();
200
            $links .= Display::url(Display::return_icon('delete.png', get_lang('Delete')), $url, ['onclick' => 'javascript:if(!confirm(\''.get_lang('Are you sure to delete?').'\')) return false;']);
201
202
            $table->setCellContents($row, $column++, $links);
203
            $row++;
204
        }
205
206
        $interbreadcrumb[] = [
207
            'name' => get_lang('Portfolio'),
208
            'url' => $this->baseUrl,
209
        ];
210
        if ($parentId > 0) {
211
            $interbreadcrumb[] = [
212
                'name' => get_lang('Categories'),
213
                'url' => $this->baseUrl.'action=list_categories',
214
            ];
215
        }
216
217
        $actions = [];
218
        $actions[] = Display::url(
219
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
220
            $this->baseUrl.($parentId > 0 ? 'action=list_categories' : '')
221
        );
222
        if ($currentUserId == $this->owner->getId() && $parentId === 0) {
223
            $actions[] = Display::url(
224
                Display::return_icon('new_folder.png', get_lang('Add category'), [], ICON_SIZE_MEDIUM),
225
                $this->baseUrl.'action=add_category'
226
            );
227
        }
228
        $content = $table->toHtml();
229
230
        $pageTitle = get_lang('Categories');
231
        if ($parentId > 0) {
232
            $em = Database::getManager();
233
            $parentCategory = $em->find(PortfolioCategory::class, $parentId);
234
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('Sub-categories');
235
        }
236
237
        $this->renderView($content, $pageTitle, $actions);
238
    }
239
240
    /**
241
     * @throws Exception
242
     */
243
    public function addCategory(): void
244
    {
245
        global $interbreadcrumb;
246
247
        Display::addFlash(
248
            Display::return_message(get_lang('Categories are for organization only in personal portfolio.'), 'info')
249
        );
250
251
        $form = new FormValidator('add_category', 'post', "{$this->baseUrl}&action=add_category");
252
253
        if (api_get_configuration_value('save_titles_as_html')) {
254
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
255
        } else {
256
            $form->addText('title', get_lang('Title'));
257
            $form->applyFilter('title', 'trim');
258
        }
259
260
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
261
262
        $parentSelect = $form->addSelect(
263
            'parent_id',
264
            get_lang('Parent category')
265
        );
266
        $parentSelect->addOption(get_lang('Level0'), 0);
267
        $categories = $this->getCategoriesForIndex(0);
268
269
        foreach ($categories as $category) {
270
            $parentSelect->addOption($category->getTitle(), $category->getId());
271
        }
272
273
        $form->addButtonCreate(get_lang('Create'));
274
275
        if ($form->validate()) {
276
            $values = $form->exportValues();
277
278
            $category = new PortfolioCategory();
279
            $category
280
                ->setTitle($values['title'])
281
                ->setDescription($values['description'])
282
                ->setParentId($values['parent_id'])
283
                ->setUser($this->owner);
284
285
            $this->em->persist($category);
286
            $this->em->flush();
287
288
            Display::addFlash(
289
                Display::return_message(get_lang('Category added'), 'success')
290
            );
291
292
            header("Location: {$this->baseUrl}action=list_categories");
293
            exit;
294
        }
295
296
        $interbreadcrumb[] = [
297
            'name' => get_lang('Portfolio'),
298
            'url' => $this->baseUrl,
299
        ];
300
        $interbreadcrumb[] = [
301
            'name' => get_lang('Categories'),
302
            'url' => $this->baseUrl.'action=list_categories',
303
        ];
304
305
        $actions = [];
306
        $actions[] = Display::url(
307
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
308
            $this->baseUrl.'action=list_categories'
309
        );
310
311
        $content = $form->returnForm();
312
313
        $this->renderView($content, get_lang('Add category'), $actions);
314
    }
315
316
    /**
317
     * @throws \Exception
318
     */
319
    public function editCategory(PortfolioCategory $category): void
320
    {
321
        global $interbreadcrumb;
322
323
        if (!api_is_platform_admin()) {
324
            api_not_allowed(true);
325
        }
326
327
        Display::addFlash(
328
            Display::return_message(get_lang('Categories are for organization only in personal portfolio.'), 'info')
329
        );
330
331
        $form = new FormValidator(
332
            'edit_category',
333
            'post',
334
            $this->baseUrl."action=edit_category&id={$category->getId()}"
335
        );
336
337
        if (api_get_configuration_value('save_titles_as_html')) {
338
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
339
        } else {
340
            $translateUrl = $this->baseUrl.'action=translate_category&id='.$category->getId();
341
            $translateButton = Display::toolbarButton(get_lang('Translate this term'), $translateUrl, 'language', 'link');
342
            $form->addText(
343
                'title',
344
                [get_lang('Title'), $translateButton]
345
            );
346
            $form->applyFilter('title', 'trim');
347
        }
348
349
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
350
        $form->addButtonUpdate(get_lang('Update'));
351
        $form->setDefaults(
352
            [
353
                'title' => $category->getTitle(),
354
                'description' => $category->getDescription(),
355
            ]
356
        );
357
358
        if ($form->validate()) {
359
            $values = $form->exportValues();
360
361
            $category
362
                ->setTitle($values['title'])
363
                ->setDescription($values['description']);
364
365
            $this->em->persist($category);
366
            $this->em->flush();
367
368
            Display::addFlash(
369
                Display::return_message(get_lang('Updated'), 'success')
370
            );
371
372
            header("Location: {$this->baseUrl}action=list_categories&parent_id=".$category->getParentId());
373
            exit;
374
        }
375
376
        $interbreadcrumb[] = [
377
            'name' => get_lang('Portfolio'),
378
            'url' => $this->baseUrl,
379
        ];
380
        $interbreadcrumb[] = [
381
            'name' => get_lang('Categories'),
382
            'url' => $this->baseUrl.'action=list_categories',
383
        ];
384
        if ($category->getParentId() > 0) {
385
            $em = Database::getManager();
386
            $parentCategory = $em->find(PortfolioCategory::class, $category->getParentId());
387
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('Sub-categories');
388
            $interbreadcrumb[] = [
389
                'name' => Security::remove_XSS($pageTitle),
390
                'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
391
            ];
392
        }
393
394
        $actions = [];
395
        $actions[] = Display::url(
396
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
397
            $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId()
398
        );
399
400
        $content = $form->returnForm();
401
402
        $this->renderView($content, get_lang('Edit this category'), $actions);
403
    }
404
405
    /**
406
     * @throws \Doctrine\ORM\OptimisticLockException
407
     * @throws \Doctrine\ORM\Exception\ORMException
408
     */
409
    public function showHideCategory(PortfolioCategory $category): never
410
    {
411
        if (!$this->categoryBelongToOwner($category)) {
412
            api_not_allowed(true);
413
        }
414
415
        $category->setIsVisible(!$category->isVisible());
416
417
        $this->em->persist($category);
418
        $this->em->flush();
419
420
        Display::addFlash(
421
            Display::return_message(get_lang('Post visibility changed'), 'success')
422
        );
423
424
        header("Location: {$this->baseUrl}action=list_categories");
425
        exit;
426
    }
427
428
    /**
429
     * @throws \Doctrine\ORM\OptimisticLockException
430
     * @throws \Doctrine\ORM\Exception\ORMException
431
     */
432
    public function deleteCategory(PortfolioCategory $category): never
433
    {
434
        if (!api_is_platform_admin()) {
435
            api_not_allowed(true);
436
        }
437
438
        $this->em->remove($category);
439
        $this->em->flush();
440
441
        Display::addFlash(
442
            Display::return_message(get_lang('The category has been deleted.'), 'success')
443
        );
444
445
        header("Location: {$this->baseUrl}action=list_categories");
446
        exit;
447
    }
448
449
    /**
450
     * @throws \Exception
451
     */
452
    public function addItem(): void
453
    {
454
        global $interbreadcrumb;
455
456
        $this->blockIsNotAllowed();
457
458
        $templates = $this->em
459
            ->getRepository(Portfolio::class)
460
            ->findBy(
461
                [
462
                    'isTemplate' => true,
463
                    'course' => $this->course,
464
                    'session' => $this->session,
465
                    'user' => $this->owner,
466
                ]
467
            );
468
469
        $form = new FormValidator('add_portfolio', 'post', $this->baseUrl.'action=add_item');
470
        $form->addSelectFromCollection(
471
            'template',
472
            [
473
                get_lang('Template'),
474
                null,
475
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
476
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
477
            ],
478
            $templates,
479
            [],
480
            true,
481
            'getTitle'
482
        );
483
484
        if (api_get_configuration_value('save_titles_as_html')) {
485
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
486
        } else {
487
            $form->addText('title', get_lang('Title'));
488
            $form->applyFilter('title', 'trim');
489
        }
490
        $editorConfig = [
491
            'ToolbarSet' => 'Documents',
492
            'Width' => '100%',
493
            'Height' => '400',
494
            'cols-size' => [2, 10, 0],
495
        ];
496
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
497
498
        $categoriesSelect = $form->addSelect(
499
            'category',
500
            [get_lang('Category'), get_lang('Categories are for organization only in personal portfolio.')]
501
        );
502
        $categoriesSelect->addOption(get_lang('Select a category'), 0);
503
        $parentCategories = $this->getCategoriesForIndex(0);
504
        foreach ($parentCategories as $parentCategory) {
505
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
506
            $subCategories = $this->getCategoriesForIndex($parentCategory->getId());
507
            if (count($subCategories) > 0) {
508
                foreach ($subCategories as $subCategory) {
509
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
510
                }
511
            }
512
        }
513
514
        $extraField = new ExtraField('portfolio');
515
        $extra = $extraField->addElements(
516
            $form,
517
            0,
518
            $this->course ? [] : ['tags']
519
        );
520
521
        $this->addAttachmentsFieldToForm($form);
522
523
        $form->addButtonCreate(get_lang('Create'));
524
525
        if ($form->validate()) {
526
            $values = $form->exportValues();
527
            $currentTime = new DateTime(
528
                api_get_utc_datetime(),
529
                new DateTimeZone('UTC')
530
            );
531
532
            $portfolio = new Portfolio();
533
            $portfolio
534
                ->setTitle($values['title'])
535
                ->setContent($values['content'])
536
                ->setUser($this->owner)
537
                ->setCourse($this->course)
538
                ->setSession($this->session)
539
                ->setCategory(
540
                    $this->em->find(PortfolioCategory::class, $values['category'])
541
                )
542
                ->setCreationDate($currentTime)
543
                ->setUpdateDate($currentTime);
544
545
            $this->em->persist($portfolio);
546
            $this->em->flush();
547
548
            $values['item_id'] = $portfolio->getId();
549
550
            $extraFieldValue = new ExtraFieldValue('portfolio');
551
            $extraFieldValue->saveFieldValues($values);
552
553
            $this->processAttachments(
554
                $form,
555
                $portfolio->getUser(),
556
                $portfolio->getId(),
557
                PortfolioAttachment::TYPE_ITEM
558
            );
559
560
            Container::getEventDispatcher()->dispatch(
561
                new PortfolioItemAddedEvent(['portfolio' => $portfolio]),
562
                Events::PORTFOLIO_ITEM_ADDED
563
            );
564
565
            if (1 == api_get_course_setting('email_alert_teachers_new_post')) {
566
                if ($this->session) {
567
                    $messageCourseTitle = "{$this->course->getTitle()} ({$this->session->getName()})";
568
569
                    $teachers = SessionManager::getCoachesByCourseSession(
570
                        $this->session->getId(),
571
                        $this->course->getId()
572
                    );
573
                    $userIdListToSend = array_values($teachers);
574
                } else {
575
                    $messageCourseTitle = $this->course->getTitle();
576
577
                    $teachers = CourseManager::get_teacher_list_from_course_code($this->course->getCode());
578
579
                    $userIdListToSend = array_keys($teachers);
580
                }
581
582
                $messageSubject = sprintf(get_lang('[Portfolio] New post in course %s'), $messageCourseTitle);
583
                $messageContent = sprintf(
584
                    get_lang('There is a new post by %s in the portfolio of course %s. To view it <a href="%s">go here</a>.'),
585
                    $this->owner->getFullName(),
586
                    $messageCourseTitle,
587
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()])
588
                );
589
                $messageContent .= '<br><br><dl>'
590
                    .'<dt>'.Security::remove_XSS($portfolio->getTitle()).'</dt>'
591
                    .'<dd>'.$portfolio->getExcerpt().'</dd>'.'</dl>';
592
593
                foreach ($userIdListToSend as $userIdToSend) {
594
                    MessageManager::send_message_simple(
595
                        $userIdToSend,
596
                        $messageSubject,
597
                        $messageContent,
598
                        0,
599
                        false,
600
                        false,
601
                        [],
602
                        false
603
                    );
604
                }
605
            }
606
607
            Display::addFlash(
608
                Display::return_message(get_lang('Portfolio item added'), 'success')
609
            );
610
611
            header("Location: $this->baseUrl");
612
            exit;
613
        }
614
615
        $interbreadcrumb[] = [
616
            'name' => get_lang('Portfolio'),
617
            'url' => $this->baseUrl,
618
        ];
619
620
        $actions = [];
621
        $actions[] = Display::url(
622
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
623
            $this->baseUrl
624
        );
625
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
626
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
627
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
628
629
        $js = '<script>
630
            $(function() {
631
                $(".scrollbar-light").scrollbar();
632
                $(".scroll-wrapper").css("height", "550px");
633
                expandColumnToogle("#hide_bar_template", {
634
                    selector: "#template_col",
635
                    width: 3
636
                }, {
637
                    selector: "#doc_form",
638
                    width: 9
639
                });
640
                CKEDITOR.on("instanceReady", function (e) {
641
                    showTemplates();
642
                });
643
                $(window).on("load", function () {
644
                    $("input[name=\'title\']").focus();
645
                });
646
                $(\'#add_portfolio_template\').on(\'change\', function () {
647
                    $(\'#portfolio-spinner\').show();
648
649
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template&item=\' + this.value)
650
                        .done(function(response) {
651
                            if (CKEDITOR.instances.title) {
652
                                CKEDITOR.instances.title.setData(response.title);
653
                            } else {
654
                                document.getElementById(\'add_portfolio_title\').value = response.title;
655
                            }
656
657
                            CKEDITOR.instances.content.setData(response.content);
658
                        })
659
                        .fail(function () {
660
                            if (CKEDITOR.instances.title) {
661
                                CKEDITOR.instances.title.setData(\'\');
662
                            } else {
663
                                document.getElementById(\'add_portfolio_title\').value = \'\';
664
                            }
665
666
                            CKEDITOR.instances.content.setData(\'\');
667
                        })
668
                        .always(function() {
669
                          $(\'#portfolio-spinner\').hide();
670
                        });
671
                });
672
                '.$extra['jquery_ready_content'].'
673
            });
674
        </script>';
675
        $content = '<div class="page-create">
676
            <div class="row" style="overflow:hidden">
677
            <div id="template_col" class="col-md-3">
678
                <div class="panel panel-default">
679
                <div class="panel-body">
680
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
681
                </div>
682
                </div>
683
            </div>
684
            <div id="doc_form" class="col-md-9">
685
                '.$form->returnForm().'
686
            </div>
687
          </div></div>';
688
689
        $this->renderView(
690
            $content.$js,
691
            get_lang('Add item to portfolio'),
692
            $actions
693
        );
694
    }
695
696
    /**
697
     * @throws \Exception
698
     */
699
    public function editItem(Portfolio $item): void
700
    {
701
        global $interbreadcrumb;
702
703
        if (!api_is_allowed_to_edit() && !$this->itemBelongToOwner($item)) {
704
            api_not_allowed(true);
705
        }
706
707
        $itemCourse = $item->getCourse();
708
        $itemSession = $item->getSession();
709
710
        $form = new FormValidator('edit_portfolio', 'post', $this->baseUrl."action=edit_item&id={$item->getId()}");
711
712
        if (api_get_configuration_value('save_titles_as_html')) {
713
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
714
        } else {
715
            $form->addText('title', get_lang('Title'));
716
            $form->applyFilter('title', 'trim');
717
        }
718
719
        if ($item->getOrigin()) {
720
            if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
721
                $origin = $this->em->find(Portfolio::class, $item->getOrigin());
722
723
                $form->addLabel(
724
                    sprintf(get_lang('Portfolio item by %s'), $origin->getUser()->getFullName()),
725
                    Display::panel(
726
                        Security::remove_XSS($origin->getContent())
727
                    )
728
                );
729
            } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
730
                $origin = $this->em->find(PortfolioComment::class, $item->getOrigin());
731
732
                $form->addLabel(
733
                    sprintf(get_lang('Comment by %s'), $origin->getAuthor()->getFullName()),
734
                    Display::panel(
735
                        Security::remove_XSS($origin->getContent())
736
                    )
737
                );
738
            }
739
        }
740
        $editorConfig = [
741
            'ToolbarSet' => 'Documents',
742
            'Width' => '100%',
743
            'Height' => '400',
744
            'cols-size' => [2, 10, 0],
745
        ];
746
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
747
        $categoriesSelect = $form->addSelect(
748
            'category',
749
            [get_lang('Category'), get_lang('Categories are for organization only in personal portfolio.')]
750
        );
751
        $categoriesSelect->addOption(get_lang('Select a category'), 0);
752
        $parentCategories = $this->getCategoriesForIndex(0);
753
        foreach ($parentCategories as $parentCategory) {
754
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
755
            $subCategories = $this->getCategoriesForIndex($parentCategory->getId());
756
            if (count($subCategories) > 0) {
757
                foreach ($subCategories as $subCategory) {
758
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
759
                }
760
            }
761
        }
762
763
        $extraField = new ExtraField('portfolio');
764
        $extra = $extraField->addElements(
765
            $form,
766
            $item->getId(),
767
            $this->course ? [] : ['tags']
768
        );
769
770
        $attachmentList = $this->generateAttachmentList($item, false);
771
772
        if (!empty($attachmentList)) {
773
            $form->addLabel(get_lang('Attachments'), $attachmentList);
774
        }
775
776
        $this->addAttachmentsFieldToForm($form);
777
778
        $form->addButtonUpdate(get_lang('Update'));
779
        $form->setDefaults(
780
            [
781
                'title' => $item->getTitle(),
782
                'content' => $item->getContent(),
783
                'category' => $item->getCategory() ? $item->getCategory()->getId() : '',
784
            ]
785
        );
786
787
        if ($form->validate()) {
788
            if ($itemCourse) {
789
                api_item_property_update(
790
                    api_get_course_info($itemCourse->getCode()),
791
                    TOOL_PORTFOLIO,
792
                    $item->getId(),
793
                    'PortfolioUpdated',
794
                    api_get_user_id(),
795
                    [],
796
                    null,
797
                    '',
798
                    '',
799
                    $itemSession ? $itemSession->getId() : 0
800
                );
801
            }
802
803
            $values = $form->exportValues();
804
            $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
805
806
            $item
807
                ->setTitle($values['title'])
808
                ->setContent($values['content'])
809
                ->setUpdateDate($currentTime)
810
                ->setCategory(
811
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
812
                );
813
814
            $values['item_id'] = $item->getId();
815
816
            $extraFieldValue = new ExtraFieldValue('portfolio');
817
            $extraFieldValue->saveFieldValues($values);
818
819
            $this->em->persist($item);
820
            $this->em->flush();
821
822
            Container::getEventDispatcher()->dispatch(
823
                new PortfolioItemEditedEvent(['portfolio' => $item]),
824
                Events::PORTFOLIO_ITEM_EDITED
825
            );
826
827
            $this->processAttachments(
828
                $form,
829
                $item->getUser(),
830
                $item->getId(),
831
                PortfolioAttachment::TYPE_ITEM
832
            );
833
834
            Display::addFlash(
835
                Display::return_message(get_lang('Item updated'), 'success')
836
            );
837
838
            header("Location: $this->baseUrl");
839
            exit;
840
        }
841
842
        $interbreadcrumb[] = [
843
            'name' => get_lang('Portfolio'),
844
            'url' => $this->baseUrl,
845
        ];
846
        $actions = [];
847
        $actions[] = Display::url(
848
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
849
            $this->baseUrl
850
        );
851
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
852
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
853
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
854
855
        $js = '<script>
856
            $(function() {
857
                $(".scrollbar-light").scrollbar();
858
                $(".scroll-wrapper").css("height", "550px");
859
                expandColumnToogle("#hide_bar_template", {
860
                    selector: "#template_col",
861
                    width: 3
862
                }, {
863
                    selector: "#doc_form",
864
                    width: 9
865
                });
866
                CKEDITOR.on("instanceReady", function (e) {
867
                    showTemplates();
868
                });
869
                $(window).on("load", function () {
870
                    $("input[name=\'title\']").focus();
871
                });
872
                '.$extra['jquery_ready_content'].'
873
            });
874
        </script>';
875
        $content = '<div class="page-create">
876
            <div class="row" style="overflow:hidden">
877
            <div id="template_col" class="col-md-3">
878
                <div class="panel panel-default">
879
                <div class="panel-body">
880
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
881
                </div>
882
                </div>
883
            </div>
884
            <div id="doc_form" class="col-md-9">
885
                '.$form->returnForm().'
886
            </div>
887
          </div></div>';
888
889
        $this->renderView(
890
            $content.$js,
891
            get_lang('Edit portfolio item'),
892
            $actions
893
        );
894
    }
895
896
    /**
897
     * @throws \Doctrine\ORM\ORMException
898
     * @throws \Doctrine\ORM\OptimisticLockException
899
     */
900
    public function showHideItem(Portfolio $item): never
901
    {
902
        if (!$this->itemBelongToOwner($item)) {
903
            api_not_allowed(true);
904
        }
905
906
        switch ($item->getVisibility()) {
907
            case Portfolio::VISIBILITY_HIDDEN:
908
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
909
                break;
910
            case Portfolio::VISIBILITY_VISIBLE:
911
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER);
912
                break;
913
            case Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER:
914
            default:
915
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN);
916
                break;
917
        }
918
919
        $this->em->persist($item);
920
        $this->em->flush();
921
922
        Display::addFlash(
923
            Display::return_message(get_lang('The visibility has been changed.'), 'success')
924
        );
925
926
        header("Location: $this->baseUrl");
927
        exit;
928
    }
929
930
    /**
931
     * @throws \Doctrine\ORM\ORMException
932
     * @throws \Doctrine\ORM\OptimisticLockException
933
     */
934
    public function deleteItem(Portfolio $item)
935
    {
936
        if (!$this->itemBelongToOwner($item)) {
937
            api_not_allowed(true);
938
        }
939
940
        Container::getEventDispatcher()->dispatch(
941
            new PortfolioItemDeletedEvent(['portfolio' => $item]),
942
            Events::PORTFOLIO_ITEM_DELETED
943
        );
944
945
        $this->em->remove($item);
946
        $this->em->flush();
947
948
        Display::addFlash(
949
            Display::return_message(get_lang('Item deleted'), 'success')
950
        );
951
952
        header("Location: $this->baseUrl");
953
        exit;
954
    }
955
956
    /**
957
     * @throws \Exception
958
     */
959
    public function index(HttpRequest $httpRequest)
960
    {
961
        $listByUser = false;
962
        $listHighlighted = $httpRequest->query->has('list_highlighted');
963
        $listAlphabetical = $httpRequest->query->has('list_alphabetical');
964
965
        if ($httpRequest->query->has('user')) {
966
            $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
967
968
            if (empty($this->owner)) {
969
                api_not_allowed(true);
970
            }
971
972
            $listByUser = true;
973
        }
974
975
        $currentUserId = api_get_user_id();
976
977
        $actions = [];
978
979
        if (api_is_platform_admin()) {
980
            $actions[] = Display::url(
981
                Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
982
                $this->baseUrl.'action=add_item'
983
            );
984
            $actions[] = Display::url(
985
                Display::return_icon('folder.png', get_lang('Add category'), [], ICON_SIZE_MEDIUM),
986
                $this->baseUrl.'action=list_categories'
987
            );
988
            $actions[] = Display::url(
989
                Display::return_icon('waiting_list.png', get_lang('Portfolio details'), [], ICON_SIZE_MEDIUM),
990
                $this->baseUrl.'action=details'
991
            );
992
        } elseif ($currentUserId == $this->owner->getId()) {
993
            if ($this->isAllowed()) {
994
                $actions[] = Display::url(
995
                    Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
996
                    $this->baseUrl.'action=add_item'
997
                );
998
                $actions[] = Display::url(
999
                    Display::return_icon('waiting_list.png', get_lang('Portfolio details'), [], ICON_SIZE_MEDIUM),
1000
                    $this->baseUrl.'action=details'
1001
                );
1002
            }
1003
        } else {
1004
            $actions[] = Display::url(
1005
                Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1006
                $this->baseUrl
1007
            );
1008
        }
1009
1010
        if (api_is_allowed_to_edit()) {
1011
            $actions[] = Display::url(
1012
                Display::return_icon('tickets.png', get_lang('Tags'), [], ICON_SIZE_MEDIUM),
1013
                $this->baseUrl.'action=tags'
1014
            );
1015
        }
1016
1017
        $frmStudentList = null;
1018
        $frmTagList = null;
1019
1020
        $categories = [];
1021
        $portfolio = [];
1022
        if ($this->course) {
1023
            $frmTagList = $this->createFormTagFilter($listByUser);
1024
            $frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted, $listAlphabetical);
1025
            $frmStudentList->setDefaults(['user' => $this->owner->getId()]);
1026
            // it translates the category title with the current user language
1027
            $categories = $this->getCategoriesForIndex(0);
1028
            if (count($categories) > 0) {
1029
                foreach ($categories as &$category) {
1030
                    $translated = $this->translateDisplayName($category->getTitle());
1031
                    $category->setTitle($translated);
1032
                }
1033
            }
1034
        } else {
1035
            // it displays the list in Network Social for the current user
1036
            $portfolio = $this->getCategoriesForIndex();
1037
        }
1038
1039
        $foundComments = [];
1040
1041
        if ($listHighlighted) {
1042
            $items = $this->getHighlightedItems();
1043
        } else {
1044
            $items = $this->getItemsForIndex($listByUser, $frmTagList, $listAlphabetical);
1045
1046
            $foundComments = $this->getCommentsForIndex($frmTagList);
1047
        }
1048
1049
        // it gets and translate the sub-categories
1050
        $categoryId = $httpRequest->query->getInt('categoryId');
1051
        $subCategoryIdsReq = isset($_REQUEST['subCategoryIds']) ? Security::remove_XSS($_REQUEST['subCategoryIds']) : '';
1052
        $subCategoryIds = $subCategoryIdsReq;
1053
        if ('all' !== $subCategoryIdsReq) {
1054
            $subCategoryIds = !empty($subCategoryIdsReq) ? explode(',', $subCategoryIdsReq) : [];
1055
        }
1056
        $subcategories = [];
1057
        if ($categoryId > 0) {
1058
            $subcategories = $this->getCategoriesForIndex($categoryId);
1059
            if (count($subcategories) > 0) {
1060
                foreach ($subcategories as &$subcategory) {
1061
                    $translated = $this->translateDisplayName($subcategory->getTitle());
1062
                    $subcategory->setTitle($translated);
1063
                }
1064
            }
1065
        }
1066
1067
        $context = [
1068
            'user' => $this->owner,
1069
            'listByUser' => $listByUser,
1070
            'course' => $this->course,
1071
            'session' => $this->session,
1072
            'portfolio' => $portfolio,
1073
            'categories' => $categories,
1074
            'uncategorized_items' => $items,
1075
            'frm_student_list' => $this->course ? $frmStudentList->returnForm() : '',
1076
            'frm_tag_list' => $this->course ? $frmTagList->returnForm() : '',
1077
            'category_id' => $categoryId,
1078
            'subcategories' => $subcategories,
1079
            'subcategory_ids' => $subCategoryIds,
1080
            'found_comments' => $foundComments,
1081
            '_p' => Template::getLegacyP(),
1082
            '_c' => Template::getLegacyC(),
1083
            'is_allowed_to_edit' => api_is_allowed_to_edit(false),
1084
        ];
1085
1086
        $js = '<script>
1087
            $(function() {
1088
                $(".category-filters").bind("click", function() {
1089
                    var categoryId = parseInt($(this).find("input[type=\'radio\']").val());
1090
                    $("input[name=\'categoryId\']").val(categoryId);
1091
                    $("input[name=\'subCategoryIds\']").val("all");
1092
                    $("#frm_tag_list_submit").trigger("click");
1093
                });
1094
                $(".subcategory-filters").bind("click", function() {
1095
                    var checkedVals = $(".subcategory-filters:checkbox:checked").map(function() {
1096
                        return this.value;
1097
                    }).get();
1098
                    $("input[name=\'subCategoryIds\']").val(checkedVals.join(","));
1099
                    $("#frm_tag_list_submit").trigger("click");
1100
                });
1101
            });
1102
        </script>';
1103
        $context['js_script'] = $js;
1104
1105
        $content = Container::getTwig()->render('@ChamiloCore/Portfolio/list.html.twig', $context);
1106
1107
        Display::addFlash(
1108
            Display::return_message(get_lang('Portfolio tool introduction'), 'info', false)
1109
        );
1110
1111
        $this->renderView($content, get_lang('Portfolio'), $actions);
1112
    }
1113
1114
    /**
1115
     * @throws \Doctrine\ORM\ORMException
1116
     * @throws \Doctrine\ORM\OptimisticLockException
1117
     * @throws \Doctrine\ORM\TransactionRequiredException
1118
     */
1119
    public function view(Portfolio $item, $urlUser)
1120
    {
1121
        global $interbreadcrumb;
1122
1123
        if (!$this->itemBelongToOwner($item)) {
1124
            if ($this->advancedSharingEnabled) {
1125
                $courseInfo = api_get_course_info_by_id($this->course->getId());
1126
                $sessionId = $this->session ? $this->session->getId() : 0;
1127
1128
                $itemPropertyVisiblity = api_get_item_visibility(
1129
                    $courseInfo,
1130
                    TOOL_PORTFOLIO,
1131
                    $item->getId(),
1132
                    $sessionId,
1133
                    $this->owner->getId(),
1134
                    'visible'
1135
                );
1136
1137
                if ($item->getVisibility() === Portfolio::VISIBILITY_PER_USER && 1 !== $itemPropertyVisiblity) {
1138
                    api_not_allowed(true);
1139
                }
1140
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN
1141
                || ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER && !api_is_allowed_to_edit())
1142
            ) {
1143
                api_not_allowed(true);
1144
            }
1145
        }
1146
1147
        Container::getEventDispatcher()->dispatch(
1148
            new PortfolioItemViewedEvent(['portfolio' => $item]),
1149
            Events::PORTFOLIO_ITEM_VIEWED
1150
        );
1151
1152
        $itemCourse = $item->getCourse();
1153
        $itemSession = $item->getSession();
1154
1155
        $form = $this->createCommentForm($item);
1156
1157
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1158
1159
        $commentsQueryBuilder = $commentsRepo->createQueryBuilder('comment');
1160
        $commentsQueryBuilder->where('comment.item = :item');
1161
1162
        if ($this->advancedSharingEnabled) {
1163
            $commentsQueryBuilder
1164
                ->leftJoin(
1165
                    CItemProperty::class,
1166
                    'cip',
1167
                    Join::WITH,
1168
                    "cip.ref = comment.id
1169
                        AND cip.tool = :cip_tool
1170
                        AND cip.course = :course
1171
                        AND cip.lasteditType = 'visible'
1172
                        AND cip.toUser = :current_user"
1173
                )
1174
                ->andWhere(
1175
                    sprintf(
1176
                        'comment.visibility = %d
1177
                            OR (
1178
                                comment.visibility = %d AND cip IS NOT NULL OR comment.author = :current_user
1179
                            )',
1180
                        PortfolioComment::VISIBILITY_VISIBLE,
1181
                        PortfolioComment::VISIBILITY_PER_USER
1182
                    )
1183
                )
1184
                ->setParameter('cip_tool', TOOL_PORTFOLIO_COMMENT)
1185
                ->setParameter('current_user', $this->owner->getId())
1186
                ->setParameter('course', $item->getCourse())
1187
            ;
1188
        }
1189
1190
        if (true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
1191
            && $this->session && !$item->getSession() && !$item->isDuplicatedInSession($this->session)
1192
        ) {
1193
            $comments = [];
1194
        } else {
1195
            $comments = $commentsQueryBuilder
1196
                ->orderBy('comment.root, comment.lft', 'ASC')
1197
                ->setParameter('item', $item)
1198
                ->getQuery()
1199
                ->getArrayResult()
1200
            ;
1201
        }
1202
1203
        $clockIcon = Display::returnFontAwesomeIcon('clock-o', '', true);
1204
1205
        $commentsHtml = $commentsRepo->buildTree(
1206
            $comments,
1207
            [
1208
                'decorate' => true,
1209
                'rootOpen' => '<div class="media-list">',
1210
                'rootClose' => '</div>',
1211
                'childOpen' => function ($node) use ($commentsRepo) {
1212
                    /** @var PortfolioComment $comment */
1213
                    $comment = $commentsRepo->find($node['id']);
1214
                    $author = $comment->getAuthor();
1215
1216
                    $userPicture = UserManager::getUserPicture(
1217
                        $comment->getAuthor()->getId(),
1218
                        USER_IMAGE_SIZE_SMALL,
1219
                        null,
1220
                        [
1221
                            'picture_uri' => $author->getPictureUri(),
1222
                            'email' => $author->getEmail(),
1223
                        ]
1224
                    );
1225
1226
                    return '<article class="media" id="comment-'.$node['id'].'">
1227
                        <div class="media-left"><img class="media-object thumbnail" src="'.$userPicture.'" alt="'
1228
                        .$author->getFullName().'"></div>
1229
                        <div class="media-body">';
1230
                },
1231
                'childClose' => '</div></article>',
1232
                'nodeDecorator' => function ($node) use ($commentsRepo, $clockIcon, $item) {
1233
                    $commentActions = [];
1234
                    /** @var PortfolioComment $comment */
1235
                    $comment = $commentsRepo->find($node['id']);
1236
1237
                    if ($this->commentBelongsToOwner($comment)) {
1238
                        $commentActions[] = Display::url(
1239
                            Display::return_icon(
1240
                                $comment->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1241
                                $comment->isTemplate() ? get_lang('Remove as a template') : get_lang('Add as a template')
1242
                            ),
1243
                            $this->baseUrl.http_build_query(['action' => 'template_comment', 'id' => $comment->getId()])
1244
                        );
1245
                    }
1246
1247
                    $commentActions[] = Display::url(
1248
                        Display::return_icon('discuss.png', get_lang('Reply to this comment')),
1249
                        '#',
1250
                        [
1251
                            'data-comment' => htmlspecialchars(
1252
                                json_encode(['id' => $comment->getId()])
1253
                            ),
1254
                            'role' => 'button',
1255
                            'class' => 'btn-reply-to',
1256
                        ]
1257
                    );
1258
                    $commentActions[] = Display::url(
1259
                        Display::return_icon('copy.png', get_lang('Copy to my portfolio')),
1260
                        $this->baseUrl.http_build_query(
1261
                            [
1262
                                'action' => 'copy',
1263
                                'copy' => 'comment',
1264
                                'id' => $comment->getId(),
1265
                            ]
1266
                        )
1267
                    );
1268
1269
                    $isAllowedToEdit = api_is_allowed_to_edit();
1270
1271
                    if ($isAllowedToEdit) {
1272
                        $commentActions[] = Display::url(
1273
                            Display::return_icon('copy.png', get_lang('Copy to student portfolio')),
1274
                            $this->baseUrl.http_build_query(
1275
                                [
1276
                                    'action' => 'teacher_copy',
1277
                                    'copy' => 'comment',
1278
                                    'id' => $comment->getId(),
1279
                                ]
1280
                            )
1281
                        );
1282
1283
                        if ($comment->isImportant()) {
1284
                            $commentActions[] = Display::url(
1285
                                Display::return_icon('drawing-pin.png', get_lang('Unmark comment as important')),
1286
                                $this->baseUrl.http_build_query(
1287
                                    [
1288
                                        'action' => 'mark_important',
1289
                                        'item' => $item->getId(),
1290
                                        'id' => $comment->getId(),
1291
                                    ]
1292
                                )
1293
                            );
1294
                        } else {
1295
                            $commentActions[] = Display::url(
1296
                                Display::return_icon('drawing-pin.png', get_lang('Mark comment as important')),
1297
                                $this->baseUrl.http_build_query(
1298
                                    [
1299
                                        'action' => 'mark_important',
1300
                                        'item' => $item->getId(),
1301
                                        'id' => $comment->getId(),
1302
                                    ]
1303
                                )
1304
                            );
1305
                        }
1306
1307
                        if ($this->course && '1' === api_get_course_setting('qualify_portfolio_comment')) {
1308
                            $commentActions[] = Display::url(
1309
                                Display::return_icon('quiz.png', get_lang('Grade this comment')),
1310
                                $this->baseUrl.http_build_query(
1311
                                    [
1312
                                        'action' => 'qualify',
1313
                                        'comment' => $comment->getId(),
1314
                                    ]
1315
                                )
1316
                            );
1317
                        }
1318
                    }
1319
1320
                    if ($this->commentBelongsToOwner($comment)) {
1321
                        if ($this->advancedSharingEnabled) {
1322
                            $commentActions[] = Display::url(
1323
                                Display::return_icon('visible.png', get_lang('Choose recipients')),
1324
                                $this->baseUrl.http_build_query(['action' => 'comment_visiblity_choose', 'id' => $comment->getId()])
1325
                            );
1326
                        }
1327
1328
                        $commentActions[] = Display::url(
1329
                            Display::return_icon('edit.png', get_lang('Edit')),
1330
                            $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()])
1331
                        );
1332
                        $commentActions[] = Display::url(
1333
                            Display::return_icon('delete.png', get_lang('Delete')),
1334
                            $this->baseUrl.http_build_query(['action' => 'delete_comment', 'id' => $comment->getId()])
1335
                        );
1336
                    }
1337
1338
                    $nodeHtml = '<div class="pull-right">'.implode(PHP_EOL, $commentActions).'</div>'.PHP_EOL
1339
                        .'<footer class="media-heading h4">'.PHP_EOL
1340
                        .'<p>'.$comment->getAuthor()->getFullName().'</p>'.PHP_EOL;
1341
1342
                    if ($comment->isImportant()
1343
                        && ($this->itemBelongToOwner($comment->getItem()) || $isAllowedToEdit)
1344
                    ) {
1345
                        $nodeHtml .= '<span class="pull-right label label-warning origin-style">'
1346
                            .get_lang('Portfolio item marked as important')
1347
                            .'</span>'.PHP_EOL;
1348
                    }
1349
1350
                    $nodeHtml .= '<small>'.$clockIcon.PHP_EOL
1351
                        .$this->getLabelForCommentDate($comment).'</small>'.PHP_EOL;
1352
1353
                    $nodeHtml .= '</footer>'.PHP_EOL
1354
                        .Security::remove_XSS($comment->getContent()).PHP_EOL;
1355
1356
                    $nodeHtml .= $this->generateAttachmentList($comment);
1357
1358
                    return $nodeHtml;
1359
                },
1360
            ]
1361
        );
1362
1363
        $template = new Template(null, false, false, false, false, false, false);
1364
        $template->assign('baseurl', $this->baseUrl);
1365
        $template->assign('item', $item);
1366
        $template->assign('item_content', $this->generateItemContent($item));
1367
        $template->assign('count_comments', count($comments));
1368
        $template->assign('comments', $commentsHtml);
1369
        $template->assign('form', $form);
1370
        $template->assign('attachment_list', $this->generateAttachmentList($item));
1371
1372
        if ($itemCourse) {
1373
            $propertyInfo = api_get_item_property_info(
1374
                $itemCourse->getId(),
1375
                TOOL_PORTFOLIO,
1376
                $item->getId(),
1377
                $itemSession ? $itemSession->getId() : 0
1378
            );
1379
1380
            if ($propertyInfo && empty($propertyInfo['to_user_id'])) {
1381
                $template->assign(
1382
                    'last_edit',
1383
                    [
1384
                        'date' => $propertyInfo['lastedit_date'],
1385
                        'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getFullName(),
1386
                    ]
1387
                );
1388
            }
1389
        }
1390
1391
        $layout = $template->get_template('Portfolio/view.html.twig');
1392
        $content = $template->fetch($layout);
1393
1394
        $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl];
1395
1396
        $editLink = Display::url(
1397
            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM),
1398
            $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
1399
        );
1400
1401
        $urlUserString = "";
1402
        if (!empty($urlUser)) {
1403
            $urlUserString = "user=".$urlUser;
1404
        }
1405
1406
        $actions = [];
1407
        $actions[] = Display::url(
1408
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1409
            $this->baseUrl.$urlUserString
1410
        );
1411
1412
        if ($this->itemBelongToOwner($item)) {
1413
            $actions[] = $editLink;
1414
1415
            $actions[] = Display::url(
1416
                Display::return_icon(
1417
                    $item->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1418
                    $item->isTemplate() ? get_lang('Remove template') : get_lang('Add as a template'),
1419
                    [],
1420
                    ICON_SIZE_MEDIUM
1421
                ),
1422
                $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()])
1423
            );
1424
1425
            if ($this->advancedSharingEnabled) {
1426
                $actions[] = Display::url(
1427
                    Display::return_icon('visible.png', get_lang('Choose recipients'), [], ICON_SIZE_MEDIUM),
1428
                    $this->baseUrl.http_build_query(['action' => 'item_visiblity_choose', 'id' => $item->getId()])
1429
                );
1430
            } else {
1431
                $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]);
1432
1433
                if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) {
1434
                    $actions[] = Display::url(
1435
                        Display::return_icon('invisible.png', get_lang('Make visible'), [], ICON_SIZE_MEDIUM),
1436
                        $visibilityUrl
1437
                    );
1438
                } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) {
1439
                    $actions[] = Display::url(
1440
                        Display::return_icon('visible.png', get_lang('Make visible for teachers'), [], ICON_SIZE_MEDIUM),
1441
                        $visibilityUrl
1442
                    );
1443
                } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) {
1444
                    $actions[] = Display::url(
1445
                        Display::return_icon('eye-slash.png', get_lang('Make invisible'), [], ICON_SIZE_MEDIUM),
1446
                        $visibilityUrl
1447
                    );
1448
                }
1449
            }
1450
1451
            $actions[] = Display::url(
1452
                Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM),
1453
                $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()])
1454
            );
1455
        } else {
1456
            $actions[] = Display::url(
1457
                Display::return_icon('copy.png', get_lang('Copy to my portfolio'), [], ICON_SIZE_MEDIUM),
1458
                $this->baseUrl.http_build_query(['action' => 'copy', 'copy' => 'item', 'id' => $item->getId()])
1459
            );
1460
        }
1461
1462
        if (api_is_allowed_to_edit()) {
1463
            $actions[] = Display::url(
1464
                Display::return_icon('copy.png', get_lang('Copy to student portfolio'), [], ICON_SIZE_MEDIUM),
1465
                $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()])
1466
            );
1467
            $actions[] = $editLink;
1468
1469
            $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]);
1470
1471
            if ($item->isHighlighted()) {
1472
                $actions[] = Display::url(
1473
                    Display::return_icon('award_red.png', get_lang('Unmark as highlighted'), [], ICON_SIZE_MEDIUM),
1474
                    $highlightedUrl
1475
                );
1476
            } else {
1477
                $actions[] = Display::url(
1478
                    Display::return_icon('award_red_na.png', get_lang('Mark as highlighted'), [], ICON_SIZE_MEDIUM),
1479
                    $highlightedUrl
1480
                );
1481
            }
1482
1483
            if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) {
1484
                $actions[] = Display::url(
1485
                    Display::return_icon('quiz.png', get_lang('Grade this item'), [], ICON_SIZE_MEDIUM),
1486
                    $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()])
1487
                );
1488
            }
1489
        }
1490
1491
        $this->renderView($content, $item->getTitle(true), $actions, false);
1492
    }
1493
1494
    /**
1495
     * @throws \Doctrine\ORM\ORMException
1496
     * @throws \Doctrine\ORM\OptimisticLockException
1497
     */
1498
    public function copyItem(Portfolio $originItem)
1499
    {
1500
        $this->blockIsNotAllowed();
1501
1502
        $currentTime = api_get_utc_datetime(null, false, true);
1503
1504
        $portfolio = new Portfolio();
1505
        $portfolio
1506
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1507
            ->setTitle(
1508
                sprintf(get_lang('Portfolio item by %s'), $originItem->getUser()->getFullName())
1509
            )
1510
            ->setContent('')
1511
            ->setUser($this->owner)
1512
            ->setOrigin($originItem->getId())
1513
            ->setOriginType(Portfolio::TYPE_ITEM)
1514
            ->setCourse($this->course)
1515
            ->setSession($this->session)
1516
            ->setCreationDate($currentTime)
1517
            ->setUpdateDate($currentTime);
1518
1519
        $this->em->persist($portfolio);
1520
        $this->em->flush();
1521
1522
        Display::addFlash(
1523
            Display::return_message(get_lang('Portfolio item added'), 'success')
1524
        );
1525
1526
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1527
        exit;
1528
    }
1529
1530
    /**
1531
     * @throws \Doctrine\ORM\ORMException
1532
     * @throws \Doctrine\ORM\OptimisticLockException
1533
     */
1534
    public function copyComment(PortfolioComment $originComment)
1535
    {
1536
        $currentTime = api_get_utc_datetime(null, false, true);
1537
1538
        $portfolio = new Portfolio();
1539
        $portfolio
1540
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1541
            ->setTitle(
1542
                sprintf(get_lang('Comment by %s'), $originComment->getAuthor()->getFullName())
1543
            )
1544
            ->setContent('')
1545
            ->setUser($this->owner)
1546
            ->setOrigin($originComment->getId())
1547
            ->setOriginType(Portfolio::TYPE_COMMENT)
1548
            ->setCourse($this->course)
1549
            ->setSession($this->session)
1550
            ->setCreationDate($currentTime)
1551
            ->setUpdateDate($currentTime);
1552
1553
        $this->em->persist($portfolio);
1554
        $this->em->flush();
1555
1556
        Display::addFlash(
1557
            Display::return_message(get_lang('Portfolio item added'), 'success')
1558
        );
1559
1560
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1561
        exit;
1562
    }
1563
1564
    /**
1565
     * @throws \Doctrine\ORM\ORMException
1566
     * @throws \Doctrine\ORM\OptimisticLockException
1567
     * @throws \Exception
1568
     */
1569
    public function teacherCopyItem(Portfolio $originItem)
1570
    {
1571
        api_protect_teacher_script();
1572
1573
        $actionParams = http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $originItem->getId()]);
1574
1575
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1576
1577
        if (api_get_configuration_value('save_titles_as_html')) {
1578
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1579
        } else {
1580
            $form->addText('title', get_lang('Title'));
1581
            $form->applyFilter('title', 'trim');
1582
        }
1583
1584
        $form->addLabel(
1585
            sprintf(get_lang('"Portfolio item by %s"'), $originItem->getUser()->getFullName()),
1586
            Display::panel(
1587
                Security::remove_XSS($originItem->getContent())
1588
            )
1589
        );
1590
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1591
1592
        $urlParams = http_build_query(
1593
            [
1594
                'a' => 'search_user_by_course',
1595
                'course_id' => $this->course->getId(),
1596
                'session_id' => $this->session ? $this->session->getId() : 0,
1597
            ]
1598
        );
1599
        $form->addSelectAjax(
1600
            'students',
1601
            get_lang('Learners'),
1602
            [],
1603
            [
1604
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1605
                'multiple' => true,
1606
            ]
1607
        );
1608
        $form->addRule('students', get_lang('Required field'), 'required');
1609
        $form->addButtonCreate(get_lang('Save'));
1610
1611
        $toolName = get_lang('Copy to student portfolio');
1612
        $content = $form->returnForm();
1613
1614
        if ($form->validate()) {
1615
            $values = $form->exportValues();
1616
1617
            $currentTime = api_get_utc_datetime(null, false, true);
1618
1619
            foreach ($values['students'] as $studentId) {
1620
                $owner = api_get_user_entity($studentId);
1621
1622
                $portfolio = new Portfolio();
1623
                $portfolio
1624
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1625
                    ->setTitle($values['title'])
1626
                    ->setContent($values['content'])
1627
                    ->setUser($owner)
1628
                    ->setOrigin($originItem->getId())
1629
                    ->setOriginType(Portfolio::TYPE_ITEM)
1630
                    ->setCourse($this->course)
1631
                    ->setSession($this->session)
1632
                    ->setCreationDate($currentTime)
1633
                    ->setUpdateDate($currentTime);
1634
1635
                $this->em->persist($portfolio);
1636
            }
1637
1638
            $this->em->flush();
1639
1640
            Display::addFlash(
1641
                Display::return_message(get_lang('Item added to students own portfolio'), 'success')
1642
            );
1643
1644
            header("Location: $this->baseUrl");
1645
            exit;
1646
        }
1647
1648
        $this->renderView($content, $toolName);
1649
    }
1650
1651
    /**
1652
     * @throws \Doctrine\ORM\ORMException
1653
     * @throws \Doctrine\ORM\OptimisticLockException
1654
     * @throws \Exception
1655
     */
1656
    public function teacherCopyComment(PortfolioComment $originComment)
1657
    {
1658
        $actionParams = http_build_query(
1659
            [
1660
                'action' => 'teacher_copy',
1661
                'copy' => 'comment',
1662
                'id' => $originComment->getId(),
1663
            ]
1664
        );
1665
1666
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1667
1668
        if (api_get_configuration_value('save_titles_as_html')) {
1669
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1670
        } else {
1671
            $form->addText('title', get_lang('Title'));
1672
            $form->applyFilter('title', 'trim');
1673
        }
1674
1675
        $form->addLabel(
1676
            sprintf(get_lang('Comment from %s'), $originComment->getAuthor()->getFullName()),
1677
            Display::panel(
1678
                Security::remove_XSS($originComment->getContent())
1679
            )
1680
        );
1681
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1682
1683
        $urlParams = http_build_query(
1684
            [
1685
                'a' => 'search_user_by_course',
1686
                'course_id' => $this->course->getId(),
1687
                'session_id' => $this->session ? $this->session->getId() : 0,
1688
            ]
1689
        );
1690
        $form->addSelectAjax(
1691
            'students',
1692
            get_lang('Learners'),
1693
            [],
1694
            [
1695
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1696
                'multiple' => true,
1697
            ]
1698
        );
1699
        $form->addRule('students', get_lang('Required field'), 'required');
1700
        $form->addButtonCreate(get_lang('Save'));
1701
1702
        $toolName = get_lang('Copy to student portfolio');
1703
        $content = $form->returnForm();
1704
1705
        if ($form->validate()) {
1706
            $values = $form->exportValues();
1707
1708
            $currentTime = api_get_utc_datetime(null, false, true);
1709
1710
            foreach ($values['students'] as $studentId) {
1711
                $owner = api_get_user_entity($studentId);
1712
1713
                $portfolio = new Portfolio();
1714
                $portfolio
1715
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1716
                    ->setTitle($values['title'])
1717
                    ->setContent($values['content'])
1718
                    ->setUser($owner)
1719
                    ->setOrigin($originComment->getId())
1720
                    ->setOriginType(Portfolio::TYPE_COMMENT)
1721
                    ->setCourse($this->course)
1722
                    ->setSession($this->session)
1723
                    ->setCreationDate($currentTime)
1724
                    ->setUpdateDate($currentTime);
1725
1726
                $this->em->persist($portfolio);
1727
            }
1728
1729
            $this->em->flush();
1730
1731
            Display::addFlash(
1732
                Display::return_message(get_lang('Item added to students own portfolio'), 'success')
1733
            );
1734
1735
            header("Location: $this->baseUrl");
1736
            exit;
1737
        }
1738
1739
        $this->renderView($content, $toolName);
1740
    }
1741
1742
    /**
1743
     * @throws \Doctrine\ORM\ORMException
1744
     * @throws \Doctrine\ORM\OptimisticLockException
1745
     */
1746
    public function markImportantCommentInItem(Portfolio $item, PortfolioComment $comment)
1747
    {
1748
        if ($comment->getItem()->getId() !== $item->getId()) {
1749
            api_not_allowed(true);
1750
        }
1751
1752
        $comment->setIsImportant(
1753
            !$comment->isImportant()
1754
        );
1755
1756
        $this->em->persist($comment);
1757
        $this->em->flush();
1758
1759
        Display::addFlash(
1760
            Display::return_message(get_lang('Portfolio item marked as important'), 'success')
1761
        );
1762
1763
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
1764
        exit;
1765
    }
1766
1767
    /**
1768
     * @throws \Exception
1769
     */
1770
    public function details(HttpRequest $httpRequest)
1771
    {
1772
        $this->blockIsNotAllowed();
1773
1774
        $currentUserId = api_get_user_id();
1775
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
1776
1777
        $actions = [];
1778
        $actions[] = Display::url(
1779
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1780
            $this->baseUrl
1781
        );
1782
        $actions[] = Display::url(
1783
            Display::return_icon('pdf.png', get_lang('Export my portfolio data in a PDF file'), [], ICON_SIZE_MEDIUM),
1784
            $this->baseUrl.http_build_query(['action' => 'export_pdf'])
1785
        );
1786
        $actions[] = Display::url(
1787
            Display::return_icon('save_pack.png', get_lang('Export my portfolio data in a ZIP file'), [], ICON_SIZE_MEDIUM),
1788
            $this->baseUrl.http_build_query(['action' => 'export_zip'])
1789
        );
1790
1791
        $frmStudent = null;
1792
1793
        if ($isAllowedToFilterStudent) {
1794
            if ($httpRequest->query->has('user')) {
1795
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
1796
1797
                if (empty($this->owner)) {
1798
                    api_not_allowed(true);
1799
                }
1800
1801
                $actions[1] = Display::url(
1802
                    Display::return_icon('pdf.png', get_lang('Export my portfolio data in a PDF file'), [], ICON_SIZE_MEDIUM),
1803
                    $this->baseUrl.http_build_query(['action' => 'export_pdf', 'user' => $this->owner->getId()])
1804
                );
1805
                $actions[2] = Display::url(
1806
                    Display::return_icon('save_pack.png', get_lang('Export my portfolio data in a ZIP file'), [], ICON_SIZE_MEDIUM),
1807
                    $this->baseUrl.http_build_query(['action' => 'export_zip', 'user' => $this->owner->getId()])
1808
                );
1809
            }
1810
1811
            $frmStudent = new FormValidator('frm_student_list', 'get');
1812
1813
            $urlParams = http_build_query(
1814
                [
1815
                    'a' => 'search_user_by_course',
1816
                    'course_id' => $this->course->getId(),
1817
                    'session_id' => $this->session ? $this->session->getId() : 0,
1818
                ]
1819
            );
1820
1821
            $frmStudent
1822
                ->addSelectAjax(
1823
                    'user',
1824
                    get_lang('Select a learner portfolio'),
1825
                    [],
1826
                    [
1827
                        'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1828
                        'placeholder' => get_lang('Search users'),
1829
                        'formatResult' => SelectAjax::templateResultForUsersInCourse(),
1830
                        'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
1831
                    ]
1832
                )
1833
                ->addOption(
1834
                    $this->owner->getFullName(),
1835
                    $this->owner->getId(),
1836
                    [
1837
                        'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
1838
                        'data-username' => $this->owner->getUsername(),
1839
                    ]
1840
                )
1841
            ;
1842
            $frmStudent->setDefaults(['user' => $this->owner->getId()]);
1843
            $frmStudent->addHidden('action', 'details');
1844
            $frmStudent->addHidden('cidReq', $this->course->getCode());
1845
            $frmStudent->addHidden('id_session', $this->session ? $this->session->getId() : 0);
1846
            $frmStudent->addButtonFilter(get_lang('Filter'));
1847
        }
1848
1849
        $itemsRepo = $this->em->getRepository(Portfolio::class);
1850
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1851
1852
        $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1853
            $qb = $itemsRepo->createQueryBuilder('i');
1854
            $qb
1855
                ->select('COUNT(i)')
1856
                ->where('i.user = :user')
1857
                ->setParameter('user', $this->owner);
1858
1859
            if ($this->course) {
1860
                $qb
1861
                    ->andWhere('i.course = :course')
1862
                    ->setParameter('course', $this->course);
1863
1864
                if ($this->session) {
1865
                    $qb
1866
                        ->andWhere('i.session = :session')
1867
                        ->setParameter('session', $this->session);
1868
                } else {
1869
                    $qb->andWhere('i.session IS NULL');
1870
                }
1871
            }
1872
1873
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1874
                $visibilityCriteria = [
1875
                    Portfolio::VISIBILITY_VISIBLE,
1876
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1877
                ];
1878
1879
                $qb->andWhere(
1880
                    $qb->expr()->in('i.visibility', $visibilityCriteria)
1881
                );
1882
            }
1883
1884
            return $qb->getQuery()->getSingleScalarResult();
1885
        };
1886
        $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1887
            $qb = $itemsRepo->createQueryBuilder('item')
1888
                ->where('item.user = :user')
1889
                ->leftJoin('item.category', 'category')
1890
                ->leftJoin('item.course', 'course')
1891
                ->leftJoin('item.session', 'session')
1892
                ->setParameter('user', $this->owner);
1893
1894
            if ($this->course) {
1895
                $qb
1896
                    ->andWhere('item.course = :course_id')
1897
                    ->setParameter('course_id', $this->course);
1898
1899
                if ($this->session) {
1900
                    $qb
1901
                        ->andWhere('item.session = :session')
1902
                        ->setParameter('session', $this->session);
1903
                } else {
1904
                    $qb->andWhere('item.session IS NULL');
1905
                }
1906
            }
1907
1908
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1909
                $visibilityCriteria = [
1910
                    Portfolio::VISIBILITY_VISIBLE,
1911
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1912
                ];
1913
1914
                $qb->andWhere(
1915
                    $qb->expr()->in('item.visibility', $visibilityCriteria)
1916
                );
1917
            }
1918
1919
            if (0 == $columnNo) {
1920
                $qb->orderBy('item.title', $orderDirection);
1921
            } elseif (1 == $columnNo) {
1922
                $qb->orderBy('item.creationDate', $orderDirection);
1923
            } elseif (2 == $columnNo) {
1924
                $qb->orderBy('item.updateDate', $orderDirection);
1925
            } elseif (3 == $columnNo) {
1926
                $qb->orderBy('category.title', $orderDirection);
1927
            } elseif (5 == $columnNo) {
1928
                $qb->orderBy('item.score', $orderDirection);
1929
            } elseif (6 == $columnNo) {
1930
                $qb->orderBy('course.title', $orderDirection);
1931
            } elseif (7 == $columnNo) {
1932
                $qb->orderBy('session.name', $orderDirection);
1933
            }
1934
1935
            $qb->setFirstResult($from)->setMaxResults($limit);
1936
1937
            return array_map(
1938
                function (Portfolio $item) {
1939
                    $category = $item->getCategory();
1940
1941
                    $row = [];
1942
                    $row[] = $item;
1943
                    $row[] = $item->getCreationDate();
1944
                    $row[] = $item->getUpdateDate();
1945
                    $row[] = $category ? $item->getCategory()->getTitle() : null;
1946
                    $row[] = $item->getComments()->count();
1947
                    $row[] = $item->getScore();
1948
1949
                    if (!$this->course) {
1950
                        $row[] = $item->getCourse();
1951
                        $row[] = $item->getSession();
1952
                    }
1953
1954
                    return $row;
1955
                },
1956
                $qb->getQuery()->getResult()
1957
            );
1958
        };
1959
1960
        $portfolioItemColumnFilter = function (Portfolio $item) {
1961
            return Display::url(
1962
                $item->getTitle(true),
1963
                $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
1964
            );
1965
        };
1966
        $convertFormatDateColumnFilter = function (DateTime $date) {
1967
            return api_convert_and_format_date($date);
1968
        };
1969
1970
        $tblItems = new SortableTable('tbl_items', $getItemsTotalNumber, $getItemsData, 1, 20, 'DESC');
1971
        $tblItems->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1972
        $tblItems->set_header(0, get_lang('Title'));
1973
        $tblItems->set_column_filter(0, $portfolioItemColumnFilter);
1974
        $tblItems->set_header(1, get_lang('CreationDate'), true, [], ['class' => 'text-center']);
1975
        $tblItems->set_column_filter(1, $convertFormatDateColumnFilter);
1976
        $tblItems->set_header(2, get_lang('LastUpdate'), true, [], ['class' => 'text-center']);
1977
        $tblItems->set_column_filter(2, $convertFormatDateColumnFilter);
1978
        $tblItems->set_header(3, get_lang('Category'));
1979
        $tblItems->set_header(4, get_lang('Comments'), false, [], ['class' => 'text-right']);
1980
        $tblItems->set_header(5, get_lang('Score'), true, [], ['class' => 'text-right']);
1981
1982
        if (!$this->course) {
1983
            $tblItems->set_header(6, get_lang('Course'));
1984
            $tblItems->set_header(7, get_lang('Session'));
1985
        }
1986
1987
        $getCommentsTotalNumber = function () use ($commentsRepo) {
1988
            $qb = $commentsRepo->createQueryBuilder('c');
1989
            $qb
1990
                ->select('COUNT(c)')
1991
                ->where('c.author = :author')
1992
                ->setParameter('author', $this->owner);
1993
1994
            if ($this->course) {
1995
                $qb
1996
                    ->innerJoin('c.item', 'i')
1997
                    ->andWhere('i.course = :course')
1998
                    ->setParameter('course', $this->course);
1999
2000
                if ($this->session) {
2001
                    $qb
2002
                        ->andWhere('i.session = :session')
2003
                        ->setParameter('session', $this->session);
2004
                } else {
2005
                    $qb->andWhere('i.session IS NULL');
2006
                }
2007
            }
2008
2009
            return $qb->getQuery()->getSingleScalarResult();
2010
        };
2011
        $getCommentsData = function ($from, $limit, $columnNo, $orderDirection) use ($commentsRepo) {
2012
            $qb = $commentsRepo->createQueryBuilder('comment');
2013
            $qb
2014
                ->where('comment.author = :user')
2015
                ->innerJoin('comment.item', 'item')
2016
                ->setParameter('user', $this->owner);
2017
2018
            if ($this->course) {
2019
                $qb
2020
                    ->innerJoin('comment.item', 'i')
2021
                    ->andWhere('item.course = :course')
2022
                    ->setParameter('course', $this->course);
2023
2024
                if ($this->session) {
2025
                    $qb
2026
                        ->andWhere('item.session = :session')
2027
                        ->setParameter('session', $this->session);
2028
                } else {
2029
                    $qb->andWhere('item.session IS NULL');
2030
                }
2031
            }
2032
2033
            if (0 == $columnNo) {
2034
                $qb->orderBy('comment.content', $orderDirection);
2035
            } elseif (1 == $columnNo) {
2036
                $qb->orderBy('comment.date', $orderDirection);
2037
            } elseif (2 == $columnNo) {
2038
                $qb->orderBy('item.title', $orderDirection);
2039
            } elseif (3 == $columnNo) {
2040
                $qb->orderBy('comment.score', $orderDirection);
2041
            }
2042
2043
            $qb->setFirstResult($from)->setMaxResults($limit);
2044
2045
            return array_map(
2046
                function (PortfolioComment $comment) {
2047
                    return [
2048
                        $comment,
2049
                        $comment->getDate(),
2050
                        $comment->getItem(),
2051
                        $comment->getScore(),
2052
                    ];
2053
                },
2054
                $qb->getQuery()->getResult()
2055
            );
2056
        };
2057
2058
        $tblComments = new SortableTable('tbl_comments', $getCommentsTotalNumber, $getCommentsData, 1, 20, 'DESC');
2059
        $tblComments->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
2060
        $tblComments->set_header(0, get_lang('Resume'));
2061
        $tblComments->set_column_filter(
2062
            0,
2063
            function (PortfolioComment $comment) {
2064
                return Display::url(
2065
                    $comment->getExcerpt(),
2066
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])
2067
                    .'#comment-'.$comment->getId()
2068
                );
2069
            }
2070
        );
2071
        $tblComments->set_header(1, get_lang('Date'), true, [], ['class' => 'text-center']);
2072
        $tblComments->set_column_filter(1, $convertFormatDateColumnFilter);
2073
        $tblComments->set_header(2, get_lang('Item title'));
2074
        $tblComments->set_column_filter(2, $portfolioItemColumnFilter);
2075
        $tblComments->set_header(3, get_lang('Score'), true, [], ['class' => 'text-right']);
2076
2077
        $content = '';
2078
2079
        if ($frmStudent) {
2080
            $content .= $frmStudent->returnForm();
2081
        }
2082
2083
        $totalNumberOfItems = $tblItems->get_total_number_of_items();
2084
        $totalNumberOfComments = $tblComments->get_total_number_of_items();
2085
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2086
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2087
2088
        $itemsSubtitle = '';
2089
2090
        if ($requiredNumberOfItems > 0) {
2091
            $itemsSubtitle = sprintf(
2092
                get_lang('%d added / %d required'),
2093
                $totalNumberOfItems,
2094
                $requiredNumberOfItems
2095
            );
2096
        }
2097
2098
        $content .= Display::page_subheader2(
2099
            get_lang('Portfolio items'),
2100
            $itemsSubtitle
2101
        ).PHP_EOL;
2102
2103
        if ($totalNumberOfItems > 0) {
2104
            $content .= $tblItems->return_table().PHP_EOL;
2105
        } else {
2106
            $content .= Display::return_message(get_lang('No items in your portfolio'), 'warning');
2107
        }
2108
2109
        $commentsSubtitle = '';
2110
2111
        if ($requiredNumberOfComments > 0) {
2112
            $commentsSubtitle = sprintf(
2113
                get_lang('%d added / %d required'),
2114
                $totalNumberOfComments,
2115
                $requiredNumberOfComments
2116
            );
2117
        }
2118
2119
        $content .= Display::page_subheader2(
2120
            get_lang('Comments made'),
2121
            $commentsSubtitle
2122
        ).PHP_EOL;
2123
2124
        if ($totalNumberOfComments > 0) {
2125
            $content .= $tblComments->return_table().PHP_EOL;
2126
        } else {
2127
            $content .= Display::return_message(get_lang('You have not commented'), 'warning');
2128
        }
2129
2130
        $this->renderView($content, get_lang('Portfolio details'), $actions);
2131
    }
2132
2133
    /**
2134
     * @throws MpdfException
2135
     */
2136
    public function exportPdf(HttpRequest $httpRequest)
2137
    {
2138
        $currentUserId = api_get_user_id();
2139
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2140
2141
        if ($isAllowedToFilterStudent) {
2142
            if ($httpRequest->query->has('user')) {
2143
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2144
2145
                if (empty($this->owner)) {
2146
                    api_not_allowed(true);
2147
                }
2148
            }
2149
        }
2150
2151
        $pdfContent = Display::page_header($this->owner->getFullName());
2152
2153
        if ($this->course) {
2154
            $pdfContent .= '<p>'.get_lang('Course').': ';
2155
2156
            if ($this->session) {
2157
                $pdfContent .= $this->session->getName().' ('.$this->course->getTitle().')';
2158
            } else {
2159
                $pdfContent .= $this->course->getTitle();
2160
            }
2161
2162
            $pdfContent .= '</p>';
2163
        }
2164
2165
        $visibility = [];
2166
2167
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2168
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2169
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2170
        }
2171
2172
        $items = $this->em
2173
            ->getRepository(Portfolio::class)
2174
            ->findItemsByUser(
2175
                $this->owner,
2176
                $this->course,
2177
                $this->session,
2178
                null,
2179
                $visibility
2180
            );
2181
        $comments = $this->em
2182
            ->getRepository(PortfolioComment::class)
2183
            ->findCommentsByUser($this->owner, $this->course, $this->session);
2184
2185
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2186
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2187
2188
        $totalNumberOfItems = count($itemsHtml);
2189
        $totalNumberOfComments = count($commentsHtml);
2190
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2191
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2192
2193
        $itemsSubtitle = '';
2194
        $commentsSubtitle = '';
2195
2196
        if ($requiredNumberOfItems > 0) {
2197
            $itemsSubtitle = sprintf(
2198
                get_lang('%d added / %d required'),
2199
                $totalNumberOfItems,
2200
                $requiredNumberOfItems
2201
            );
2202
        }
2203
2204
        if ($requiredNumberOfComments > 0) {
2205
            $commentsSubtitle = sprintf(
2206
                get_lang('%d added / %d required'),
2207
                $totalNumberOfComments,
2208
                $requiredNumberOfComments
2209
            );
2210
        }
2211
2212
        $pdfContent .= Display::page_subheader2(
2213
            get_lang('Portfolio items'),
2214
            $itemsSubtitle
2215
        );
2216
2217
        if ($totalNumberOfItems > 0) {
2218
            $pdfContent .= implode(PHP_EOL, $itemsHtml);
2219
        } else {
2220
            $pdfContent .= Display::return_message(get_lang('No items in your portfolio'), 'warning');
2221
        }
2222
2223
        $pdfContent .= Display::page_subheader2(
2224
            get_lang('Comments made'),
2225
            $commentsSubtitle
2226
        );
2227
2228
        if ($totalNumberOfComments > 0) {
2229
            $pdfContent .= implode(PHP_EOL, $commentsHtml);
2230
        } else {
2231
            $pdfContent .= Display::return_message(get_lang('You have not commented'), 'warning');
2232
        }
2233
2234
        $pdfName = $this->owner->getFullName()
2235
            .($this->course ? '_'.$this->course->getCode() : '')
2236
            .'_'.get_lang('Portfolio');
2237
2238
        Container::getEventDispatcher()->dispatch(
2239
            new PortfolioItemDownloadedEvent(['owner' => $this->owner]),
2240
            Events::PORTFOLIO_DOWNLOADED,
2241
        );
2242
2243
        $pdf = new PDF();
2244
        $pdf->content_to_pdf(
2245
            $pdfContent,
2246
            null,
2247
            $pdfName,
2248
            $this->course ? $this->course->getCode() : null,
2249
            'D',
2250
            false,
2251
            null,
2252
            false,
2253
            true
2254
        );
2255
    }
2256
2257
    public function exportZip(HttpRequest $httpRequest)
2258
    {
2259
        $currentUserId = api_get_user_id();
2260
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2261
2262
        if ($isAllowedToFilterStudent) {
2263
            if ($httpRequest->query->has('user')) {
2264
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2265
2266
                if (empty($this->owner)) {
2267
                    api_not_allowed(true);
2268
                }
2269
            }
2270
        }
2271
2272
        $itemsRepo = $this->em->getRepository(Portfolio::class);
2273
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
2274
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
2275
2276
        $visibility = [];
2277
2278
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2279
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2280
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2281
        }
2282
2283
        $items = $itemsRepo->findItemsByUser(
2284
            $this->owner,
2285
            $this->course,
2286
            $this->session,
2287
            null,
2288
            $visibility
2289
        );
2290
        $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session);
2291
2292
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2293
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2294
2295
        $sysArchivePath = api_get_path(SYS_ARCHIVE_PATH);
2296
        $tempPortfolioDirectory = $sysArchivePath."portfolio/{$this->owner->getId()}";
2297
2298
        $userDirectory = UserManager::getUserPathById($this->owner->getId(), 'system');
2299
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2300
2301
        $tblItemsHeaders = [];
2302
        $tblItemsHeaders[] = get_lang('Title');
2303
        $tblItemsHeaders[] = get_lang('CreationDate');
2304
        $tblItemsHeaders[] = get_lang('LastUpdate');
2305
        $tblItemsHeaders[] = get_lang('Category');
2306
        $tblItemsHeaders[] = get_lang('Category');
2307
        $tblItemsHeaders[] = get_lang('Score');
2308
        $tblItemsHeaders[] = get_lang('Course');
2309
        $tblItemsHeaders[] = get_lang('Session');
2310
        $tblItemsData = [];
2311
2312
        $tblCommentsHeaders = [];
2313
        $tblCommentsHeaders[] = get_lang('Resume');
2314
        $tblCommentsHeaders[] = get_lang('Date');
2315
        $tblCommentsHeaders[] = get_lang('Item title');
2316
        $tblCommentsHeaders[] = get_lang('Score');
2317
        $tblCommentsData = [];
2318
2319
        $filenames = [];
2320
2321
        $fs = new Filesystem();
2322
2323
        /**
2324
         * @var int       $i
2325
         * @var Portfolio $item
2326
         */
2327
        foreach ($items as $i => $item) {
2328
            $itemCategory = $item->getCategory();
2329
            $itemCourse = $item->getCourse();
2330
            $itemSession = $item->getSession();
2331
2332
            $itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
2333
2334
            $itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
2335
            $imagePaths = [];
2336
            $itemFileContent = $this->fixMediaSourcesToHtml($itemsHtml[$i], $imagePaths);
2337
2338
            $fs->dumpFile($itemFilename, $itemFileContent);
2339
2340
            $filenames[] = $itemFilename;
2341
2342
            foreach ($imagePaths as $imagePath) {
2343
                $inlineFile = dirname($itemFilename).'/'.basename($imagePath);
2344
2345
                try {
2346
                    $filenames[] = $inlineFile;
2347
                    $fs->copy($imagePath, $inlineFile);
2348
                } catch (FileNotFoundException $notFoundException) {
2349
                    continue;
2350
                }
2351
            }
2352
2353
            $attachments = $attachmentsRepo->findFromItem($item);
2354
2355
            /** @var PortfolioAttachment $attachment */
2356
            foreach ($attachments as $attachment) {
2357
                $attachmentFilename = sprintf(
2358
                    '%s/items/%s/attachments/%s',
2359
                    $tempPortfolioDirectory,
2360
                    $itemDirectory,
2361
                    $attachment->getFilename()
2362
                );
2363
2364
                try {
2365
                    $fs->copy(
2366
                        $attachmentsDirectory.$attachment->getPath(),
2367
                        $attachmentFilename
2368
                    );
2369
                    $filenames[] = $attachmentFilename;
2370
                } catch (FileNotFoundException $notFoundException) {
2371
                    continue;
2372
                }
2373
            }
2374
2375
            $tblItemsData[] = [
2376
                Display::url(
2377
                    Security::remove_XSS($item->getTitle()),
2378
                    sprintf('items/%s/item.html', $itemDirectory)
2379
                ),
2380
                api_convert_and_format_date($item->getCreationDate()),
2381
                api_convert_and_format_date($item->getUpdateDate()),
2382
                $itemCategory ? $itemCategory->getTitle() : null,
2383
                $item->getComments()->count(),
2384
                $item->getScore(),
2385
                $itemCourse->getTitle(),
2386
                $itemSession ? $itemSession->getName() : null,
2387
            ];
2388
        }
2389
2390
        /**
2391
         * @var int              $i
2392
         * @var PortfolioComment $comment
2393
         */
2394
        foreach ($comments as $i => $comment) {
2395
            $commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
2396
2397
            $imagePaths = [];
2398
            $commentFileContent = $this->fixMediaSourcesToHtml($commentsHtml[$i], $imagePaths);
2399
            $commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
2400
2401
            $fs->dumpFile($commentFilename, $commentFileContent);
2402
2403
            $filenames[] = $commentFilename;
2404
2405
            foreach ($imagePaths as $imagePath) {
2406
                $inlineFile = dirname($commentFilename).'/'.basename($imagePath);
2407
2408
                try {
2409
                    $filenames[] = $inlineFile;
2410
                    $fs->copy($imagePath, $inlineFile);
2411
                } catch (FileNotFoundException $notFoundException) {
2412
                    continue;
2413
                }
2414
            }
2415
2416
            $attachments = $attachmentsRepo->findFromComment($comment);
2417
2418
            /** @var PortfolioAttachment $attachment */
2419
            foreach ($attachments as $attachment) {
2420
                $attachmentFilename = sprintf(
2421
                    '%s/comments/%s/attachments/%s',
2422
                    $tempPortfolioDirectory,
2423
                    $commentDirectory,
2424
                    $attachment->getFilename()
2425
                );
2426
2427
                try {
2428
                    $fs->copy(
2429
                        $attachmentsDirectory.$attachment->getPath(),
2430
                        $attachmentFilename
2431
                    );
2432
                    $filenames[] = $attachmentFilename;
2433
                } catch (FileNotFoundException $notFoundException) {
2434
                    continue;
2435
                }
2436
            }
2437
2438
            $tblCommentsData[] = [
2439
                Display::url(
2440
                    $comment->getExcerpt(),
2441
                    sprintf('comments/%s/comment.html', $commentDirectory)
2442
                ),
2443
                api_convert_and_format_date($comment->getDate()),
2444
                Security::remove_XSS($comment->getItem()->getTitle()),
2445
                $comment->getScore(),
2446
            ];
2447
        }
2448
2449
        $tblItems = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2450
        $tblItems->setHeaders($tblItemsHeaders);
2451
        $tblItems->setData($tblItemsData);
2452
2453
        $tblComments = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2454
        $tblComments->setHeaders($tblCommentsHeaders);
2455
        $tblComments->setData($tblCommentsData);
2456
2457
        $itemFilename = sprintf('%s/index.html', $tempPortfolioDirectory);
2458
2459
        $filenames[] = $itemFilename;
2460
2461
        $fs->dumpFile(
2462
            $itemFilename,
2463
            $this->formatZipIndexFile($tblItems, $tblComments)
2464
        );
2465
2466
        $zipName = $this->owner->getFullName()
2467
            .($this->course ? '_'.$this->course->getCode() : '')
2468
            .'_'.get_lang('Portfolio');
2469
        $tempZipFile = $sysArchivePath."portfolio/$zipName.zip";
2470
        $zip = new PclZip($tempZipFile);
2471
2472
        foreach ($filenames as $filename) {
2473
            $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $tempPortfolioDirectory);
2474
        }
2475
2476
        Container::getEventDispatcher()->dispatch(
2477
            new PortfolioItemDownloadedEvent(['owner' => $this->owner]),
2478
            Events::PORTFOLIO_DOWNLOADED,
2479
        );
2480
2481
        DocumentManager::file_send_for_download($tempZipFile, true, "$zipName.zip");
2482
2483
        $fs->remove($tempPortfolioDirectory);
2484
        $fs->remove($tempZipFile);
2485
    }
2486
2487
    public function qualifyItem(Portfolio $item)
2488
    {
2489
        global $interbreadcrumb;
2490
2491
        $em = Database::getManager();
2492
2493
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]);
2494
2495
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2496
        $form->addUserAvatar('user', get_lang('Author'));
2497
        $form->addLabel(get_lang('Title'), $item->getTitle());
2498
2499
        $itemContent = $this->generateItemContent($item);
2500
2501
        $form->addLabel(get_lang('Content'), $itemContent);
2502
        $form->addNumeric(
2503
            'score',
2504
            [get_lang('Score'), null, ' / '.api_get_course_setting('portfolio_max_score')]
2505
        );
2506
        $form->addButtonSave(get_lang('Grade this item'));
2507
2508
        if ($form->validate()) {
2509
            $values = $form->exportValues();
2510
2511
            $item->setScore($values['score']);
2512
2513
            $em->persist($item);
2514
            $em->flush();
2515
2516
            Container::getEventDispatcher()->dispatch(
2517
                new PortfolioItemScoredEvent(['portfolio' => $item]),
2518
                Events::PORTFOLIO_ITEM_SCORED
2519
            );
2520
2521
            Display::addFlash(
2522
                Display::return_message(get_lang('Portfolio item was graded'), 'success')
2523
            );
2524
2525
            header("Location: $formAction");
2526
            exit();
2527
        }
2528
2529
        $form->setDefaults(
2530
            [
2531
                'user' => $item->getUser(),
2532
                'score' => (float) $item->getScore(),
2533
            ]
2534
        );
2535
2536
        $interbreadcrumb[] = [
2537
            'name' => get_lang('Portfolio'),
2538
            'url' => $this->baseUrl,
2539
        ];
2540
        $interbreadcrumb[] = [
2541
            'name' => $item->getTitle(true),
2542
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2543
        ];
2544
2545
        $actions = [];
2546
        $actions[] = Display::url(
2547
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2548
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2549
        );
2550
2551
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2552
    }
2553
2554
    public function qualifyComment(PortfolioComment $comment)
2555
    {
2556
        global $interbreadcrumb;
2557
2558
        $em = Database::getManager();
2559
2560
        $item = $comment->getItem();
2561
        $commentPath = $em->getRepository(PortfolioComment::class)->getPath($comment);
2562
2563
        $commentContext = Container::getTwig()->render(
2564
            '@ChamiloCore/Portfolio/comment_context.html.twig',
2565
            [
2566
                'item' => $item,
2567
                'comment_path' => $commentPath,
2568
            ]
2569
        );
2570
2571
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'comment' => $comment->getId()]);
2572
2573
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2574
        $form->addHtml($commentContext);
2575
        $form->addUserAvatar('user', get_lang('Author'));
2576
        $form->addLabel(get_lang('Comment'), $comment->getContent());
2577
        $form->addNumeric(
2578
            'score',
2579
            [get_lang('Score'), null, '/ '.api_get_course_setting('portfolio_max_score')]
2580
        );
2581
        $form->addButtonSave(get_lang('Grade this comment'));
2582
2583
        if ($form->validate()) {
2584
            $values = $form->exportValues();
2585
2586
            $comment->setScore($values['score']);
2587
2588
            $em->persist($comment);
2589
            $em->flush();
2590
2591
            Container::getEventDispatcher()->dispatch(
2592
                new PortfolioCommentScoredEvent(['comment' => $comment]),
2593
                Events::PORTFOLIO_COMMENT_SCORED
2594
            );
2595
2596
            Display::addFlash(
2597
                Display::return_message(get_lang('Portfolio comment was graded'), 'success')
2598
            );
2599
2600
            header("Location: $formAction");
2601
            exit();
2602
        }
2603
2604
        $form->setDefaults(
2605
            [
2606
                'user' => $comment->getAuthor(),
2607
                'score' => (float) $comment->getScore(),
2608
            ]
2609
        );
2610
2611
        $interbreadcrumb[] = [
2612
            'name' => get_lang('Portfolio'),
2613
            'url' => $this->baseUrl,
2614
        ];
2615
        $interbreadcrumb[] = [
2616
            'name' => $item->getTitle(true),
2617
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2618
        ];
2619
2620
        $actions = [];
2621
        $actions[] = Display::url(
2622
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2623
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2624
        );
2625
2626
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2627
    }
2628
2629
    public function downloadAttachment(HttpRequest $httpRequest)
2630
    {
2631
        $path = $httpRequest->query->get('file');
2632
2633
        if (empty($path)) {
2634
            api_not_allowed(true);
2635
        }
2636
2637
        $em = Database::getManager();
2638
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2639
2640
        $attachment = $attachmentRepo->findOneByPath($path);
2641
2642
        if (empty($attachment)) {
2643
            api_not_allowed(true);
2644
        }
2645
2646
        $originOwnerId = 0;
2647
2648
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2649
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2650
2651
            $originOwnerId = $item->getUser()->getId();
2652
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2653
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2654
2655
            $originOwnerId = $comment->getAuthor()->getId();
2656
        } else {
2657
            api_not_allowed(true);
2658
        }
2659
2660
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2661
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2662
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2663
2664
        if (!Security::check_abs_path($attachmentFilename, $attachmentsDirectory)) {
2665
            api_not_allowed(true);
2666
        }
2667
2668
        $downloaded = DocumentManager::file_send_for_download(
2669
            $attachmentFilename,
2670
            true,
2671
            $attachment->getFilename()
2672
        );
2673
2674
        if (!$downloaded) {
2675
            api_not_allowed(true);
2676
        }
2677
    }
2678
2679
    public function deleteAttachment(HttpRequest $httpRequest)
2680
    {
2681
        $currentUserId = api_get_user_id();
2682
2683
        $path = $httpRequest->query->get('file');
2684
2685
        if (empty($path)) {
2686
            api_not_allowed(true);
2687
        }
2688
2689
        $em = Database::getManager();
2690
        $fs = new Filesystem();
2691
2692
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2693
        $attachment = $attachmentRepo->findOneByPath($path);
2694
2695
        if (empty($attachment)) {
2696
            api_not_allowed(true);
2697
        }
2698
2699
        $originOwnerId = 0;
2700
        $itemId = 0;
2701
2702
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2703
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2704
            $originOwnerId = $item->getUser()->getId();
2705
            $itemId = $item->getId();
2706
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2707
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2708
            $originOwnerId = $comment->getAuthor()->getId();
2709
            $itemId = $comment->getItem()->getId();
2710
        }
2711
2712
        if ($currentUserId !== $originOwnerId) {
2713
            api_not_allowed(true);
2714
        }
2715
2716
        $em->remove($attachment);
2717
        $em->flush();
2718
2719
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2720
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2721
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2722
2723
        $fs->remove($attachmentFilename);
2724
2725
        if ($httpRequest->isXmlHttpRequest()) {
2726
            echo Display::return_message(get_lang('The attached file has been deleted'), 'success');
2727
        } else {
2728
            Display::addFlash(
2729
                Display::return_message(get_lang('The attached file has been deleted'), 'success')
2730
            );
2731
2732
            $url = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $itemId]);
2733
2734
            if (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType() && isset($comment)) {
2735
                $url .= '#comment-'.$comment->getId();
2736
            }
2737
2738
            header("Location: $url");
2739
        }
2740
2741
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2742
    }
2743
2744
    /**
2745
     * @throws \Doctrine\ORM\OptimisticLockException
2746
     * @throws \Doctrine\ORM\ORMException
2747
     */
2748
    public function markAsHighlighted(Portfolio $item)
2749
    {
2750
        if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) {
2751
            api_not_allowed(true);
2752
        }
2753
2754
        $item->setIsHighlighted(
2755
            !$item->isHighlighted()
2756
        );
2757
2758
        Database::getManager()->flush();
2759
2760
        if ($item->isHighlighted()) {
2761
            Container::getEventDispatcher()->dispatch(
2762
                new \Chamilo\CoreBundle\Event\PortfolioItemHighlightedEvent(['portfolio' => $item]),
2763
                Events::PORTFOLIO_ITEM_HIGHLIGHTED
2764
            );
2765
        }
2766
2767
        Display::addFlash(
2768
            Display::return_message(
2769
                $item->isHighlighted() ? get_lang('Marked as highlighted') : get_lang('Unmarked as highlighted'),
2770
                'success'
2771
            )
2772
        );
2773
2774
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2775
        exit;
2776
    }
2777
2778
    public function markAsTemplate(Portfolio $item)
2779
    {
2780
        if (!$this->itemBelongToOwner($item)) {
2781
            api_not_allowed(true);
2782
        }
2783
2784
        $item->setIsTemplate(
2785
            !$item->isTemplate()
2786
        );
2787
2788
        Database::getManager()->flush($item);
2789
2790
        Display::addFlash(
2791
            Display::return_message(
2792
                $item->isTemplate() ? get_lang('Portfolio item set as a new template') : get_lang('Portfolio item unset as template'),
2793
                'success'
2794
            )
2795
        );
2796
2797
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2798
        exit;
2799
    }
2800
2801
    public function markAsTemplateComment(PortfolioComment $comment)
2802
    {
2803
        if (!$this->commentBelongsToOwner($comment)) {
2804
            api_not_allowed(true);
2805
        }
2806
2807
        $comment->setIsTemplate(
2808
            !$comment->isTemplate()
2809
        );
2810
2811
        Database::getManager()->flush();
2812
2813
        Display::addFlash(
2814
            Display::return_message(
2815
                $comment->isTemplate() ? get_lang('Portfolio comment set as a new template') : get_lang('Portfolio comment unset as template'),
2816
                'success'
2817
            )
2818
        );
2819
2820
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()]));
2821
        exit;
2822
    }
2823
2824
    public function listTags(HttpRequest $request)
2825
    {
2826
        global $interbreadcrumb;
2827
2828
        api_protect_course_script();
2829
        api_protect_teacher_script();
2830
2831
        $em = Database::getManager();
2832
        $tagRepo = $em->getRepository(Tag::class);
2833
2834
        $tagsQuery = $tagRepo->findForPortfolioInCourseQuery($this->course, $this->session);
2835
2836
        $tag = $request->query->has('id')
2837
            ? $tagRepo->find($request->query->getInt('id'))
2838
            : null;
2839
2840
        $formAction = ['action' => $request->query->get('action')];
2841
2842
        if ($tag) {
2843
            $formAction['id'] = $tag->getId();
2844
        }
2845
2846
        $form = new FormValidator('frm_add_tag', 'post', $this->baseUrl.http_build_query($formAction));
2847
        $form->addText('name', get_lang('Tag'));
2848
2849
        if ($tag) {
2850
            $form->addButtonUpdate(get_lang('Edit'));
2851
        } else {
2852
            $form->addButtonCreate(get_lang('Add'));
2853
        }
2854
2855
        if ($form->validate()) {
2856
            $values = $form->exportValues();
2857
2858
            $extraFieldInfo = (new ExtraField('portfolio'))->get_handler_field_info_by_field_variable('tags');
2859
2860
            if (!$tag) {
2861
                $tag = (new Tag())->setCount(0);
2862
2863
                $portfolioRelTag = (new PortfolioRelTag())
2864
                    ->setTag($tag)
2865
                    ->setCourse($this->course)
2866
                    ->setSession($this->session)
2867
                ;
2868
2869
                $em->persist($tag);
2870
                $em->persist($portfolioRelTag);
2871
            }
2872
2873
            $tag
2874
                ->setTag($values['name'])
2875
                ->setFieldId((int) $extraFieldInfo['id'])
2876
            ;
2877
2878
            $em->flush();
2879
2880
            Display::addFlash(
2881
                Display::return_message(get_lang('Tag saved'), 'success')
2882
            );
2883
2884
            header('Location: '.$this->baseUrl.http_build_query($formAction));
2885
            exit();
2886
        } else {
2887
            $form->protect();
2888
2889
            if ($tag) {
2890
                $form->setDefaults(['name' => $tag->getTag()]);
2891
            }
2892
        }
2893
2894
        $langTags = get_lang('Tags');
2895
        $langEdit = get_lang('Edit');
2896
2897
        $deleteIcon = Display::return_icon('delete.png', get_lang('Delete'));
2898
        $editIcon = Display::return_icon('edit.png', $langEdit);
2899
2900
        $table = new SortableTable(
2901
            'portfolio_tags',
2902
            function () use ($tagsQuery) {
2903
                return (int) $tagsQuery
2904
                    ->select('COUNT(t)')
2905
                    ->getQuery()
2906
                    ->getSingleScalarResult()
2907
                ;
2908
            },
2909
            function ($from, $limit, $column, $direction) use ($tagsQuery) {
2910
                $data = [];
2911
2912
                /** @var array<int, Tag> $tags */
2913
                $tags = $tagsQuery
2914
                    ->select('t')
2915
                    ->orderBy('t.tag', $direction)
2916
                    ->setFirstResult($from)
2917
                    ->setMaxResults($limit)
2918
                    ->getQuery()
2919
                    ->getResult();
2920
2921
                foreach ($tags as $tag) {
2922
                    $data[] = [
2923
                        $tag->getTag(),
2924
                        $tag->getId(),
2925
                    ];
2926
                }
2927
2928
                return $data;
2929
            },
2930
            0,
2931
            40
2932
        );
2933
        $table->set_header(0, get_lang('Name'));
2934
        $table->set_header(1, get_lang('Actions'), false, ['class' => 'text-right'], ['class' => 'text-right']);
2935
        $table->set_column_filter(
2936
            1,
2937
            function ($id) use ($editIcon, $deleteIcon) {
2938
                $editParams = http_build_query(['action' => 'edit_tag', 'id' => $id]);
2939
                $deleteParams = http_build_query(['action' => 'delete_tag', 'id' => $id]);
2940
2941
                return Display::url($editIcon, $this->baseUrl.$editParams).PHP_EOL
2942
                    .Display::url($deleteIcon, $this->baseUrl.$deleteParams).PHP_EOL;
2943
            }
2944
        );
2945
        $table->set_additional_parameters(
2946
            [
2947
                'action' => 'tags',
2948
                'cidReq' => $this->course->getCode(),
2949
                'id_session' => $this->session ? $this->session->getId() : 0,
2950
                'gidReq' => 0,
2951
            ]
2952
        );
2953
2954
        $content = $form->returnForm().PHP_EOL
2955
            .$table->return_table();
2956
2957
        $interbreadcrumb[] = [
2958
            'name' => get_lang('Portfolio'),
2959
            'url' => $this->baseUrl,
2960
        ];
2961
2962
        $pageTitle = $langTags;
2963
2964
        if ($tag) {
2965
            $pageTitle = $langEdit;
2966
2967
            $interbreadcrumb[] = [
2968
                'name' => $langTags,
2969
                'url' => $this->baseUrl.'action=tags',
2970
            ];
2971
        }
2972
2973
        $this->renderView($content, $pageTitle);
2974
    }
2975
2976
    public function deleteTag(Tag $tag)
2977
    {
2978
        api_protect_course_script();
2979
        api_protect_teacher_script();
2980
2981
        $em = Database::getManager();
2982
        $portfolioTagRepo = $em->getRepository(PortfolioRelTag::class);
2983
2984
        $portfolioTag = $portfolioTagRepo
2985
            ->findOneBy(['tag' => $tag, 'course' => $this->course, 'session' => $this->session]);
2986
2987
        if ($portfolioTag) {
2988
            $em->remove($portfolioTag);
2989
            $em->flush();
2990
2991
            Display::addFlash(
2992
                Display::return_message(get_lang('Tag deleted'), 'success')
2993
            );
2994
        }
2995
2996
        header('Location: '.$this->baseUrl.http_build_query(['action' => 'tags']));
2997
        exit();
2998
    }
2999
3000
    /**
3001
     * @throws \Doctrine\ORM\OptimisticLockException
3002
     * @throws \Doctrine\ORM\ORMException
3003
     */
3004
    public function editComment(PortfolioComment $comment)
3005
    {
3006
        global $interbreadcrumb;
3007
3008
        if (!$this->commentBelongsToOwner($comment)) {
3009
            api_not_allowed(true);
3010
        }
3011
3012
        $item = $comment->getItem();
3013
        $commmentCourse = $item->getCourse();
3014
        $commmentSession = $item->getSession();
3015
3016
        $formAction = $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()]);
3017
3018
        $form = new FormValidator('frm_comment', 'post', $formAction);
3019
        $form->addLabel(
3020
            get_lang('Date'),
3021
            $this->getLabelForCommentDate($comment)
3022
        );
3023
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3024
        $form->applyFilter('content', 'trim');
3025
3026
        $this->addAttachmentsFieldToForm($form);
3027
3028
        $form->addButtonUpdate(get_lang('Update'));
3029
3030
        if ($form->validate()) {
3031
            if ($commmentCourse) {
3032
                api_item_property_update(
3033
                    api_get_course_info($commmentCourse->getCode()),
3034
                    TOOL_PORTFOLIO_COMMENT,
3035
                    $comment->getId(),
3036
                    'PortfolioCommentUpdated',
3037
                    api_get_user_id(),
3038
                    [],
3039
                    null,
3040
                    '',
3041
                    '',
3042
                    $commmentSession ? $commmentSession->getId() : 0
3043
                );
3044
            }
3045
3046
            $values = $form->exportValues();
3047
3048
            $comment->setContent($values['content']);
3049
3050
            $this->em->flush();
3051
3052
            $this->processAttachments(
3053
                $form,
3054
                $comment->getAuthor(),
3055
                $comment->getId(),
3056
                PortfolioAttachment::TYPE_COMMENT
3057
            );
3058
3059
            Container::getEventDispatcher()->dispatch(
3060
                new PortfolioCommentEditedEvent(['comment' => $comment]),
3061
                Events::PORTFOLIO_COMMENT_EDITED
3062
            );
3063
3064
            Display::addFlash(
3065
                Display::return_message(get_lang('Item updated'), 'success')
3066
            );
3067
3068
            header("Location: $this->baseUrl"
3069
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
3070
                .'#comment-'.$comment->getId()
3071
            );
3072
            exit;
3073
        }
3074
3075
        $form->setDefaults([
3076
            'content' => $comment->getContent(),
3077
        ]);
3078
3079
        $interbreadcrumb[] = [
3080
            'name' => get_lang('Portfolio'),
3081
            'url' => $this->baseUrl,
3082
        ];
3083
        $interbreadcrumb[] = [
3084
            'name' => $item->getTitle(true),
3085
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3086
        ];
3087
3088
        $actions = [];
3089
        $actions[] = Display::url(
3090
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3091
            $this->baseUrl
3092
        );
3093
3094
        $content = $form->returnForm()
3095
            .PHP_EOL
3096
            .'<div class="row"> <div class="col-sm-8 col-sm-offset-2">'
3097
            .$this->generateAttachmentList($comment)
3098
            .'</div></div>';
3099
3100
        $this->renderView(
3101
            $content,
3102
            get_lang('Edit portfolio comment'),
3103
            $actions
3104
        );
3105
    }
3106
3107
    /**
3108
     * @throws \Doctrine\ORM\OptimisticLockException
3109
     * @throws \Doctrine\ORM\ORMException
3110
     */
3111
    public function deleteComment(PortfolioComment $comment)
3112
    {
3113
        if (!$this->commentBelongsToOwner($comment)) {
3114
            api_not_allowed(true);
3115
        }
3116
3117
        $this->em->remove($comment);
3118
3119
        $this->em
3120
            ->getRepository(PortfolioAttachment::class)
3121
            ->removeFromComment($comment);
3122
3123
        $this->em->flush();
3124
3125
        Display::addFlash(
3126
            Display::return_message(get_lang('The comment has been deleted.'), 'success')
3127
        );
3128
3129
        header("Location: $this->baseUrl");
3130
        exit;
3131
    }
3132
3133
    public function itemVisibilityChooser(Portfolio $item)
3134
    {
3135
        global $interbreadcrumb;
3136
3137
        if (!$this->itemBelongToOwner($item)) {
3138
            api_not_allowed(true);
3139
        }
3140
3141
        $em = Database::getManager();
3142
        $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
3143
3144
        $courseId = $this->course->getId();
3145
        $sessionId = $this->session ? $this->session->getId() : 0;
3146
3147
        $formAction = $this->baseUrl.http_build_query(['action' => 'item_visiblity_choose', 'id' => $item->getId()]);
3148
3149
        $form = new FormValidator('visibility', 'post', $formAction);
3150
        CourseManager::addUserGroupMultiSelect($form, ['USER:'.$this->owner->getId()]);
3151
        $form->addLabel(
3152
            '',
3153
            Display::return_message(
3154
                get_lang('Only selected users will see the content')
3155
                    .'<br>'.get_lang('Leave empty to enable the content for everyone'),
3156
                'info',
3157
                false
3158
            )
3159
        );
3160
        $form->addCheckBox('hidden', '', get_lang('Hidden but visible for me'));
3161
        $form->addButtonSave(get_lang('Save'));
3162
3163
        if ($form->validate()) {
3164
            $values = $form->exportValues();
3165
            $recipients = CourseManager::separateUsersGroups($values['users'])['users'];
3166
            $courseInfo = api_get_course_info_by_id($courseId);
3167
3168
            Database::delete(
3169
                $tblItemProperty,
3170
                [
3171
                    'c_id = ? ' => [$courseId],
3172
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO, $item->getId()],
3173
                    'AND lastedit_type = ? ' => ['visible'],
3174
                ]
3175
            );
3176
3177
            if (empty($recipients) && empty($values['hidden'])) {
3178
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
3179
            } else {
3180
                if (empty($values['hidden'])) {
3181
                    foreach ($recipients as $userId) {
3182
                        api_item_property_update(
3183
                            $courseInfo,
3184
                            TOOL_PORTFOLIO,
3185
                            $item->getId(),
3186
                            'visible',
3187
                            api_get_user_id(),
3188
                            [],
3189
                            $userId,
3190
                            '',
3191
                            '',
3192
                            $sessionId
3193
                        );
3194
                    }
3195
                }
3196
3197
                $item->setVisibility(Portfolio::VISIBILITY_PER_USER);
3198
            }
3199
3200
            $em->flush();
3201
3202
            Container::getEventDispatcher()->dispatch(
3203
                new PortfolioItemVisibilityChangedEvent([
3204
                    'portfolio' => $item,
3205
                    'recipients' => array_values($recipients),
3206
                ]),
3207
                Events::PORTFOLIO_ITEM_VISIBILITY_CHANGED
3208
            );
3209
3210
            Display::addFlash(
3211
                Display::return_message(get_lang('Post visibility changed'), 'success')
3212
            );
3213
3214
            header("Location: $formAction");
3215
            exit;
3216
        }
3217
3218
        $result = Database::select(
3219
            'to_user_id',
3220
            $tblItemProperty,
3221
            [
3222
                'where' => [
3223
                    'c_id = ? ' => [$courseId],
3224
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO, $item->getId()],
3225
                    'AND to_user_id IS NOT NULL ' => [],
3226
                ],
3227
            ]
3228
        );
3229
3230
        $recipients = array_map(
3231
            function (array $item): string {
3232
                return 'USER:'.$item['to_user_id'];
3233
            },
3234
            $result
3235
        );
3236
3237
        $defaults = ['users' => $recipients];
3238
3239
        if (empty($recipients) && Portfolio::VISIBILITY_PER_USER === $item->getVisibility()) {
3240
            $defaults['hidden'] = true;
3241
        }
3242
3243
        $form->setDefaults($defaults);
3244
        $form->protect();
3245
3246
        $interbreadcrumb[] = [
3247
            'name' => get_lang('Portfolio'),
3248
            'url' => $this->baseUrl,
3249
        ];
3250
        $interbreadcrumb[] = [
3251
            'name' => $item->getTitle(true),
3252
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3253
        ];
3254
3255
        $actions = [];
3256
        $actions[] = Display::url(
3257
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3258
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
3259
        );
3260
3261
        $this->renderView(
3262
            $form->returnForm(),
3263
            get_lang('Choose recipients'),
3264
            $actions
3265
        );
3266
    }
3267
3268
    public function commentVisibilityChooser(PortfolioComment $comment): void
3269
    {
3270
        global $interbreadcrumb;
3271
3272
        if (!$this->commentBelongsToOwner($comment)) {
3273
            api_not_allowed(true);
3274
        }
3275
3276
        $em = Database::getManager();
3277
        $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
3278
3279
        $courseId = $this->course->getId();
3280
        $sessionId = $this->session ? $this->session->getId() : 0;
3281
        $item = $comment->getItem();
3282
3283
        $formAction = $this->baseUrl.http_build_query(['action' => 'comment_visiblity_choose', 'id' => $comment->getId()]);
3284
3285
        $form = new FormValidator('visibility', 'post', $formAction);
3286
        CourseManager::addUserGroupMultiSelect($form, ['USER:'.$this->owner->getId()]);
3287
        $form->addLabel(
3288
            '',
3289
            Display::return_message(
3290
                get_lang('Only selected users will see the content')
3291
                    .'<br>'.get_lang('Leave empty to enable the content for everyone'),
3292
                'info',
3293
                false
3294
            )
3295
        );
3296
        $form->addCheckBox('hidden', '', get_lang('Hidden but visible for me'));
3297
        $form->addButtonSave(get_lang('Save'));
3298
3299
        if ($form->validate()) {
3300
            $values = $form->exportValues();
3301
            $recipients = CourseManager::separateUsersGroups($values['users'])['users'];
3302
            $courseInfo = api_get_course_info_by_id($courseId);
3303
3304
            Database::delete(
3305
                $tblItemProperty,
3306
                [
3307
                    'c_id = ? ' => [$courseId],
3308
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO_COMMENT, $comment->getId()],
3309
                    'AND lastedit_type = ? ' => ['visible'],
3310
                ]
3311
            );
3312
3313
            if (empty($recipients) && empty($values['hidden'])) {
3314
                $comment->setVisibility(PortfolioComment::VISIBILITY_VISIBLE);
3315
            } else {
3316
                if (empty($values['hidden'])) {
3317
                    foreach ($recipients as $userId) {
3318
                        api_item_property_update(
3319
                            $courseInfo,
3320
                            TOOL_PORTFOLIO_COMMENT,
3321
                            $comment->getId(),
3322
                            'visible',
3323
                            api_get_user_id(),
3324
                            [],
3325
                            $userId,
3326
                            '',
3327
                            '',
3328
                            $sessionId
3329
                        );
3330
                    }
3331
                }
3332
3333
                $comment->setVisibility(PortfolioComment::VISIBILITY_PER_USER);
3334
            }
3335
3336
            $em->flush();
3337
3338
            Display::addFlash(
3339
                Display::return_message(get_lang('The visibility has been changed.'), 'success')
3340
            );
3341
3342
            header("Location: $formAction");
3343
            exit;
3344
        }
3345
3346
        $result = Database::select(
3347
            'to_user_id',
3348
            $tblItemProperty,
3349
            [
3350
                'where' => [
3351
                    'c_id = ? ' => [$courseId],
3352
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO_COMMENT, $comment->getId()],
3353
                    'AND to_user_id IS NOT NULL ' => [],
3354
                ],
3355
            ]
3356
        );
3357
3358
        $recipients = array_map(
3359
            function (array $itemProperty): string {
3360
                return 'USER:'.$itemProperty['to_user_id'];
3361
            },
3362
            $result
3363
        );
3364
3365
        $defaults = ['users' => $recipients];
3366
3367
        if (empty($recipients) && PortfolioComment::VISIBILITY_PER_USER === $comment->getVisibility()) {
3368
            $defaults['hidden'] = true;
3369
        }
3370
3371
        $form->setDefaults($defaults);
3372
        $form->protect();
3373
3374
        $interbreadcrumb[] = [
3375
            'name' => get_lang('Portfolio'),
3376
            'url' => $this->baseUrl,
3377
        ];
3378
        $interbreadcrumb[] = [
3379
            'name' => $item->getTitle(true),
3380
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3381
        ];
3382
        $interbreadcrumb[] = [
3383
            'name' => $comment->getExcerpt(40),
3384
            'url' => $this->baseUrl
3385
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
3386
                .'#comment-'.$comment->getId(),
3387
        ];
3388
3389
        $actions = [];
3390
        $actions[] = Display::url(
3391
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3392
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
3393
        );
3394
3395
        $this->renderView(
3396
            $form->returnForm(),
3397
            get_lang('Choose recipients'),
3398
            $actions
3399
        );
3400
    }
3401
3402
    private function isAllowed(): bool
3403
    {
3404
        $isSubscribedInCourse = false;
3405
3406
        if ($this->course) {
3407
            $isSubscribedInCourse = CourseManager::is_user_subscribed_in_course(
3408
                api_get_user_id(),
3409
                $this->course->getCode(),
3410
                (bool) $this->session,
3411
                $this->session ? $this->session->getId() : 0
3412
            );
3413
        }
3414
3415
        if (!$this->course || $isSubscribedInCourse) {
3416
            return true;
3417
        }
3418
3419
        return false;
3420
    }
3421
3422
    private function blockIsNotAllowed(): void
3423
    {
3424
        if (!$this->isAllowed()) {
3425
            api_not_allowed(true);
3426
        }
3427
    }
3428
3429
    /**
3430
     * @param  bool  $showHeader
3431
     */
3432
    private function renderView(string $content, string $toolName, array $actions = [], bool $showHeader = true): void
3433
    {
3434
        global $this_section;
3435
3436
        $this_section = $this->course ? SECTION_COURSES : SECTION_SOCIAL;
3437
3438
        $view = new Template($toolName);
3439
3440
        if ($showHeader) {
3441
            $view->assign('header', $toolName);
3442
        }
3443
3444
        $actionsStr = '';
3445
3446
        if ($this->course) {
3447
            $actionsStr .= Display::return_introduction_section(TOOL_PORTFOLIO);
3448
        }
3449
3450
        if ($actions) {
3451
            $actions = implode('', $actions);
3452
3453
            $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]);
3454
        }
3455
3456
        $view->assign('baseurl', $this->baseUrl);
3457
        $view->assign('actions', $actionsStr);
3458
3459
        $view->assign('content', $content);
3460
        $view->display_one_col_template();
3461
    }
3462
3463
    private function categoryBelongToOwner(PortfolioCategory $category): bool
3464
    {
3465
        if ($category->getUser()->getId() != $this->owner->getId()) {
3466
            return false;
3467
        }
3468
3469
        return true;
3470
    }
3471
3472
    private function addAttachmentsFieldToForm(FormValidator $form): void
3473
    {
3474
        $form->addButton('add_attachment', get_lang('Add attachment'), 'plus');
3475
        $form->addHtml('<div id="container-attachments" style="display: none;">');
3476
        $form->addFile('attachment_file[]', get_lang('Files attachments'));
3477
        $form->addText('attachment_comment[]', get_lang('Description'), false);
3478
        $form->addHtml('</div>');
3479
3480
        $script = "$(function () {
3481
            var attachmentsTemplate = $('#container-attachments').html();
3482
            var \$btnAdd = $('[name=\"add_attachment\"]');
3483
            var \$reference = \$btnAdd.parents('.form-group');
3484
3485
            \$btnAdd.on('click', function (e) {
3486
                e.preventDefault();
3487
3488
                $(attachmentsTemplate).insertBefore(\$reference);
3489
            });
3490
        })";
3491
3492
        $form->addHtml("<script>$script</script>");
3493
    }
3494
3495
    private function processAttachments(
3496
        FormValidator $form,
3497
        User $user,
3498
        int $originId,
3499
        int $originType
3500
    ): void {
3501
        $em = Database::getManager();
3502
        $fs = new Filesystem();
3503
3504
        $comments = $form->getSubmitValue('attachment_comment');
3505
3506
        foreach ($_FILES['attachment_file']['error'] as $i => $attachmentFileError) {
3507
            if ($attachmentFileError != UPLOAD_ERR_OK) {
3508
                continue;
3509
            }
3510
3511
            $_file = [
3512
                'name' => $_FILES['attachment_file']['name'][$i],
3513
                'type' => $_FILES['attachment_file']['type'][$i],
3514
                'tmp_name' => $_FILES['attachment_file']['tmp_name'][$i],
3515
                'size' => $_FILES['attachment_file']['size'][$i],
3516
            ];
3517
3518
            if (empty($_file['type'])) {
3519
                $_file['type'] = DocumentManager::file_get_mime_type($_file['name']);
3520
            }
3521
3522
            $newFileName = add_ext_on_mime(stripslashes($_file['name']), $_file['type']);
3523
3524
            if (!filter_extension($newFileName)) {
3525
                Display::addFlash(Display::return_message(get_lang('File upload failed: this file extension or file type is prohibited'), 'error'));
3526
                continue;
3527
            }
3528
3529
            $newFileName = uniqid();
3530
            $attachmentsDirectory = UserManager::getUserPathById($user->getId(), 'system').'portfolio_attachments/';
3531
3532
            if (!$fs->exists($attachmentsDirectory)) {
3533
                $fs->mkdir($attachmentsDirectory, api_get_permissions_for_new_directories());
3534
            }
3535
3536
            $attachmentFilename = $attachmentsDirectory.$newFileName;
3537
3538
            if (is_uploaded_file($_file['tmp_name'])) {
3539
                $moved = move_uploaded_file($_file['tmp_name'], $attachmentFilename);
3540
3541
                if (!$moved) {
3542
                    Display::addFlash(Display::return_message(get_lang('The uploaded file could not be saved (perhaps a permission problem?)'), 'error'));
3543
                    continue;
3544
                }
3545
            }
3546
3547
            $attachment = new PortfolioAttachment();
3548
            $attachment
3549
                ->setFilename($_file['name'])
3550
                ->setComment($comments[$i])
3551
                ->setPath($newFileName)
3552
                ->setOrigin($originId)
3553
                ->setOriginType($originType)
3554
                ->setSize($_file['size']);
3555
3556
            $em->persist($attachment);
3557
            $em->flush();
3558
        }
3559
    }
3560
3561
    private function itemBelongToOwner(Portfolio $item): bool
3562
    {
3563
        if ($item->getUser()->getId() != $this->owner->getId()) {
3564
            return false;
3565
        }
3566
3567
        return true;
3568
    }
3569
3570
    private function commentBelongsToOwner(PortfolioComment $comment): bool
3571
    {
3572
        return $comment->getAuthor() === $this->owner;
3573
    }
3574
3575
    private function createFormTagFilter(bool $listByUser = false): FormValidator
3576
    {
3577
        $tags = Database::getManager()
3578
            ->getRepository(Tag::class)
3579
            ->findForPortfolioInCourseQuery($this->course, $this->session)
3580
            ->getQuery()
3581
            ->getResult()
3582
        ;
3583
3584
        $frmTagList = new FormValidator(
3585
            'frm_tag_list',
3586
            'get',
3587
            $this->baseUrl.($listByUser ? 'user='.$this->owner->getId() : ''),
3588
            '',
3589
            [],
3590
            FormValidator::LAYOUT_BOX
3591
        );
3592
3593
        $frmTagList->addDatePicker('date', get_lang('Creation date'));
3594
3595
        $frmTagList->addSelectFromCollection(
3596
            'tags',
3597
            get_lang('Tags'),
3598
            $tags,
3599
            ['multiple' => 'multiple'],
3600
            false,
3601
            'getTag'
3602
        );
3603
3604
        $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search');
3605
        $frmTagList->applyFilter('text', 'trim');
3606
        $frmTagList->addHtml('<br>');
3607
        $frmTagList->addButtonFilter(get_lang('Filter'));
3608
3609
        if ($this->course) {
3610
            $frmTagList->addHidden('cidReq', $this->course->getCode());
3611
            $frmTagList->addHidden('id_session', $this->session ? $this->session->getId() : 0);
3612
            $frmTagList->addHidden('gidReq', 0);
3613
            $frmTagList->addHidden('gradebook', 0);
3614
            $frmTagList->addHidden('origin', '');
3615
            $frmTagList->addHidden('categoryId', 0);
3616
            $frmTagList->addHidden('subCategoryIds', '');
3617
3618
            if ($listByUser) {
3619
                $frmTagList->addHidden('user', $this->owner->getId());
3620
            }
3621
        }
3622
3623
        return $frmTagList;
3624
    }
3625
3626
    /**
3627
     * @throws Exception
3628
     */
3629
    private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false, bool $listAlphabeticalOrder = false): FormValidator
3630
    {
3631
        $frmStudentList = new FormValidator(
3632
            'frm_student_list',
3633
            'get',
3634
            $this->baseUrl,
3635
            '',
3636
            [],
3637
            FormValidator::LAYOUT_BOX
3638
        );
3639
3640
        $urlParams = http_build_query(
3641
            [
3642
                'a' => 'search_user_by_course',
3643
                'course_id' => $this->course->getId(),
3644
                'session_id' => $this->session ? $this->session->getId() : 0,
3645
            ]
3646
        );
3647
3648
        /** @var SelectAjax $slctUser */
3649
        $slctUser = $frmStudentList->addSelectAjax(
3650
            'user',
3651
            get_lang('Select a learner portfolio'),
3652
            [],
3653
            [
3654
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
3655
                'placeholder' => get_lang('Search users'),
3656
                'formatResult' => SelectAjax::templateResultForUsersInCourse(),
3657
                'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
3658
            ]
3659
        );
3660
3661
        if ($listByUser) {
3662
            $slctUser->addOption(
3663
                $this->owner->getFullName(),
3664
                $this->owner->getId(),
3665
                [
3666
                    'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
3667
                    'data-username' => $this->owner->getUsername(),
3668
                ]
3669
            );
3670
3671
            $link = Display::url(
3672
                get_lang('Back to the main course portfolio'),
3673
                $this->baseUrl
3674
            );
3675
        } else {
3676
            $link = Display::url(
3677
                get_lang('See my portfolio in this course'),
3678
                $this->baseUrl.http_build_query(['user' => api_get_user_id()])
3679
            );
3680
        }
3681
3682
        $frmStudentList->addHtml("<p>$link</p>");
3683
3684
        if ($listHighlighted) {
3685
            $link = Display::url(
3686
                get_lang('Back to the main course portfolio'),
3687
                $this->baseUrl
3688
            );
3689
        } else {
3690
            $link = Display::url(
3691
                get_lang('See highlights'),
3692
                $this->baseUrl.http_build_query(['list_highlighted' => true])
3693
            );
3694
        }
3695
3696
        $frmStudentList->addHtml("<p>$link</p>");
3697
3698
        if (true !== api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
3699
            if ($listAlphabeticalOrder) {
3700
                $link = Display::url(
3701
                    get_lang('Return to the chronological order'),
3702
                    $this->baseUrl
3703
                );
3704
            } else {
3705
                $link = Display::url(
3706
                    get_lang('View in alphabetical order'),
3707
                    $this->baseUrl.http_build_query(['list_alphabetical' => true])
3708
                );
3709
            }
3710
3711
            $frmStudentList->addHtml("<p>$link</p>");
3712
        }
3713
3714
        return $frmStudentList;
3715
    }
3716
3717
    private function getCategoriesForIndex(?int $parentId = null): array
3718
    {
3719
        $categoriesCriteria = [];
3720
3721
        if (!api_is_platform_admin() && null !== $this->owner->getId()) {
3722
            $categoriesCriteria['isVisible'] = true;
3723
        }
3724
        if (isset($parentId)) {
3725
            $categoriesCriteria['parent'] = $parentId;
3726
        }
3727
3728
        return $this->em
3729
            ->getRepository(PortfolioCategory::class)
3730
            ->findBy($categoriesCriteria);
3731
    }
3732
3733
    private function getHighlightedItems()
3734
    {
3735
        $queryBuilder = $this->em->createQueryBuilder();
3736
        $queryBuilder
3737
            ->select('pi')
3738
            ->from(Portfolio::class, 'pi')
3739
            ->where('pi.course = :course')
3740
            ->andWhere('pi.isHighlighted = TRUE')
3741
            ->setParameter('course', $this->course);
3742
3743
        if ($this->session) {
3744
            $queryBuilder->andWhere('pi.session = :session');
3745
            $queryBuilder->setParameter('session', $this->session);
3746
        } else {
3747
            $queryBuilder->andWhere('pi.session IS NULL');
3748
        }
3749
3750
        if ($this->advancedSharingEnabled) {
3751
            $queryBuilder
3752
                ->leftJoin(
3753
                    CItemProperty::class,
3754
                    'cip',
3755
                    Join::WITH,
3756
                    "cip.ref = pi.id
3757
                        AND cip.tool = :cip_tool
3758
                        AND cip.course = pi.course
3759
                        AND cip.lasteditType = 'visible'
3760
                        AND cip.toUser = :current_user"
3761
                )
3762
                ->andWhere(
3763
                    sprintf(
3764
                        'pi.visibility = %d
3765
                            OR (
3766
                                pi.visibility = %d AND cip IS NOT NULL OR pi.user = :current_user
3767
                            )',
3768
                        Portfolio::VISIBILITY_VISIBLE,
3769
                        Portfolio::VISIBILITY_PER_USER
3770
                    )
3771
                )
3772
                ->setParameter('cip_tool', TOOL_PORTFOLIO)
3773
            ;
3774
        } else {
3775
            $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3776
3777
            if (api_is_allowed_to_edit()) {
3778
                $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3779
            }
3780
3781
            $queryBuilder->andWhere(
3782
                $queryBuilder->expr()->orX(
3783
                    'pi.user = :current_user',
3784
                    $queryBuilder->expr()->andX(
3785
                        'pi.user != :current_user',
3786
                        $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3787
                    )
3788
                )
3789
            );
3790
        }
3791
3792
        $queryBuilder->setParameter('current_user', api_get_user_id());
3793
        $queryBuilder->orderBy('pi.creationDate', 'DESC');
3794
3795
        return $queryBuilder->getQuery()->getResult();
3796
    }
3797
3798
    private function getItemsForIndex(
3799
        bool $listByUser = false,
3800
        FormValidator $frmFilterList = null,
3801
        bool $alphabeticalOrder = false
3802
    ) {
3803
        $currentUserId = api_get_user_id();
3804
3805
        if ($this->course) {
3806
            $showBaseContentInSession = $this->session
3807
                && true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions');
3808
3809
            $queryBuilder = $this->em->createQueryBuilder();
3810
            $queryBuilder
3811
                ->select('pi')
3812
                ->from(Portfolio::class, 'pi')
3813
                ->where('pi.course = :course');
3814
3815
            $queryBuilder->setParameter('course', $this->course);
3816
3817
            if ($this->session) {
3818
                $queryBuilder->andWhere(
3819
                    $showBaseContentInSession ? 'pi.session = :session OR pi.session IS NULL' : 'pi.session = :session'
3820
                );
3821
                $queryBuilder->setParameter('session', $this->session);
3822
            } else {
3823
                $queryBuilder->andWhere('pi.session IS NULL');
3824
            }
3825
3826
            if ($frmFilterList && $frmFilterList->validate()) {
3827
                $values = $frmFilterList->exportValues();
3828
3829
                if (!empty($values['date'])) {
3830
                    $queryBuilder
3831
                        ->andWhere('pi.creationDate >= :date')
3832
                        ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
3833
                    ;
3834
                }
3835
3836
                if (!empty($values['tags'])) {
3837
                    $queryBuilder
3838
                        ->innerJoin(ExtraFieldRelTag::class, 'efrt', Join::WITH, 'efrt.itemId = pi.id')
3839
                        ->innerJoin(ExtraFieldEntity::class, 'ef', Join::WITH, 'ef.id = efrt.fieldId')
3840
                        ->andWhere('ef.extraFieldType = :efType')
3841
                        ->andWhere('ef.variable = :variable')
3842
                        ->andWhere('efrt.tagId IN (:tags)');
3843
3844
                    $queryBuilder->setParameter('efType', ExtraFieldEntity::PORTFOLIO_TYPE);
3845
                    $queryBuilder->setParameter('variable', 'tags');
3846
                    $queryBuilder->setParameter('tags', $values['tags']);
3847
                }
3848
3849
                if (!empty($values['text'])) {
3850
                    $queryBuilder->andWhere(
3851
                        $queryBuilder->expr()->orX(
3852
                            $queryBuilder->expr()->like('pi.title', ':text'),
3853
                            $queryBuilder->expr()->like('pi.content', ':text')
3854
                        )
3855
                    );
3856
3857
                    $queryBuilder->setParameter('text', '%'.$values['text'].'%');
3858
                }
3859
3860
                // Filters by category level 0
3861
                $searchCategories = [];
3862
                if (!empty($values['categoryId'])) {
3863
                    $searchCategories[] = $values['categoryId'];
3864
                    $subCategories = $this->getCategoriesForIndex($values['categoryId']);
3865
                    if (count($subCategories) > 0) {
3866
                        foreach ($subCategories as $subCategory) {
3867
                            $searchCategories[] = $subCategory->getId();
3868
                        }
3869
                    }
3870
                    $queryBuilder->andWhere('pi.category IN('.implode(',', $searchCategories).')');
3871
                }
3872
3873
                // Filters by sub-category, don't show the selected values
3874
                $diff = [];
3875
                if (!empty($values['subCategoryIds']) && !('all' === $values['subCategoryIds'])) {
3876
                    $subCategoryIds = explode(',', $values['subCategoryIds']);
3877
                    $diff = array_diff($searchCategories, $subCategoryIds);
3878
                } else {
3879
                    if (trim($values['subCategoryIds']) === '') {
3880
                        $diff = $searchCategories;
3881
                    }
3882
                }
3883
                if (!empty($diff)) {
3884
                    unset($diff[0]);
3885
                    if (!empty($diff)) {
3886
                        $queryBuilder->andWhere('pi.category NOT IN('.implode(',', $diff).')');
3887
                    }
3888
                }
3889
            }
3890
3891
            if ($listByUser) {
3892
                $queryBuilder
3893
                    ->andWhere('pi.user = :user')
3894
                    ->setParameter('user', $this->owner);
3895
            }
3896
3897
            if ($this->advancedSharingEnabled) {
3898
                $queryBuilder
3899
                    ->leftJoin(
3900
                        CItemProperty::class,
3901
                        'cip',
3902
                        Join::WITH,
3903
                        "cip.ref = pi.id
3904
                            AND cip.tool = :cip_tool
3905
                            AND cip.course = pi.course
3906
                            AND cip.lasteditType = 'visible'
3907
                            AND cip.toUser = :current_user"
3908
                    )
3909
                    ->andWhere(
3910
                        sprintf(
3911
                            'pi.visibility = %d
3912
                            OR (
3913
                                pi.visibility = %d AND cip IS NOT NULL OR pi.user = :current_user
3914
                            )',
3915
                            Portfolio::VISIBILITY_VISIBLE,
3916
                            Portfolio::VISIBILITY_PER_USER
3917
                        )
3918
                    )
3919
                    ->setParameter('cip_tool', TOOL_PORTFOLIO)
3920
                ;
3921
            } else {
3922
                $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3923
3924
                if (api_is_allowed_to_edit()) {
3925
                    $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3926
                }
3927
3928
                $queryBuilder->andWhere(
3929
                    $queryBuilder->expr()->orX(
3930
                        'pi.user = :current_user',
3931
                        $queryBuilder->expr()->andX(
3932
                            'pi.user != :current_user',
3933
                            $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3934
                        )
3935
                    )
3936
                );
3937
            }
3938
3939
            $queryBuilder->setParameter('current_user', $currentUserId);
3940
            if ($alphabeticalOrder || true === api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
3941
                $queryBuilder->orderBy('pi.title', 'ASC');
3942
            } else {
3943
                $queryBuilder->orderBy('pi.creationDate', 'DESC');
3944
            }
3945
3946
            $items = $queryBuilder->getQuery()->getResult();
3947
3948
            if ($showBaseContentInSession) {
3949
                $items = array_filter(
3950
                    $items,
3951
                    fn (Portfolio $item) => !($this->session && !$item->getSession() && $item->isDuplicatedInSession($this->session))
3952
                );
3953
            }
3954
3955
            return $items;
3956
        } else {
3957
            $itemsCriteria = [];
3958
            $itemsCriteria['category'] = null;
3959
            $itemsCriteria['user'] = $this->owner;
3960
3961
            if ($currentUserId !== $this->owner->getId()) {
3962
                $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE;
3963
            }
3964
3965
            $items = $this->em
3966
                ->getRepository(Portfolio::class)
3967
                ->findBy($itemsCriteria, ['creationDate' => 'DESC']);
3968
        }
3969
3970
        return $items;
3971
    }
3972
3973
    /**
3974
     * @throws \Doctrine\ORM\ORMException
3975
     * @throws \Doctrine\ORM\OptimisticLockException
3976
     * @throws \Doctrine\ORM\TransactionRequiredException
3977
     */
3978
    private function createCommentForm(Portfolio $item): string
3979
    {
3980
        $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
3981
3982
        $templates = $this->em
3983
            ->getRepository(PortfolioComment::class)
3984
            ->findBy(
3985
                [
3986
                    'isTemplate' => true,
3987
                    'author' => $this->owner,
3988
                ]
3989
            );
3990
3991
        $form = new FormValidator('frm_comment', 'post', $formAction);
3992
        $form->addHeader(get_lang('Add a new comment'));
3993
        $form->addSelectFromCollection(
3994
            'template',
3995
            [
3996
                get_lang('Template'),
3997
                null,
3998
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
3999
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
4000
            ],
4001
            $templates,
4002
            [],
4003
            true,
4004
            'getExcerpt'
4005
        );
4006
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
4007
        $form->addHidden('item', $item->getId());
4008
        $form->addHidden('parent', 0);
4009
        $form->applyFilter('content', 'trim');
4010
4011
        $this->addAttachmentsFieldToForm($form);
4012
4013
        $form->addButtonSave(get_lang('Save'));
4014
4015
        if ($form->validate()) {
4016
            if ($this->session
4017
                && true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
4018
                && !$item->getSession()
4019
            ) {
4020
                $duplicate = $item->duplicateInSession($this->session);
4021
4022
                $this->em->persist($duplicate);
4023
                $this->em->flush();
4024
4025
                $item = $duplicate;
4026
4027
                $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
4028
            }
4029
4030
            $values = $form->exportValues();
4031
4032
            $parentComment = $this->em->find(PortfolioComment::class, $values['parent']);
4033
4034
            $comment = new PortfolioComment();
4035
            $comment
4036
                ->setAuthor($this->owner)
4037
                ->setParent($parentComment)
4038
                ->setContent($values['content'])
4039
                ->setDate(api_get_utc_datetime(null, false, true))
4040
                ->setItem($item);
4041
4042
            $this->em->persist($comment);
4043
            $this->em->flush();
4044
4045
            $this->processAttachments(
4046
                $form,
4047
                $comment->getAuthor(),
4048
                $comment->getId(),
4049
                PortfolioAttachment::TYPE_COMMENT
4050
            );
4051
4052
            Container::getEventDispatcher()->dispatch(
4053
                new PortfolioItemCommentedEvent(['comment' => $comment]),
4054
                Events::PORTFOLIO_ITEM_COMMENTED
4055
            );
4056
4057
            PortfolioNotifier::notifyTeachersAndAuthor($comment);
4058
4059
            Display::addFlash(
4060
                Display::return_message(get_lang('You comment has been added'), 'success')
4061
            );
4062
4063
            header("Location: $formAction");
4064
            exit;
4065
        }
4066
4067
        $js = '<script>
4068
            $(function() {
4069
                $(\'#frm_comment_template\').on(\'change\', function () {
4070
                    $(\'#portfolio-spinner\').show();
4071
4072
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template_comment&comment=\' + this.value)
4073
                        .done(function(response) {
4074
                            CKEDITOR.instances.content.setData(response.content);
4075
                        })
4076
                        .fail(function () {
4077
                            CKEDITOR.instances.content.setData(\'\');
4078
                        })
4079
                        .always(function() {
4080
                          $(\'#portfolio-spinner\').hide();
4081
                        });
4082
                });
4083
            });
4084
        </script>';
4085
4086
        return $form->returnForm().$js;
4087
    }
4088
4089
    private function generateAttachmentList($post, bool $includeHeader = true): string
4090
    {
4091
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
4092
4093
        $postOwnerId = 0;
4094
4095
        if ($post instanceof Portfolio) {
4096
            $attachments = $attachmentsRepo->findFromItem($post);
4097
4098
            $postOwnerId = $post->getUser()->getId();
4099
        } elseif ($post instanceof PortfolioComment) {
4100
            $attachments = $attachmentsRepo->findFromComment($post);
4101
4102
            $postOwnerId = $post->getAuthor()->getId();
4103
        }
4104
4105
        if (empty($attachments)) {
4106
            return '';
4107
        }
4108
4109
        $currentUserId = api_get_user_id();
4110
4111
        $listItems = '<ul class="fa-ul">';
4112
4113
        $deleteIcon = Display::return_icon(
4114
            'delete.png',
4115
            get_lang('Delete attachment'),
4116
            ['style' => 'display: inline-block'],
4117
            ICON_SIZE_TINY
4118
        );
4119
        $deleteAttrs = ['class' => 'btn-portfolio-delete'];
4120
4121
        /** @var PortfolioAttachment $attachment */
4122
        foreach ($attachments as $attachment) {
4123
            $downloadParams = http_build_query(['action' => 'download_attachment', 'file' => $attachment->getPath()]);
4124
            $deleteParams = http_build_query(['action' => 'delete_attachment', 'file' => $attachment->getPath()]);
4125
4126
            $listItems .= '<li>'
4127
                .'<span class="fa-li fa fa-paperclip" aria-hidden="true"></span>'
4128
                .Display::url(
4129
                    Security::remove_XSS($attachment->getFilename()),
4130
                    $this->baseUrl.$downloadParams
4131
                );
4132
4133
            if ($currentUserId === $postOwnerId) {
4134
                $listItems .= PHP_EOL.Display::url($deleteIcon, $this->baseUrl.$deleteParams, $deleteAttrs);
4135
            }
4136
4137
            if ($attachment->getComment()) {
4138
                $listItems .= '<p class="text-muted">'.Security::remove_XSS($attachment->getComment()).'</p>';
4139
            }
4140
4141
            $listItems .= '</li>';
4142
        }
4143
4144
        $listItems .= '</ul>';
4145
4146
        if ($includeHeader) {
4147
            $listItems = '<h1 class="h4">'.get_lang('Files attachments').'</h1>'
4148
                .$listItems;
4149
        }
4150
4151
        return $listItems;
4152
    }
4153
4154
    private function generateItemContent(Portfolio $item): string
4155
    {
4156
        $originId = $item->getOrigin();
4157
4158
        if (empty($originId)) {
4159
            return $item->getContent();
4160
        }
4161
4162
        $em = Database::getManager();
4163
4164
        $originContent = '';
4165
        $originContentFooter = '';
4166
4167
        if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
4168
            $origin = $em->find(Portfolio::class, $item->getOrigin());
4169
4170
            if ($origin) {
4171
                $originContent = Security::remove_XSS($origin->getContent());
4172
                $originContentFooter = vsprintf(
4173
                    get_lang('Originally published as "%s" by %s'),
4174
                    [
4175
                        "<cite>{$origin->getTitle(true)}</cite>",
4176
                        $origin->getUser()->getFullName(),
4177
                    ]
4178
                );
4179
            }
4180
        } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
4181
            $origin = $em->find(PortfolioComment::class, $item->getOrigin());
4182
4183
            if ($origin) {
4184
                $originContent = Security::remove_XSS($origin->getContent());
4185
                $originContentFooter = vsprintf(
4186
                    get_lang('Originally commented by %s in "%s"'),
4187
                    [
4188
                        $origin->getAuthor()->getFullName(),
4189
                        "<cite>{$origin->getItem()->getTitle(true)}</cite>",
4190
                    ]
4191
                );
4192
            }
4193
        }
4194
4195
        if ($originContent) {
4196
            return "<figure>
4197
                    <blockquote>$originContent</blockquote>
4198
                    <figcaption style=\"margin-bottom: 10px;\">$originContentFooter</figcaption>
4199
                </figure>
4200
                <div class=\"clearfix\">".Security::remove_XSS($item->getContent()).'</div>'
4201
            ;
4202
        }
4203
4204
        return Security::remove_XSS($item->getContent());
4205
    }
4206
4207
    private function getItemsInHtmlFormatted(array $items): array
4208
    {
4209
        $itemsHtml = [];
4210
4211
        /** @var Portfolio $item */
4212
        foreach ($items as $item) {
4213
            $itemCourse = $item->getCourse();
4214
            $itemSession = $item->getSession();
4215
4216
            $creationDate = api_convert_and_format_date($item->getCreationDate());
4217
            $updateDate = api_convert_and_format_date($item->getUpdateDate());
4218
4219
            $metadata = '<ul class="list-unstyled text-muted">';
4220
4221
            if ($itemSession) {
4222
                $metadata .= '<li>'.get_lang('Course').': '.$itemSession->getName().' ('
4223
                    .$itemCourse->getTitle().') </li>';
4224
            } elseif ($itemCourse) {
4225
                $metadata .= '<li>'.get_lang('Course').': '.$itemCourse->getTitle().'</li>';
4226
            }
4227
4228
            $metadata .= '<li>'.sprintf(get_lang('Creation date: %s'), $creationDate).'</li>';
4229
4230
            if ($itemCourse) {
4231
                $propertyInfo = api_get_item_property_info(
4232
                    $itemCourse->getId(),
4233
                    TOOL_PORTFOLIO,
4234
                    $item->getId(),
4235
                    $itemSession ? $itemSession->getId() : 0
4236
                );
4237
4238
                if ($propertyInfo) {
4239
                    $metadata .= '<li>'
4240
                        .sprintf(
4241
                            get_lang('Updated on %s by %s'),
4242
                            api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
4243
                            api_get_user_entity($propertyInfo['lastedit_user_id'])->getFullName()
4244
                        )
4245
                        .'</li>';
4246
                }
4247
            } else {
4248
                $metadata .= '<li>'.sprintf(get_lang('Update date: %s'), $updateDate).'</li>';
4249
            }
4250
4251
            if ($item->getCategory()) {
4252
                $metadata .= '<li>'.sprintf(get_lang('Category: %s'), $item->getCategory()->getTitle()).'</li>';
4253
            }
4254
4255
            $metadata .= '</ul>';
4256
4257
            $itemContent = $this->generateItemContent($item);
4258
4259
            $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata);
4260
        }
4261
4262
        return $itemsHtml;
4263
    }
4264
4265
    private function getCommentsInHtmlFormatted(array $comments): array
4266
    {
4267
        $commentsHtml = [];
4268
4269
        /** @var PortfolioComment $comment */
4270
        foreach ($comments as $comment) {
4271
            $item = $comment->getItem();
4272
            $date = api_convert_and_format_date($comment->getDate());
4273
4274
            $metadata = '<ul class="list-unstyled text-muted">';
4275
            $metadata .= '<li>'.sprintf(get_lang('Date: %s'), $date).'</li>';
4276
            $metadata .= '<li>'.sprintf(get_lang('Item title: %s'), Security::remove_XSS($item->getTitle()))
4277
                .'</li>';
4278
            $metadata .= '</ul>';
4279
4280
            $commentsHtml[] = Display::panel(
4281
                Security::remove_XSS($comment->getContent()),
4282
                '',
4283
                '',
4284
                'default',
4285
                $metadata
4286
            );
4287
        }
4288
4289
        return $commentsHtml;
4290
    }
4291
4292
    /**
4293
     * @param string $htmlContent
4294
     * @param array $imagePaths Relative paths found in $htmlContent
4295
     *
4296
     * @return string
4297
     */
4298
    private function fixMediaSourcesToHtml(string $htmlContent, array &$imagePaths): string
4299
    {
4300
        $doc = new DOMDocument();
4301
        @$doc->loadHTML($htmlContent);
4302
4303
        $tagsWithSrc = ['img', 'video', 'audio', 'source'];
4304
        /** @var array<int, \DOMElement> $elements */
4305
        $elements = [];
4306
4307
        foreach ($tagsWithSrc as $tag) {
4308
            foreach ($doc->getElementsByTagName($tag) as $element) {
4309
                if ($element->hasAttribute('src')) {
4310
                    $elements[] = $element;
4311
                }
4312
            }
4313
        }
4314
4315
        if (empty($elements)) {
4316
            return $htmlContent;
4317
        }
4318
4319
        /** @var array<int, \DOMElement> $anchorElements */
4320
        $anchorElements = $doc->getElementsByTagName('a');
4321
4322
        $webPath = api_get_path(WEB_PATH);
4323
        $sysPath = rtrim(api_get_path(SYS_PATH), '/');
4324
4325
        $paths = [
4326
            '/app/upload/' => $sysPath,
4327
            '/courses/' => $sysPath.'/app'
4328
        ];
4329
4330
        foreach ($elements as $element) {
4331
            $src = trim($element->getAttribute('src'));
4332
4333
            if (!str_starts_with($src, '/')
4334
                && !str_starts_with($src, $webPath)
4335
            ) {
4336
                continue;
4337
            }
4338
4339
            // to search anchors linking to files
4340
            if ($anchorElements->length > 0) {
4341
                foreach ($anchorElements as $anchorElement) {
4342
                    if (!$anchorElement->hasAttribute('href')) {
4343
                        continue;
4344
                    }
4345
4346
                    if ($src === $anchorElement->getAttribute('href')) {
4347
                        $anchorElement->setAttribute('href', basename($src));
4348
                    }
4349
                }
4350
            }
4351
4352
            $src = str_replace($webPath, '/', $src);
4353
4354
            foreach ($paths as $prefix => $basePath) {
4355
                if (str_starts_with($src, $prefix)) {
4356
                    $imagePaths[] = $basePath.urldecode($src);
4357
                    $element->setAttribute('src', basename($src));
4358
                }
4359
            }
4360
        }
4361
4362
        return $doc->saveHTML();
4363
    }
4364
4365
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
4366
    {
4367
        $htmlContent = Display::page_header($this->owner->getFullNameWithUsername());
4368
        $htmlContent .= Display::page_subheader2(get_lang('Portfolio items'));
4369
4370
        $htmlContent .= $tblItems->getRowCount() > 0
4371
            ? $tblItems->toHtml()
4372
            : Display::return_message(get_lang('No items in your portfolio'), 'warning');
4373
4374
        $htmlContent .= Display::page_subheader2(get_lang('Comments made'));
4375
4376
        $htmlContent .= $tblComments->getRowCount() > 0
4377
            ? $tblComments->toHtml()
4378
            : Display::return_message(get_lang('You have not commented'), 'warning');
4379
4380
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
4381
4382
        $doc = new DOMDocument();
4383
        @$doc->loadHTML($htmlContent);
4384
4385
        $stylesheet1 = $doc->createElement('link');
4386
        $stylesheet1->setAttribute('rel', 'stylesheet');
4387
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
4388
        $stylesheet2 = $doc->createElement('link');
4389
        $stylesheet2->setAttribute('rel', 'stylesheet');
4390
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
4391
        $stylesheet3 = $doc->createElement('link');
4392
        $stylesheet3->setAttribute('rel', 'stylesheet');
4393
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
4394
4395
        $head = $doc->createElement('head');
4396
        $head->appendChild($stylesheet1);
4397
        $head->appendChild($stylesheet2);
4398
        $head->appendChild($stylesheet3);
4399
4400
        $doc->documentElement->insertBefore(
4401
            $head,
4402
            $doc->getElementsByTagName('body')->item(0)
4403
        );
4404
4405
        return $doc->saveHTML();
4406
    }
4407
4408
    /**
4409
     * It parsers a title for a variable in lang.
4410
     *
4411
     * @param $defaultDisplayText
4412
     *
4413
     * @return string
4414
     */
4415
    private function getLanguageVariable($defaultDisplayText)
4416
    {
4417
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
4418
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
4419
        if (is_numeric($variableLanguage[0])) {
4420
            $variableLanguage = '_'.$variableLanguage;
4421
        }
4422
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
4423
4424
        return $variableLanguage;
4425
    }
4426
4427
    /**
4428
     * It translates the text as parameter.
4429
     *
4430
     * @param $defaultDisplayText
4431
     *
4432
     * @return mixed
4433
     */
4434
    private function translateDisplayName($defaultDisplayText)
4435
    {
4436
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
4437
4438
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
4439
    }
4440
4441
    private function getCommentsForIndex(FormValidator $frmFilterList = null): array
4442
    {
4443
        if (null === $frmFilterList) {
4444
            return [];
4445
        }
4446
4447
        if (!$frmFilterList->validate()) {
4448
            return [];
4449
        }
4450
4451
        $values = $frmFilterList->exportValues();
4452
4453
        if (empty($values['date']) && empty($values['text'])) {
4454
            return [];
4455
        }
4456
4457
        $queryBuilder = $this->em->createQueryBuilder()
4458
            ->select('c')
4459
            ->from(PortfolioComment::class, 'c')
4460
        ;
4461
4462
        if (!empty($values['date'])) {
4463
            $queryBuilder
4464
                ->andWhere('c.date >= :date')
4465
                ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
4466
            ;
4467
        }
4468
4469
        if (!empty($values['text'])) {
4470
            $queryBuilder
4471
                ->andWhere('c.content LIKE :text')
4472
                ->setParameter('text', '%'.$values['text'].'%')
4473
            ;
4474
        }
4475
4476
        if ($this->advancedSharingEnabled) {
4477
            $queryBuilder
4478
                ->leftJoin(
4479
                    CItemProperty::class,
4480
                    'cip',
4481
                    Join::WITH,
4482
                    "cip.ref = c.id
4483
                        AND cip.tool = :cip_tool
4484
                        AND cip.course = :course
4485
                        AND cip.lasteditType = 'visible'
4486
                        AND cip.toUser = :current_user"
4487
                )
4488
                ->andWhere(
4489
                    sprintf(
4490
                        'c.visibility = %d
4491
                            OR (
4492
                                c.visibility = %d AND cip IS NOT NULL OR c.author = :current_user
4493
                            )',
4494
                        PortfolioComment::VISIBILITY_VISIBLE,
4495
                        PortfolioComment::VISIBILITY_PER_USER
4496
                    )
4497
                )
4498
                ->setParameter('cip_tool', TOOL_PORTFOLIO_COMMENT)
4499
                ->setParameter('current_user', $this->owner->getId())
4500
                ->setParameter('course', $this->course)
4501
            ;
4502
        }
4503
4504
        $queryBuilder->orderBy('c.date', 'DESC');
4505
4506
        return $queryBuilder->getQuery()->getResult();
4507
    }
4508
4509
    private function getLabelForCommentDate(PortfolioComment $comment): string
4510
    {
4511
        $item = $comment->getItem();
4512
        $commmentCourse = $item->getCourse();
4513
        $commmentSession = $item->getSession();
4514
4515
        $dateLabel = Display::dateToStringAgoAndLongDate($comment->getDate()).PHP_EOL;
4516
4517
        if ($commmentCourse) {
4518
            $propertyInfo = api_get_item_property_info(
4519
                $commmentCourse->getId(),
4520
                TOOL_PORTFOLIO_COMMENT,
4521
                $comment->getId(),
4522
                $commmentSession ? $commmentSession->getId() : 0
4523
            );
4524
4525
            if ($propertyInfo) {
4526
                $dateLabel .= '|'.PHP_EOL
4527
                    .sprintf(
4528
                        get_lang('Updated %s'),
4529
                        Display::dateToStringAgoAndLongDate($propertyInfo['lastedit_date'])
4530
                    );
4531
            }
4532
        }
4533
4534
        return $dateLabel;
4535
    }
4536
}
4537