/
mod_forumng_discussion.php
2923 lines (2636 loc) · 112 KB
/
mod_forumng_discussion.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Represents a forum discussion.
* @see mod_forumng_discussion_list
* @see forum
* @see mod_forumng_post
* @package mod
* @subpackage forumng
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_forumng_discussion {
/** Posts are cached for 10 minutes */
const CACHE_TIMEOUT = 600;
/** Max number of discussions to cache in session */
const CACHE_COUNT = 2;
/**
* Max size (total bytes of messages) before not caching discussion.
* I analysed our current discussions. At time of writing, there are 50 that
* are bigger than 200,000 bytes (ok characters but close enough) and
* 336,168 that are smaller, so I think this should generally be OK.
*/
const CACHE_MAX_SIZE = 200000;
/** Used for edit_settings when not changing a value */
const NOCHANGE = -999;
/**
* Used in the numreadposts field to indicate that read information is not
* stored because a discussion is too old.
*/
const PAST_SELL_BY = 1000000;
// Object variables and accessors
/*///////////////////////////////*/
private $forum, $discussionfields, $full, $rootpost, $timeretrieved,
$pretendtimeread, $foruserid;
private $postscache, $groupscache, $incache;
private $ismakingsearchchange;
private $totalsize = 0;
/** @return mod_forumng The forum that this discussion comes from */
public function get_forum() {
return $this->forum;
}
/** @return object Moodle course object */
public function get_course() {
return $this->forum->get_course();
}
/** @return object Moodle course-module object */
public function get_course_module() {
return $this->forum->get_course_module();
}
/** @return int ID of this discussion */
public function get_id() {
return $this->discussionfields->id;
}
/** @return int Group ID for this discussion or null if any group */
public function get_group_id() {
return $this->discussionfields->groupid;
}
/** @return int Group name for this discussion */
public function get_group_name() {
if (is_null($this->discussionfields->groupid)) {
return get_string('allparticipants');
} else {
return $this->discussionfields->groupname;
}
}
/**
* Obtains subject. Note this results in a DB query if the discussion
* was not fully loaded in the first place.
* @param bool $expectingquery True if code expects there to be a query;
* this just avoids a debugging() call.
* @return string Subject or null if none
*/
public function get_subject($expectingquery = false) {
global $DB;
if (!isset($this->discussionfields->subject)) {
if (!$expectingquery) {
debugging('This get method made a DB query; if this is expected,
set the flag to say so', DEBUG_DEVELOPER);
}
$this->discussionfields->subject = $DB->get_field(
'forumng_posts', 'subject', array('id' => $this->discussionfields->postid));
}
return $this->discussionfields->subject;
}
/**
* For use only by mod_forumng_post when updating in-memory representation
* after an edit.
* @param string $subject New subject
*/
public function hack_subject($subject) {
$this->discussionfields->subject = $subject;
}
/** @return bool True if discussion is 'sticky' */
public function is_sticky() {
return $this->discussionfields->sticky ? true : false;
}
/** @return bool True if discussion is locked */
public function is_locked() {
return $this->discussionfields->locked ? true : false;
}
/** @return bool True if discussion is auto locked */
public function is_auto_locked() {
if ($this->discussionfields->locked == 2) {
return true;
} else {
return false;
}
}
/**
* @return int Time this discussion becomes visible (seconds since epoch)
* or null if no start time
*/
public function get_time_start() {
return $this->discussionfields->timestart;
}
/**
* @return int Time this discussion stops being visible (seconds since
* epoch) or null if no end time
*/
public function get_time_end() {
return $this->discussionfields->timeend;
}
/**
* Obtains details of user who originally posted this discussion.
* @return object Moodle user object (selected fields)
*/
public function get_poster() {
$this->check_full();
return $this->discussionfields->firstuser;
}
/**
* Obtains details of user who posted the last reply to this discussion.
* @return object Moodle user object (selected fields)
*/
public function get_last_post_user() {
$this->check_full();
return $this->discussionfields->lastuser;
}
/**
* Obtains ID of last post
* @return int ID of last post
*/
public function get_last_post_id() {
return $this->discussionfields->lastpostid;
}
/**
* If the discussion is locked, this function returns the explanatory post.
* Will retrieve discussion posts if not already obtained.
* @return mod_forumng_post Lock post or null if none
*/
public function get_lock_post() {
if ($this->is_locked() && !$this->is_auto_locked() ) {
return $this->get_root_post()->find_child(
$this->discussionfields->lastpostid);
} else {
return null;
}
}
/**
* Checks that the discussion is fully loaded. There are two load states: full
* (includes all data retrieved when loading discussion list) and partial
* (includes only minimal data required when creating discussion). Note that
* full data state does not imply that the actual posts are in memory yet,
* post storage is tracked separately.
* @throws coding_exception If discussion is not loaded
*/
private function check_full() {
if (!$this->full) {
throw new coding_exception('This function is not available unless
the discussion has been fully loaded.');
}
}
/**
* @return string URL of this discussion for log table, relative to the
* module's URL
*/
public function get_log_url() {
return 'discuss.php?' . $this->get_link_params(mod_forumng::PARAM_PLAIN);
}
/**
* @return mixed Number of unread posts as integer, possibly 0; or empty
* string if unread data is no longer tracked for this post
*/
public function get_num_unread_posts() {
if (!isset($this->discussionfields->numreadposts)) {
throw new coding_exception('Unread post count not obtained');
}
if ($this->discussionfields->numreadposts == self::PAST_SELL_BY) {
return '';
} else {
$unreadpost = $this->discussionfields->numposts - $this->discussionfields->numreadposts;
$numposts = $this->get_forum()->get_type()->calculate_number_of_unread_posts($unreadpost, $this);
if (!is_bool($numposts)) {
return $numposts;
}
return $unreadpost;
}
}
/**
* @return int Number of discussions
*/
public function get_num_posts() {
if (!isset($this->discussionfields->numposts)) {
throw new coding_exception('Post count not obtained');
}
$numposts = $this->get_forum()->get_type()->calculate_number_of_posts($this->discussionfields->numposts);
// Check if number of post return result is not boolean.
if (!is_bool($numposts)) {
return $numposts;
}
return $this->discussionfields->numposts;
}
/**
* @return int Time of last post
*/
public function get_time_modified() {
if (!isset($this->discussionfields->timemodified)) {
throw new coding_exception('Time modified not obtained');
}
return $this->discussionfields->timemodified;
}
/**
* @return moodle_url URL of discussion
*/
public function get_moodle_url() {
return new moodle_url('/mod/forumng/discuss.php', $this->get_link_params_array());
}
/**
* @return string URL of this discussion
*/
public function get_url($type = mod_forumng::PARAM_PLAIN) {
global $CFG;
return $CFG->wwwroot . '/mod/forumng/discuss.php?' .
$this->get_link_params($type);
}
/**
* Obtains details of user who posted the first post to this discussion.
* @return object Moodle user object (selected fields)
*/
public function get_poster_anon() {
$this->check_full();
return is_null($this->discussionfields->firstasmoderator)
? mod_forumng::ASMODERATOR_NO : $this->discussionfields->firstasmoderator;
}
/**
* Obtains details of user who posted the last post to this discussion.
* @return object Moodle user object (selected fields)
*/
public function get_last_post_anon() {
$this->check_full();
return is_null($this->discussionfields->lastasmoderator)
? mod_forumng::ASMODERATOR_NO : $this->discussionfields->lastasmoderator;
}
/*
* @return int boolean 0 or 1 flagged
*/
public function get_flagged() {
return $this->discussionfields->flagged;
}
/**
* @return bool True if can flag
*/
public function can_flag() {
// The guest user cannot flag.
if (isguestuser()) {
return false;
}
// Cannot flag for deleted discussion unless already flagged.
if ($this->is_deleted() && (!$this->is_flagged())) {
return false;
}
return true;
}
/** @return bool True if post is flagged by current user */
public function is_flagged() {
if (!property_exists($this->discussionfields, 'flagged')) {
throw new coding_exception('Flagged information not available here');
}
return $this->discussionfields->flagged ? true : false;
}
/**
* @param bool $flag True to set flag
* @param int $userid User ID or 0 for current
*/
public function set_flagged($flag, $userid = 0) {
global $DB;
$userid = mod_forumng_utils::get_real_userid($userid);
if ($flag) {
// Check there is not already a row.
if (!$DB->record_exists('forumng_flags',
array('discussionid' => $this->get_id(), 'userid' => $userid))) {
// Insert new row.
$newflag = (object) array('discussionid' => $this->get_id(),
'userid' => $userid, 'postid' => 0, 'flagged' => time());
$DB->insert_record('forumng_flags', $newflag);
$this->discussionfields->flagged = 1;
}
} else {
$DB->delete_records('forumng_flags',
array('discussionid' => $this->get_id(), 'userid' => $userid));
$this->discussionfields->flagged = 0;
}
}
// Factory method
/*///////////////*/
/**
* Creates a forum discussion object, forum object, and all related data from a
* single forum discussion ID. Intended when entering a page which uses
* discussion ID as a parameter.
* @param int $id ID of forum discussion
* @param int $cloneid ID of clone (or 0 or mod_forumng::CLONE_DIRECT as relevant)
* @param int $userid User ID; 0 = current user, -1 = do not get unread data
* @param bool $usecache True if cache should be used (if available)
* @param bool $storecache True if newly-retrieved discussion should be
* stored to cache
* @return mod_forumng_discussion Discussion object
*/
public static function get_from_id($id, $cloneid, $userid=0,
$usecache=false, $storecache=false) {
if ($usecache) {
global $SESSION;
self::check_cache();
foreach ($SESSION->forumng_cache->discussions as $info) {
if ($info->userid==mod_forumng_utils::get_real_userid($userid) &&
$info->id==$id && $info->cloneid==$cloneid) {
$info->lastused = time();
$result = self::create_from_cache($info);
if ($result) {
return $result;
}
}
}
}
return self::get_base('fd.id=?', array($id), $userid, $storecache, $cloneid);
}
/**
* Creates a forum discussion object, forum object, and all related data from a
* forum post ID (the discussion related to that post). Intended when
* requesting a post if we want 'context' data too
* @param int $postid ID of forum post
* @param int $userid User ID; 0 = current user, -1 = do not get unread data
* @param bool $usecache True if cache should be used (if available)
* @param bool $storecache True if newly-retrieved discussion should be
* stored to cache
* @return mod_forumng_discussion Discussion object
*/
public static function get_from_post_id($postid, $cloneid, $userid=0,
$usecache=false, $storecache=false) {
if ($usecache) {
global $SESSION;
self::check_cache();
foreach ($SESSION->forumng_cache->discussions as $info) {
if ($info->userid!=mod_forumng_utils::get_real_userid($userid)) {
continue;
}
// Check whether this discussion contains the desired
// post
if (in_array($postid, $info->posts)) {
$info->lastused = time();
$result = self::create_from_cache($info);
if ($result) {
return $result;
}
}
}
}
return self::get_base("fd.id =
(SELECT discussionid FROM {forumng_posts} WHERE id = ?)", array($postid),
$userid, $storecache, $cloneid);
}
private static function get_base($where, $whereparams, $userid, $cache, $cloneid) {
$forumuserid = $userid == -1 ? 0 : $userid;
// If user isn't logged in, don't get unread data
if (!isloggedin()) {
$userid = -1;
}
// Get discussion data (including read status)
$rs = self::query_discussions($where, $whereparams, $userid, 'id', 0, 1, null, true);
$discussionfields = false;
if (!$rs->valid()) {
throw new dml_exception('Unable to retrieve relevant discussion');
}
$discussionfields = $rs->current();
$rs->close();
// Get forum and construct discussion
$forum = mod_forumng::get_from_id($discussionfields->forumngid, $cloneid, true, null, $forumuserid);
$result = new mod_forumng_discussion($forum, $discussionfields, true,
mod_forumng_utils::get_real_userid($userid));
if ($cache) {
$result->cache($userid);
}
return $result;
}
// Discussion caching
/*///////////////////*/
/**
* Caches the specified discussion in session.
* Replaces the least-recently-used, if the number exceeds the
* limit.
* @param mod_forumng_discussion $discussion
*/
private function cache() {
global $SESSION;
self::check_cache();
if (!$this->full) {
// Only cache 'full' data
return;
}
if ($this->totalsize > self::CACHE_MAX_SIZE) {
// Don't cache huge discussions
return;
}
// Remove any existing data for this discussion id
$oldest = -1;
$oldesttime = 0;
foreach ($SESSION->forumng_cache->discussions as $key => $info) {
if ($info->id == $this->get_id()) {
unset($SESSION->forumng_cache->discussions[$key]);
} else {
if ($oldest==-1 || $info->lastused<$oldesttime) {
$oldest = $key;
}
}
}
// If there are too many, discard oldest
if (count($SESSION->forumng_cache->discussions) > self::CACHE_COUNT) {
unset($SESSION->forumng_cache->discussions[$oldest]);
}
// Cache this data
$info = new stdClass;
$info->lastused = time();
$info->id = $this->get_id();
$info->timemodified = $this->get_time_modified();
$info->discussionfields = serialize($this->discussionfields);
$info->postscache = $this->postscache;
$info->groupscache = serialize($this->groupscache);
$info->userid = $this->get_unread_data_user_id();
$info->posts = array();
$info->settingshash = $this->get_forum()->get_settings_hash();
$info->cloneid = $this->get_forum()->get_course_module_id();
if ($this->rootpost) {
$this->rootpost->list_child_ids($info->posts);
}
$this->incache = $info;
$SESSION->forumng_cache->discussions[] = $info;
}
/**
* Removes any instances of this discussion from current user's cache.
* Used so that current user sees changes immediately (other users will
* still wait 10 minutes).
*/
public function uncache() {
global $SESSION;
if (isset($SESSION->forumng_cache->discussions)) {
foreach ($SESSION->forumng_cache->discussions as $key => $info) {
if ($info->id == $this->get_id()) {
unset($SESSION->forumng_cache->discussions[$key]);
}
}
}
}
/**
* Obtains a discussion from the cache.
* @param object $info Object from session cache
* @return mod_forumng_discussion New discussion object or null if there is a
* problem and you should re-cache
*/
private static function create_from_cache($info) {
$discussionfields = unserialize($info->discussionfields);
$forum = mod_forumng::get_from_id($discussionfields->forumngid, $info->cloneid);
if ($forum->get_settings_hash() != $info->settingshash) {
return null;
}
$result = new mod_forumng_discussion(
$forum, $discussionfields, true, $info->userid);
$result->groupscache = unserialize($info->groupscache);
$result->postscache = $info->postscache;
$result->incache = true;
return $result;
}
/**
* Checks whether the current discussion object is newer (contains
* newer posts) than an equivalent discussion stored in the cache.
* If so, removes the cached value.
*/
public function maybe_invalidate_cache() {
global $SESSION;
self::check_cache();
foreach ($SESSION->forumng_cache->discussions as $key => $info) {
if ($info->id == $this->get_id()
&& $info->timemodified != $this->get_time_modified()) {
unset($SESSION->forumng_cache->discussions[$key]);
}
}
}
/**
* Updates the discussion cache, discarding old data.
*/
public static function check_cache() {
global $SESSION;
// Check cache variable exists
if (!isset($SESSION->forumng_cache)) {
$SESSION->forumng_cache = new stdClass;
}
if (!isset($SESSION->forumng_cache->discussions)) {
$SESSION->forumng_cache->discussions = array();
}
// Remove old cache data
foreach ($SESSION->forumng_cache->discussions as $key => $info) {
if (time() - $info->lastused > self::CACHE_TIMEOUT) {
unset($SESSION->forumng_cache->discussions[$key]);
}
}
}
// Object methods
/*///////////////*/
/**
* Initialises the discussion. Used internally by forum - don't call directly.
* @param mod_forumng $forum Forum object
* @param object $discussionfields Discussion fields from db table (plus
* some extra fields provided by query in forum method)
* @param bool $full True if the parameter includes 'full' data via the
* various joins, false if it's only the fields from the discussions table.
* @param int $foruserid The user ID that was used to obtain the discussion
* data (may be -1 for no unread data)
*/
public function __construct($forum, $discussionfields, $full, $foruserid) {
if ($full && !isset($discussionfields->firstuser)) {
// Extract the user details into Moodle user-like objects
$discussionfields->firstuser = mod_forumng_utils::extract_subobject($discussionfields,
'fu_');
$discussionfields->lastuser = mod_forumng_utils::extract_subobject($discussionfields,
'lu_');
}
$this->forum = $forum;
$this->discussionfields = $discussionfields;
$this->full = $full;
$this->foruserid = $foruserid;
$this->rootpost = null;
$this->timeretrieved = time();
$this->postscache = null;
$this->groupscache = null;
$this->ismakingsearchchange = false;
}
/**
* Fills discussion data (loaded from db) for given user.
* @param int $foruserid User ID or -1 if no unread data is required
* @param bool $usecache True to use cache if available
* @param bool $storecache True to sstore retrieved value in cache
*/
public function fill($foruserid=0, $usecache=false, $storecache=false) {
if ($this->full && ($this->foruserid == $foruserid || $foruserid==-1)) {
return;
}
$new = self::get_from_id($this->discussionfields->id,
$this->get_forum()->get_course_module_id(), $foruserid, $usecache, $storecache);
foreach (get_class_vars('mod_forumng_discussion') as $field => $dontcare) {
$this->{$field} = $new->{$field};
}
}
/**
* Obtains the root post of the discussion. This actually requests all
* posts from the database; the first is returned, but others are
* accessible from methods in the first.
* If available, cached information is used unless
* you set $usecache to false. The cache is stored within the discussion
* object so will not persist beyond a request unless you make the
* discussion object persist too.
* @param bool $usecache True to use cache if available, false to
* request fresh data
* @param int $userid User ID to get user-specific data (initially, post
* flags) for; 0 = current
* @return mod_forumng_post Post object
*/
public function get_root_post($usecache=true, $userid=0) {
global $CFG, $USER;
require_once($CFG->dirroot . '/rating/lib.php');
if (!$usecache || !$this->rootpost) {
if (!$usecache || !$this->postscache) {
$read = !mod_forumng::mark_read_automatically($userid);
// Retrieve most posts in the discussion - even deleted
// ones. These are necessary in case somebody deletes a post that has
// replies. They will display as 'deleted post'. We don't retrieve
// old versions of edited posts. Posts are retrieved in created order
// so that the order of replies remains constant when we build the tree.
$posts = mod_forumng_post::query_posts('fp.discussionid=? AND fp.oldversion=0',
array($this->discussionfields->id), 'fp.created',
$this->forum->has_ratings(), true, false, $userid, false, false, '', '', $read);
// Load standard ratings.
if ($this->get_forum()->get_enableratings() == mod_forumng::FORUMNG_STANDARD_RATING) {
// If grading is 'No grading' or 'Teacher grades students'.
if ($this->get_forum()->get_grading() == mod_forumng::GRADING_NONE ||
$this->get_forum()->get_grading() == mod_forumng::GRADING_MANUAL ) {
// Set the aggregation method.
if ($this->get_forum()->get_rating_scale() > 0) {
$aggregate = RATING_AGGREGATE_AVERAGE;
} else {
$aggregate = RATING_AGGREGATE_COUNT;
}
} else {
$aggregate = $this->get_forum()->get_grading();
}
$ratingoptions = new stdClass();
$ratingoptions->context = $this->get_forum()->get_context();
$ratingoptions->component = 'mod_forumng';
$ratingoptions->ratingarea = 'post';
$ratingoptions->items = $posts;
$ratingoptions->aggregate = $aggregate;
$ratingoptions->scaleid = $this->get_forum()->get_rating_scale();
$ratingoptions->userid = $USER->id;
$ratingoptions->assesstimestart = $this->forum->get_ratingfrom();
$ratingoptions->assesstimefinish = $this->forum->get_ratinguntil();
$ratingoptions->returnurl = $this->get_moodle_url();
$rm = new rating_manager();
$posts = $rm->get_ratings($ratingoptions);
}
$this->postscache = serialize($posts);
} else {
$posts = unserialize($this->postscache);
}
// Add numbers to parent post.
$i = 1;
foreach ($posts as $post) {
if (is_null($post->parentpostid)) {
$post->number = $i;
$i++;
break;
}
}
// Obtain post relationships
$children = array();
foreach ($posts as $id => $fields) {
// Add numbers to posts.
if (!is_null($fields->parentpostid)) {
$fields->number = $i++;
}
if (!array_key_exists($fields->parentpostid, $children)) {
$children[$fields->parentpostid] = array();
}
$children[$fields->parentpostid][] = $id;
}
// Recursively build posts
$this->rootpost = $this->build_posts($posts, $children,
$this->discussionfields->postid, null);
// Update the 'next/previous' unread lists stored in posts
if ($this->get_unread_data_user_id() != -1) {
$linear = array();
$this->rootpost->build_linear_children($linear);
$nextunread = array();
$dump = '';
foreach ($linear as $index => $post) {
$nextunread[$index] = null;
if ($post->is_unread() &&
(!$post->get_deleted() || $post->can_undelete($dump))) {
for ($j = $index-1; $j>=0; $j--) {
if ($nextunread[$j]) {
break;
}
$nextunread[$j] = $post;
}
}
}
$previous = null;
foreach ($linear as $index => $post) {
$post->set_unread_list($nextunread[$index], $previous);
if ($post->is_unread() &&
(!$post->get_deleted() || $post->can_undelete($dump))) {
$previous = $post;
}
}
// Update cached version to include this data
if ($this->incache) {
$this->cache();
}
}
}
return $this->rootpost;
}
/**
* Internal method. Queries for a number of discussions, including additional
* data about unread posts etc. Returns the database result.
* @param string $conditions WHERE clause (may refer to aliases 'd' for discussion)
* @param array $conditionparams Parameters for conditions
* @param int $userid User ID, 0 = current user, -1 = no unread data is needed
* @param string $orderby ORDER BY clause
* @param int $limitfrom Limit on results
* @param int $limitnum Limit on results
* @param mod_forumng $typeforum If set, this forum is used to potentially restrict
* the results based on forum type limits
* @param boolean $flags set to indicate that flagged discussions are to be returned
* @param boolean hastag set to indicate that tagged discussions are to be returned
* @return adodb_recordset Database query results
*/
public static function query_discussions($conditions, $conditionparams, $userid, $orderby,
$limitfrom='', $limitnum='', $typeforum=null, $flags = false, $hastag = false) {
global $USER, $DB;
// For read tracking, we get a count of total number of posts in
// discussion, and total number of read posts in the discussion (this
// is so we can display the number of UNread posts, but the query
// works that way around because it will return 0 if no read
// information is stored).
if (mod_forumng::enabled_read_tracking() && $userid!=-1) {
if (!$userid) {
$userid = $USER->id;
}
$deadline = mod_forumng::get_read_tracking_deadline();
$readjoin1 = "";
$readwhere1 = "";
$readtrackingparams = array($deadline, $userid, $userid, $deadline);
$readtrackingjoinparams = array($userid);
if (!mod_forumng::mark_read_automatically($userid)) {
// Ind Mark read - check individual read_posts state.
$readjoin1 = "LEFT JOIN {forumng_read_posts} frp2 on frp2.postid = fp3.id AND frp2.userid = ?";
$readwhere1 = "OR frp2.id IS NOT NULL";
$readtrackingparams = array($deadline, $userid, $userid, $userid, $deadline);
}
// Get unread count only when last added post is newer than deadline.
// When PAST_SELL_BY, posts modified later than last will be unread but not picked up.
$readtracking = "
, (CASE WHEN fplast.modified IS NOT NULL AND fplast.modified < ? THEN " .
self::PAST_SELL_BY . " ELSE (SELECT COUNT(1)
FROM {forumng_posts} fp3
$readjoin1
WHERE fp3.discussionid = fd.id AND fp3.oldversion = 0
AND fp3.deleted = 0
AND (fp3.modified < fr.time OR fp3.edituserid = ?
$readwhere1
OR (fp3.edituserid IS NULL AND fp3.userid = ?)
OR fp3.modified < ?)) END) AS numreadposts,
fr.time AS timeread";
// Join read info, get posts not authored by user: get latest modified post time.
$readtrackingjoin = "LEFT JOIN {forumng_read} fr ON fd.id = fr.discussionid AND fr.userid = ?";
} else {
$readtracking = ", 0 AS numreadposts, NULL AS timeread";
$readtrackingjoin = "";
$readtrackingparams = array();
$readtrackingjoinparams = array();
}
$order = ($orderby) ? 'ORDER BY ' . $orderby : '';
// Handle forum type restriction
$typejoin = '';
$typeparams = array();
$flagsjoin = '';
$flagsquery = '';
$flagparams = array();
if ($typeforum && $userid != -1) {
$type = $typeforum->get_type();
if ($type->has_unread_restriction()) {
list($restrictionsql, $restrictionparams) =
$type->get_unread_restriction_sql($typeforum, $userid);
} else {
$restrictionsql = false;
}
if ($restrictionsql) {
$typejoin = "
INNER JOIN {forumng} f ON f.id = fd.forumngid
INNER JOIN {course} c ON c.id = f.course
INNER JOIN {course_modules} cm ON cm.instance = f.id AND cm.course = f.course
INNER JOIN {modules} m ON m.id = cm.module";
$conditions .= " AND m.name = 'forumng' AND $restrictionsql";
$conditionparams = array_merge($conditionparams, $restrictionparams);
}
}
if ($flags && $userid != -1) {
$flagsjoin = "LEFT JOIN {forumng_flags} ff ON ff.discussionid = fd.id AND ff.userid = ?";
$flagsquery = ', ff.flagged';
$flagparams = array($userid);
}
// Tag join sql if needed.
$tagjoin = '';
if ($hastag) {
$tagjoin = "LEFT JOIN {tag_instance} ti on ti.itemid = fd.id
AND ti.itemtype = 'forumng_discussions'
AND ti.component = 'mod_forumng'";
}
// Main query. This retrieves:
// * Basic discussion information.
// * Information about the discussion that is obtained from the first and
// last post.
// * Information about the users responsible for first and last post.
$rs = $DB->get_recordset_sql("
SELECT * FROM (SELECT
fd.*,
fpfirst.created AS timecreated,
fplast.modified AS timemodified,
fpfirst.subject AS subject,
fplast.subject AS lastsubject,
fplast.message AS lastmessage,
fpfirst.asmoderator AS firstasmoderator,
fplast.asmoderator AS lastasmoderator,
".mod_forumng_utils::select_username_fields('fu').",
".mod_forumng_utils::select_username_fields('lu').",
(SELECT COUNT(1)
FROM {forumng_posts} fp2
WHERE fp2.discussionid = fd.id AND fp2.deleted = 0 AND fp2.oldversion = 0)
AS numposts,
g.name AS groupname
$readtracking
$flagsquery
FROM
{forumng_discussions} fd
INNER JOIN {forumng_posts} fpfirst ON fd.postid = fpfirst.id
INNER JOIN {user} fu ON fpfirst.userid = fu.id
INNER JOIN {forumng_posts} fplast ON fd.lastpostid = fplast.id
INNER JOIN {user} lu ON fplast.userid = lu.id
LEFT JOIN {groups} g ON g.id = fd.groupid
$readtrackingjoin
$typejoin
$flagsjoin
$tagjoin
WHERE
$conditions) x $order
",
array_merge($readtrackingparams, $readtrackingjoinparams, $flagparams, $conditionparams),
$limitfrom, $limitnum);
return $rs;
}
/**
* Constructs a post object and (recursively) all of its children from
* information retrieved from the database.
* @param $posts Array of post ID => fields from DB query
* @param $children Array of post ID => array of child IDs
* @param $id ID of post to construct
* @param $parent Parent post or NULL if none
* @return mod_forumng_post Newly-created post
* @throws mod_forumng_exception If ID is invalid
*/
private function build_posts(&$posts, &$children, $id, $parent) {
if (!array_key_exists($id, $posts)) {
$msg = "No such post: $id (discussion " . $this->get_id() . '); ' .
'posts';
foreach ($posts as $id => $junk) {
$msg .= ' ' . $id;
}
$msg .= '; children';
foreach ($children as $id => $junk) {
$msg .= ' ' . $id;
}
throw new dml_exception($msg);
}
$post = new mod_forumng_post($this, $posts[$id], $parent);
$this->totalsize += strlen($posts[$id]->message);
$post->init_children();
if (array_key_exists($id, $children)) {
foreach ($children[$id] as $childid) {
$post->add_child(
$this->build_posts($posts, $children, $childid, $post));
}
}
return $post;
}
/**
* Used by forum when creating a discussion. Do not call directly.
* @param string $subject Subject
* @param string $message Message
* @param int $format Moodle format used for message
* @param bool $attachments True if post contains attachments
* @param bool $mailnow If true, sends mail ASAP
* @param int $userid User ID (0 = current)
* @param int $asmoderator values are ASMODERATOR_NO, ASMODERATOR_IDENTIFY or ASMODERATOR_ANON
* @return int ID of newly-created post
*/
public function create_root_post($subject, $message, $format,
$attachments=false, $mailnow=false, $userid=0, $asmoderator = mod_forumng::ASMODERATOR_NO) {
return $this->create_reply(null, $subject, $message, $format,
$attachments, false, $mailnow, $userid, $asmoderator);
}
/**
* Used by mod_forumng_post when creating a reply. Do not call directly.
* @param mod_forumng_post $parentpost Parent post object (NULL when creating root post)
* @param string $subject Subject
* @param string $message Message
* @param int $format Moodle format used for message
* @param bool $attachments True if post contains attachments
* @param bool $setimportant If true, highlight the post
* @param bool $mailnow If true, sends mail ASAP
* @param int $userid User ID (0 = current)
* @param int $asmoderator values are ASMODERATOR_NO, ASMODERATOR_IDENTIFY or ASMODERATOR_ANON
* @return int ID of newly-created post
*/
public function create_reply($parentpost, $subject, $message, $format,
$attachments=false, $setimportant=false, $mailnow=false, $userid=0, $asmoderator = mod_forumng::ASMODERATOR_NO) {
global $DB;
$userid = mod_forumng_utils::get_real_userid($userid);
// Security checks.
if ($setimportant && !$this->forum->can_set_important($userid)) {
$setimportant = false;
}
if ($asmoderator && !$this->forum->can_indicate_moderator($userid)) {
$asmoderator = mod_forumng::ASMODERATOR_NO;
}
if ($asmoderator == mod_forumng::ASMODERATOR_ANON && !$this->forum->can_post_anonymously($userid)) {
$asmoderator = mod_forumng::ASMODERATOR_NO;
}
// Prepare post object