Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Exported post builder class.
19
 *
20
 * @package    mod_forum
21
 * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace mod_forum\local\builders;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use mod_forum\local\entities\discussion as discussion_entity;
30
use mod_forum\local\entities\forum as forum_entity;
31
use mod_forum\local\entities\post as post_entity;
32
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
33
use mod_forum\local\factories\exporter as exporter_factory;
34
use mod_forum\local\factories\vault as vault_factory;
35
use mod_forum\local\factories\manager as manager_factory;
36
use core_tag_tag;
37
use moodle_exception;
38
use renderer_base;
39
use stdClass;
40
 
41
/**
42
 * Exported post builder class.
43
 *
44
 * This class is an implementation of the builder pattern (loosely). It is responsible
45
 * for taking a set of related forums, discussions, and posts and generate the exported
46
 * version of the posts.
47
 *
48
 * It encapsulates the complexity involved with exporting posts. All of the relevant
49
 * additional resources will be loaded by this class in order to ensure the exporting
50
 * process can happen.
51
 *
52
 * See this doc for more information on the builder pattern:
53
 * https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
54
 *
55
 * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
56
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
57
 */
58
class exported_posts {
59
    /** @var renderer_base $renderer Core renderer */
60
    private $renderer;
61
 
62
    /** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */
63
    private $legacydatamapperfactory;
64
 
65
    /** @var exporter_factory $exporterfactory Exporter factory */
66
    private $exporterfactory;
67
 
68
    /** @var vault_factory $vaultfactory Vault factory */
69
    private $vaultfactory;
70
 
71
    /** @var rating_manager $ratingmanager Rating manager */
72
    private $ratingmanager;
73
 
74
    /** @var manager_factory $managerfactory Manager factory */
75
    private $managerfactory;
76
 
77
    /**
78
     * Constructor.
79
     *
80
     * @param renderer_base $renderer Core renderer
81
     * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
82
     * @param exporter_factory $exporterfactory Exporter factory
83
     * @param vault_factory $vaultfactory Vault factory
84
     * @param manager_factory $managerfactory Manager factory
85
     */
86
    public function __construct(
87
        renderer_base $renderer,
88
        legacy_data_mapper_factory $legacydatamapperfactory,
89
        exporter_factory $exporterfactory,
90
        vault_factory $vaultfactory,
91
        manager_factory $managerfactory
92
    ) {
93
        $this->renderer = $renderer;
94
        $this->legacydatamapperfactory = $legacydatamapperfactory;
95
        $this->exporterfactory = $exporterfactory;
96
        $this->vaultfactory = $vaultfactory;
97
        $this->managerfactory = $managerfactory;
98
        $this->ratingmanager = $managerfactory->get_rating_manager();
99
    }
100
 
101
    /**
102
     * Build the exported posts for a given set of forums, discussions, and posts.
103
     *
104
     * This will typically be used for a list of posts in the same discussion/forum however
105
     * it does support exporting any arbitrary list of posts as long as the caller also provides
106
     * a unique list of all discussions for the list of posts and all forums for the list of discussions.
107
     *
108
     * Increasing the number of different forums being processed will increase the processing time
109
     * due to processing multiple contexts (for things like capabilities, files, etc). The code attempts
110
     * to load the additional resources as efficiently as possible but there is no way around some of
111
     * the additional overhead.
112
     *
113
     * Note: Some posts will be removed as part of the build process according to capabilities.
114
     * A one-to-one mapping should not be expected.
115
     *
116
     * @param stdClass $user The user to export the posts for.
117
     * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to
118
     * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to
119
     * @param post_entity[] $posts The list of posts to export.
120
     * @param bool $includeinlineattachments Whether inline attachments should be included or not.
121
     * @return stdClass[] List of exported posts in the same order as the $posts array.
122
     */
123
    public function build(
124
        stdClass $user,
125
        array $forums,
126
        array $discussions,
127
        array $posts,
128
        bool $includeinlineattachments = false
129
    ): array {
130
        // Format the forums and discussion to make them more easily accessed later.
131
        $forums = array_reduce($forums, function($carry, $forum) {
132
            $carry[$forum->get_id()] = $forum;
133
            return $carry;
134
        }, []);
135
        $discussions = array_reduce($discussions, function($carry, $discussion) {
136
            $carry[$discussion->get_id()] = $discussion;
137
            return $carry;
138
        }, []);
139
 
140
        // Group the posts by discussion and forum so that we can load the resources in
141
        // batches to improve performance.
142
        $groupedposts = $this->group_posts_by_discussion($forums, $discussions, $posts);
143
        // Load all of the resources we need in order to export the posts.
144
        $authorsbyid = $this->get_authors_for_posts($posts);
145
        $authorcontextids = $this->get_author_context_ids(array_keys($authorsbyid));
146
        $attachmentsbypostid = $this->get_attachments_for_posts($groupedposts);
147
        $inlineattachments = [];
148
        if ($includeinlineattachments) {
149
            $inlineattachments = $this->get_inline_attachments_for_posts($groupedposts);
150
        }
151
        $groupsbycourseandauthorid = $this->get_author_groups_from_posts($groupedposts);
152
        $tagsbypostid = $this->get_tags_from_posts($posts);
153
        $ratingbypostid = $this->get_ratings_from_posts($user, $groupedposts);
154
        $readreceiptcollectionbyforumid = $this->get_read_receipts_from_posts($user, $groupedposts);
155
        $exportedposts = [];
156
 
157
        // Export each set of posts per discussion because it's the largest chunks we can
158
        // break them into due to constraints on capability checks.
159
        foreach ($groupedposts as $grouping) {
160
            [
161
                'forum' => $forum,
162
                'discussion' => $discussion,
163
                'posts' => $groupedposts
164
            ] = $grouping;
165
 
166
            // Exclude posts the user cannot see, such as certain posts in Q and A forums.
167
            $capabilitymanager = $this->managerfactory->get_capability_manager($forum);
168
            $groupedposts = array_filter($groupedposts, function($post) use ($capabilitymanager, $user, $discussion) {
169
                return $capabilitymanager->can_view_post($user, $discussion, $post);
170
            });
171
 
172
            $forumid = $forum->get_id();
173
            $courseid = $forum->get_course_record()->id;
174
            $postsexporter = $this->exporterfactory->get_posts_exporter(
175
                $user,
176
                $forum,
177
                $discussion,
178
                $groupedposts,
179
                $authorsbyid,
180
                $authorcontextids,
181
                $attachmentsbypostid,
182
                $groupsbycourseandauthorid[$courseid],
183
                $readreceiptcollectionbyforumid[$forumid] ?? null,
184
                $tagsbypostid,
185
                $ratingbypostid,
186
                true,
187
                $inlineattachments
188
            );
189
            ['posts' => $exportedgroupedposts] = (array) $postsexporter->export($this->renderer);
190
            $exportedposts = array_merge($exportedposts, $exportedgroupedposts);
191
        }
192
 
193
        if (count($forums) == 1 && count($discussions) == 1) {
194
            // All of the posts belong to a single discussion in a single forum so
195
            // the exported order will match the given $posts array.
196
            return $exportedposts;
197
        } else {
198
            // Since we grouped the posts by discussion and forum the ordering of the
199
            // exported posts may be different to the given $posts array so we should
200
            // sort it back into the correct order for the caller.
201
            return $this->sort_exported_posts($posts, $exportedposts);
202
        }
203
    }
204
 
205
    /**
206
     * Group the posts by which discussion they belong to in order for them to be processed
207
     * in chunks by the exporting.
208
     *
209
     * Returns a list of groups where each group has a forum, discussion, and list of posts.
210
     * E.g.
211
     * [
212
     *      [
213
     *          'forum' => <forum_entity>,
214
     *          'discussion' => <discussion_entity>,
215
     *          'posts' => [
216
     *              <post_entity in discussion>,
217
     *              <post_entity in discussion>,
218
     *              <post_entity in discussion>
219
     *          ]
220
     *      ]
221
     * ]
222
     *
223
     * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to, indexed by id.
224
     * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to, indexed by id.
225
     * @param post_entity[] $posts The list of posts to process.
226
     * @return array List of grouped posts. Each group has a discussion, forum, and posts.
227
     */
228
    private function group_posts_by_discussion(array $forums, array $discussions, array $posts): array {
229
        return array_reduce($posts, function($carry, $post) use ($forums, $discussions) {
230
            $discussionid = $post->get_discussion_id();
231
            if (!isset($discussions[$discussionid])) {
232
                throw new moodle_exception('Unable to find discussion with id ' . $discussionid);
233
            }
234
 
235
            if (isset($carry[$discussionid])) {
236
                $carry[$discussionid]['posts'][] = $post;
237
            } else {
238
                $discussion = $discussions[$discussionid];
239
                $forumid = $discussion->get_forum_id();
240
 
241
                if (!isset($forums[$forumid])) {
242
                    throw new moodle_exception('Unable to find forum with id ' . $forumid);
243
                }
244
 
245
                $carry[$discussionid] = [
246
                    'forum' => $forums[$forumid],
247
                    'discussion' => $discussions[$discussionid],
248
                    'posts' => [$post]
249
                ];
250
            }
251
 
252
            return $carry;
253
        }, []);
254
    }
255
 
256
    /**
257
     * Load the list of authors for the given posts.
258
     *
259
     * The list of authors will be indexed by the author id.
260
     *
261
     * @param post_entity[] $posts The list of posts to process.
262
     * @return author_entity[]
263
     */
264
    private function get_authors_for_posts(array $posts): array {
265
        $authorvault = $this->vaultfactory->get_author_vault();
266
        return $authorvault->get_authors_for_posts($posts);
267
    }
268
 
269
    /**
270
     * Get the user context ids for each of the authors.
271
     *
272
     * @param int[] $authorids The list of author ids to fetch context ids for.
273
     * @return int[] Context ids indexed by author id
274
     */
275
    private function get_author_context_ids(array $authorids): array {
276
        $authorvault = $this->vaultfactory->get_author_vault();
277
        return $authorvault->get_context_ids_for_author_ids($authorids);
278
    }
279
 
280
    /**
281
     * Load the list of all inline attachments for the posts. The list of attachments will be
282
     * indexed by the post id.
283
     *
284
     * @param array $groupedposts List of posts grouped by discussions.
285
     * @return stored_file[]
286
     */
287
    private function get_inline_attachments_for_posts(array $groupedposts): array {
288
        $inlineattachmentsbypostid = [];
289
        $postattachmentvault = $this->vaultfactory->get_post_attachment_vault();
290
        $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
291
            ['forum' => $forum, 'posts' => $posts] = $grouping;
292
 
293
            $forumid = $forum->get_id();
294
            if (!isset($carry[$forumid])) {
295
                $carry[$forumid] = [
296
                    'forum' => $forum,
297
                    'posts' => []
298
                ];
299
            }
300
 
301
            $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
302
            return $carry;
303
        }, []);
304
 
305
        foreach ($postsbyforum as $grouping) {
306
            ['forum' => $forum, 'posts' => $posts] = $grouping;
307
            $inlineattachments = $postattachmentvault->get_inline_attachments_for_posts($forum->get_context(), $posts);
308
 
309
            // Have to loop in order to maintain the correct indexes since they are numeric.
310
            foreach ($inlineattachments as $postid => $attachment) {
311
                $inlineattachmentsbypostid[$postid] = $attachment;
312
            }
313
        }
314
 
315
        return $inlineattachmentsbypostid;
316
    }
317
 
318
    /**
319
     * Load the list of all attachments for the posts. The list of attachments will be
320
     * indexed by the post id.
321
     *
322
     * @param array $groupedposts List of posts grouped by discussions.
323
     * @return stored_file[]
324
     */
325
    private function get_attachments_for_posts(array $groupedposts): array {
326
        $attachmentsbypostid = [];
327
        $postattachmentvault = $this->vaultfactory->get_post_attachment_vault();
328
        $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
329
            ['forum' => $forum, 'posts' => $posts] = $grouping;
330
 
331
            $forumid = $forum->get_id();
332
            if (!isset($carry[$forumid])) {
333
                $carry[$forumid] = [
334
                    'forum' => $forum,
335
                    'posts' => []
336
                ];
337
            }
338
 
339
            $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
340
            return $carry;
341
        }, []);
342
 
343
        foreach ($postsbyforum as $grouping) {
344
            ['forum' => $forum, 'posts' => $posts] = $grouping;
345
            $attachments = $postattachmentvault->get_attachments_for_posts($forum->get_context(), $posts);
346
 
347
            // Have to loop in order to maintain the correct indexes since they are numeric.
348
            foreach ($attachments as $postid => $attachment) {
349
                $attachmentsbypostid[$postid] = $attachment;
350
            }
351
        }
352
 
353
        return $attachmentsbypostid;
354
    }
355
 
356
    /**
357
     * Get the groups for each author of the given posts.
358
     *
359
     * The results are grouped by course and then author id because the groups are
360
     * contextually related to the course, e.g. a single author can be part of two different
361
     * sets of groups in two different courses.
362
     *
363
     * @param array $groupedposts List of posts grouped by discussions.
364
     * @return array List of groups indexed by forum id and then author id.
365
     */
366
    private function get_author_groups_from_posts(array $groupedposts): array {
367
        $groupsbyauthorid = [];
368
        $authoridsbycourseid = [];
369
 
370
        // Get the unique list of author ids for each course in the grouped
371
        // posts. Grouping by course is the largest grouping we can achieve.
372
        foreach ($groupedposts as $grouping) {
373
            ['forum' => $forum, 'posts' => $posts] = $grouping;
374
            $course = $forum->get_course_record();
375
            $courseid = $course->id;
376
 
377
            if (!isset($authoridsbycourseid[$courseid])) {
378
                $coursemodule = $forum->get_course_module_record();
379
                $authoridsbycourseid[$courseid] = [
380
                    'groupingid' => $coursemodule->groupingid,
381
                    'authorids' => []
382
                ];
383
            }
384
 
385
            $authorids = array_map(function($post) {
386
                return $post->get_author_id();
387
            }, $posts);
388
 
389
            foreach ($authorids as $authorid) {
390
                $authoridsbycourseid[$courseid]['authorids'][$authorid] = $authorid;
391
            }
392
        }
393
 
394
        // Load each set of groups per course.
395
        foreach ($authoridsbycourseid as $courseid => $values) {
396
            ['groupingid' => $groupingid, 'authorids' => $authorids] = $values;
397
            $authorgroups = groups_get_all_groups(
398
                $courseid,
399
                array_keys($authorids),
400
                $groupingid,
401
                'g.*, gm.id, gm.groupid, gm.userid'
402
            );
403
 
404
            if (!isset($groupsbyauthorid[$courseid])) {
405
                $groupsbyauthorid[$courseid] = [];
406
            }
407
 
408
            foreach ($authorgroups as $group) {
409
                // Clean up data returned from groups_get_all_groups.
410
                $userid = $group->userid;
411
                $groupid = $group->groupid;
412
 
413
                unset($group->userid);
414
                unset($group->groupid);
415
                $group->id = $groupid;
416
 
417
                if (!isset($groupsbyauthorid[$courseid][$userid])) {
418
                    $groupsbyauthorid[$courseid][$userid] = [];
419
                }
420
 
421
                $groupsbyauthorid[$courseid][$userid][] = $group;
422
            }
423
        }
424
 
425
        return $groupsbyauthorid;
426
    }
427
 
428
    /**
429
     * Get the list of tags for each of the posts. The tags will be returned in an
430
     * array indexed by the post id.
431
     *
432
     * @param post_entity[] $posts The list of posts to load tags for.
433
     * @return array Sets of tags indexed by post id.
434
     */
435
    private function get_tags_from_posts(array $posts): array {
436
        $postids = array_map(function($post) {
437
            return $post->get_id();
438
        }, $posts);
439
        return core_tag_tag::get_items_tags('mod_forum', 'forum_posts', $postids);
440
    }
441
 
442
    /**
443
     * Get the list of ratings for each post. The ratings are returned in an array
444
     * indexed by the post id.
445
     *
446
     * @param stdClass $user The user viewing the ratings.
447
     * @param array $groupedposts List of posts grouped by discussions.
448
     * @return array Sets of ratings indexed by post id.
449
     */
450
    private function get_ratings_from_posts(stdClass $user, array $groupedposts) {
451
        $ratingsbypostid = [];
452
        $postsdatamapper = $this->legacydatamapperfactory->get_post_data_mapper();
453
        $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
454
            ['forum' => $forum, 'posts' => $posts] = $grouping;
455
 
456
            $forumid = $forum->get_id();
457
            if (!isset($carry[$forumid])) {
458
                $carry[$forumid] = [
459
                    'forum' => $forum,
460
                    'posts' => []
461
                ];
462
            }
463
 
464
            $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
465
            return $carry;
466
        }, []);
467
 
468
        foreach ($postsbyforum as $grouping) {
469
            ['forum' => $forum, 'posts' => $posts] = $grouping;
470
 
471
            if (!$forum->has_rating_aggregate()) {
472
                continue;
473
            }
474
 
475
            $items = $postsdatamapper->to_legacy_objects($posts);
476
            $ratingoptions = (object) [
477
                'context' => $forum->get_context(),
478
                'component' => 'mod_forum',
479
                'ratingarea' => 'post',
480
                'items' => $items,
481
                'aggregate' => $forum->get_rating_aggregate(),
482
                'scaleid' => $forum->get_scale(),
483
                'userid' => $user->id,
484
                'assesstimestart' => $forum->get_assess_time_start(),
485
                'assesstimefinish' => $forum->get_assess_time_finish()
486
            ];
487
 
488
            $rm = $this->ratingmanager;
489
            $items = $rm->get_ratings($ratingoptions);
490
 
491
            foreach ($items as $item) {
492
                $ratingsbypostid[$item->id] = empty($item->rating) ? null : $item->rating;
493
            }
494
        }
495
 
496
        return $ratingsbypostid;
497
    }
498
 
499
    /**
500
     * Get the read receipt collections for the given viewing user and each forum. The
501
     * receipt collections will only be loaded for posts in forums that the user is tracking.
502
     *
503
     * The receipt collections are returned in an array indexed by the forum ids.
504
     *
505
     * @param stdClass $user The user viewing the posts.
506
     * @param array $groupedposts List of posts grouped by discussions.
507
     */
508
    private function get_read_receipts_from_posts(stdClass $user, array $groupedposts) {
509
        $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
510
        $trackedforums = [];
511
        $trackedpostids = [];
512
 
513
        foreach ($groupedposts as $group) {
514
            ['forum' => $forum, 'posts' => $posts] = $group;
515
            $forumid = $forum->get_id();
516
 
517
            if (!isset($trackedforums[$forumid])) {
518
                $forumrecord = $forumdatamapper->to_legacy_object($forum);
519
                $trackedforums[$forumid] = forum_tp_is_tracked($forumrecord, $user);
520
            }
521
 
522
            if ($trackedforums[$forumid]) {
523
                foreach ($posts as $post) {
524
                    $trackedpostids[] = $post->get_id();
525
                }
526
            }
527
        }
528
 
529
        if (empty($trackedpostids)) {
530
            return [];
531
        }
532
 
533
        // We can just load a single receipt collection for all tracked posts.
534
        $receiptvault = $this->vaultfactory->get_post_read_receipt_collection_vault();
535
        $readreceiptcollection = $receiptvault->get_from_user_id_and_post_ids($user->id, $trackedpostids);
536
        $receiptsbyforumid = [];
537
 
538
        // Assign the collection to all forums that are tracked.
539
        foreach ($trackedforums as $forumid => $tracked) {
540
            if ($tracked) {
541
                $receiptsbyforumid[$forumid] = $readreceiptcollection;
542
            }
543
        }
544
 
545
        return $receiptsbyforumid;
546
    }
547
 
548
    /**
549
     * Sort the list of exported posts back into the same order as the given posts.
550
     * The ordering of the exported posts can often deviate from the given posts due
551
     * to the process of exporting them so we need to sort them back into the order
552
     * that the calling code expected.
553
     *
554
     * @param post_entity[] $posts The posts in the expected order.
555
     * @param stdClass[] $exportedposts The list of exported posts in any order.
556
     * @return stdClass[] Sorted exported posts.
557
     */
558
    private function sort_exported_posts(array $posts, array $exportedposts) {
559
        $postindexes = [];
560
        foreach (array_values($posts) as $index => $post) {
561
            $postindexes[$post->get_id()] = $index;
562
        }
563
 
564
        $sortedexportedposts = [];
565
 
566
        foreach ($exportedposts as $exportedpost) {
567
            $index = $postindexes[$exportedpost->id];
568
            $sortedexportedposts[$index] = $exportedpost;
569
        }
570
 
571
        return $sortedexportedposts;
572
    }
573
}