Merge branch 'mod_forum-discussion-poc' of https://github.com/ryanwyllie/moodle

This commit is contained in:
Jun Pataleta
2019-03-22 11:04:35 +08:00
130 changed files with 17071 additions and 1701 deletions
@@ -4,122 +4,124 @@ Feature: Change number of discussions displayed
As a teacher
I need to edit the course and change the number of sections displayed.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | format |
| Course 1 | C1 | 0 | social |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 10 |
| Message | This is forum post ten |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 9 |
| Message | This is forum post nine |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 8 |
| Message | This is forum post eight |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 7 |
| Message | This is forum post seven |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 6 |
| Message | This is forum post six |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 5 |
| Message | This is forum post five |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 4 |
| Message | This is forum post four |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 3 |
| Message | This is forum post three |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 2 |
| Message | This is forum post two |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
And I wait "1" seconds
And I press "Add a new discussion topic"
And I set the following fields to these values:
| Subject | Forum Post 1 |
| Message | This is forum post one |
And I press "Post to forum"
And I wait to be redirected
And I am on "Course 1" course homepage
Scenario: When number of discussions is decreased fewer discussions appear
Given I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| numdiscussions | 5 |
When I press "Save and display"
Then I should see "This is forum post one"
And I should see "This is forum post five"
And I should not see "This is forum post six"
Scenario: When number of discussions is decreased to less than 1 only 1 discussion should appear
Given I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| numdiscussions | -1 |
When I press "Save and display"
Then I should see "This is forum post one"
And I should not see "This is forum post two"
And I should not see "This is forum post ten"
Scenario: When number of discussions is increased more discussions appear
Given I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| numdiscussions | 9 |
When I press "Save and display"
Then I should see "This is forum post one"
And I should see "This is forum post five"
And I should see "This is forum post nine"
And I should not see "This is forum post ten"
# Uncomment these tests when the forum_print_latest_discussions function is
# deprecated in MDL-65082.
# Background:
# Given the following "users" exist:
# | username | firstname | lastname | email |
# | teacher1 | Teacher | 1 | teacher1@example.com |
# And the following "courses" exist:
# | fullname | shortname | category | format |
# | Course 1 | C1 | 0 | social |
# And the following "course enrolments" exist:
# | user | course | role |
# | teacher1 | C1 | editingteacher |
# And I log in as "teacher1"
# And I am on "Course 1" course homepage
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 10 |
# | Message | This is forum post ten |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 9 |
# | Message | This is forum post nine |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 8 |
# | Message | This is forum post eight |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 7 |
# | Message | This is forum post seven |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 6 |
# | Message | This is forum post six |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 5 |
# | Message | This is forum post five |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 4 |
# | Message | This is forum post four |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 3 |
# | Message | This is forum post three |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 2 |
# | Message | This is forum post two |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
# And I wait "1" seconds
# And I press "Add a new discussion topic"
# And I set the following fields to these values:
# | Subject | Forum Post 1 |
# | Message | This is forum post one |
# And I press "Post to forum"
# And I wait to be redirected
# And I am on "Course 1" course homepage
#
# Scenario: When number of discussions is decreased fewer discussions appear
# Given I navigate to "Edit settings" in current page administration
# And I set the following fields to these values:
# | numdiscussions | 5 |
# When I press "Save and display"
# Then I should see "This is forum post one"
# And I should see "This is forum post five"
# And I should not see "This is forum post six"
#
# Scenario: When number of discussions is decreased to less than 1 only 1 discussion should appear
# Given I navigate to "Edit settings" in current page administration
# And I set the following fields to these values:
# | numdiscussions | -1 |
# When I press "Save and display"
# Then I should see "This is forum post one"
# And I should not see "This is forum post two"
# And I should not see "This is forum post ten"
#
# Scenario: When number of discussions is increased more discussions appear
# Given I navigate to "Edit settings" in current page administration
# And I set the following fields to these values:
# | numdiscussions | 9 |
# When I press "Save and display"
# Then I should see "This is forum post one"
# And I should see "This is forum post five"
# And I should see "This is forum post nine"
# And I should not see "This is forum post ten"
+2 -2
View File
@@ -22,14 +22,14 @@ Feature: Managers can create courses
And I add the "Latest announcements" block
Then "Latest announcements" "block" should exist
And I follow "Announcements"
And "Add a new topic" "button" should exist
And "Add a new topic" "link" should exist
And "Subscription mode > Forced subscription" "link" should not exist in current page administration
And "Subscription mode > Forced subscription" "text" should exist in current page administration
And I log out
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Announcements"
And "Add a new topic" "button" should not exist
And "Add a new topic" "link" should not exist
And "Forced subscription" "text" should exist in current page administration
Scenario: Create a course from the management interface and return to it
+4
View File
@@ -54,6 +54,7 @@ class stored_file_exporter extends \core\external\exporter {
$data->filepath = $file->get_filepath();
$data->filename = $file->get_filename();
$data->isdir = $file->is_directory();
$data->isimage = $file->is_valid_image();
$data->timemodified = $file->get_timemodified();
$data->timecreated = $file->get_timecreated();
$data->filesize = $file->get_filesize();
@@ -94,6 +95,9 @@ class stored_file_exporter extends \core\external\exporter {
'isdir' => array(
'type' => PARAM_BOOL
),
'isimage' => array(
'type' => PARAM_BOOL
),
'timemodified' => array(
'type' => PARAM_INT
),
+20 -4
View File
@@ -261,16 +261,32 @@ abstract class exporter {
final public static function read_properties_definition() {
$properties = static::properties_definition();
$customprops = static::define_other_properties();
foreach ($customprops as $property => $definition) {
$customprops = static::format_properties($customprops);
$properties += $customprops;
return $properties;
}
/**
* Recursively formats a given property definition with the default fields required.
*
* @param array $properties List of properties to format
* @return array Formatted array
*/
final public static function format_properties($properties) {
foreach ($properties as $property => $definition) {
// Ensures that null is set to its default.
if (!isset($definition['null'])) {
$customprops[$property]['null'] = NULL_NOT_ALLOWED;
$properties[$property]['null'] = NULL_NOT_ALLOWED;
}
if (!isset($definition['description'])) {
$customprops[$property]['description'] = $property;
$properties[$property]['description'] = $property;
}
// If an array is provided, it may be a nested array that is unformatted so rinse and repeat.
if (is_array($definition['type'])) {
$properties[$property]['type'] = static::format_properties($definition['type']);
}
}
$properties += $customprops;
return $properties;
}
+131
View File
@@ -0,0 +1,131 @@
<?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/>.
/**
* Paged Content exporter.
*
* @package core
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
/**
* Paged Content exporter.
*
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class paged_content_exporter extends exporter {
/** @var int pagesize The number of records to show on each page */
private $pagesize;
/** @var int pagenumber The current page number */
private $pagenumber;
/** @var int recordcount The total number of records available */
private $recordcount;
/** @var callable The callback to use to determine a page URL */
private $pageurlcallback;
/**
* Constructor.
*
* @param int $pagesize The number of records to show on each page
* @param int $pagenumber The current page number
* @param int $recordcount The total number of records available
* @param callable $pageurlcallback The callback to use to determine a page URL
* @param array $related List of related elements
*/
public function __construct(int $pagesize, int $pagenumber, int $recordcount, callable $pageurlcallback, array $related = []) {
$this->pagesize = $pagesize;
$this->pagenumber = $pagenumber;
$this->recordcount = $recordcount;
$this->pageurlcallback = $pageurlcallback;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'itemsperpage' => ['type' => PARAM_INT],
'buttons' => [
'type' => [
'first' => ['type' => PARAM_BOOL],
'previous' => ['type' => PARAM_BOOL],
'next' => ['type' => PARAM_BOOL],
'last' => ['type' => PARAM_BOOL],
],
],
'pages' => [
'multiple' => true,
'type' => [
'page' => ['type' => PARAM_INT],
'url' => ['type' => PARAM_URL],
'active' => ['type' => PARAM_BOOL],
'content' => [
'optional' => true,
'type' => PARAM_RAW,
],
],
],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$pagecount = ceil($this->recordcount / $this->pagesize);
$pages = [];
if ($pagecount > 1) {
for ($pageno = 1; $pageno <= $pagecount; $pageno++) {
$pages[] = [
'page' => $pageno,
'url' => call_user_func_array($this->pageurlcallback, [$pageno, $this->pagesize]),
'active' => $pageno === $this->pagenumber,
'content' => null,
];
}
}
return [
'itemsperpage' => $this->pagesize,
'buttons' => [
'first' => false,
'previous' => false,
'next' => false,
'last' => false,
],
'pages' => $pages,
];
}
}
+4
View File
@@ -162,6 +162,10 @@ class notification implements \renderable, \templatable {
'extraclasses' => implode(' ', $this->extraclasses),
'announce' => $this->announce,
'closebutton' => $this->closebutton,
'issuccess' => $this->messagetype === 'success',
'isinfo' => $this->messagetype === 'info',
'iswarning' => $this->messagetype === 'warning',
'iserror' => $this->messagetype === 'error',
);
}
+59
View File
@@ -828,6 +828,65 @@ abstract class moodle_database {
return array($sql, $params);
}
/**
* Get the SELECT SQL to preload columns for the specified fieldlist and table alias.
*
* This function is intended to be used in combination with get_preload_columns_sql and extract_from_fields.
*
* @param array $fieldlist The list of fields from get_preload_columns
* @param string $tablealias The table alias used in the FROM/JOIN field
* @return string The SQL to use in the SELECT
*/
public function get_preload_columns_sql(array $fieldlist, string $tablealias) : string {
return implode(', ', array_map(function($fieldname, $alias) use ($tablealias) {
return "{$tablealias}.{$fieldname} AS {$alias}";
}, $fieldlist, array_keys($fieldlist)));
}
/**
* Extract fields from the specified data.
* The fields are removed from the original object.
*
* This function is intended to be used in combination with get_preload_columns and get_preload_columns_sql.
*
* @param array $fieldlist The list of fields from get_preload_columns
* @param \stdClass $data The data retrieved from the database with fields to be extracted
* @return string The SQL to use in the SELECT
*/
public function extract_fields_from_object(array $fieldlist, \stdClass $data) : \stdClass {
$newdata = (object) [];
foreach ($fieldlist as $alias => $fieldname) {
if (property_exists($data, $alias)) {
$newdata->$fieldname = $data->$alias;
unset($data->$alias);
} else {
debugging("Field '{$fieldname}' not found", DEBUG_DEVELOPER);
}
}
return $newdata;
}
/**
* Get the preload columns for the specified table and use the specified prefix in the column alias.
*
* This function is intended to be used in combination with get_preload_columns_sql and extract_from_fields.
*
* @param string $table
* @param string $prefix
* @return array The list of columns in a table. The array key is the column name with an applied prefix.
*/
public function get_preload_columns(string $table, string $prefix) : array {
global $DB;
$fields = [];
foreach (array_keys($this->get_columns($table)) as $fieldname) {
$fields["{$prefix}{$fieldname}"] = $fieldname;
}
return $fields;
}
/**
* Converts short table name {tablename} to the real prefixed table name in given sql.
* @param string $sql The sql to be operated on.
+130
View File
@@ -794,6 +794,136 @@ class core_dml_testcase extends database_driver_testcase {
$this->assertFalse($columns['id']->auto_increment);
}
public function test_get_preload_columns() {
$DB = $this->tdb;
$dbman = $this->tdb->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$expected = [
'aid' => 'id',
'acourse' => 'course',
'aname' => 'name',
];
$columns = $DB->get_preload_columns($tablename, 'a');
$this->assertCount(3, $columns);
$this->assertEquals($expected, $columns);
}
/**
* Ensure that get_preload_columns_sql works as expected.
*
* @dataProvider get_preload_columns_sql_provider
* @param array $fieldlist The list of fields
* @param string $tablealias The alias to use
* @param string $expected The string to match
*/
public function test_get_preload_columns_sql(array $fieldlist, string $tablealias, string $expected) {
$this->assertEquals($expected, $this->tdb->get_preload_columns_sql($fieldlist, $tablealias));
}
/**
* Data provider for get_preload_columns_sql tests.
*
* @return array
*/
public function get_preload_columns_sql_provider() : array {
return [
'single field' => [
[
'xid' => 'id',
],
'x',
'x.id AS xid',
],
'multiple fields' => [
[
'bananaid' => 'id',
'bananacourse' => 'course',
'bananafoo' => 'foo',
],
'banana',
'banana.id AS bananaid, banana.course AS bananacourse, banana.foo AS bananafoo',
],
];
}
/**
* Ensure that extract_fields_from_object works as expected.
*
* @dataProvider extract_fields_from_object_provider
* @param array $fieldlist The list of fields
* @param stdClass $in Input values for the test
* @param stdClass $out The expected output
* @param stdClass $modified Expected value of $in after it's been modified
*/
public function test_extract_fields_from_object(array $fieldlist, \stdClass $in, \stdClass $out, \stdClass $modified) {
$result = $this->tdb->extract_fields_from_object($fieldlist, $in);
$this->assertEquals($out, $result);
$this->assertEquals($modified, $in);
}
/**
* Data provider for extract_fields_from_object tests.
*
* @return array
*/
public function extract_fields_from_object_provider() : array {
return [
'single table' => [
[
'sid' => 'id',
'scourse' => 'course',
'sflag' => 'flag',
],
(object) [
'sid' => 1,
'scourse' => 42,
'sflag' => 'foo',
],
(object) [
'id' => 1,
'course' => 42,
'flag' => 'foo',
],
(object) [
],
],
'single table amongst others' => [
[
'sid' => 'id',
'scourse' => 'course',
'sflag' => 'flag',
],
(object) [
'sid' => 1,
'scourse' => 42,
'sflag' => 'foo',
'oid' => 'id',
'ocourse' => 'course',
'oflag' => 'flag',
],
(object) [
'id' => 1,
'course' => 42,
'flag' => 'foo',
],
(object) [
'oid' => 'id',
'ocourse' => 'course',
'oflag' => 'flag',
],
],
];
}
public function test_get_manager() {
$DB = $this->tdb;
$dbman = $this->tdb->get_manager();
+5 -3
View File
@@ -598,7 +598,7 @@ class file_storage {
* @param int $contextid context ID
* @param string $component component
* @param mixed $filearea file area/s, you cannot specify multiple fileareas as well as an itemid
* @param int $itemid item ID or all files if not specified
* @param int|int[]|false $itemid item ID(s) or all files if not specified
* @param string $sort A fragment of SQL to use for sorting
* @param bool $includedirs whether or not include directories
* @param int $updatedsince return files updated since this time
@@ -617,8 +617,10 @@ class file_storage {
if ($itemid !== false && is_array($filearea)) {
throw new coding_exception('You cannot specify multiple fileareas as well as an itemid.');
} else if ($itemid !== false) {
$itemidsql = ' AND f.itemid = :itemid ';
$conditions['itemid'] = $itemid;
$itemids = is_array($itemid) ? $itemid : [$itemid];
list($itemidinorequalsql, $itemidconditions) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
$itemidsql = " AND f.itemid {$itemidinorequalsql}";
$conditions = array_merge($conditions, $itemidconditions);
} else {
$itemidsql = '';
}
@@ -92,7 +92,7 @@ Feature: Using the activity grade form element
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test forum name"
And I press "Add a new discussion topic"
And I click on "Add a new discussion topic" "link"
And I set the following fields to these values:
| Subject | Discussion subject |
| Message | Discussion message |
@@ -170,7 +170,7 @@ Feature: Using the activity grade form element
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test forum name"
And I press "Add a new discussion topic"
And I click on "Add a new discussion topic" "link"
And I set the following fields to these values:
| Subject | Discussion subject |
| Message | Discussion message |
+6
View File
@@ -200,3 +200,9 @@ define('PORTFOLIO_ADD_TEXT_LINK', 4);
* this replaces the old portfolio_fake_add_url function
*/
define('PORTFOLIO_ADD_FAKE_URL', 5);
/**
* PORTFOLIO_ADD_MOODULE_URL - hacky way to turn the button class into a moodle_url to redirect to
* this replaces the old portfolio_fake_add_url function
*/
define('PORTFOLIO_ADD_MOODLE_URL', 6);
+6 -1
View File
@@ -259,7 +259,7 @@ class portfolio_add_button {
* Optional, defaults to PORTFOLIO_ADD_FULL_FORM
* @param string $addstr string to use for the button or icon alt text or link text.
* This is whole string, not key. optional, defaults to 'Add to portfolio';
* @return void|string
* @return void|string|moodle_url
*/
public function to_html($format=null, $addstr=null) {
global $CFG, $COURSE, $OUTPUT, $USER;
@@ -327,6 +327,11 @@ class portfolio_add_button {
return;
}
}
// If we just want a moodle_url to redirect to, do it now.
if ($format == PORTFOLIO_ADD_MOODLE_URL) {
return $url;
}
// if we just want a url to redirect to, do it now
if ($format == PORTFOLIO_ADD_FAKE_URL) {
return $url->out(false);
+50
View File
@@ -0,0 +1,50 @@
{{!
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/>.
}}
{{!
@template core/notification
Moodle notification template.
The purpose of this template is to render a notification using other notification templates.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* message A cleaned string (use clean_text()) to display.
* extraclasses Additional classes to apply to the notification.
* closebutton Whether a close button should be displayed to dismiss the message.
* announce Whether the notification should be announced to screen readers.
Example context (json):
{ "message": "Your pants are on fire!", "closebutton": 1, "announce": 1, "extraclasses": "foo bar", "issuccess": 1}
}}
{{#issuccess}}
{{> core/notification_success}}
{{/issuccess}}
{{#isinfo}}
{{> core/notification_info}}
{{/isinfo}}
{{#iswarning}}
{{> core/notification_warning}}
{{/iswarning}}
{{#iserror}}
{{> core/notification_error}}
{{/iserror}}
+37 -37
View File
@@ -40,55 +40,55 @@ Feature: Context freezing apply to child contexts
Given I log in as "admin"
And I am on "courseaa1" course homepage
And I follow "faa1"
And "Add a new discussion topic" "button" should exist
And "Add a new discussion topic" "link" should exist
When I follow "Freeze this context"
And I click on "Continue" "button"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then I should see "Turn editing on"
When I follow "faa1b"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseaa2" course homepage
Then I should see "Turn editing on"
When I follow "faa2"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseb" course homepage
Then I should see "Turn editing on"
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
And I log out
When I log in as "teacher"
And I am on "courseaa1" course homepage
And I follow "faa1"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then I should see "Turn editing on"
When I follow "faa1b"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseaa2" course homepage
Then I should see "Turn editing on"
When I follow "faa2"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseb" course homepage
Then I should see "Turn editing on"
When I follow "fb"
And "Add a new discussion topic" "button" should exist
And "Add a new discussion topic" "link" should exist
And I log out
When I log in as "student1"
And I am on "courseaa1" course homepage
And I follow "faa1"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
When I follow "faa1b"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseaa2" course homepage
When I follow "faa2"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseb" course homepage
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
Scenario: Freeze course should freeze all children
Given I log in as "admin"
@@ -97,54 +97,54 @@ Feature: Context freezing apply to child contexts
When I follow "Freeze this context"
And I click on "Continue" "button"
Then I should not see "Turn editing on"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then I should not see "Turn editing on"
And "Unfreeze this context" "link" should exist in current page administration
When I follow "faa1b"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
And "Unfreeze this context" "link" should not exist in current page administration
When I am on "courseaa2" course homepage
Then I should see "Turn editing on"
When I follow "faa2"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseb" course homepage
Then I should see "Turn editing on"
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
And I log out
When I log in as "teacher"
And I am on "courseaa1" course homepage
And I follow "faa1"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then I should not see "Turn editing on"
When I follow "faa1b"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
Then I should see "Turn editing on"
When I follow "faa2"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseb" course homepage
Then I should see "Turn editing on"
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
And I log out
When I log in as "student1"
And I am on "courseaa1" course homepage
And I follow "faa1"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
When I follow "faa1b"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
When I follow "faa2"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
When I am on "courseb" course homepage
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
Scenario: Freeze course category should freeze all children
Given I log in as "admin"
@@ -153,53 +153,53 @@ Feature: Context freezing apply to child contexts
And I click on "Continue" "button"
And I am on "courseaa1" course homepage
And I should not see "Turn editing on"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then I should not see "Turn editing on"
And "Unfreeze this context" "link" should not exist in current page administration
When I follow "faa1b"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
And "Unfreeze this context" "link" should not exist in current page administration
When I am on "courseaa2" course homepage
Then I should not see "Turn editing on"
When I follow "faa2"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
And "Unfreeze this context" "link" should not exist in current page administration
When I am on "courseb" course homepage
Then I should see "Turn editing on"
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
And I log out
When I log in as "teacher"
And I am on "courseaa1" course homepage
Then I should not see "Turn editing on"
And I follow "faa1"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then I should not see "Turn editing on"
When I follow "faa1b"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
Then I should not see "Turn editing on"
When I follow "faa2"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseb" course homepage
Then I should see "Turn editing on"
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
And I log out
When I log in as "student1"
And I am on "courseaa1" course homepage
And I follow "faa1"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
When I follow "faa1b"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
When I follow "faa2"
Then "Add a new discussion topic" "button" should not exist
Then "Add a new discussion topic" "link" should not exist
When I am on "courseb" course homepage
When I follow "fb"
Then "Add a new discussion topic" "button" should exist
Then "Add a new discussion topic" "link" should exist
+9
View File
@@ -179,6 +179,8 @@ class core_exporter_testcase extends advanced_testcase {
$this->assertEquals('otherstring description', $properties['otherstring']['description']);
// Other properties default description.
$this->assertEquals('otherstrings', $properties['otherstrings']['description']);
// Assert nested elements are formatted correctly.
$this->assertEquals('id', $properties['nestedarray']['type']['id']['description']);
}
}
@@ -228,6 +230,13 @@ class core_testable_exporter extends \core\external\exporter {
'otherstrings' => array(
'type' => PARAM_TEXT,
'multiple' => true
),
'nestedarray' => array(
'multiple' => true,
'optional' => true,
'type' => [
'id' => ['type' => PARAM_INT]
]
)
);
}
+5
View File
@@ -591,6 +591,11 @@ class core_externallib_testcase extends advanced_testcase {
$files = external_util::get_area_files($context, $component, $filearea, false);
$this->assertEquals($expectedfiles, $files);
$DB->method('get_in_or_equal')->willReturn([
'= :mock1',
['mock1' => $itemid]
]);
// Get just the file indicated by $itemid.
$files = external_util::get_area_files($context, $component, $filearea, $itemid);
$this->assertEquals($expectedfiles, $files);
+1
View File
@@ -0,0 +1 @@
define(["mod_forum/subscription_toggle"],function(a){return{init:function(b){a.init(b)}}});
+1
View File
@@ -0,0 +1 @@
define(["core/ajax"],function(a){var b=function(b,c,d){var e={methodname:"mod_forum_set_subscription_state",args:{forumid:b,discussionid:c,targetstate:d}};return a.call([e])[0]};return{setDiscussionSubscriptionState:b}});
+1
View File
@@ -0,0 +1 @@
define([],function(){return{subscription:{toggle:"[data-type='subscription-toggle'][data-action='toggle']"}}});
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/templates","core/notification","mod_forum/repository","mod_forum/selectors"],function(a,b,c,d,e){var f=function(f){f.on("click",e.subscription.toggle,function(e){var f=a(this),g=f.data("forumid"),h=f.data("discussionid"),i=f.data("targetstate");d.setDiscussionSubscriptionState(g,h,i).then(function(a){return b.render("mod_forum/discussion_subscription_toggle",a)}).then(function(a,c){return b.replaceNode(f,a,c)})["catch"](c.exception),e.preventDefault()})};return{init:function(a){f(a)}}});
+30
View File
@@ -0,0 +1,30 @@
// 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/>.
/**
* Module for the list of discussions on when viewing a forum.
*
* @module mod_forum/discussion_list
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['mod_forum/subscription_toggle'], function(SubscriptionToggle) {
return {
init: function(root) {
SubscriptionToggle.init(root);
}
};
});
+49
View File
@@ -0,0 +1,49 @@
// 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/>.
/**
* Forum repository class to encapsulate all of the AJAX requests that
* can be sent for forum.
*
* @module mod_forum/repository
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['core/ajax'], function(Ajax) {
/**
* Set the subscription state for a discussion in a forum.
*
* @param {number} forumId ID of the forum the discussion belongs to
* @param {number} discussionId ID of the discussion with the subscription state
* @param {boolean} targetState Set the subscribed state. True == subscribed; false == unsubscribed.
* @return {object} jQuery promise
*/
var setDiscussionSubscriptionState = function(forumId, discussionId, targetState) {
var request = {
methodname: 'mod_forum_set_subscription_state',
args: {
forumid: forumId,
discussionid: discussionId,
targetstate: targetState
}
};
return Ajax.call([request])[0];
};
return {
setDiscussionSubscriptionState: setDiscussionSubscriptionState,
};
});
+30
View File
@@ -0,0 +1,30 @@
// 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/>.
/**
* Common CSS selectors for the forum UI.
*
* @module mod_forum/selectors
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
return {
subscription: {
toggle: "[data-type='subscription-toggle'][data-action='toggle']",
}
};
});
+69
View File
@@ -0,0 +1,69 @@
// 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/>.
/**
* Handle discussion subscription toggling on a discussion list in
* the forum view.
*
* @module mod_forum/subscription_toggle
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/notification',
'mod_forum/repository',
'mod_forum/selectors',
], function(
$,
Templates,
Notification,
Repository,
Selectors
) {
/**
* Register event listeners for the subscription toggle.
*
* @param {object} root The discussion list root element
*/
var registerEventListeners = function(root) {
root.on('click', Selectors.subscription.toggle, function(e) {
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var subscriptionState = toggleElement.data('targetstate');
Repository.setDiscussionSubscriptionState(forumId, discussionId, subscriptionState)
.then(function(context) {
return Templates.render('mod_forum/discussion_subscription_toggle', context);
})
.then(function(html, js) {
return Templates.replaceNode(toggleElement, html, js);
})
.catch(Notification.exception);
e.preventDefault();
});
};
return {
init: function(root) {
registerEventListeners(root);
}
};
});
@@ -0,0 +1,198 @@
<?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/>.
/**
* Exported discussion summaries builder class.
*
* @package mod_forum
* @copyright 2019 Mihail Geshoski <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\builders;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use rating_manager;
use renderer_base;
use stdClass;
/**
* Exported discussion summaries builder class.
*
* This class is an implementation of the builder pattern (loosely). It is responsible
* for taking a set of related forums, discussions, and posts and generate the exported
* version of the discussion summaries.
*
* It encapsulates the complexity involved with exporting discussions summaries. All of the relevant
* additional resources will be loaded by this class in order to ensure the exporting
* process can happen.
*
* See this doc for more information on the builder pattern:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
*
* @package mod_forum
* @copyright 2019 Mihail Geshoski <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exported_discussion_summaries {
/** @var renderer_base $renderer Core renderer */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/**
* Constructor.
*
* @param renderer_base $renderer Core renderer
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param rating_manager $ratingmanager Rating manager
*/
public function __construct(
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
rating_manager $ratingmanager
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->ratingmanager = $ratingmanager;
}
/**
* Build the exported discussion summaries for a given set of discussions.
*
* This will typically be used for a list of discussions in the same forum.
*
* @param stdClass $user The user to export the posts for.
* @param forum_entity $forum The forum that each of the $discussions belong to
* @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to
* @return stdClass[] List of exported posts in the same order as the $posts array.
*/
public function build(
stdClass $user,
forum_entity $forum,
array $discussions
) : array {
$discussionids = array_keys($discussions);
$postvault = $this->vaultfactory->get_post_vault();
$posts = $postvault->get_from_discussion_ids($discussionids);
$groupsbyid = $this->get_groups_available_in_forum($forum);
$groupsbyauthorid = $this->get_author_groups_from_posts($posts, $forum);
$replycounts = $postvault->get_reply_count_for_discussion_ids($discussionids);
$latestposts = $postvault->get_latest_post_id_for_discussion_ids($discussionids);
$unreadcounts = [];
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$forumrecord = $forumdatamapper->to_legacy_object($forum);
if (forum_tp_can_track_forums($forumrecord)) {
$unreadcounts = $postvault->get_unread_count_for_discussion_ids($user, $discussionids);
}
$summaryexporter = $this->exporterfactory->get_discussion_summaries_exporter(
$user,
$forum,
$discussions,
$groupsbyid,
$groupsbyauthorid,
$replycounts,
$unreadcounts,
$latestposts
);
return (array) $summaryexporter->export($this->renderer);
}
/**
* Get the groups details for all groups available to the forum.
* @param forum_entity $forum The forum entity
* @return stdClass[]
*/
private function get_groups_available_in_forum($forum) : array {
$course = $forum->get_course_record();
$coursemodule = $forum->get_course_module_record();
return groups_get_all_groups($course->id, 0, $coursemodule->groupingid);
}
/**
* Get the author's groups for a list of posts.
*
* @param post_entity[] $posts The list of posts
* @param forum_entity $forum The forum entity
* @return array Author groups indexed by author id
*/
private function get_author_groups_from_posts(array $posts, $forum) : array {
$course = $forum->get_course_record();
$coursemodule = $forum->get_course_module_record();
$authorids = array_reduce($posts, function($carry, $post) {
$carry[$post->get_author_id()] = true;
return $carry;
}, []);
$authorgroups = groups_get_all_groups($course->id, array_keys($authorids), $coursemodule->groupingid,
'g.*, gm.id, gm.groupid, gm.userid');
$authorgroups = array_reduce($authorgroups, function($carry, $group) {
// Clean up data returned from groups_get_all_groups.
$userid = $group->userid;
$groupid = $group->groupid;
unset($group->userid);
unset($group->groupid);
$group->id = $groupid;
if (!isset($carry[$userid])) {
$carry[$userid] = [$group];
} else {
$carry[$userid][] = $group;
}
return $carry;
}, []);
foreach (array_diff(array_keys($authorids), array_keys($authorgroups)) as $authorid) {
$authorgroups[$authorid] = [];
}
return $authorgroups;
}
}
@@ -0,0 +1,503 @@
<?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/>.
/**
* Exported post builder class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\builders;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use context;
use core_tag_tag;
use moodle_exception;
use rating_manager;
use renderer_base;
use stdClass;
/**
* Exported post builder class.
*
* This class is an implementation of the builder pattern (loosely). It is responsible
* for taking a set of related forums, discussions, and posts and generate the exported
* version of the posts.
*
* It encapsulates the complexity involved with exporting posts. All of the relevant
* additional resources will be loaded by this class in order to ensure the exporting
* process can happen.
*
* See this doc for more information on the builder pattern:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exported_posts {
/** @var renderer_base $renderer Core renderer */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/**
* Constructor.
*
* @param renderer_base $renderer Core renderer
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param rating_manager $ratingmanager Rating manager
*/
public function __construct(
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
rating_manager $ratingmanager
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->ratingmanager = $ratingmanager;
}
/**
* Build the exported posts for a given set of forums, discussions, and posts.
*
* This will typically be used for a list of posts in the same discussion/forum however
* it does support exporting any arbitrary list of posts as long as the caller also provides
* a unique list of all discussions for the list of posts and all forums for the list of discussions.
*
* Increasing the number of different forums being processed will increase the processing time
* due to processing multiple contexts (for things like capabilities, files, etc). The code attempts
* to load the additional resources as efficiently as possible but there is no way around some of
* the additional overhead.
*
* @param stdClass $user The user to export the posts for.
* @param forum_entity[] $forums A list of all forums that each of the $discussions belong to
* @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to
* @param post_entity[] $posts The list of posts to export.
* @return stdClass[] List of exported posts in the same order as the $posts array.
*/
public function build(
stdClass $user,
array $forums,
array $discussions,
array $posts
) : array {
// Format the forums and discussion to make them more easily accessed later.
$forums = array_reduce($forums, function($carry, $forum) {
$carry[$forum->get_id()] = $forum;
return $carry;
}, []);
$discussions = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
// Group the posts by discussion and forum so that we can load the resources in
// batches to improve performance.
$groupedposts = $this->group_posts_by_discussion($forums, $discussions, $posts);
// Load all of the resources we need in order to export the posts.
$authorsbyid = $this->get_authors_for_posts($posts);
$attachmentsbypostid = $this->get_attachments_for_posts($groupedposts);
$groupsbycourseandauthorid = $this->get_author_groups_from_posts($groupedposts);
$tagsbypostid = $this->get_tags_from_posts($posts);
$ratingbypostid = $this->get_ratings_from_posts($user, $groupedposts);
$readreceiptcollectionbyforumid = $this->get_read_receipts_from_posts($user, $groupedposts);
$exportedposts = [];
// Export each set of posts per discussion because it's the largest chunks we can
// break them into due to constraints on capability checks.
foreach ($groupedposts as $grouping) {
[
'forum' => $forum,
'discussion' => $discussion,
'posts' => $groupedposts
] = $grouping;
$forumid = $forum->get_id();
$courseid = $forum->get_course_record()->id;
$postsexporter = $this->exporterfactory->get_posts_exporter(
$user,
$forum,
$discussion,
$groupedposts,
$authorsbyid,
$attachmentsbypostid,
$groupsbycourseandauthorid[$courseid],
$readreceiptcollectionbyforumid[$forumid] ?? null,
$tagsbypostid,
$ratingbypostid,
true
);
['posts' => $exportedgroupedposts] = (array) $postsexporter->export($this->renderer);
$exportedposts = array_merge($exportedposts, $exportedgroupedposts);
}
if (count($forums) == 1 && count($discussions) == 1) {
// All of the posts belong to a single discussion in a single forum so
// the exported order will match the given $posts array.
return $exportedposts;
} else {
// Since we grouped the posts by discussion and forum the ordering of the
// exported posts may be different to the given $posts array so we should
// sort it back into the correct order for the caller.
return $this->sort_exported_posts($posts, $exportedposts);
}
}
/**
* Group the posts by which discussion they belong to in order for them to be processed
* in chunks by the exporting.
*
* Returns a list of groups where each group has a forum, discussion, and list of posts.
* E.g.
* [
* [
* 'forum' => <forum_entity>,
* 'discussion' => <discussion_entity>,
* 'posts' => [
* <post_entity in discussion>,
* <post_entity in discussion>,
* <post_entity in discussion>
* ]
* ]
* ]
*
* @param forum_entity[] $forums A list of all forums that each of the $discussions belong to, indexed by id.
* @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to, indexed by id.
* @param post_entity[] $posts The list of posts to process.
* @return array List of grouped posts. Each group has a discussion, forum, and posts.
*/
private function group_posts_by_discussion(array $forums, array $discussions, array $posts) : array {
return array_reduce($posts, function($carry, $post) use ($forums, $discussions) {
$discussionid = $post->get_discussion_id();
if (!isset($discussions[$discussionid])) {
throw new moodle_exception('Unable to find discussion with id ' . $discussionid);
}
if (isset($carry[$discussionid])) {
$carry[$discussionid]['posts'][] = $post;
} else {
$discussion = $discussions[$discussionid];
$forumid = $discussion->get_forum_id();
if (!isset($forums[$forumid])) {
throw new moodle_exception('Unable to find forum with id ' . $forumid);
}
$carry[$discussionid] = [
'forum' => $forums[$forumid],
'discussion' => $discussions[$discussionid],
'posts' => [$post]
];
}
return $carry;
}, []);
}
/**
* Load the list of authors for the given posts.
*
* The list of authors will be indexed by the author id.
*
* @param post_entity[] $posts The list of posts to process.
* @return author_entity[]
*/
private function get_authors_for_posts(array $posts) : array {
$authorvault = $this->vaultfactory->get_author_vault();
return $authorvault->get_authors_for_posts($posts);
}
/**
* Load the list of all attachments for the posts. The list of attachments will be
* indexed by the post id.
*
* @param array $groupedposts List of posts grouped by discussions.
* @return stored_file[]
*/
private function get_attachments_for_posts(array $groupedposts) : array {
$attachmentsbypostid = [];
$postattachmentvault = $this->vaultfactory->get_post_attachment_vault();
$postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$forumid = $forum->get_id();
if (!isset($carry[$forumid])) {
$carry[$forumid] = [
'forum' => $forum,
'posts' => []
];
}
$carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
return $carry;
}, []);
foreach ($postsbyforum as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$attachments = $postattachmentvault->get_attachments_for_posts($forum->get_context(), $posts);
// Have to loop in order to maintain the correct indexes since they are numeric.
foreach ($attachments as $postid => $attachment) {
$attachmentsbypostid[$postid] = $attachment;
}
}
return $attachmentsbypostid;
}
/**
* Get the groups for each author of the given posts.
*
* The results are grouped by course and then author id because the groups are
* contextually related to the course, e.g. a single author can be part of two different
* sets of groups in two different courses.
*
* @param array $groupedposts List of posts grouped by discussions.
* @return array List of groups indexed by forum id and then author id.
*/
private function get_author_groups_from_posts(array $groupedposts) : array {
$groupsbyauthorid = [];
$authoridsbycourseid = [];
// Get the unique list of author ids for each course in the grouped
// posts. Grouping by course is the largest grouping we can achieve.
foreach ($groupedposts as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$course = $forum->get_course_record();
$courseid = $course->id;
if (!isset($authoridsbycourseid[$courseid])) {
$coursemodule = $forum->get_course_module_record();
$authoridsbycourseid[$courseid] = [
'groupingid' => $coursemodule->groupingid,
'authorids' => []
];
}
$authorids = array_map(function($post) {
return $post->get_author_id();
}, $posts);
foreach ($authorids as $authorid) {
$authoridsbycourseid[$courseid]['authorids'][$authorid] = $authorid;
}
}
// Load each set of groups per course.
foreach ($authoridsbycourseid as $courseid => $values) {
['groupingid' => $groupingid, 'authorids' => $authorids] = $values;
$authorgroups = groups_get_all_groups(
$courseid,
array_keys($authorids),
$groupingid,
'g.*, gm.id, gm.groupid, gm.userid'
);
if (!isset($groupsbyauthorid[$courseid])) {
$groupsbyauthorid[$courseid] = [];
}
foreach ($authorgroups as $group) {
// Clean up data returned from groups_get_all_groups.
$userid = $group->userid;
$groupid = $group->groupid;
unset($group->userid);
unset($group->groupid);
$group->id = $groupid;
if (!isset($groupsbyauthorid[$courseid][$userid])) {
$groupsbyauthorid[$courseid][$userid] = [];
}
$groupsbyauthorid[$courseid][$userid][] = $group;
}
}
return $groupsbyauthorid;
}
/**
* Get the list of tags for each of the posts. The tags will be returned in an
* array indexed by the post id.
*
* @param post_entity[] $posts The list of posts to load tags for.
* @return array Sets of tags indexed by post id.
*/
private function get_tags_from_posts(array $posts) : array {
$postids = array_map(function($post) {
return $post->get_id();
}, $posts);
return core_tag_tag::get_items_tags('mod_forum', 'forum_posts', $postids);
}
/**
* Get the list of ratings for each post. The ratings are returned in an array
* indexed by the post id.
*
* @param stdClass $user The user viewing the ratings.
* @param array $groupedposts List of posts grouped by discussions.
* @return array Sets of ratings indexed by post id.
*/
private function get_ratings_from_posts(stdClass $user, array $groupedposts) {
$ratingsbypostid = [];
$postsdatamapper = $this->legacydatamapperfactory->get_post_data_mapper();
$postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$forumid = $forum->get_id();
if (!isset($carry[$forumid])) {
$carry[$forumid] = [
'forum' => $forum,
'posts' => []
];
}
$carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
return $carry;
}, []);
foreach ($postsbyforum as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
if (!$forum->has_rating_aggregate()) {
continue;
}
$items = $postsdatamapper->to_legacy_objects($posts);
$ratingoptions = (object) [
'context' => $forum->get_context(),
'component' => 'mod_forum',
'ratingarea' => 'post',
'items' => $items,
'aggregate' => $forum->get_rating_aggregate(),
'scaleid' => $forum->get_scale(),
'userid' => $user->id,
'assesstimestart' => $forum->get_assess_time_start(),
'assesstimefinish' => $forum->get_assess_time_finish()
];
$rm = $this->ratingmanager;
$items = $rm->get_ratings($ratingoptions);
foreach ($items as $item) {
$ratingsbypostid[$item->id] = empty($item->rating) ? null : $item->rating;
}
}
return $ratingsbypostid;
}
/**
* Get the read receipt collections for the given viewing user and each forum. The
* receipt collections will only be loaded for posts in forums that the user is tracking.
*
* The receipt collections are returned in an array indexed by the forum ids.
*
* @param stdClass $user The user viewing the posts.
* @param array $groupedposts List of posts grouped by discussions.
*/
private function get_read_receipts_from_posts(stdClass $user, array $groupedposts) {
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$trackedforums = [];
$trackedpostids = [];
foreach ($groupedposts as $group) {
['forum' => $forum, 'posts' => $posts] = $group;
$forumid = $forum->get_id();
if (!isset($trackedforums[$forumid])) {
$forumrecord = $forumdatamapper->to_legacy_object($forum);
$trackedforums[$forumid] = forum_tp_is_tracked($forumrecord, $user);
}
if ($trackedforums[$forumid]) {
foreach ($posts as $post) {
$trackedpostids[] = $post->get_id();
}
}
}
if (empty($trackedpostids)) {
return [];
}
// We can just load a single receipt collection for all tracked posts.
$receiptvault = $this->vaultfactory->get_post_read_receipt_collection_vault();
$readreceiptcollection = $receiptvault->get_from_user_id_and_post_ids($user->id, $trackedpostids);
$receiptsbyforumid = [];
// Assign the collection to all forums that are tracked.
foreach ($trackedforums as $forumid => $tracked) {
if ($tracked) {
$receiptsbyforumid[$forumid] = $readreceiptcollection;
}
}
return $receiptsbyforumid;
}
/**
* Sort the list of exported posts back into the same order as the given posts.
* The ordering of the exported posts can often deviate from the given posts due
* to the process of exporting them so we need to sort them back into the order
* that the calling code expected.
*
* @param post_entity[] $posts The posts in the expected order.
* @param stdClass[] $exportedposts The list of exported posts in any order.
* @return stdClass[] Sorted exported posts.
*/
private function sort_exported_posts(array $posts, array $exportedposts) {
$postindexes = [];
foreach (array_values($posts) as $index => $post) {
$postindexes[$post->get_id()] = $index;
}
$sortedexportedposts = [];
foreach ($exportedposts as $exportedpost) {
$index = $postindexes[$exportedpost->id];
$sortedexportedposts[$index] = $exportedpost;
}
return $sortedexportedposts;
}
}
+152
View File
@@ -0,0 +1,152 @@
<?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/>.
/**
* Container class.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\factories\renderer as renderer_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\builder as builder_factory;
use mod_forum\local\factories\url as url_factory;
/**
* Container class.
*
* This class provides helper methods with static configurations to get any
* of the factories from the "local" namespace.
*
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class container {
/**
* Create the renderer factory.
*
* @return renderer_factory
*/
public static function get_renderer_factory() : renderer_factory {
global $PAGE;
return new renderer_factory(
self::get_legacy_data_mapper_factory(),
self::get_exporter_factory(),
self::get_vault_factory(),
self::get_manager_factory(),
self::get_entity_factory(),
self::get_builder_factory(),
self::get_url_factory(),
$PAGE
);
}
/**
* Create the legacy data mapper factory.
*
* @return legacy_data_mapper_factory
*/
public static function get_legacy_data_mapper_factory() : legacy_data_mapper_factory {
return new legacy_data_mapper_factory();
}
/**
* Create the exporter factory.
*
* @return exporter_factory
*/
public static function get_exporter_factory() : exporter_factory {
return new exporter_factory(
self::get_legacy_data_mapper_factory(),
self::get_manager_factory(),
self::get_url_factory()
);
}
/**
* Create the vault factory.
*
* @return vault_factory
*/
public static function get_vault_factory() : vault_factory {
global $DB;
return new vault_factory(
$DB,
self::get_entity_factory(),
get_file_storage()
);
}
/**
* Create the manager factory.
*
* @return manager_factory
*/
public static function get_manager_factory() : manager_factory {
return new manager_factory(
self::get_legacy_data_mapper_factory()
);
}
/**
* Create the entity factory.
*
* @return entity_factory
*/
public static function get_entity_factory() : entity_factory {
return new entity_factory();
}
/**
* Create the builder factory.
*
* @return builder_factory
*/
public static function get_builder_factory() : builder_factory {
global $PAGE;
return new builder_factory(
self::get_legacy_data_mapper_factory(),
self::get_exporter_factory(),
self::get_vault_factory(),
self::get_manager_factory(),
$PAGE->get_renderer('mod_forum')
);
}
/**
* Create the URL factory.
*
* @return url_factory
*/
public static function get_url_factory() : url_factory {
return new url_factory(
self::get_legacy_data_mapper_factory()
);
}
}
@@ -0,0 +1,72 @@
<?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/>.
/**
* Author data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use stdClass;
/**
* Convert an author entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author {
/**
* Convert a list of author entities into stdClasses.
*
* @param author_entity[] $authors The authors to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $authors) : array {
return array_map(function(author_entity $author) {
return (object) [
'id' => $author->get_id(),
'picture' => $author->get_picture_item_id(),
'firstname' => $author->get_first_name(),
'lastname' => $author->get_last_name(),
'fullname' => $author->get_full_name(),
'email' => $author->get_email(),
'middlename' => $author->get_middle_name(),
'firstnamephonetic' => $author->get_first_name_phonetic(),
'lastnamephonetic' => $author->get_last_name_phonetic(),
'alternatename' => $author->get_alternate_name(),
'imagealt' => $author->get_image_alt()
];
}, $authors);
}
/**
* Convert an author entity into an stdClass.
*
* @param author_entity $author The author to convert.
* @return stdClass
*/
public function to_legacy_object(author_entity $author) : stdClass {
return $this->to_legacy_objects([$author])[0];
}
}
@@ -0,0 +1,74 @@
<?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/>.
/**
* Discussion data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use stdClass;
/**
* Convert a discussion entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion {
/**
* Convert a list of discussion entities into stdClasses.
*
* @param discussion_entity[] $authors The authors to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $discussions) : array {
return array_map(function(discussion_entity $discussion) {
return (object) [
'id' => $discussion->get_id(),
'course' => $discussion->get_course_id(),
'forum' => $discussion->get_forum_id(),
'name' => $discussion->get_name(),
'firstpost' => $discussion->get_first_post_id(),
'userid' => $discussion->get_user_id(),
'groupid' => $discussion->get_group_id(),
'assessed' => $discussion->is_assessed(),
'timemodified' => $discussion->get_time_modified(),
'usermodified' => $discussion->get_user_modified(),
'timestart' => $discussion->get_time_start(),
'timeend' => $discussion->get_time_end(),
'pinned' => $discussion->is_pinned()
];
}, $discussions);
}
/**
* Convert a discussion entity into an stdClass.
*
* @param discussion_entity $discussion The discussion to convert.
* @return stdClass
*/
public function to_legacy_object(discussion_entity $discussion) : stdClass {
return $this->to_legacy_objects([$discussion])[0];
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Forum data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use stdClass;
/**
* Convert a forum entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum {
/**
* Convert a list of forum entities into stdClasses.
*
* @param forum_entity[] $forums The forums to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $forums) : array {
return array_map(function(forum_entity $forum) {
return (object) [
'id' => $forum->get_id(),
'course' => $forum->get_course_id(),
'type' => $forum->get_type(),
'name' => $forum->get_name(),
'intro' => $forum->get_intro(),
'introformat' => $forum->get_intro_format(),
'assessed' => $forum->get_rating_aggregate(),
'assesstimestart' => $forum->get_assess_time_start(),
'assesstimefinish' => $forum->get_assess_time_finish(),
'scale' => $forum->get_scale(),
'maxbytes' => $forum->get_max_bytes(),
'maxattachments' => $forum->get_max_attachments(),
'forcesubscribe' => $forum->get_subscription_mode(),
'trackingtype' => $forum->get_tracking_type(),
'rsstype' => $forum->get_rss_type(),
'rssarticles' => $forum->get_rss_articles(),
'timemodified' => $forum->get_time_modified(),
'warnafter' => $forum->get_warn_after(),
'blockafter' => $forum->get_block_after(),
'blockperiod' => $forum->get_block_period(),
'completiondiscussions' => $forum->get_completion_discussions(),
'completionreplies' => $forum->get_completion_replies(),
'completionposts' => $forum->get_completion_posts(),
'displaywordcount' => $forum->should_display_word_count(),
'lockdiscussionafter' => $forum->get_lock_discussions_after()
];
}, $forums);
}
/**
* Convert a forum entity into an stdClass.
*
* @param forum_entity $forum The forum to convert.
* @return stdClass
*/
public function to_legacy_object(forum_entity $forum) : stdClass {
return $this->to_legacy_objects([$forum])[0];
}
}
@@ -0,0 +1,76 @@
<?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/>.
/**
* Post data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use stdClass;
/**
* Convert a post entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post {
/**
* Convert a list of post entities into stdClasses.
*
* @param post_entity[] $posts The posts to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $posts) : array {
return array_map(function(post_entity $post) {
return (object) [
'id' => $post->get_id(),
'discussion' => $post->get_discussion_id(),
'parent' => $post->get_parent_id(),
'userid' => $post->get_author_id(),
'created' => $post->get_time_created(),
'modified' => $post->get_time_modified(),
'mailed' => $post->has_been_mailed(),
'subject' => $post->get_subject(),
'message' => $post->get_message(),
'messageformat' => $post->get_message_format(),
'messagetrust' => $post->is_message_trusted(),
'attachment' => $post->has_attachments(),
'totalscore' => $post->get_total_score(),
'mailnow' => $post->should_mail_now(),
'deleted' => $post->is_deleted()
];
}, $posts);
}
/**
* Convert a post entity into an stdClass.
*
* @param post_entity $post The post to convert.
* @return stdClass
*/
public function to_legacy_object(post_entity $post) : stdClass {
return $this->to_legacy_objects([$post])[0];
}
}
+198
View File
@@ -0,0 +1,198 @@
<?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/>.
/**
* Author class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
/**
* Author class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author {
/** @var int $id ID */
private $id;
/** @var int $pictureitemid Picture item id */
private $pictureitemid;
/** @var string $firstname First name */
private $firstname;
/** @var string $lastname Last name */
private $lastname;
/** @var string $fullname Full name */
private $fullname;
/** @var string $email Email */
private $email;
/** @var string $middlename Middle name */
private $middlename;
/** @var string $firstnamephonetic Phonetic spelling of first name */
private $firstnamephonetic;
/** @var string $lastnamephonetic Phonetic spelling of last name */
private $lastnamephonetic;
/** @var string $alternatename Altername name */
private $alternatename;
/** @var string $imagealt Image alt */
private $imagealt;
/**
* Constructor.
*
* @param int $id ID
* @param int $pictureitemid Picture item id
* @param string $firstname First name
* @param string $lastname Last name
* @param string $fullname Full name
* @param string $email Email
* @param string|null $middlename Middle name
* @param string|null $firstnamephonetic Phonetic spelling of first name
* @param string|null $lastnamephonetic Phonetic spelling of last name
* @param string|null $alternatename Altername name
* @param string|null $imagealt Image alt
*/
public function __construct(
int $id,
int $pictureitemid,
string $firstname,
string $lastname,
string $fullname,
string $email,
string $middlename = null,
string $firstnamephonetic = null,
string $lastnamephonetic = null,
string $alternatename = null,
string $imagealt = null
) {
$this->id = $id;
$this->pictureitemid = $pictureitemid;
$this->firstname = $firstname;
$this->lastname = $lastname;
$this->fullname = $fullname;
$this->email = $email;
$this->middlename = $middlename;
$this->firstnamephonetic = $firstnamephonetic;
$this->lastnamephonetic = $lastnamephonetic;
$this->alternatename = $alternatename;
$this->imagealt = $imagealt;
}
/**
* Return the id.
*
* @return int
*/
public function get_id() : int {
return $this->id;
}
/**
* Return the picture item id.
*
* @return int
*/
public function get_picture_item_id() : int {
return $this->pictureitemid;
}
/**
* Return the first name.
*
* @return string
*/
public function get_first_name() : string {
return $this->firstname;
}
/**
* Return the last name.
*
* @return string
*/
public function get_last_name() : string {
return $this->lastname;
}
/**
* Return the full name.
*
* @return string
*/
public function get_full_name() : string {
return $this->fullname;
}
/**
* Return the email.
*
* @return string
*/
public function get_email() : string {
return $this->email;
}
/**
* Return the middle name.
*
* @return string|null
*/
public function get_middle_name() : ?string {
return $this->middlename;
}
/**
* Return the first name phonetic.
*
* @return string|null
*/
public function get_first_name_phonetic() : ?string {
return $this->firstnamephonetic;
}
/**
* Return the last name phonetic.
*
* @return string|null
*/
public function get_last_name_phonetic() : ?string {
return $this->lastnamephonetic;
}
/**
* Return the alternate name.
*
* @return string|null
*/
public function get_alternate_name() : ?string {
return $this->alternatename;
}
/**
* Return the image alt.
*
* @return string|null
*/
public function get_image_alt() : ?string {
return $this->imagealt;
}
}
@@ -0,0 +1,290 @@
<?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/>.
/**
* Discussion class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
/**
* Discussion class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion {
/** @var int $id ID */
private $id;
/** @var int $courseid Course id */
private $courseid;
/** @var int $forumid Forum id */
private $forumid;
/** @var string $name Discussion name */
private $name;
/** @var int $firstpostid Id of the first post in the discussion */
private $firstpostid;
/** @var int $userid Id of the user that created the discussion */
private $userid;
/** @var int $groupid Group id if it's a group dicussion */
private $groupid;
/** @var bool $assessed Is the discussion assessed? */
private $assessed;
/** @var int $timemodified Timestamp for last modification to the discussion */
private $timemodified;
/** @var int $usermodified Id of user that last modified the discussion */
private $usermodified;
/** @var int $timestart Start time for the discussion */
private $timestart;
/** @var int $timeend End time for the discussion */
private $timeend;
/** @var bool $pinned Is the discussion pinned? */
private $pinned;
/**
* Constructor.
*
* @param int $id ID
* @param int $courseid Course id
* @param int $forumid Forum id
* @param string $name Discussion name
* @param int $firstpostid Id of the first post in the discussion
* @param int $userid Id of the user that created the discussion
* @param int $groupid Group id if it's a group dicussion
* @param bool $assessed Is the discussion assessed?
* @param int $timemodified Timestamp for last modification to the discussion
* @param int $usermodified Id of user that last modified the discussion
* @param int $timestart Start time for the discussion
* @param int $timeend End time for the discussion
* @param bool $pinned Is the discussion pinned?
*/
public function __construct(
int $id,
int $courseid,
int $forumid,
string $name,
int $firstpostid,
int $userid,
int $groupid,
bool $assessed,
int $timemodified,
int $usermodified,
int $timestart,
int $timeend,
bool $pinned
) {
$this->id = $id;
$this->courseid = $courseid;
$this->forumid = $forumid;
$this->name = $name;
$this->firstpostid = $firstpostid;
$this->userid = $userid;
$this->groupid = $groupid;
$this->assessed = $assessed;
$this->timemodified = $timemodified;
$this->usermodified = $usermodified;
$this->timestart = $timestart;
$this->timeend = $timeend;
$this->pinned = $pinned;
}
/**
* Get the discussion id.
*
* @return int
*/
public function get_id() : int {
return $this->id;
}
/**
* Get the course id.
*
* @return int
*/
public function get_course_id() : int {
return $this->courseid;
}
/**
* Get the forum id.
*
* @return int
*/
public function get_forum_id() : int {
return $this->forumid;
}
/**
* Get the name of the discussion.
*
* @return string
*/
public function get_name() : string {
return $this->name;
}
/**
* Get the id of the fist post in the discussion.
*
* @return int
*/
public function get_first_post_id() : int {
return $this->firstpostid;
}
/**
* Get the id of the user that created the discussion.
*
* @return int
*/
public function get_user_id() : int {
return $this->userid;
}
/**
* Get the id of the group that this discussion belongs to.
*
* @return int
*/
public function get_group_id() : int {
return $this->groupid;
}
/**
* Check if this discussion is assessed.
*
* @return bool
*/
public function is_assessed() : bool {
return $this->assessed;
}
/**
* Get the timestamp for when this discussion was last modified.
*
* @return int
*/
public function get_time_modified() : int {
return $this->timemodified;
}
/**
* Get the id of the user that last modified this discussion.
*
* @return int
*/
public function get_user_modified() : int {
return $this->usermodified;
}
/**
* Get the start time of this discussion. Returns zero if the discussion
* has no designated start time.
*
* @return int
*/
public function get_time_start() : int {
return $this->timestart;
}
/**
* Get the end time of this discussion. Returns zero if the discussion
* has no designated end time.
*
* @return int
*/
public function get_time_end() : int {
return $this->timeend;
}
/**
* Check if this discussion is pinned.
*
* @return bool
*/
public function is_pinned() : bool {
return $this->pinned;
}
/**
* Check if the given post is the first post in this discussion.
*
* @param post_entity $post The post to check
* @return bool
*/
public function is_first_post(post_entity $post) : bool {
return $this->get_first_post_id() === $post->get_id();
}
/**
* Check if the discussion has started yet.
*
* @return bool
*/
public function has_started() : bool {
$startime = $this->get_time_start();
return empty($startime) || $startime < time();
}
/**
* Check if the discussion has ended.
*
* @return bool
*/
public function has_ended() : bool {
$endtime = $this->get_time_end();
return !empty($endtime) && $endtime >= time();
}
/**
* Check if the discussion belongs to a group.
*
* @return bool
*/
public function has_group() : bool {
return $this->get_group_id() > 0;
}
/**
* Check if the discussion is timed.
*
* @return bool
*/
public function is_timed_discussion() : bool {
global $CFG;
return !empty($CFG->forum_enabletimedposts) &&
($this->get_time_start() || $this->get_time_end());
}
/**
* Check if the timed discussion is visible.
*
* @return bool
*/
public function is_timed_discussion_visible() : bool {
return !$this->is_timed_discussion() || ($this->has_started() && !$this->has_ended());
}
}
@@ -0,0 +1,104 @@
<?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/>.
/**
* Discussion summary class.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\author as author_entity;
/**
* Discussion summary class.
*
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_summary {
/** @var discussion_entity $discussion The discussion being summarised */
private $discussion;
/** @var author_entity $firstpostauthor Author of the first post in the discussion */
private $firstpostauthor;
/** @var post_entity $firstpost First post in the discussion */
private $firstpost;
/** @var author_entity $latestpostauthor Author of the last post in the discussion */
private $latestpostauthor;
/**
* Constructor.
*
* @param discussion_entity $discussion The discussion being summarised
* @param post_entity $firstpost First post in the discussion
* @param author_entity $firstpostauthor Author of the first post in the discussion
* @param author_entity $latestpostauthor Author of the last post in the discussion
*/
public function __construct(
discussion_entity $discussion,
post_entity $firstpost,
author_entity $firstpostauthor,
author_entity $latestpostauthor
) {
$this->discussion = $discussion;
$this->firstpostauthor = $firstpostauthor;
$this->firstpost = $firstpost;
$this->latestpostauthor = $latestpostauthor;
}
/**
* Get the discussion entity.
*
* @return discussion_entity
*/
public function get_discussion() : discussion_entity {
return $this->discussion;
}
/**
* Get the author entity for the first post.
*
* @return author_entity
*/
public function get_first_post_author() : author_entity {
return $this->firstpostauthor;
}
/**
* Get the author entity for the last post.
*
* @return author_entity
*/
public function get_latest_post_author() : author_entity {
return $this->latestpostauthor;
}
/**
* Get the post entity for the first post.
*
* @return post_entity
*/
public function get_first_post() : post_entity {
return $this->firstpost;
}
}
+549
View File
@@ -0,0 +1,549 @@
<?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/>.
/**
* Forum class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once($CFG->dirroot . '/rating/lib.php');
use mod_forum\local\entities\discussion as discussion_entity;
use context;
use stdClass;
/**
* Forum class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum {
/** @var context $context The forum module context */
private $context;
/** @var stdClass $coursemodule The forum course module record */
private $coursemodule;
/** @var stdClass $course The forum course record */
private $course;
/** @var int $effectivegroupmode The effective group mode */
private $effectivegroupmode;
/** @var int $id ID */
private $id;
/** @var int $courseid Id of the course this forum is in */
private $courseid;
/** @var string $type The forum type, e.g. single, qanda, etc */
private $type;
/** @var string $name Name of the forum */
private $name;
/** @var string $intro Intro text */
private $intro;
/** @var int $introformat Format of the intro text */
private $introformat;
/** @var int $assessed The forum rating aggregate */
private $assessed;
/** @var int $assesstimestart Timestamp to begin assessment */
private $assesstimestart;
/** @var int $assesstimefinish Timestamp to end assessment */
private $assesstimefinish;
/** @var int $scale The rating scale */
private $scale;
/** @var int $maxbytes Maximum attachment size */
private $maxbytes;
/** @var int $maxattachments Maximum number of attachments */
private $maxattachments;
/** @var int $forcesubscribe Does the forum force users to subscribe? */
private $forcesubscribe;
/** @var int $trackingtype Tracking type */
private $trackingtype;
/** @var int $rsstype RSS type */
private $rsstype;
/** @var int $rssarticles RSS articles */
private $rssarticles;
/** @var int $timemodified Timestamp when the forum was last modified */
private $timemodified;
/** @var int $warnafter Warn after */
private $warnafter;
/** @var int $blockafter Block after */
private $blockafter;
/** @var int $blockperiod Block period */
private $blockperiod;
/** @var int $completiondiscussions Completion discussions */
private $completiondiscussions;
/** @var int $completionreplies Completion replies */
private $completionreplies;
/** @var int $completionposts Completion posts */
private $completionposts;
/** @var bool $displaywordcounts Should display word counts in posts */
private $displaywordcounts;
/** @var bool $lockdiscussionafter Timestamp after which discussions should be locked */
private $lockdiscussionafter;
/**
* Constructor
*
* @param context $context The forum module context
* @param stdClass $coursemodule The forum course module record
* @param stdClass $course The forum course record
* @param int $effectivegroupmode The effective group mode
* @param int $id ID
* @param int $courseid Id of the course this forum is in
* @param string $type The forum type, e.g. single, qanda, etc
* @param string $name Name of the forum
* @param string $intro Intro text
* @param int $introformat Format of the intro text
* @param int $assessed The forum rating aggregate
* @param int $assesstimestart Timestamp to begin assessment
* @param int $assesstimefinish Timestamp to end assessment
* @param int $scale The rating scale
* @param int $maxbytes Maximum attachment size
* @param int $maxattachments Maximum number of attachments
* @param int $forcesubscribe Does the forum force users to subscribe?
* @param int $trackingtype Tracking type
* @param int $rsstype RSS type
* @param int $rssarticles RSS articles
* @param int $timemodified Timestamp when the forum was last modified
* @param int $warnafter Warn after
* @param int $blockafter Block after
* @param int $blockperiod Block period
* @param int $completiondiscussions Completion discussions
* @param int $completionreplies Completion replies
* @param int $completionposts Completion posts
* @param bool $displaywordcount Should display word counts in posts
* @param int $lockdiscussionafter Timestamp after which discussions should be locked
*/
public function __construct(
context $context,
stdClass $coursemodule,
stdClass $course,
int $effectivegroupmode,
int $id,
int $courseid,
string $type,
string $name,
string $intro,
int $introformat,
int $assessed,
int $assesstimestart,
int $assesstimefinish,
int $scale,
int $maxbytes,
int $maxattachments,
int $forcesubscribe,
int $trackingtype,
int $rsstype,
int $rssarticles,
int $timemodified,
int $warnafter,
int $blockafter,
int $blockperiod,
int $completiondiscussions,
int $completionreplies,
int $completionposts,
bool $displaywordcount,
int $lockdiscussionafter
) {
$this->context = $context;
$this->coursemodule = $coursemodule;
$this->course = $course;
$this->effectivegroupmode = $effectivegroupmode;
$this->id = $id;
$this->courseid = $courseid;
$this->type = $type;
$this->name = $name;
$this->intro = $intro;
$this->introformat = $introformat;
$this->assessed = $assessed;
$this->assesstimestart = $assesstimestart;
$this->assesstimefinish = $assesstimefinish;
$this->scale = $scale;
$this->maxbytes = $maxbytes;
$this->maxattachments = $maxattachments;
$this->forcesubscribe = $forcesubscribe;
$this->trackingtype = $trackingtype;
$this->rsstype = $rsstype;
$this->rssarticles = $rssarticles;
$this->timemodified = $timemodified;
$this->warnafter = $warnafter;
$this->blockafter = $blockafter;
$this->blockperiod = $blockperiod;
$this->completiondiscussions = $completiondiscussions;
$this->completionreplies = $completionreplies;
$this->completionposts = $completionposts;
$this->displaywordcount = $displaywordcount;
$this->lockdiscussionafter = $lockdiscussionafter;
}
/**
* Get the forum module context.
*
* @return context
*/
public function get_context() : context {
return $this->context;
}
/**
* Get the forum course module record
*
* @return stdClass
*/
public function get_course_module_record() : stdClass {
return $this->coursemodule;
}
/**
* Get the effective group mode.
*
* @return int
*/
public function get_effective_group_mode() : int {
return $this->effectivegroupmode;
}
/**
* Check if the forum is set to group mode.
*
* @return bool
*/
public function is_in_group_mode() : bool {
return $this->get_effective_group_mode() !== NOGROUPS;
}
/**
* Get the course record.
*
* @return stdClass
*/
public function get_course_record() : stdClass {
return $this->course;
}
/**
* Get the forum id.
*
* @return int
*/
public function get_id() : int {
return $this->id;
}
/**
* Get the id of the course that the forum belongs to.
*
* @return int
*/
public function get_course_id() : int {
return $this->courseid;
}
/**
* Get the forum type.
*
* @return string
*/
public function get_type() : string {
return $this->type;
}
/**
* Get the forum name.
*
* @return string
*/
public function get_name() : string {
return $this->name;
}
/**
* Get the forum intro text.
*
* @return string
*/
public function get_intro() : string {
return $this->intro;
}
/**
* Get the forum intro text format.
*
* @return int
*/
public function get_intro_format() : int {
return $this->introformat;
}
/**
* Get the rating aggregate.
*
* @return int
*/
public function get_rating_aggregate() : int {
return $this->assessed;
}
/**
* Does the forum have a rating aggregate?
*
* @return bool
*/
public function has_rating_aggregate() : bool {
return $this->get_rating_aggregate() != RATING_AGGREGATE_NONE;
}
/**
* Get the timestamp for when the assessment period begins.
*
* @return int
*/
public function get_assess_time_start() : int {
return $this->assesstimestart;
}
/**
* Get the timestamp for when the assessment period ends.
*
* @return int
*/
public function get_assess_time_finish() : int {
return $this->assesstimefinish;
}
/**
* Get the rating scale.
*
* @return int
*/
public function get_scale() : int {
return $this->scale;
}
/**
* Get the maximum bytes.
*
* @return int
*/
public function get_max_bytes() : int {
return $this->maxbytes;
}
/**
* Get the maximum number of attachments.
*
* @return int
*/
public function get_max_attachments() : int {
return $this->maxattachments;
}
/**
* Get the subscription mode.
*
* @return int
*/
public function get_subscription_mode() : int {
return $this->forcesubscribe;
}
/**
* Is the subscription mode set to optional.
*
* @return bool
*/
public function is_subscription_optional() : bool {
return $this->get_subscription_mode() === FORUM_CHOOSESUBSCRIBE;
}
/**
* Is the subscription mode set to forced.
*
* @return bool
*/
public function is_subscription_forced() : bool {
return $this->get_subscription_mode() === FORUM_FORCESUBSCRIBE;
}
/**
* Is the subscription mode set to automatic.
*
* @return bool
*/
public function is_subscription_automatic() : bool {
return $this->get_subscription_mode() === FORUM_INITIALSUBSCRIBE;
}
/**
* Is the subscription mode set to disabled.
*
* @return bool
*/
public function is_subscription_disabled() : bool {
return $this->get_subscription_mode() === FORUM_DISALLOWSUBSCRIBE;
}
/**
* Get the tracking type.
*
* @return int
*/
public function get_tracking_type() : int {
return $this->trackingtype;
}
/**
* Get the RSS type.
*
* @return int
*/
public function get_rss_type() : int {
return $this->rsstype;
}
/**
* Get the RSS articles.
*
* @return int
*/
public function get_rss_articles() : int {
return $this->rssarticles;
}
/**
* Get the timestamp for when the forum was last modified.
*
* @return int
*/
public function get_time_modified() : int {
return $this->timemodified;
}
/**
* Get warn after.
*
* @return int
*/
public function get_warn_after() : int {
return $this->warnafter;
}
/**
* Get block after.
*
* @return int
*/
public function get_block_after() : int {
return $this->blockafter;
}
/**
* Get the block period.
*
* @return int
*/
public function get_block_period() : int {
return $this->blockperiod;
}
/**
* Does the forum have blocking enabled?
*
* @return bool
*/
public function has_blocking_enabled() : bool {
return !empty($this->get_block_after()) && !empty($this->get_block_period());
}
/**
* Get the completion discussions.
*
* @return int
*/
public function get_completion_discussions() : int {
return $this->completiondiscussions;
}
/**
* Get the completion replies.
*
* @return int
*/
public function get_completion_replies() : int {
return $this->completionreplies;
}
/**
* Get the completion posts.
*
* @return int
*/
public function get_completion_posts() : int {
return $this->completionposts;
}
/**
* Should the word counts be shown in the posts?
*
* @return bool
*/
public function should_display_word_count() : bool {
return $this->displaywordcount;
}
/**
* Get the timestamp after which the discussion should be locked.
*
* @return int
*/
public function get_lock_discussions_after() : int {
return $this->lockdiscussionafter;
}
/**
* Does the forum have a discussion locking timestamp?
*
* @return bool
*/
public function has_lock_discussions_after() : bool {
return !empty($this->get_lock_discussions_after());
}
/**
* Is the discussion locked?
*
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function is_discussion_locked(discussion_entity $discussion) : bool {
if (!$this->has_lock_discussions_after()) {
return false;
}
if ($this->get_type() === 'single') {
// It does not make sense to lock a single discussion forum.
return false;
}
return (($discussion->get_time_modified() + $this->get_lock_discussions_after()) < time());
}
}
+284
View File
@@ -0,0 +1,284 @@
<?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/>.
/**
* Post class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use stdClass;
/**
* Post class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post {
/** @var int $id ID */
private $id;
/** @var int $discussionid The id of the discussion this post belongs to */
private $discussionid;
/** @var int $parentid The id of the post that this post is replying to. Zero if it isn't a reply. */
private $parentid;
/** @var int $authorid The id of user who authored the post */
private $authorid;
/** @var int $timecreated Timestamp for when the post was created */
private $timecreated;
/** @var int $timemodified Timestamp for when the post last modified */
private $timemodified;
/** @var bool $mailed If the post has been mailed */
private $mailed;
/** @var string $subject Post subject */
private $subject;
/** @var string $message Post message */
private $message;
/** @var int $messageformat Format of the post message */
private $messageformat;
/** @var bool $messagetrust Is this a trusted message, i.e. created by a trusted user. */
private $messagetrust;
/** @var bool $hasattachments Does the post have attachments */
private $hasattachments;
/** @var int $totalscore Total score */
private $totalscore;
/** @var bool $mailnow Should this post be mailed immediately */
private $mailnow;
/** @var bool $deleted Is the post deleted */
private $deleted;
/**
* Constructor.
*
* @param int $id ID
* @param int $discussionid The id of the discussion this post belongs to
* @param int $parentid The id of the post that this post is replying to. Zero if it isn't a reply.
* @param int $authorid The id of user who authored the post
* @param int $timecreated Timestamp for when the post was created
* @param int $timemodified Timestamp for when the post last modified
* @param bool $mailed If the post has been mailed
* @param string $subject Post subject
* @param string $message Post message
* @param int $messageformat Format of the post message
* @param bool $messagetrust Is this a trusted message, i.e. created by a trusted user.
* @param bool $hasattachments Does the post have attachments
* @param int $totalscore Total score
* @param bool $mailnow Should this post be mailed immediately
* @param bool $deleted Is the post deleted
*/
public function __construct(
int $id,
int $discussionid,
int $parentid,
int $authorid,
int $timecreated,
int $timemodified,
bool $mailed,
string $subject,
string $message,
int $messageformat,
bool $messagetrust,
bool $hasattachments,
int $totalscore,
bool $mailnow,
bool $deleted
) {
$this->id = $id;
$this->discussionid = $discussionid;
$this->parentid = $parentid;
$this->authorid = $authorid;
$this->timecreated = $timecreated;
$this->timemodified = $timemodified;
$this->mailed = $mailed;
$this->subject = $subject;
$this->message = $message;
$this->messageformat = $messageformat;
$this->messagetrust = $messagetrust;
$this->hasattachments = $hasattachments;
$this->totalscore = $totalscore;
$this->mailnow = $mailnow;
$this->deleted = $deleted;
}
/**
* Get the post id.
*
* @return int
*/
public function get_id() : int {
return $this->id;
}
/**
* Get the discussion id.
*
* @return int
*/
public function get_discussion_id() : int {
return $this->discussionid;
}
/**
* Get the id of the parent post. Returns zero if this post is not a reply.
*
* @return int
*/
public function get_parent_id() : int {
return $this->parentid;
}
/**
* Does this post have a parent? I.e. is it a reply?
*
* @return bool
*/
public function has_parent() : bool {
return $this->get_parent_id() > 0;
}
/**
* Get the id of the user that authored the post.
*
* @return int
*/
public function get_author_id() : int {
return $this->authorid;
}
/**
* Get the timestamp for when this post was created.
*
* @return int
*/
public function get_time_created() : int {
return $this->timecreated;
}
/**
* Get the timestamp for when this post was last modified.
*
* @return int
*/
public function get_time_modified() : int {
return $this->timemodified;
}
/**
* Has this post been mailed?
*
* @return bool
*/
public function has_been_mailed() : bool {
return $this->mailed;
}
/**
* Get the post subject.
*
* @return string
*/
public function get_subject() : string {
return $this->subject;
}
/**
* Get the post message.
*
* @return string
*/
public function get_message() : string {
return $this->message;
}
/**
* Get the post message format.
*
* @return int
*/
public function get_message_format() : int {
return $this->messageformat;
}
/**
* Is this a trusted message? I.e. was it authored by a trusted user?
*
* @return bool
*/
public function is_message_trusted() : bool {
return $this->messagetrust;
}
/**
* Does this post have attachments?
*
* @return bool
*/
public function has_attachments() : bool {
return $this->hasattachments;
}
/**
* Get the total score.
*
* @return int
*/
public function get_total_score() : int {
return $this->totalscore;
}
/**
* Should this post be mailed now?
*
* @return bool
*/
public function should_mail_now() : bool {
return $this->mailnow;
}
/**
* Is this post deleted?
*
* @return bool
*/
public function is_deleted() : bool {
return $this->deleted;
}
/**
* Get the post's age in seconds.
*
* @return int
*/
public function get_age() : int {
return time() - $this->get_time_created();
}
/**
* Check if the given user authored this post.
*
* @param stdClass $user The user to check.
* @return bool
*/
public function is_owned_by_user(stdClass $user) : bool {
return $this->get_author_id() == $user->id;
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Post read receipt collection class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use stdClass;
/**
* Post read receipt collection class.
*
* Contains the list of read receipts for posts.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_read_receipt_collection {
/** @var stdClass[] $receiptsbypostid Receipt records indexed by post id */
private $receiptsbypostid = [];
/**
* Constructor.
*
* @param array $records The list of post read receipt records.
*/
public function __construct(array $records) {
foreach ($records as $record) {
$postid = $record->postid;
if (isset($this->receiptsbypostid[$postid])) {
$this->receiptsbypostid[$postid][] = $record;
} else {
$this->receiptsbypostid[$postid] = [$record];
}
}
}
/**
* Check whether a user has read a post.
*
* @param stdClass $user The user to check
* @param post_entity $post The post to check
* @return bool
*/
public function has_user_read_post(stdClass $user, post_entity $post) : bool {
global $CFG;
$isoldpost = ($post->get_time_modified() < (time() - ($CFG->forum_oldpostdays * 24 * 3600)));
if ($isoldpost) {
return true;
}
$receipts = isset($this->receiptsbypostid[$post->get_id()]) ? $this->receiptsbypostid[$post->get_id()] : [];
foreach ($receipts as $receipt) {
if ($receipt->userid == $user->id) {
return true;
}
}
return false;
}
}
+152
View File
@@ -0,0 +1,152 @@
<?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/>.
/**
* Class to sort items.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
/**
* Class to sort lists of items.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sorter {
/** @var callable $getid Function used to get the id from an item */
private $getid;
/** @var callable $getparentid Function used to get the parent id from an item */
private $getparentid;
/**
* Constructor.
*
* Allows the calling code to provide 2 functions to get the id and parent id from
* the list of items it is intended to process.
*
* This allows this class to be composed in numerous different ways to support various
* types of items while keeping the underlying sorting algorithm consistent.
*
* @param callable $getid Function used to get the id from an item
* @param callable $getparentid Function used to get the parent id from an item
*/
public function __construct(callable $getid, callable $getparentid) {
$this->getid = $getid;
$this->getparentid = $getparentid;
}
/**
* Sort a list of items into a parent/child data structure. The resulting data structure
* is a recursive array of arrays where the first element is the parent and the second is
* an array of it's children.
*
* For example
* If we have an array of items A, B, C, and D where D is a child of C, B and C are children
* of A.
*
* This function would sort them into the following:
* [
* [
* A,
* [
* [
* B,
* []
* ],
* [
* C,
* [
* [
* D,
* []
* ]
* ]
* ]
* ]
* ]
* ]
*
* @param array $items The list of items to sort.
* @return array
*/
public function sort_into_children(array $items) : array {
$ids = array_reduce($items, function($carry, $item) {
$carry[($this->getid)($item)] = true;
return $carry;
}, []);
// Split out the items into "parents" and "replies" (children). These are unsorted
// at this point.
[$parents, $replies] = array_reduce($items, function($carry, $item) use ($ids) {
$parentid = ($this->getparentid)($item);
if (!empty($ids[$parentid])) {
// This is a child to another item in the list so add it to the children list.
$carry[1][] = $item;
} else {
// This isn't a child to anything in our list so it's a parent.
$carry[0][] = $item;
}
return $carry;
}, [[], []]);
if (empty($replies)) {
return array_map(function($parent) {
return [$parent, []];
}, $parents);
}
// Recurse to sort the replies into the correct nesting.
$sortedreplies = $this->sort_into_children($replies);
// Sort the parents and sorted replies into their matching pairs.
return array_map(function($parent) use ($sortedreplies) {
$parentid = ($this->getid)($parent);
return [
$parent,
array_values(array_filter($sortedreplies, function($replydata) use ($parentid) {
return ($this->getparentid)($replydata[0]) == $parentid;
}))
];
}, $parents);
}
/**
* Take the data structure returned from "sort_into_children" and flatten it back
* into an array. It does a depth first flatten which maintains the reply ordering.
*
* @param array $items Items in the data structure returned by "sort_into_children"
* @return array A flat array.
*/
public function flatten_children(array $items) : array {
$result = [];
foreach ($items as [$item, $children]) {
$result[] = $item;
$result = array_merge($result, $this->flatten_children($children));
}
return $result;
}
}
@@ -0,0 +1,174 @@
<?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/>.
/**
* Author exporter.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use core\external\exporter;
use renderer_base;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Author exporter.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author extends exporter {
/** @var author_entity $author Author entity */
private $author;
/** @var array $authorgroups List of groups that the author belongs to */
private $authorgroups;
/** @var bool $canview Should the author be anonymised? */
private $canview;
/**
* Constructor.
*
* @param author_entity $author The author entity to export
* @param stdClass[] $authorgroups The list of groups that the author belongs to
* @param bool $canview Can the requesting user view this author or should it be anonymised?
* @param array $related The related data for the export.
*/
public function __construct(author_entity $author, array $authorgroups = [], bool $canview = true, array $related = []) {
$this->author = $author;
$this->authorgroups = $authorgroups;
$this->canview = $canview;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'fullname' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'groups' => [
'multiple' => true,
'optional' => true,
'type' => [
'id' => ['type' => PARAM_INT],
'urls' => [
'type' => [
'image' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
]
]
],
'urls' => [
'type' => [
'profile' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'profileimage' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
]
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$author = $this->author;
$urlfactory = $this->related['urlfactory'];
if ($this->canview) {
$groups = array_map(function($group) {
$imageurl = get_group_picture_url($group, $group->courseid);
return [
'id' => $group->id,
'urls' => [
'image' => $imageurl ? $imageurl->out(false) : null
]
];
}, $this->authorgroups);
return [
'id' => $author->get_id(),
'fullname' => $author->get_full_name(),
'groups' => $groups,
'urls' => [
'profile' => ($urlfactory->get_author_profile_url($author))->out(false),
'profileimage' => ($urlfactory->get_author_profile_image_url($author))->out(false)
]
];
} else {
// The author should be anonymised.
return [
'id' => null,
'fullname' => get_string('forumauthorhidden', 'mod_forum'),
'groups' => [],
'urls' => [
'profile' => null,
'profileimage' => null
]
];
}
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'urlfactory' => 'mod_forum\local\factories\url',
'context' => 'context'
];
}
}
@@ -0,0 +1,253 @@
<?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/>.
/**
* Discussion exporter class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\exporters\post as post_exporter;
use mod_forum\local\factories\exporter as exporter_factory;
use core\external\exporter;
use renderer_base;
/**
* Discussion exporter class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion extends exporter {
/** @var discussion_entity $discussion Discussion to export */
private $discussion;
/**
* Constructor.
*
* @param discussion_entity $discussion Discussion to export
* @param array $related The related export data
*/
public function __construct(discussion_entity $discussion, array $related = []) {
$this->discussion = $discussion;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => ['type' => PARAM_INT],
'forumid' => ['type' => PARAM_INT],
'pinned' => ['type' => PARAM_BOOL],
'name' => ['type' => PARAM_TEXT],
'group' => [
'optional' => true,
'type' => [
'name' => ['type' => PARAM_TEXT],
'urls' => [
'type' => [
'picture' => [
'optional' => true,
'type' => PARAM_URL,
],
'userlist' => [
'optional' => true,
'type' => PARAM_URL,
],
],
],
],
],
'times' => [
'type' => [
'modified' => ['type' => PARAM_INT],
'start' => ['type' => PARAM_INT],
'end' => ['type' => PARAM_INT],
],
],
'userstate' => [
'type' => [
'subscribed' => ['type' => PARAM_BOOL],
],
],
'capabilities' => [
'type' => [
'subscribe' => ['type' => PARAM_BOOL],
'move' => ['type' => PARAM_BOOL],
'pin' => ['type' => PARAM_BOOL],
'post' => ['type' => PARAM_BOOL]
]
],
'urls' => [
'type' => [
'view' => ['type' => PARAM_URL],
'viewlatest' => [
'optional' => true,
'type' => PARAM_URL
],
'viewfirstunread' => [
'optional' => true,
'type' => PARAM_URL,
],
'markasread' => ['type' => PARAM_URL],
'subscribe' => ['type' => PARAM_URL]
],
],
'timed' => [
'type' => [
'istimed' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'visible' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$capabilitymanager = $this->related['capabilitymanager'];
$urlfactory = $this->related['urlfactory'];
$forum = $this->related['forum'];
$forumrecord = $this->get_forum_record();
$user = $this->related['user'];
$discussion = $this->discussion;
$groupdata = null;
if ($discussion->has_group() && $group = $this->related['groupsbyid'][$discussion->get_group_id()]) {
$groupdata = [
'name' => $group->name,
'urls' => [],
];
$canviewparticipants = $capabilitymanager->can_view_participants($user, $discussion);
if (!$group->hidepicture) {
$url = get_group_picture_url($group, $forum->get_course_id());
if (!empty($url)) {
$groupdata['urls']['picture'] = $url;
}
}
if ($canviewparticipants) {
$groupdata['urls']['userlist'] = (new \moodle_url('/user/index.php', [
'id' => $forum->get_course_id(),
'group' => $group->id,
]));
}
}
$viewfirstunreadurl = $urlfactory->get_discussion_view_first_unread_post_url_from_discussion($discussion);
$data = [
'id' => $discussion->get_id(),
'forumid' => $forum->get_id(),
'pinned' => $discussion->is_pinned(),
'name' => format_string($discussion->get_name(), true, [
'context' => $this->related['context']
]),
'times' => [
'modified' => $discussion->get_time_modified(),
'start' => $discussion->get_time_start(),
'end' => $discussion->get_time_end(),
],
'userstate' => [
'subscribed' => \mod_forum\subscriptions::is_subscribed($user->id, $forumrecord, $discussion->get_id()),
],
'capabilities' => [
'subscribe' => $capabilitymanager->can_subscribe_to_discussion($user, $discussion),
'move' => $capabilitymanager->can_move_discussion($user, $discussion),
'pin' => $capabilitymanager->can_pin_discussion($user, $discussion),
'post' => $capabilitymanager->can_post_in_discussion($user, $discussion)
],
'urls' => [
'view' => $urlfactory->get_discussion_view_url_from_discussion($discussion)->out(false),
'viewfirstunread' => $viewfirstunreadurl->out(false),
'markasread' => $urlfactory->get_mark_discussion_as_read_url_from_discussion($forum, $discussion)->out(false),
'subscribe' => $urlfactory->get_discussion_subscribe_url($discussion)->out(false)
]
];
if (!empty($this->related['latestpostid'])) {
$data['urls']['viewlatest'] = $urlfactory->get_discussion_view_latest_post_url_from_discussion(
$discussion,
$this->related['latestpostid']
)->out(false);
}
if ($groupdata) {
$data['group'] = $groupdata;
}
$canviewhiddentimedposts = $capabilitymanager->can_view_hidden_posts($user);
$canalwaysseetimedpost = $user->id == $discussion->get_user_id() || $canviewhiddentimedposts;
$data['timed']['istimed'] = $canalwaysseetimedpost ? $discussion->is_timed_discussion() : null;
$data['timed']['visible'] = $canalwaysseetimedpost ? $discussion->is_timed_discussion_visible() : null;
return $data;
}
/**
* Get the legacy forum record from the forum entity.
*
* @return stdClass
*/
private function get_forum_record() {
$forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper();
return $forumdbdatamapper->to_legacy_object($this->related['forum']);
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
'groupsbyid' => 'stdClass[]',
'latestpostid' => 'int?'
];
}
}
@@ -0,0 +1,157 @@
<?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/>.
/**
* Discussion summaries exporter.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\exporters\post as post_exporter;
use core\external\exporter;
use renderer_base;
/**
* Discussion summaries exporter.
*
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_summaries extends exporter {
/** @var discussion_summary_entity[] The list of discussion summaries to export */
private $discussions;
/** @var stdClass[] The group information for each author */
private $groupsbyid;
/** @var stdClass[] The group information for each author */
private $groupsbyauthorid;
/** @var int[] Discussion reply counts indexed by dicussion id */
private $discussionreplycount;
/** @var int[] Discussion unread counts indexed by dicussion id */
private $discussionunreadcount;
/** @var array The latest post in each discussion */
private $latestpostids;
/**
* Constructor.
*
* @param discussion_summary_entity[] $discussion The list of discussion summaries to export
* @param stdClass[] $groupsbyid The group information for each author
* @param stdClass[] $groupsbyauthorid The group information for each author
* @param int[] $discussionreplycount Discussion reply counts indexed by dicussion id
* @param int[] $discussionunreadcount Discussion unread counts indexed by dicussion id
* @param int[] $latestpostids List of latest post ids indexed by discussion id
* @param array $related The related
*/
public function __construct(
array $discussions,
array $groupsbyid,
array $groupsbyauthorid,
array $discussionreplycount,
array $discussionunreadcount,
array $latestpostids,
array $related = []
) {
$this->discussions = $discussions;
$this->groupsbyid = $groupsbyid;
$this->groupsbyauthorid = $groupsbyauthorid;
$this->discussionreplycount = $discussionreplycount;
$this->discussionunreadcount = $discussionunreadcount;
$this->latestpostids = $latestpostids;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'summaries' => [
'type' => discussion_summary::read_properties_definition(),
'multiple' => true
],
'state' => [
'type' => [
'hasdiscussions' => ['type' => PARAM_BOOL],
],
],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$exporteddiscussions = [];
$related = $this->related;
foreach ($this->discussions as $discussion) {
$discussionid = $discussion->get_discussion()->get_id();
$replycount = isset($this->discussionreplycount[$discussionid]) ? $this->discussionreplycount[$discussionid] : 0;
$unreadcount = isset($this->discussionunreadcount[$discussionid]) ? $this->discussionunreadcount[$discussionid] : 0;
$latestpostid = isset($this->latestpostids[$discussionid]) ? $this->latestpostids[$discussionid] : 0;
$exporter = new discussion_summary(
$discussion,
$this->groupsbyid,
$this->groupsbyauthorid,
$replycount,
$unreadcount,
$latestpostid,
$related
);
$exporteddiscussions[] = $exporter->export($output);
}
return [
'summaries' => $exporteddiscussions,
'state' => [
'hasdiscussions' => !empty($exporteddiscussions),
],
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
];
}
}
@@ -0,0 +1,180 @@
<?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/>.
/**
* Discussion summary exporter class.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion_summary as discussion_summary_entity;
use core\external\exporter;
use renderer_base;
/**
* Discussion summary exporter class.
*
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_summary extends exporter {
/** @var discussion_summary_entity The discussion summary information */
private $summary;
/** @var stdClass[] The group information for each author */
private $groupsbyid;
/** @var stdClass[] The group information for each author */
private $groupsbyauthorid;
/** @var int The number of replies to the discussion */
private $replycount;
/** @var int number of unread posts if the user is tracking these */
private $unreadcount;
/** @var int The latest post id in the discussion */
private $latestpostid;
/**
* Constructor.
*
* @param discussion_summary_entity $summary The discussion summary information
* @param stdClass[] $groupsbyid The group information for each author
* @param stdClass[] $groupsbyauthorid The group information for each author
* @param int $replycount The number of replies to the discussion
* @param int $unreadcount number of unread posts if the user is tracking these
* @param int $latestpostid The latest post id in the discussion
* @param array $related The related objects
*/
public function __construct(
discussion_summary_entity $summary,
array $groupsbyid,
array $groupsbyauthorid,
int $replycount,
int $unreadcount,
int $latestpostid,
array $related = []
) {
$this->summary = $summary;
$this->groupsbyid = $groupsbyid;
$this->groupsbyauthorid = $groupsbyauthorid;
$this->replycount = $replycount;
$this->unreadcount = $unreadcount;
$this->latestpostid = $latestpostid;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => ['type' => PARAM_INT],
'discussion' => [
'type' => discussion::read_properties_definition(),
],
'replies' => ['type' => PARAM_INT],
'unread' => ['type' => PARAM_INT],
'firstpostauthor' => [
'type' => author::read_properties_definition(),
],
'latestpostauthor' => [
'type' => author::read_properties_definition(),
],
'latestpostid' => ['type' => PARAM_INT],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$capabilitymanager = $this->related['capabilitymanager'];
$forum = $this->related['forum'];
$user = $this->related['user'];
$discussion = $this->summary->get_discussion();
$related = (array) (object) $this->related;
$related['latestpostid'] = $this->latestpostid;
$related['groupsbyid'] = $this->groupsbyid;
$discussionexporter = new discussion($discussion, $related);
$related = [
'urlfactory' => $this->related['urlfactory'],
'context' => $this->related['forum']->get_context(),
];
$firstpostauthor = new author(
$this->summary->get_first_post_author(),
$this->groupsbyauthorid[$this->summary->get_first_post_author()->get_id()],
$capabilitymanager->can_view_post(
$user,
$discussion,
$this->summary->get_first_post()
),
$related
);
$latestpostauthor = new author(
$this->summary->get_latest_post_author(),
[],
$capabilitymanager->can_view_post(
$user,
$discussion,
$this->summary->get_first_post()
),
$related
);
return [
'id' => $discussion->get_id(),
'discussion' => $discussionexporter->export($output),
'replies' => $this->replycount,
'unread' => $this->unreadcount,
'firstpostauthor' => $firstpostauthor->export($output),
'latestpostauthor' => $latestpostauthor->export($output),
'latestpostid' => $this->latestpostid,
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
];
}
}
+146
View File
@@ -0,0 +1,146 @@
<?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/>.
/**
* Forum Exporter.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\exporters\post as post_exporter;
use core\external\exporter;
use renderer_base;
use stdClass;
/**
* Forum class.
*
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum extends exporter {
/** @var forum_entity The entity relating to the forum being displayed */
private $forum;
/**
* Constructor for the forum exporter.
*
* @param forum_entity $forum The forum being displayed
* @param array $related The related objects
*/
public function __construct(forum_entity $forum, $related = []) {
$this->forum = $forum;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => ['type' => PARAM_INT],
'state' => [
'type' => [
'groupmode' => ['type' => PARAM_INT],
],
],
'userstate' => [
'type' => [
'tracked' => ['type' => PARAM_INT],
],
],
'capabilities' => [
'type' => [
'viewdiscussions' => ['type' => PARAM_BOOL],
'create' => ['type' => PARAM_BOOL],
'subscribe' => ['type' => PARAM_BOOL],
]
],
'urls' => [
'type' => [
'create' => ['type' => PARAM_URL],
'markasread' => ['type' => PARAM_URL],
],
],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$capabilitymanager = $this->related['capabilitymanager'];
$urlfactory = $this->related['urlfactory'];
$user = $this->related['user'];
$currentgroup = $this->related['currentgroup'];
return [
'id' => $this->forum->get_id(),
'state' => [
'groupmode' => $this->forum->get_effective_group_mode(),
],
'userstate' => [
'tracked' => forum_tp_is_tracked($this->get_forum_record(), $this->related['user']),
],
'capabilities' => [
'viewdiscussions' => $capabilitymanager->can_view_discussions($user),
'create' => $capabilitymanager->can_create_discussions($user, $currentgroup),
'subscribe' => $capabilitymanager->can_subscribe_to_forum($user),
],
'urls' => [
'create' => $urlfactory->get_discussion_create_url($this->forum)->out(false),
'markasread' => $urlfactory->get_mark_all_discussions_as_read_url($this->forum)->out(false),
],
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
'currentgroup' => 'int?',
];
}
/**
* Get the legacy forum record for this forum.
*
* @return stdClass
*/
private function get_forum_record() : stdClass {
$forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper();
return $forumdbdatamapper->to_legacy_object($this->forum);
}
}
+531
View File
@@ -0,0 +1,531 @@
<?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/>.
/**
* Post exporter class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\exporters\author as author_exporter;
use mod_forum\local\factories\exporter as exporter_factory;
use core\external\exporter;
use core_files\external\stored_file_exporter;
use context;
use core_tag_tag;
use renderer_base;
use stdClass;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Post exporter class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post extends exporter {
/** @var post_entity $post The post to export */
private $post;
/**
* Constructor.
*
* @param post_entity $post The post to export
* @param array $related List of related data
*/
public function __construct(post_entity $post, array $related = []) {
$this->post = $post;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
$attachmentdefinition = stored_file_exporter::read_properties_definition();
$attachmentdefinition['urls'] = [
'type' => [
'export' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
];
$attachmentdefinition['html'] = [
'type' => [
'plagiarism' => [
'type' => PARAM_RAW,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
]
];
return [
'id' => ['type' => PARAM_INT],
'subject' => ['type' => PARAM_TEXT],
'message' => ['type' => PARAM_RAW],
'messageformat' => ['type' => PARAM_INT],
'author' => ['type' => author_exporter::read_properties_definition()],
'discussionid' => ['type' => PARAM_INT],
'hasparent' => ['type' => PARAM_BOOL],
'parentid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'timecreated' => ['type' => PARAM_INT],
'unread' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'isdeleted' => ['type' => PARAM_BOOL],
'haswordcount' => ['type' => PARAM_BOOL],
'wordcount' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'capabilities' => [
'type' => [
'view' => ['type' => PARAM_BOOL],
'edit' => ['type' => PARAM_BOOL],
'delete' => ['type' => PARAM_BOOL],
'split' => ['type' => PARAM_BOOL],
'reply' => ['type' => PARAM_BOOL],
'export' => ['type' => PARAM_BOOL],
'controlreadstatus' => ['type' => PARAM_BOOL]
]
],
'urls' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => [
'view' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'viewisolated' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'viewparent' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'edit' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'delete' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'split' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'reply' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'export' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'markasread' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'markasunread' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'discuss' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
],
'attachments' => [
'multiple' => true,
'type' => $attachmentdefinition
],
'tags' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'multiple' => true,
'type' => [
'id' => ['type' => PARAM_INT],
'tagid' => ['type' => PARAM_INT],
'isstandard' => ['type' => PARAM_BOOL],
'displayname' => ['type' => PARAM_TEXT],
'flag' => ['type' => PARAM_BOOL],
'urls' => [
'type' => [
'view' => ['type' => PARAM_URL]
]
]
]
],
'html' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => [
'rating' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW
],
'taglist' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW
],
'authorsubheading' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW
],
]
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$post = $this->post;
$authorgroups = $this->related['authorgroups'];
$forum = $this->related['forum'];
$discussion = $this->related['discussion'];
$author = $this->related['author'];
$user = $this->related['user'];
$readreceiptcollection = $this->related['readreceiptcollection'];
$rating = $this->related['rating'];
$tags = $this->related['tags'];
$attachments = $this->related['attachments'];
$includehtml = $this->related['includehtml'];
$isdeleted = $post->is_deleted();
$hasrating = $rating != null;
$hastags = !empty($tags);
$discussionid = $post->get_discussion_id();
$parentid = $post->get_parent_id();
$capabilitymanager = $this->related['capabilitymanager'];
$canview = $capabilitymanager->can_view_post($user, $discussion, $post);
$canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
$candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
$cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
$canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
$canexport = $capabilitymanager->can_export_post($user, $post);
$cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
$urlfactory = $this->related['urlfactory'];
$viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
$viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
$viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
$editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
$deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
$spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
$replyurl = $canreply ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
$exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
$markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
$markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
$discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
$authorexporter = new author_exporter($author, $authorgroups, ($canview && !$isdeleted), $this->related);
$exportedauthor = $authorexporter->export($output);
// Only bother loading the content if the user can see it.
$loadcontent = $canview && !$isdeleted;
$exportattachments = $loadcontent && !empty($attachments);
if ($loadcontent) {
$subject = $post->get_subject();
$timecreated = $post->get_time_created();
$message = $this->get_message($post);
} else {
$subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
$message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
$timecreated = null;
if ($isdeleted) {
$exportedauthor->fullname = null;
}
}
return [
'id' => $post->get_id(),
'subject' => $subject,
'message' => $message,
'messageformat' => $post->get_message_format(),
'author' => $exportedauthor,
'discussionid' => $post->get_discussion_id(),
'hasparent' => $post->has_parent(),
'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
'timecreated' => $timecreated,
'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
'isdeleted' => $isdeleted,
'haswordcount' => $forum->should_display_word_count(),
'wordcount' => $forum->should_display_word_count() ? count_words($message) : null,
'capabilities' => [
'view' => $canview,
'edit' => $canedit,
'delete' => $candelete,
'split' => $cansplit,
'reply' => $canreply,
'export' => $canexport,
'controlreadstatus' => $cancontrolreadstatus
],
'urls' => [
'view' => $viewurl ? $viewurl->out(false) : null,
'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
'edit' => $editurl ? $editurl->out(false) : null,
'delete' => $deleteurl ? $deleteurl->out(false) : null,
'split' => $spliturl ? $spliturl->out(false) : null,
'reply' => $replyurl ? $replyurl->out(false) : null,
'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
'discuss' => $discussurl ? $discussurl->out(false) : null,
],
'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
'html' => $includehtml ? [
'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
] : null
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'capabilitymanager' => 'mod_forum\local\managers\capability',
'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
'urlfactory' => 'mod_forum\local\factories\url',
'forum' => 'mod_forum\local\entities\forum',
'discussion' => 'mod_forum\local\entities\discussion',
'author' => 'mod_forum\local\entities\author',
'user' => 'stdClass',
'context' => 'context',
'authorgroups' => 'stdClass[]',
'attachments' => '\stored_file[]?',
'tags' => '\core_tag_tag[]?',
'rating' => 'rating?',
'includehtml' => 'bool'
];
}
/**
* Get the message text from a post.
*
* @param post_entity $post The post
* @return string
*/
private function get_message(post_entity $post) : string {
global $CFG;
$context = $this->related['context'];
$message = file_rewrite_pluginfile_urls(
$post->get_message(),
'pluginfile.php',
$context->id,
'mod_forum',
'post',
$post->get_id()
);
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
$forum = $this->related['forum'];
$message .= plagiarism_get_links([
'userid' => $post->get_author_id(),
'content' => $message,
'cmid' => $forum->get_course_module_record()->id,
'course' => $forum->get_course_id(),
'forum' => $forum->get_id()
]);
}
$message = format_text(
$message,
$post->get_message_format(),
(object) [
'para' => false,
'trusted' => $post->is_message_trusted(),
'context' => $context
]
);
return $message;
}
/**
* Get the exported attachments for a post.
*
* @param stored_file[] $attachments The list of attachments for the post
* @param post_entity $post The post being exported
* @param renderer_base $output Renderer base
* @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
* @return array
*/
private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
global $CFG;
$urlfactory = $this->related['urlfactory'];
$enableplagiarism = $CFG->enableplagiarism;
$forum = $this->related['forum'];
$context = $this->related['context'];
if ($enableplagiarism) {
require_once($CFG->libdir . '/plagiarismlib.php' );
}
return array_map(function($attachment) use (
$output,
$enableplagiarism,
$canexport,
$context,
$forum,
$post,
$urlfactory
) {
$exporter = new stored_file_exporter($attachment, ['context' => $context]);
$exportedattachment = $exporter->export($output);
$exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
if ($enableplagiarism) {
$plagiarismhtml = plagiarism_get_links([
'userid' => $post->get_author_id(),
'file' => $attachment,
'cmid' => $forum->get_course_module_record()->id,
'course' => $forum->get_course_id(),
'forum' => $forum->get_id()
]);
} else {
$plagiarismhtml = null;
}
$exportedattachment->urls = [
'export' => $exporturl ? $exporturl->out(false) : null
];
$exportedattachment->html = [
'plagiarism' => $plagiarismhtml
];
return $exportedattachment;
}, $attachments);
}
/**
* Export the list of tags.
*
* @param core_tag_tag[] $tags List of tags to export
* @return array
*/
private function export_tags(array $tags) : array {
$user = $this->related['user'];
$context = $this->related['context'];
$capabilitymanager = $this->related['capabilitymanager'];
$canmanagetags = $capabilitymanager->can_manage_tags($user);
return array_values(array_map(function($tag) use ($context, $canmanagetags) {
$viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
return [
'id' => $tag->taginstanceid,
'tagid' => $tag->id,
'isstandard' => $tag->isstandard,
'displayname' => $tag->get_display_name(),
'flag' => $canmanagetags && !empty($tag->flag),
'urls' => [
'view' => $viewurl->out(false)
]
];
}, $tags));
}
/**
* Get the HTML to display as a subheading in a post.
*
* @param stdClass $exportedauthor The exported author object
* @param int $timecreated The post time created timestamp if it's to be displayed
* @return string
*/
private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
$fullname = $exportedauthor->fullname;
$profileurl = $exportedauthor->urls['profile'] ?? null;
$formatteddate = userdate($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
$name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
$date = "<time>{$formatteddate}</time>";
return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
}
}
+164
View File
@@ -0,0 +1,164 @@
<?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/>.
/**
* Posts exporter class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\exporters\post as post_exporter;
use core\external\exporter;
use renderer_base;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Posts exporter class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class posts extends exporter {
/** @var post_entity[] $posts List of posts to export */
private $posts;
/** @var author_entity[] $authorsbyid List of authors for the posts indexed by author id */
private $authorsbyid;
/** @var array $attachmentsbypostid List of attachments indexed by post id */
private $attachmentsbypostid;
/** @var array $groupsbyauthorid List of author's groups indexed by author id */
private $groupsbyauthorid;
/** @var array $tagsbypostid List of tags indexed by post id */
private $tagsbypostid;
/** @var array $ratingbypostid List of ratings indexed by post id */
private $ratingbypostid;
/**
* Constructor.
*
* @param post_entity[] $posts List of posts to export
* @param author_entity[] $authorsbyid List of authors for the posts indexed by author id
* @param array $attachmentsbypostid List of attachments indexed by post id
* @param array $groupsbyauthorid List of author's groups indexed by author id
* @param array $tagsbypostid List of tags indexed by post id
* @param array $ratingbypostid List of ratings indexed by post id
* @param array $related The related objects for exporting
*/
public function __construct(
array $posts,
array $authorsbyid = [],
array $attachmentsbypostid = [],
array $groupsbyauthorid = [],
array $tagsbypostid = [],
array $ratingbypostid = [],
array $related = []
) {
$this->posts = $posts;
$this->authorsbyid = $authorsbyid;
$this->attachmentsbypostid = $attachmentsbypostid;
$this->groupsbyauthorid = $groupsbyauthorid;
$this->tagsbypostid = $tagsbypostid;
$this->ratingbypostid = $ratingbypostid;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'posts' => [
'type' => post_exporter::read_properties_definition(),
'multiple' => true
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$related = $this->related;
$authorsbyid = $this->authorsbyid;
$attachmentsbypostid = $this->attachmentsbypostid;
$groupsbyauthorid = $this->groupsbyauthorid;
$tagsbypostid = $this->tagsbypostid;
$ratingbypostid = $this->ratingbypostid;
$exportedposts = array_map(
function($post) use (
$related,
$authorsbyid,
$attachmentsbypostid,
$groupsbyauthorid,
$tagsbypostid,
$ratingbypostid,
$output
) {
$authorid = $post->get_author_id();
$postid = $post->get_id();
$author = isset($authorsbyid[$authorid]) ? $authorsbyid[$authorid] : [];
$attachments = isset($attachmentsbypostid[$postid]) ? $attachmentsbypostid[$postid] : [];
$authorgroups = isset($groupsbyauthorid[$authorid]) ? $groupsbyauthorid[$authorid] : [];
$tags = isset($tagsbypostid[$postid]) ? $tagsbypostid[$postid] : [];
$rating = isset($ratingbypostid[$postid]) ? $ratingbypostid[$postid] : null;
$exporter = new post_exporter($post, array_merge($related, [
'author' => $author,
'attachments' => $attachments,
'authorgroups' => $authorgroups,
'tags' => $tags,
'rating' => $rating
]));
return $exporter->export($output);
},
$this->posts
);
return [
'posts' => $exportedposts
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'forum' => 'mod_forum\local\entities\forum',
'discussion' => 'mod_forum\local\entities\discussion',
'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
'user' => 'stdClass',
'context' => 'context',
'includehtml' => 'bool'
];
}
}
@@ -0,0 +1,111 @@
<?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/>.
/**
* Builder factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\builders\exported_posts as exported_posts_builder;
use mod_forum\local\builders\exported_discussion_summaries as exported_discussion_summaries_builder;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use renderer_base;
/**
* Builder factory to construct any builders for forum.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class builder {
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/** @var renderer_base $rendererbase Renderer base */
private $rendererbase;
/**
* Constructor.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param manager_factory $managerfactory Manager factory
* @param renderer_base $rendererbase Renderer base
*/
public function __construct(
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory,
renderer_base $rendererbase
) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->rendererbase = $rendererbase;
}
/**
* Get an instance of the exported posts builder.
*
* @return exported_posts_builder
*/
public function get_exported_posts_builder() : exported_posts_builder {
return new exported_posts_builder(
$this->rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory->get_rating_manager()
);
}
/**
* Get an instance of the exported discussion summaries builder.
*
* @return exported_discussion_summaries_builder
*/
public function get_exported_discussion_summaries_builder() : exported_discussion_summaries_builder {
return new exported_discussion_summaries_builder(
$this->rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory->get_rating_manager()
);
}
}
@@ -0,0 +1,249 @@
<?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/>.
/**
* Entity factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\discussion_summary as discussion_summary_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\post_read_receipt_collection as post_read_receipt_collection_entity;
use mod_forum\local\entities\sorter as sorter_entity;
use stdClass;
use context;
use cm_info;
use user_picture;
use moodle_url;
/**
* Entity factory to create the forum entities.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class entity {
/**
* Create a forum entity from a stdClass (legacy forum object).
*
* @param stdClass $record The forum record
* @param context $context The forum module context
* @param stdClass $coursemodule Course module record for the forum
* @param stdClass $course Course the forum belongs to
* @return forum_entity
*/
public function get_forum_from_stdclass(
stdClass $record,
context $context,
stdClass $coursemodule,
stdClass $course
) : forum_entity {
// Note: cm_info::create loads a cm_info in the context of the current user which
// creates hidden dependency on the logged in user (very bad) however it's the best
// option to load some data we need which doesn't require the logged in user.
// Only use properties which do not require the logged in user.
$cm = \cm_info::create($coursemodule);
return new forum_entity(
$context,
$coursemodule,
$course,
// This property is a general module property that isn't affected by the logged in user.
$cm->effectivegroupmode,
$record->id,
$record->course,
$record->type,
$record->name,
$record->intro,
$record->introformat,
$record->assessed,
$record->assesstimestart,
$record->assesstimefinish,
$record->scale,
$record->maxbytes,
$record->maxattachments,
$record->forcesubscribe,
$record->trackingtype,
$record->rsstype,
$record->rssarticles,
$record->timemodified,
$record->warnafter,
$record->blockafter,
$record->blockperiod,
$record->completiondiscussions,
$record->completionreplies,
$record->completionposts,
$record->displaywordcount,
$record->lockdiscussionafter
);
}
/**
* Create a discussion entity from an stdClass (legacy dicussion object).
*
* @param stdClass $record Discussion record
* @return discussion_entity
*/
public function get_discussion_from_stdclass(stdClass $record) : discussion_entity {
return new discussion_entity(
$record->id,
$record->course,
$record->forum,
$record->name,
$record->firstpost,
$record->userid,
$record->groupid,
$record->assessed,
$record->timemodified,
$record->usermodified,
$record->timestart,
$record->timeend,
$record->pinned
);
}
/**
* Create a post entity from an stdClass (legacy post object).
*
* @param stdClass $record The post record
* @return post_entity
*/
public function get_post_from_stdclass(stdClass $record) : post_entity {
return new post_entity(
$record->id,
$record->discussion,
$record->parent,
$record->userid,
$record->created,
$record->modified,
$record->mailed,
$record->subject,
$record->message,
$record->messageformat,
$record->messagetrust,
$record->attachment,
$record->totalscore,
$record->mailnow,
$record->deleted
);
}
/**
* Create an author entity from a user record.
*
* @param stdClass $record The user record
* @return author_entity
*/
public function get_author_from_stdclass(stdClass $record) : author_entity {
return new author_entity(
$record->id,
$record->picture,
$record->firstname,
$record->lastname,
fullname($record),
$record->email,
$record->middlename,
$record->firstnamephonetic,
$record->lastnamephonetic,
$record->alternatename,
$record->imagealt
);
}
/**
* Create a discussion summary enttiy from stdClasses.
*
* @param stdClass $discussion The discussion record
* @param stdClass $firstpost A post record for the first post in the discussion
* @param stdClass $firstpostauthor A user record for the author of the first post
* @param stdClass $latestpostauthor A user record for the author of the latest post in the discussion
* @return discussion_summary_entity
*/
public function get_discussion_summary_from_stdclass(
stdClass $discussion,
stdClass $firstpost,
stdClass $firstpostauthor,
stdClass $latestpostauthor
) : discussion_summary_entity {
$firstpostauthorentity = $this->get_author_from_stdclass($firstpostauthor);
return new discussion_summary_entity(
$this->get_discussion_from_stdclass($discussion),
$this->get_post_from_stdclass($firstpost, $firstpostauthorentity),
$firstpostauthorentity,
$this->get_author_from_stdclass($latestpostauthor)
);
}
/**
* Create a post read receipt collection entity from a list of read receipt records.
*
* @param array $records A list of read receipt records.
* @return post_read_receipt_collection_entity
*/
public function get_post_read_receipt_collection_from_stdclasses(array $records) : post_read_receipt_collection_entity {
return new post_read_receipt_collection_entity($records);
}
/**
* Create a sorter entity to sort post entities.
*
* @return sorter_entity
*/
public function get_posts_sorter() : sorter_entity {
return new sorter_entity(
// Get id function for a post_entity.
function(post_entity $post) {
return $post->get_id();
},
// Get parent id function for a post_entity.
function(post_entity $post) {
return $post->get_parent_id();
}
);
}
/**
* Create a sorter entity to sort exported posts.
*
* @return sorter_entity
*/
public function get_exported_posts_sorter() : sorter_entity {
return new sorter_entity(
// Get id function for an exported post.
function(stdClass $post) {
return $post->id;
},
// Get parent id function for an exported post.
function(stdClass $post) {
return $post->parentid;
}
);
}
}
@@ -0,0 +1,247 @@
<?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/>.
/**
* Forum Exporter factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\post_read_receipt_collection as post_read_receipt_collection_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\exporters\forum as forum_exporter;
use mod_forum\local\exporters\discussion as discussion_exporter;
use mod_forum\local\exporters\discussion_summaries as discussion_summaries_exporter;
use mod_forum\local\exporters\post as post_exporter;
use mod_forum\local\exporters\posts as posts_exporter;
use context;
use rating;
use stdClass;
/**
* The exporter factory class used to fetch an instance of the different exporter types.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exporter {
/** @var legacy_data_mapper_factory The factory to fetch a legacy data mapper */
private $legacydatamapperfactory;
/** @var manager_factory The factory to fetch a new manager */
private $managerfactory;
/** @var url_factory The factory to create urls */
private $urlfactory;
/**
* Constructor for the exporter factory.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory The factory to fetch a legacy data mapper instance
* @param manager_factory $managerfactory The factory fo fetch a manager instance
* @param url_factory $urlfactory The factory to create urls
*/
public function __construct(
legacy_data_mapper_factory $legacydatamapperfactory,
manager_factory $managerfactory,
url_factory $urlfactory
) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->managerfactory = $managerfactory;
$this->urlfactory = $urlfactory;
}
/**
* Construct a new forum exporter for the specified user and forum.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param int $currentgroup The group currently being viewed
* @return forum_exporter
*/
public function get_forum_exporter(
stdClass $user,
forum_entity $forum,
?int $currentgroup
) : forum_exporter {
return new forum_exporter($forum, [
'legacydatamapperfactory' => $this->legacydatamapperfactory,
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'user' => $user,
'currentgroup' => $currentgroup,
]);
}
/**
* Fetch the structure of the forum exporter.
*
* @return array
*/
public static function get_forum_export_structure() : array {
return forum_exporter::read_properties_definition();
}
/**
* Construct a new discussion exporter for the specified user and forum discussion.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param discussion_entity $discussion The discussion being viewed
* @param stdClass[] $groupsbyid The list of groups in the forum
* @return discussion_exporter
*/
public function get_discussion_exporter(
stdClass $user,
forum_entity $forum,
discussion_entity $discussion,
array $groupsbyid = []
) : discussion_exporter {
return new discussion_exporter($discussion, [
'context' => $forum->get_context(),
'forum' => $forum,
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'user' => $user,
'legacydatamapperfactory' => $this->legacydatamapperfactory,
'latestpostid' => null,
'groupsbyid' => $groupsbyid
]);
}
/**
* Fetch the structure of the discussion exporter.
*
* @return array
*/
public static function get_discussion_export_structure() {
return discussion_exporter::read_properties_definition();
}
/**
* Construct a new discussion summaries exporter for the specified user and set of discussions.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param discussion_entity[] $discussions The set of discussions to be shown
* @param stdClass[] $groupsbyauthorid The set of groups in an associative array for each author
* @param stdClass[] $groupsbyid The set of groups in the forum in an associative array for each group
* @param int[] $discussionreplycount The number of replies for each discussion
* @param int[] $discussionunreadcount The number of unread posts for each discussion
* @param int[] $latestpostids The latest post id for each discussion
* @return discussion_summaries_exporter
*/
public function get_discussion_summaries_exporter(
stdClass $user,
forum_entity $forum,
array $discussions,
array $groupsbyid = [],
array $groupsbyauthorid = [],
array $discussionreplycount = [],
array $discussionunreadcount = [],
array $latestpostid = []
) : discussion_summaries_exporter {
return new discussion_summaries_exporter(
$discussions,
$groupsbyid,
$groupsbyauthorid,
$discussionreplycount,
$discussionunreadcount,
$latestpostid,
[
'legacydatamapperfactory' => $this->legacydatamapperfactory,
'context' => $forum->get_context(),
'forum' => $forum,
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'user' => $user,
]
);
}
/**
* Fetch the structure of the discussion summaries exporter.
*
* @return array
*/
public static function get_discussion_summaries_export_structure() {
return discussion_summaries_exporter::read_properties_definition();
}
/**
* Construct a new post exporter for the specified user and set of post.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param discussion_entity $discussion The discussion that the post is in
* @param post_entity[] $posts The set of posts to be exported
* @param author_entity[] $authorsbyid List of authors indexed by author id
* @param array $attachmentsbypostid List of attachments for each post indexed by post id
* @param array $groupsbyauthorid List of groups for the post authors indexed by author id
* @param post_read_receipt_collection_entity|null $readreceiptcollection Details of read receipts for each post
* @param array $tagsbypostid List of tags for each post indexed by post id
* @param rating[] $ratingbypostid List of ratings for each post indexed by post id
* @param bool $includehtml Include some pre-constructed HTML in the export
* @return post_exporter
*/
public function get_posts_exporter(
stdClass $user,
forum_entity $forum,
discussion_entity $discussion,
array $posts,
array $authorsbyid = [],
array $attachmentsbypostid = [],
array $groupsbyauthorid = [],
post_read_receipt_collection_entity $readreceiptcollection = null,
array $tagsbypostid = [],
array $ratingbypostid = [],
bool $includehtml = false
) : posts_exporter {
return new posts_exporter($posts, $authorsbyid, $attachmentsbypostid, $groupsbyauthorid, $tagsbypostid, $ratingbypostid, [
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'forum' => $forum,
'discussion' => $discussion,
'user' => $user,
'context' => $forum->get_context(),
'readreceiptcollection' => $readreceiptcollection,
'includehtml' => $includehtml
]);
}
/**
* Fetch the structure of the posts exporter.
*
* @return array
*/
public static function get_posts_export_structure() {
return posts_exporter::read_properties_definition();
}
}
@@ -0,0 +1,79 @@
<?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/>.
/**
* Legacy data mapper factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\data_mappers\legacy\author as author_data_mapper;
use mod_forum\local\data_mappers\legacy\discussion as discussion_data_mapper;
use mod_forum\local\data_mappers\legacy\forum as forum_data_mapper;
use mod_forum\local\data_mappers\legacy\post as post_data_mapper;
/**
* Legacy data mapper factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class legacy_data_mapper {
/**
* Create a legacy forum data mapper.
*
* @return forum_data_mapper
*/
public function get_forum_data_mapper() : forum_data_mapper {
return new forum_data_mapper();
}
/**
* Create a legacy discussion data mapper.
*
* @return discussion_data_mapper
*/
public function get_discussion_data_mapper() : discussion_data_mapper {
return new discussion_data_mapper();
}
/**
* Create a legacy post data mapper.
*
* @return post_data_mapper
*/
public function get_post_data_mapper() : post_data_mapper {
return new post_data_mapper();
}
/**
* Create a legacy author data mapper.
*
* @return author_data_mapper
*/
public function get_author_data_mapper() : author_data_mapper {
return new author_data_mapper();
}
}
@@ -0,0 +1,81 @@
<?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/>.
/**
* Managers factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/rating/lib.php');
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\managers\capability as capability_manager;
use rating_manager;
/**
* Managers factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/** @var legacy_data_mapper $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/**
* Constructor.
*
* @param legacy_data_mapper $legacydatamapperfactory Legacy data mapper factory
*/
public function __construct(legacy_data_mapper $legacydatamapperfactory) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
}
/**
* Create a capability manager for the given forum.
*
* @param forum_entity $forum The forum to manage capabilities for
* @return capability_manager
*/
public function get_capability_manager(forum_entity $forum) {
return new capability_manager(
$forum,
$this->legacydatamapperfactory->get_forum_data_mapper(),
$this->legacydatamapperfactory->get_discussion_data_mapper(),
$this->legacydatamapperfactory->get_post_data_mapper()
);
}
/**
* Create a rating manager.
*
* @return rating_manager
*/
public function get_rating_manager() : rating_manager {
return new rating_manager();
}
}
@@ -0,0 +1,494 @@
<?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/>.
/**
* Renderer factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\builder as builder_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\renderers\discussion as discussion_renderer;
use mod_forum\local\renderers\discussion_list as discussion_list_renderer;
use mod_forum\local\renderers\posts as posts_renderer;
use moodle_page;
use core\output\notification;
/**
* Renderer factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer {
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var builder_factory $builderfactory Builder factory */
private $builderfactory;
/** @var url_factory $urlfactory URL factory */
private $urlfactory;
/** @var renderer_base $rendererbase Renderer base */
private $rendererbase;
/** @var moodle_page $page Moodle page */
private $page;
/**
* Constructor.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param manager_factory $managerfactory Manager factory
* @param entity_factory $entityfactory Entity factory
* @param builder_factory $builderfactory Builder factory
* @param url_factory $urlfactory URL factory
* @param moodle_page $page Moodle page
*/
public function __construct(
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory,
entity_factory $entityfactory,
builder_factory $builderfactory,
url_factory $urlfactory,
moodle_page $page
) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->entityfactory = $entityfactory;
$this->builderfactory = $builderfactory;
$this->urlfactory = $urlfactory;
$this->page = $page;
$this->rendererbase = $page->get_renderer('mod_forum');
}
/**
* Create a discussion renderer for the given forum and discussion.
*
* @param forum_entity $forum Forum the discussion belongs to
* @param discussion_entity $discussion Discussion to render
* @param int $displaymode How should the posts be formatted?
* @return discussion_renderer
*/
public function get_discussion_renderer(
forum_entity $forum,
discussion_entity $discussion,
int $displaymode
) : discussion_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$ratingmanager = $this->managerfactory->get_rating_manager();
$rendererbase = $this->rendererbase;
$baseurl = $this->urlfactory->get_discussion_view_url_from_discussion($discussion);
$notifications = [];
return new discussion_renderer(
$forum,
$discussion,
$displaymode,
$rendererbase,
$this->get_single_discussion_posts_renderer($displaymode, false),
$this->page,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$capabilitymanager,
$ratingmanager,
$this->entityfactory->get_exported_posts_sorter(),
$baseurl,
$notifications
);
}
/**
* Create a posts renderer to render posts without defined parent/reply relationships.
*
* @return posts_renderer
*/
public function get_posts_renderer() : posts_renderer {
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
'mod_forum/forum_discussion_posts'
);
}
/**
* Create a posts renderer to render a list of posts in a single discussion.
*
* @param int|null $displaymode How should the posts be formatted?
* @param bool $readonly Should the posts include the actions to reply, delete, etc?
* @return posts_renderer
*/
public function get_single_discussion_posts_renderer(int $displaymode = null, bool $readonly = false) : posts_renderer {
$exportedpostssorter = $this->entityfactory->get_exported_posts_sorter();
switch ($displaymode) {
case FORUM_MODE_THREADED:
$template = 'mod_forum/forum_discussion_threaded_posts';
break;
case FORUM_MODE_NESTED:
$template = 'mod_forum/forum_discussion_nested_posts';
break;
default;
$template = 'mod_forum/forum_discussion_posts';
break;
}
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
$template,
// Post process the exported posts for our template. This function will add the "replies"
// and "hasreplies" properties to the exported posts. It will also sort them into the
// reply tree structure if the display mode requires it.
function($exportedposts, $forums) use ($displaymode, $readonly, $exportedpostssorter) {
$forum = array_shift($forums);
$seenfirstunread = false;
$exportedposts = array_map(
function($exportedpost) use ($forum, $readonly, $seenfirstunread) {
if ($forum->get_type() == 'single' && !$exportedpost->hasparent) {
// Remove the author from any posts that don't have a parent.
unset($exportedpost->author);
}
$exportedpost->firstpost = false;
$exportedpost->readonly = $readonly;
$exportedpost->hasreplies = false;
$exportedpost->replies = [];
$exportedpost->isfirstunread = false;
if (!$seenfirstunread && $exportedpost->unread) {
$exportedpost->isfirstunread = true;
$seenfirstunread = true;
}
return $exportedpost;
},
$exportedposts
);
if ($displaymode === FORUM_MODE_NESTED || $displaymode === FORUM_MODE_THREADED) {
$sortedposts = $exportedpostssorter->sort_into_children($exportedposts);
$sortintoreplies = function($nestedposts) use (&$sortintoreplies) {
return array_map(function($postdata) use (&$sortintoreplies) {
[$post, $replies] = $postdata;
$post->replies = $sortintoreplies($replies);
$post->hasreplies = !empty($post->replies);
return $post;
}, $nestedposts);
};
// Set the "replies" property on the exported posts.
$exportedposts = $sortintoreplies($sortedposts);
} else if ($displaymode === FORUM_MODE_FLATNEWEST || $displaymode === FORUM_MODE_FLATOLDEST) {
$exportedfirstpost = array_shift($exportedposts);
$exportedfirstpost->replies = $exportedposts;
$exportedfirstpost->hasreplies = true;
$exportedposts = [$exportedfirstpost];
}
if (!empty($exportedposts)) {
// Need to identify the first post so that we can use it in behat tests.
$exportedposts[0]->firstpost = true;
}
return $exportedposts;
}
);
}
/**
* Create a posts renderer to render posts in the forum search results.
*
* @param string[] $searchterms The search terms to be highlighted in the posts
* @return posts_renderer
*/
public function get_posts_search_results_renderer(array $searchterms) : posts_renderer {
$urlfactory = $this->urlfactory;
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
'mod_forum/forum_posts_with_context_links',
// Post process the exported posts to add the highlighting of the search terms to the post
// and also the additional context links in the subject.
function($exportedposts, $forumsbyid, $discussionsbyid) use ($searchterms, $urlfactory) {
$highlightwords = implode(' ', $searchterms);
return array_map(
function($exportedpost) use (
$forumsbyid,
$discussionsbyid,
$searchterms,
$highlightwords,
$urlfactory
) {
$discussion = $discussionsbyid[$exportedpost->discussionid];
$forum = $forumsbyid[$discussion->get_forum_id()];
$viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
$exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false);
$exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false);
$exportedpost->subject = highlight($highlightwords, $exportedpost->subject);
$exportedpost->forumname = format_string($forum->get_name(), true);
$exportedpost->discussionname = highlight($highlightwords, format_string($discussion->get_name(), true));
$exportedpost->showdiscussionname = $forum->get_type() != 'single';
// Identify search terms only found in HTML markup, and add a warning about them to
// the start of the message text. This logic was copied exactly as is from the previous
// implementation.
$missingterms = '';
$exportedpost->message = highlight(
$highlightwords,
$exportedpost->message,
0,
'<fgw9sdpq4>',
'</fgw9sdpq4>'
);
foreach ($searchterms as $searchterm) {
if (
preg_match("/$searchterm/i", $exportedpost->message) &&
!preg_match('/<fgw9sdpq4>' . $searchterm . '<\/fgw9sdpq4>/i', $exportedpost->message)
) {
$missingterms .= " $searchterm";
}
}
$exportedpost->message = str_replace('<fgw9sdpq4>', '<span class="highlight">', $exportedpost->message);
$exportedpost->message = str_replace('</fgw9sdpq4>', '</span>', $exportedpost->message);
if ($missingterms) {
$strmissingsearchterms = get_string('missingsearchterms', 'forum');
$exportedpost->message = '<p class="highlight2">' . $strmissingsearchterms . ' '
. $missingterms . '</p>' . $exportedpost->message;
}
return $exportedpost;
},
$exportedposts
);
}
);
}
/**
* Create a posts renderer to render posts in mod/forum/user.php.
*
* @param bool $addlinkstocontext Should links to the course, forum, and discussion be included?
* @return posts_renderer
*/
public function get_user_forum_posts_report_renderer(bool $addlinkstocontext) : posts_renderer {
$urlfactory = $this->urlfactory;
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
'mod_forum/forum_posts_with_context_links',
function($exportedposts, $forumsbyid, $discussionsbyid) use ($urlfactory, $addlinkstocontext) {
return array_map(function($exportedpost) use ($forumsbyid, $discussionsbyid, $addlinkstocontext, $urlfactory) {
$discussion = $discussionsbyid[$exportedpost->discussionid];
$forum = $forumsbyid[$discussion->get_forum_id()];
$courserecord = $forum->get_course_record();
if ($addlinkstocontext) {
$viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
$exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false);
$exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false);
$exportedpost->urls['viewcourse'] = $urlfactory->get_course_url_from_forum($forum)->out(false);
}
$exportedpost->forumname = format_string($forum->get_name(), true);
$exportedpost->discussionname = format_string($discussion->get_name(), true);
$exportedpost->coursename = format_string($courserecord->shortname, true);
$exportedpost->showdiscussionname = $forum->get_type() != 'single';
return $exportedpost;
}, $exportedposts);
}
);
}
/**
* Create a standard type discussion list renderer.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return discussion_list_renderer
*/
public function get_discussion_list_renderer(
forum_entity $forum
) : discussion_list_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$rendererbase = $this->rendererbase;
$notifications = [];
return new discussion_list_renderer(
$forum,
$rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->builderfactory,
$capabilitymanager,
$this->urlfactory,
$notifications,
function($discussions, $user, $forum) {
$exporteddiscussionsummarybuilder = $this->builderfactory->get_exported_discussion_summaries_builder();
return $exporteddiscussionsummarybuilder->build(
$user,
$forum,
$discussions
);
}
);
}
/**
* Create a blog type discussion list renderer.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return discussion_list_renderer
*/
public function get_blog_discussion_list_renderer(
forum_entity $forum
) : discussion_list_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$rendererbase = $this->rendererbase;
$notifications = [];
return new discussion_list_renderer(
$forum,
$rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->builderfactory,
$capabilitymanager,
$this->urlfactory,
$notifications,
function($discussions, $user, $forum) {
$exportedpostsbuilder = $this->builderfactory->get_exported_posts_builder();
$discussionentries = [];
$postentries = [];
foreach ($discussions as $discussion) {
$discussionentries[] = $discussion->get_discussion();
$discussionentriesids[] = $discussion->get_discussion()->get_id();
$postentries[] = $discussion->get_first_post();
}
$exportedposts['posts'] = $exportedpostsbuilder->build(
$user,
[$forum],
$discussionentries,
$postentries
);
$postvault = $this->vaultfactory->get_post_vault();
$discussionrepliescount = $postvault->get_reply_count_for_discussion_ids($discussionentriesids);
array_walk($exportedposts['posts'], function($post) use ($discussionrepliescount) {
$post->discussionrepliescount = $discussionrepliescount[$post->discussionid] ?? 0;
// TODO: Find a better solution due to language differences when defining the singular and plural form.
$post->isreplyplural = $post->discussionrepliescount != 1 ? true : false;
});
$exportedposts['state']['hasdiscussions'] = $exportedposts['posts'] ? true : false;
return $exportedposts;
}
);
}
/**
* Create a single type discussion list renderer.
*
* @param forum_entity $forum Forum the discussion belongs to
* @param discussion_entity $discussion The discussion entity
* @param bool $hasmultiplediscussions Whether the forum has multiple discussions (more than one)
* @param int $displaymode How should the posts be formatted?
* @return discussion_renderer
*/
public function get_single_discussion_list_renderer(
forum_entity $forum,
discussion_entity $discussion,
bool $hasmultiplediscussions,
int $displaymode
) : discussion_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$ratingmanager = $this->managerfactory->get_rating_manager();
$rendererbase = $this->rendererbase;
$cmid = $forum->get_course_module_record()->id;
$baseurl = $this->urlfactory->get_forum_view_url_from_course_module_id($cmid);
$notifications = array();
if ($hasmultiplediscussions) {
$notifications[] = (new notification(get_string('warnformorepost', 'forum')))
->set_show_closebutton(true);
}
return new discussion_renderer(
$forum,
$discussion,
$displaymode,
$rendererbase,
$this->get_single_discussion_posts_renderer($displaymode, false),
$this->page,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$capabilitymanager,
$ratingmanager,
$this->entityfactory->get_exported_posts_sorter(),
$baseurl,
$notifications
);
}
}
+460
View File
@@ -0,0 +1,460 @@
<?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/>.
/**
* A URL factory for the forum.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use moodle_url;
use stored_file;
use user_picture;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* A URL factory for the forum.
*
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class url {
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/**
* Constructor.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
*/
public function __construct(legacy_data_mapper_factory $legacydatamapperfactory) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
}
/**
* Get the course url from the given course id.
*
* @param int $courseid The course id
* @return moodle_url
*/
public function get_course_url_from_courseid(int $courseid) : moodle_url {
return new moodle_url('/course/view.php', [
'id' => $courseid,
]);
}
/**
* Get the course url from the given forum entity.
*
* @param forum_entity $forum The forum entity
* @return moodle_url
*/
public function get_course_url_from_forum(forum_entity $forum) : moodle_url {
return $this->get_course_url_from_courseid($forum->get_course_id());
}
/**
* Get the create discussion url for the given forum.
*
* @param forum_entity $forum The forum entity
* @return moodle_url
*/
public function get_discussion_create_url(forum_entity $forum) : moodle_url {
return new moodle_url('/mod/forum/post.php', [
'forum' => $forum->get_id(),
]);
}
/**
* Get the view forum url for the given forum and optionally a page number.
*
* @param forum_entity $forum The forum entity
* @param int|null $pageno The page number
* @return moodle_url
*/
public function get_forum_view_url_from_forum(forum_entity $forum, ?int $pageno = null) : moodle_url {
return $this->get_forum_view_url_from_course_module_id($forum->get_course_module_record()->id, $pageno);
}
/**
* Get the view forum url for the given course module id and optionally a page number.
*
* @param int $coursemoduleid The course module id
* @param int|null $pageno The page number
* @return moodle_url
*/
public function get_forum_view_url_from_course_module_id(int $coursemoduleid, ?int $pageno = null) : moodle_url {
$url = new moodle_url('/mod/forum/view.php', [
'id' => $coursemoduleid,
]);
if (null !== $pageno) {
$url->param('page', $pageno);
}
return $url;
}
/**
* Get the view discussion url from the given discussion id.
*
* @param int $discussionid The discussion id
* @return moodle_url
*/
public function get_discussion_view_url_from_discussion_id(int $discussionid) : moodle_url {
return new moodle_url('/mod/forum/discuss.php', [
'd' => $discussionid
]);
}
/**
* Get the view discussion url from the given discussion.
*
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_discussion_view_url_from_discussion(discussion_entity $discussion) : moodle_url {
return $this->get_discussion_view_url_from_discussion_id($discussion->get_id());
}
/**
* Get the url to view the first unread post in a discussion.
*
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_discussion_view_first_unread_post_url_from_discussion(discussion_entity $discussion) {
$viewurl = $this->get_discussion_view_url_from_discussion_id($discussion->get_id());
$viewurl->set_anchor('unread');
return $viewurl;
}
/**
* Get the url to view the latest post in a discussion.
*
* @param discussion_entity $discussion The discussion
* @param int|null $latestpost The id of the latest post
* @return moodle_url
*/
public function get_discussion_view_latest_post_url_from_discussion(discussion_entity $discussion, ?int $latestpost) {
$viewurl = $this->get_discussion_view_url_from_discussion_id($discussion->get_id());
if (null === $latestpost) {
return $viewurl;
} else {
return new moodle_url($viewurl, ['parent' => $latestpost]);
}
}
/**
* Get the url to view a discussion from a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_discussion_view_url_from_post(post_entity $post) : moodle_url {
return $this->get_discussion_view_url_from_discussion_id($post->get_discussion_id());
}
/**
* Get the url to view a discussion from a discussion id and post id.
*
* @param int $discussionid The discussion id
* @param int $postid The post id
* @return moodle_url
*/
public function get_view_post_url_from_post_id(int $discussionid, int $postid) : moodle_url {
$url = $this->get_discussion_view_url_from_discussion_id($discussionid);
$url->set_anchor('p' . $postid);
return $url;
}
/**
* Get the url to view a post in the context of the rest of the discussion.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_view_post_url_from_post(post_entity $post) : moodle_url {
return $this->get_view_post_url_from_post_id($post->get_discussion_id(), $post->get_id());
}
/**
* Get the url to view a post and it's replies in isolation without the rest of the
* discussion.
*
* @param int $discussionid The discussion id
* @param int $postid The post id
* @return moodle_url
*/
public function get_view_isolated_post_url_from_post_id(int $discussionid, int $postid) : moodle_url {
$url = $this->get_discussion_view_url_from_discussion_id($discussionid);
$url->params(['parent' => $postid]);
return $url;
}
/**
* Get the url to view a post and it's replies in isolation without the rest of the
* discussion.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_view_isolated_post_url_from_post(post_entity $post) : moodle_url {
return $this->get_view_isolated_post_url_from_post_id($post->get_discussion_id(), $post->get_id());
}
/**
* Get the url to edit a post.
*
* @param forum_entity $forum The forum the post belongs to
* @param post_entity $post The post
* @return moodle_url
*/
public function get_edit_post_url_from_post(forum_entity $forum, post_entity $post) : moodle_url {
if ($forum->get_type() == 'single') {
return new moodle_url('/course/modedit.php', [
'update' => $forum->get_course_module_record()->id,
'sesskey' => sesskey(),
'return' => 1
]);
} else {
return new moodle_url('/mod/forum/post.php', [
'edit' => $post->get_id()
]);
}
}
/**
* Get the url to split a discussion at a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_split_discussion_at_post_url_from_post(post_entity $post) : moodle_url {
return new moodle_url('/mod/forum/post.php', [
'prune' => $post->get_id()
]);
}
/**
* Get the url to delete a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_delete_post_url_from_post(post_entity $post) : moodle_url {
return new moodle_url('/mod/forum/post.php', [
'delete' => $post->get_id()
]);
}
/**
* Get the url to reply to a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_reply_to_post_url_from_post(post_entity $post) : moodle_url {
return new moodle_url('/mod/forum/post.php#mformforum', [
'reply' => $post->get_id()
]);
}
/**
* Get the url to export (see portfolios) a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_export_post_url_from_post(post_entity $post) : ?moodle_url {
global $CFG;
require_once($CFG->libdir . '/portfoliolib.php');
$button = new \portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', ['postid' => $post->get_id()], 'mod_forum');
if ($post->has_attachments()) {
$button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
} else {
$button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
}
$url = $button->to_html(PORTFOLIO_ADD_MOODLE_URL);
return $url ?: null;
}
/**
* Get the url to mark a post as read.
*
* @param post_entity $post The post
* @param int $displaymode The display mode to show the forum in after marking as read
* @return moodle_url
*/
public function get_mark_post_as_read_url_from_post(post_entity $post, int $displaymode = FORUM_MODE_THREADED) : moodle_url {
$params = [
'd' => $post->get_discussion_id(),
'postid' => $post->get_id(),
'mark' => 'read'
];
$url = new moodle_url('/mod/forum/discuss.php', $params);
if ($displaymode == FORUM_MODE_THREADED) {
$url->param('parent', $post->get_parent_id());
} else {
$url->set_anchor('p' . $post->get_id());
}
return $url;
}
/**
* Get the url to mark a post as unread.
*
* @param post_entity $post The post
* @param int $displaymode The display mode to show the forum in after marking as unread
* @return moodle_url
*/
public function get_mark_post_as_unread_url_from_post(post_entity $post, int $displaymode = FORUM_MODE_THREADED) : moodle_url {
$params = [
'd' => $post->get_discussion_id(),
'postid' => $post->get_id(),
'mark' => 'unread'
];
$url = new moodle_url('/mod/forum/discuss.php', $params);
if ($displaymode == FORUM_MODE_THREADED) {
$url->param('parent', $post->get_parent_id());
} else {
$url->set_anchor('p' . $post->get_id());
}
return $url;
}
/**
* Get the url to export attachments for a post.
*
* @param post_entity $post The post
* @param stored_file $attachment
* @return moodle_url|null
*/
public function get_export_attachment_url_from_post_and_attachment(post_entity $post, stored_file $attachment) : ?moodle_url {
global $CFG;
require_once($CFG->libdir . '/portfoliolib.php');
$button = new \portfolio_add_button();
$button->set_callback_options(
'forum_portfolio_caller',
['postid' => $post->get_id(), 'attachment' => $attachment->get_id()],
'mod_forum'
);
$button->set_format_by_file($attachment);
$url = $button->to_html(PORTFOLIO_ADD_MOODLE_URL);
return $url ?: null;
}
/**
* Get the url to view an author's profile.
*
* @param author_entity $author The author
* @return moodle_url
*/
public function get_author_profile_url(author_entity $author) : moodle_url {
return new moodle_url('/user/view.php', [
'id' => $author->get_id()
]);
}
/**
* Get the url to view the author's profile image.
*
* @param author_entity $author The author
* @return moodle_url
*/
public function get_author_profile_image_url(author_entity $author) : moodle_url {
global $PAGE;
$datamapper = $this->legacydatamapperfactory->get_author_data_mapper();
$record = $datamapper->to_legacy_object($author);
$userpicture = new user_picture($record);
$userpicture->size = 2;
return $userpicture->get_url($PAGE);
}
/**
* Get the url to mark a discussion as read.
*
* @param forum_entity $forum The forum that the discussion belongs to
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_mark_discussion_as_read_url_from_discussion(
forum_entity $forum,
discussion_entity $discussion
) : moodle_url {
return new moodle_url('/mod/forum/markposts.php', [
'f' => $discussion->get_forum_id(),
'd' => $discussion->get_id(),
'mark' => 'read',
'sesskey' => sesskey(),
'return' => $this->get_forum_view_url_from_forum($forum)->out(),
]);
}
/**
* Get the url to mark all discussions as read.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return moodle_url
*/
public function get_mark_all_discussions_as_read_url(forum_entity $forum) : moodle_url {
return new moodle_url('/mod/forum/markposts.php', [
'f' => $forum->get_id(),
'mark' => 'read',
'sesskey' => sesskey(),
'return' => $this->get_forum_view_url_from_forum($forum)->out(),
]);
}
/**
* Get the url to subscribe to a discussion.
*
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_discussion_subscribe_url(discussion_entity $discussion) : moodle_url {
return new moodle_url('/mod/forum/subscribe.php', [
'sesskey' => sesskey(),
'id' => $discussion->get_forum_id(),
'd' => $discussion->get_id()
]);
}
}
+152
View File
@@ -0,0 +1,152 @@
<?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/>.
/**
* Vault factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\vaults\author as author_vault;
use mod_forum\local\vaults\discussion as discussion_vault;
use mod_forum\local\vaults\discussion_list as discussion_list_vault;
use mod_forum\local\vaults\forum as forum_vault;
use mod_forum\local\vaults\post as post_vault;
use mod_forum\local\vaults\post_attachment as post_attachment_vault;
use mod_forum\local\vaults\post_read_receipt_collection as post_read_receipt_collection_vault;
use file_storage;
use moodle_database;
/**
* Vault factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class vault {
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var moodle_database $db A moodle database */
private $db;
/** @var file_storage $filestorage A file storage instance */
private $filestorage;
/**
* Constructor.
*
* @param moodle_database $db A moodle database
* @param entity_factory $entityfactory Entity factory
* @param file_storage $filestorage A file storage instance
*/
public function __construct(moodle_database $db, entity_factory $entityfactory, file_storage $filestorage) {
$this->db = $db;
$this->entityfactory = $entityfactory;
$this->filestorage = $filestorage;
}
/**
* Create a forum vault.
*
* @return forum_vault
*/
public function get_forum_vault() : forum_vault {
return new forum_vault(
$this->db,
$this->entityfactory
);
}
/**
* Create a discussion vault.
*
* @return discussion_vault
*/
public function get_discussion_vault() : discussion_vault {
return new discussion_vault(
$this->db,
$this->entityfactory
);
}
/**
* Create a discussion list vault.
*
* @return discussion_list_vault
*/
public function get_discussions_in_forum_vault() : discussion_list_vault {
return new discussion_list_vault(
$this->db,
$this->entityfactory
);
}
/**
* Create a post vault.
*
* @return post_vault
*/
public function get_post_vault() : post_vault {
return new post_vault(
$this->db,
$this->entityfactory
);
}
/**
* Create an author vault.
*
* @return author_vault
*/
public function get_author_vault() : author_vault {
return new author_vault(
$this->db,
$this->entityfactory
);
}
/**
* Create a post read receipt collection vault.
*
* @return post_read_receipt_collection_vault
*/
public function get_post_read_receipt_collection_vault() : post_read_receipt_collection_vault {
return new post_read_receipt_collection_vault(
$this->db,
$this->entityfactory
);
}
/**
* Create a post attachment vault.
*
* @return post_attachment_vault
*/
public function get_post_attachment_vault() : post_attachment_vault {
return new post_attachment_vault(
$this->filestorage
);
}
}
@@ -0,0 +1,514 @@
<?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/>.
/**
* Capability manager for the forum.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\managers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper;
use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper;
use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\subscriptions;
use context;
use context_system;
use stdClass;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Capability manager for the forum.
*
* Defines all the business rules for what a user can and can't do in the forum.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class capability {
/** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */
private $forumdatamapper;
/** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */
private $discussiondatamapper;
/** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */
private $postdatamapper;
/** @var forum_entity $forum Forum entity */
private $forum;
/** @var stdClass $forumrecord Legacy forum record */
private $forumrecord;
/** @var context $context Module context for the forum */
private $context;
/**
* Constructor.
*
* @param forum_entity $forum The forum entity to manage capabilities for.
* @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper
* @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper
* @param legacy_post_data_mapper $postdatamapper Legacy post data mapper
*/
public function __construct(
forum_entity $forum,
legacy_forum_data_mapper $forumdatamapper,
legacy_discussion_data_mapper $discussiondatamapper,
legacy_post_data_mapper $postdatamapper
) {
$this->forumdatamapper = $forumdatamapper;
$this->discussiondatamapper = $discussiondatamapper;
$this->postdatamapper = $postdatamapper;
$this->forum = $forum;
$this->forumrecord = $forumdatamapper->to_legacy_object($forum);
$this->context = $forum->get_context();
}
/**
* Can the user subscribe to this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_subscribe_to_forum(stdClass $user) : bool {
if ($this->forum->get_type() == 'single') {
return false;
}
return !is_guest($this->get_context(), $user) &&
subscriptions::is_subscribable($this->get_forum_record());
}
/**
* Can the user create discussions in this forum?
*
* @param stdClass $user The user to check
* @param int|null $groupid The current activity group id
* @return bool
*/
public function can_create_discussions(stdClass $user, int $groupid = null) : bool {
if (isguestuser($user) or !isloggedin()) {
return false;
}
switch ($this->forum->get_type()) {
case 'news':
$capability = 'mod/forum:addnews';
break;
case 'qanda':
$capability = 'mod/forum:addquestion';
break;
default:
$capability = 'mod/forum:startdiscussion';
}
if (!has_capability($capability, $this->forum->get_context(), $user)) {
return false;
}
if ($this->forum->get_type() == 'eachuser') {
if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) {
return false;
}
}
if ($this->forum->is_in_group_mode()) {
return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user);
} else {
return true;
}
}
/**
* Can the user access all groups?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_access_all_groups(stdClass $user) : bool {
return has_capability('moodle/site:accessallgroups', $this->get_context(), $user);
}
/**
* Can the user access the given group?
*
* @param stdClass $user The user to check
* @param int $groupid The id of the group that the forum is set to
* @return bool
*/
public function can_access_group(stdClass $user, int $groupid) : bool {
if ($this->can_access_all_groups($user)) {
// This user has access to all groups.
return true;
}
// This is a group discussion for a forum in separate groups mode.
// Check if the user is a member.
// This is the most expensive check.
return groups_is_member($groupid, $user->id);
}
/**
* Can the user post to their groups?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_post_to_my_groups(stdClass $user) : bool {
return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user);
}
/**
* Can the user view discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_view_discussions(stdClass $user) : bool {
return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user);
}
/**
* Can the user move discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_move_discussions(stdClass $user) : bool {
$forum = $this->get_forum();
return $forum->get_type() !== 'single' &&
has_capability('mod/forum:movediscussions', $this->get_context(), $user);
}
/**
* Can the user pin discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_pin_discussions(stdClass $user) : bool {
return $this->forum->get_type() !== 'single' &&
has_capability('mod/forum:pindiscussions', $this->get_context(), $user);
}
/**
* Can the user split discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_split_discussions(stdClass $user) : bool {
$forum = $this->get_forum();
return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user);
}
/**
* Can the user export (see portfolios) discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_export_discussions(stdClass $user) : bool {
global $CFG;
return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user);
}
/**
* Can the user manually mark posts as read/unread in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_manually_control_post_read_status(stdClass $user) : bool {
global $CFG;
return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user);
}
/**
* Is the user required to post in the discussion before they can view it?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool {
$forum = $this->get_forum();
if ($forum->get_type() === 'qanda') {
// If it's a Q and A forum then the user must either have the capability to view without
// posting or the user must have posted before they can view the discussion.
return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) &&
!forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id);
} else {
// No other forum types require posting before viewing.
return false;
}
}
/**
* Can the user subscribe to the give discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool {
return $this->can_subscribe_to_forum($user);
}
/**
* Can the user move the discussion in this forum?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool {
return $this->can_move_discussions($user);
}
/**
* Is the user pin the discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool {
return $this->can_pin_discussions($user);
}
/**
* Can the user post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool {
$forum = $this->get_forum();
$forumrecord = $this->get_forum_record();
$discussionrecord = $this->get_discussion_record($discussion);
$context = $this->get_context();
$coursemodule = $forum->get_course_module_record();
$course = $forum->get_course_record();
return forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context);
}
/**
* Can the user view the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to view
* @return bool
*/
public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
$forum = $this->get_forum();
$forumrecord = $this->get_forum_record();
$discussionrecord = $this->get_discussion_record($discussion);
$postrecord = $this->get_post_record($post);
$coursemodule = $forum->get_course_module_record();
return forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false);
}
/**
* Can the user edit the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to edit
* @return bool
*/
public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
global $CFG;
$context = $this->get_context();
$ownpost = $post->is_owned_by_user($user);
$ineditingtime = $post->get_age() < $CFG->maxeditingtime;
switch ($this->forum->get_type()) {
case 'news':
// Allow editing of news posts once the discussion has started.
$ineditingtime = !$post->has_parent() && $discussion->has_started();
break;
case 'single':
return $discussion->is_first_post($post) && has_capability('moodle/course:manageactivities', $context, $user);
}
return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user);
}
/**
* Can the user delete the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to delete
* @return bool
*/
public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
global $CFG;
$forum = $this->get_forum();
if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) {
// Do not allow deleting of first post in single simple type.
return false;
} else {
$context = $this->get_context();
$ownpost = $post->is_owned_by_user($user);
$ineditingtime = $post->get_age() < $CFG->maxeditingtime;
return ($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user)) ||
has_capability('mod/forum:deleteanypost', $context, $user);
}
}
/**
* Can the user split the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to split
* @return bool
*/
public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
return $this->can_split_discussions($user) && $post->has_parent();
}
/**
* Can the user reply to the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to reply to
* @return bool
*/
public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
return $this->can_post_in_discussion($user, $discussion);
}
/**
* Can the user export (see portfolios) the post in this discussion?
*
* @param stdClass $user The user to check
* @param post_entity $post The post the user wants to export
* @return bool
*/
public function can_export_post(stdClass $user, post_entity $post) : bool {
global $CFG;
$context = $this->get_context();
return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) ||
($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user)));
}
/**
* Get the forum entity for this capability manager.
*
* @return forum_entity
*/
protected function get_forum() : forum_entity {
return $this->forum;
}
/**
* Get the legacy forum record for this forum.
*
* @return stdClass
*/
protected function get_forum_record() : stdClass {
return $this->forumrecord;
}
/**
* Get the context for this capability manager.
*
* @return context
*/
protected function get_context() : context {
return $this->context;
}
/**
* Get the legacy discussion record for the given discussion entity.
*
* @param discussion_entity $discussion The discussion to convert
* @return stdClass
*/
protected function get_discussion_record(discussion_entity $discussion) : stdClass {
return $this->discussiondatamapper->to_legacy_object($discussion);
}
/**
* Get the legacy post record for the given post entity.
*
* @param post_entity $post The post to convert
* @return stdClass
*/
protected function get_post_record(post_entity $post) : stdClass {
return $this->postdatamapper->to_legacy_object($post);
}
/**
* Can the user view the participants of this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool {
return course_can_view_participants($this->get_context()) &&
!$this->must_post_before_viewing_discussion($user, $discussion);
}
/**
* Can the user view hidden posts in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_view_hidden_posts(stdClass $user) : bool {
return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user);
}
/**
* Can the user manage this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_manage_forum(stdClass $user) {
return has_capability('moodle/course:manageactivities', $this->get_context(), $user);
}
/**
* Can the user manage tags on the site?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_manage_tags(stdClass $user) : bool {
return has_capability('moodle/tag:manage', context_system::instance(), $user);
}
}
@@ -0,0 +1,422 @@
<?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/>.
/**
* Discussion renderer.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\renderers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\sorter as sorter_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\managers\capability as capability_manager;
use mod_forum\local\renderers\posts as posts_renderer;
use core\output\notification;
use context;
use context_module;
use html_writer;
use moodle_exception;
use moodle_page;
use moodle_url;
use rating_manager;
use renderer_base;
use single_button;
use single_select;
use stdClass;
use url_select;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Discussion renderer class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion {
/** @var forum_entity $forum The forum that the discussion belongs to */
private $forum;
/** @var discussion_entity $discussion The discussion entity */
private $discussion;
/** @var stdClass $discussionrecord Legacy discussion record */
private $discussionrecord;
/** @var stdClass $forumrecord Legacy forum record */
private $forumrecord;
/** @var int $displaymode The display mode to render the discussion in */
private $displaymode;
/** @var renderer_base $renderer Renderer base */
private $renderer;
/** @var posts_renderer $postsrenderer A posts renderer */
private $postsrenderer;
/** @var moodle_page $page The page this discussion is being rendered for */
private $page;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var capability_manager $capabilitymanager Capability manager */
private $capabilitymanager;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/** @var moodle_url $baseurl The base URL for the discussion */
private $baseurl;
/** @var array $notifications List of HTML notifications to display */
private $notifications;
/** @var sorter_entity $exportedpostsorter Sorter for the exported posts */
private $exportedpostsorter;
/**
* Constructor.
*
* @param forum_entity $forum The forum that the discussion belongs to
* @param discussion_entity $discussion The discussion entity
* @param int $displaymode The display mode to render the discussion in
* @param renderer_base $renderer Renderer base
* @param posts_renderer $postsrenderer A posts renderer
* @param moodle_page $page The page this discussion is being rendered for
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param capability_manager $capabilitymanager Capability manager
* @param rating_manager $ratingmanager Rating manager
* @param sorter_entity $exportedpostsorter Sorter for the exported posts
* @param moodle_url $baseurl The base URL for the discussion
* @param array $notifications List of HTML notifications to display
*/
public function __construct(
forum_entity $forum,
discussion_entity $discussion,
int $displaymode,
renderer_base $renderer,
posts_renderer $postsrenderer,
moodle_page $page,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
capability_manager $capabilitymanager,
rating_manager $ratingmanager,
sorter_entity $exportedpostsorter,
moodle_url $baseurl,
array $notifications = []
) {
$this->forum = $forum;
$this->discussion = $discussion;
$this->displaymode = $displaymode;
$this->renderer = $renderer;
$this->postsrenderer = $postsrenderer;
$this->page = $page;
$this->baseurl = $baseurl;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->capabilitymanager = $capabilitymanager;
$this->ratingmanager = $ratingmanager;
$this->notifications = $notifications;
$this->exportedpostsorter = $exportedpostsorter;
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$this->forumrecord = $forumdatamapper->to_legacy_object($forum);
$discussiondatamapper = $this->legacydatamapperfactory->get_discussion_data_mapper();
$this->discussionrecord = $discussiondatamapper->to_legacy_object($discussion);
}
/**
* Render the discussion for the given user in the specified display mode.
*
* @param stdClass $user The user viewing the discussion
* @param post_entity $firstpost The first post in the discussion
* @param array $replies List of replies to the first post
* @return string HTML for the discussion
*/
public function render(
stdClass $user,
post_entity $firstpost,
array $replies
) : string {
global $CFG;
$displaymode = $this->displaymode;
$capabilitymanager = $this->capabilitymanager;
// Make sure we can render.
if (!$capabilitymanager->can_view_discussions($user)) {
throw new moodle_exception('noviewdiscussionspermission', 'mod_forum');
}
$posts = array_merge([$firstpost], array_values($replies));
$exporteddiscussion = $this->get_exported_discussion($user);
$exporteddiscussion = array_merge($exporteddiscussion, [
'notifications' => $this->get_notifications($user),
'html' => [
'posts' => $this->postsrenderer->render($user, [$this->forum], [$this->discussion], $posts),
'modeselectorform' => $this->get_display_mode_selector_html($displaymode),
'subscribe' => null,
'movediscussion' => null,
'pindiscussion' => null,
'neighbourlinks' => $this->get_neighbour_links_html(),
'exportdiscussion' => !empty($CFG->enableportfolios) ? $this->get_export_discussion_html() : null
]
]);
$capabilities = (array) $exporteddiscussion['capabilities'];
if ($capabilities['subscribe']) {
$exporteddiscussion['html']['subscribe'] = $this->get_subscription_button_html();
}
if ($capabilities['move']) {
$exporteddiscussion['html']['movediscussion'] = $this->get_move_discussion_html();
}
if ($capabilities['pin']) {
$exporteddiscussion['html']['pindiscussion'] = $this->get_pin_discussion_html();
}
return $this->renderer->render_from_template('mod_forum/forum_discussion', $exporteddiscussion);
}
/**
* Get the groups details for all groups available to the forum.
*
* @return stdClass[]
*/
private function get_groups_available_in_forum() : array {
$course = $this->forum->get_course_record();
$coursemodule = $this->forum->get_course_module_record();
return groups_get_all_groups($course->id, 0, $coursemodule->groupingid);
}
/**
* Get the exported discussion.
*
* @param stdClass $user The user viewing the discussion
* @return array
*/
private function get_exported_discussion(stdClass $user) : array {
$discussionexporter = $this->exporterfactory->get_discussion_exporter(
$user,
$this->forum,
$this->discussion,
$this->get_groups_available_in_forum()
);
return (array) $discussionexporter->export($this->renderer);
}
/**
* Get the HTML for the display mode selector.
*
* @param int $displaymode The current display mode
* @return string
*/
private function get_display_mode_selector_html(int $displaymode) : string {
$baseurl = $this->baseurl;
$select = new single_select(
$baseurl,
'mode',
forum_get_layout_modes(),
$displaymode,
null,
'mode'
);
$select->set_label(get_string('displaymode', 'forum'), ['class' => 'accesshide']);
return $this->renderer->render($select);
}
/**
* Get the HTML to render the subscription button.
*
* @return string
*/
private function get_subscription_button_html() : string {
global $PAGE;
$forumrecord = $this->forumrecord;
$discussion = $this->discussion;
$html = html_writer::div(
forum_get_discussion_subscription_icon($forumrecord, $discussion->get_id(), null, true),
'discussionsubscription'
);
$html .= forum_get_discussion_subscription_icon_preloaders();
// Add the subscription toggle JS.
$PAGE->requires->yui_module('moodle-mod_forum-subscriptiontoggle', 'Y.M.mod_forum.subscriptiontoggle.init');
return $html;
}
/**
* Get the HTML to render the move discussion selector and button.
*
* @return string
*/
private function get_move_discussion_html() : string {
global $DB;
$forum = $this->forum;
$discussion = $this->discussion;
$courseid = $forum->get_course_id();
// Popup menu to move discussions to other forums. The discussion in a
// single discussion forum can't be moved.
$modinfo = get_fast_modinfo($courseid);
if (isset($modinfo->instances['forum'])) {
$forummenu = [];
// Check forum types and eliminate simple discussions.
$forumcheck = $DB->get_records('forum', ['course' => $courseid], '', 'id, type');
foreach ($modinfo->instances['forum'] as $forumcm) {
if (!$forumcm->uservisible || !has_capability('mod/forum:startdiscussion',
context_module::instance($forumcm->id))) {
continue;
}
$section = $forumcm->sectionnum;
$sectionname = get_section_name($courseid, $section);
if (empty($forummenu[$section])) {
$forummenu[$section] = [$sectionname => []];
}
$forumidcompare = $forumcm->instance != $forum->get_id();
$forumtypecheck = $forumcheck[$forumcm->instance]->type !== 'single';
if ($forumidcompare and $forumtypecheck) {
$url = "/mod/forum/discuss.php?d={$discussion->get_id()}&move=$forumcm->instance&sesskey=".sesskey();
$forummenu[$section][$sectionname][$url] = format_string($forumcm->name);
}
}
if (!empty($forummenu)) {
$html = '<div class="movediscussionoption">';
$select = new url_select($forummenu, '',
['/mod/forum/discuss.php?d=' . $discussion->get_id() => get_string("movethisdiscussionto", "forum")],
'forummenu', get_string('move'));
$html .= $this->renderer->render($select);
$html .= "</div>";
return $html;
}
}
return null;
}
/**
* Get the HTML to render the pin discussion button.
*
* @return string
*/
private function get_pin_discussion_html() : string {
$discussion = $this->discussion;
if ($discussion->is_pinned()) {
$pinlink = FORUM_DISCUSSION_UNPINNED;
$pintext = get_string('discussionunpin', 'forum');
} else {
$pinlink = FORUM_DISCUSSION_PINNED;
$pintext = get_string('discussionpin', 'forum');
}
$button = new single_button(
new moodle_url('discuss.php', ['pin' => $pinlink, 'd' => $discussion->get_id()]),
$pintext,
'post'
);
return $this->renderer->render($button);
}
/**
* Get the HTML to render the export discussion button.
*
* @return string|null
*/
private function get_export_discussion_html() : ?string {
global $CFG;
require_once($CFG->libdir . '/portfoliolib.php');
$discussion = $this->discussion;
$button = new \portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', ['discussionid' => $discussion->get_id()], 'mod_forum');
$button = $button->to_html(PORTFOLIO_ADD_FULL_FORM, get_string('exportdiscussion', 'mod_forum'));
return $button ?: null;
}
/**
* Get a list of notification HTML to render in the page.
*
* @param stdClass $user The user viewing the discussion
* @return string[]
*/
private function get_notifications($user) : array {
$notifications = $this->notifications;
$discussion = $this->discussion;
$forum = $this->forum;
$renderer = $this->renderer;
if ($forum->is_discussion_locked($discussion)) {
$notifications[] = (new notification(
get_string('discussionlocked', 'forum'),
notification::NOTIFY_INFO
))
->set_extra_classes(['discussionlocked'])
->set_show_closebutton();
}
if ($forum->get_type() == 'qanda') {
if ($this->capabilitymanager->must_post_before_viewing_discussion($user, $discussion)) {
$notifications[] = (new notification(
get_string('qandanotify', 'forum')
))->set_show_closebutton(true);
}
}
if ($forum->has_blocking_enabled()) {
$notifications[] = (new notification(
get_string('thisforumisthrottled', 'forum', [
'blockafter' => $forum->get_block_after(),
'blockperiod' => get_string('secondstotime' . $forum->get_block_period())
])
))->set_show_closebutton();
}
return array_map(function($notification) {
return $notification->export_for_template($this->renderer);
}, $notifications);
}
/**
* Get HTML to display the neighbour links.
*
* @return string
*/
private function get_neighbour_links_html() : string {
$forum = $this->forum;
$coursemodule = $forum->get_course_module_record();
$neighbours = forum_get_discussion_neighbours($coursemodule, $this->discussionrecord, $this->forumrecord);
return $this->renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']);
}
}
@@ -0,0 +1,391 @@
<?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/>.
/**
* Discussion list renderer.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\renderers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\managers\capability as capability_manager;
use mod_forum\local\vaults\discussion_list as discussion_list_vault;
use renderer_base;
use stdClass;
use core\output\notification;
use mod_forum\local\factories\builder as builder_factory;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* The discussion list renderer.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_list {
/** @var forum_entity The forum being rendered */
private $forum;
/** @var stdClass The DB record for the forum being rendered */
private $forumrecord;
/** @var renderer_base The renderer used to render the view */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var capability_manager $capabilitymanager Capability manager */
private $capabilitymanager;
/** @var url_factory $urlfactory URL factory */
private $urlfactory;
/** @var array $notifications List of notification HTML */
private $notifications;
/** @var builder_factory $builderfactory Builder factory */
private $builderfactory;
/** @var callable $postprocessfortemplate Function to process exported posts before template rendering */
private $postprocessfortemplate;
/**
* Constructor for a new discussion list renderer.
*
* @param forum_entity $forum The forum entity to be rendered
* @param renderer_base $renderer The renderer used to render the view
* @param legacy_data_mapper_factory $legacydatamapperfactory The factory used to fetch a legacy record
* @param exporter_factory $exporterfactory The factory used to fetch exporter instances
* @param vault_factory $vaultfactory The factory used to fetch the vault instances
* @param builder_factory $builderfactory The factory used to fetch the builder instances
* @param capability_manager $capabilitymanager The managed used to check capabilities on the forum
* @param url_factory $urlfactory The factory used to create URLs in the forum
* @param notification[] $notifications A list of any notifications to be displayed within the page
* @param callable|null $postprocessfortemplate Callback function to process discussion lists for templates
*/
public function __construct(
forum_entity $forum,
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
builder_factory $builderfactory,
capability_manager $capabilitymanager,
url_factory $urlfactory,
array $notifications = [],
callable $postprocessfortemplate = null
) {
$this->forum = $forum;
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->builderfactory = $builderfactory;
$this->capabilitymanager = $capabilitymanager;
$this->urlfactory = $urlfactory;
$this->notifications = $notifications;
$this->postprocessfortemplate = $postprocessfortemplate;
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$this->forumrecord = $forumdatamapper->to_legacy_object($forum);
}
/**
* Render for the specified user.
*
* @param stdClass $user The user to render for
* @param cm_info $cm The course module info for this discussion list
* @param int $groupid The group to render
* @param int $sortorder The sort order to use when selecting the discussions in the list
* @param int $pageno The zero-indexed page number to use
* @param int $pagesize The number of discussions to show on the page
* @return string The rendered content for display
*/
public function render(stdClass $user, \cm_info $cm, ?int $groupid, ?int $sortorder, ?int $pageno, ?int $pagesize) : string {
global $PAGE;
$forum = $this->forum;
$pagesize = $this->get_page_size($pagesize);
$pageno = $this->get_page_number($pageno);
$groupids = $this->get_groups_from_groupid($user, $groupid);
$forumexporter = $this->exporterfactory->get_forum_exporter(
$user,
$this->forum,
$groupid
);
// Count all forum discussion posts.
$alldiscussionscount = $this->get_count_all_discussions($user, $groupids);
// Get all forum discussions posts.
$discussions = $this->get_discussions($user, $groupids, $sortorder, $pageno, $pagesize);
$forumview = [
'forum' => (array) $forumexporter->export($this->renderer),
'groupchangemenu' => groups_print_activity_menu(
$cm,
$this->urlfactory->get_forum_view_url_from_forum($forum),
true
),
'notifications' => $this->get_notifications($user, $groupid)
];
if (!$discussions) {
return $this->renderer->render_from_template($this->get_template(), $forumview);
}
if ($this->postprocessfortemplate !== null) {
// We've got some post processing to do!
$exportedposts = ($this->postprocessfortemplate) ($discussions, $user, $forum);
}
$forumview = array_merge(
$forumview,
[
'pagination' => $this->renderer->render(new \paging_bar($alldiscussionscount, $pageno, $pagesize, $PAGE->url, 'p')),
],
$exportedposts
);
return $this->renderer->render_from_template($this->get_template(), $forumview);
}
/**
* Get the list of groups to show based on the current user and requested groupid.
*
* @param stdClass $user The user viewing
* @param int $groupid The groupid requested
* @return array The list of groups to show
*/
private function get_groups_from_groupid(stdClass $user, ?int $groupid) : ?array {
$forum = $this->forum;
$effectivegroupmode = $forum->get_effective_group_mode();
if (empty($effectivegroupmode)) {
// This forum is not in a group mode. Show all posts always.
return null;
}
if (null == $groupid) {
// No group was specified.
$showallgroups = (VISIBLEGROUPS == $effectivegroupmode);
$showallgroups = $showallgroups || $this->capabilitymanager->can_access_all_groups($user);
if ($showallgroups) {
// Return null to show all groups.
return null;
} else {
// No group was specified. Only show the users current groups.
return array_keys(
groups_get_all_groups(
$forum->get_course_id(),
$user->id,
$forum->get_course_module_record()->groupingid
)
);
}
} else {
// A group was specified. Just show that group.
return [$groupid];
}
}
/**
* Fetch the data used to display the discussions on the current page.
*
* @param stdClass $user The user to render for
* @param int[]|null $groupids The group ids for this list of discussions
* @param int|null $sortorder The sort order to use when selecting the discussions in the list
* @param int|null $pageno The zero-indexed page number to use
* @param int|null $pagesize The number of discussions to show on the page
* @return stdClass The data to use for display
*/
private function get_discussions(stdClass $user, ?array $groupids, ?int $sortorder, ?int $pageno, ?int $pagesize) {
$forum = $this->forum;
$discussionvault = $this->vaultfactory->get_discussions_in_forum_vault();
if (null === $groupids) {
return $discussions = $discussionvault->get_from_forum_id(
$forum->get_id(),
$this->capabilitymanager->can_view_hidden_posts($user),
$user->id,
$sortorder,
$this->get_page_size($pagesize),
$this->get_page_number($pageno) * $this->get_page_size($pagesize));
} else {
return $discussions = $discussionvault->get_from_forum_id_and_group_id(
$forum->get_id(),
$groupids,
$this->capabilitymanager->can_view_hidden_posts($user),
$user->id,
$sortorder,
$this->get_page_size($pagesize),
$this->get_page_number($pageno) * $this->get_page_size($pagesize));
}
}
/**
* Get a count of all discussions in a forum.
*
* @param stdClass $user The user to render for
* @param array $groupids The array of groups to render
* @return int The number of discussions in a forum
*/
public function get_count_all_discussions(stdClass $user, ?array $groupids) {
$discussionvault = $this->vaultfactory->get_discussions_in_forum_vault();
if (null === $groupids) {
return $discussionvault->get_total_discussion_count_from_forum_id(
$this->forum->get_id(),
$this->capabilitymanager->can_view_hidden_posts($user),
$user->id);
} else {
return $discussionvault->get_total_discussion_count_from_forum_id_and_group_id(
$this->forum->get_id(),
$groupids,
$this->capabilitymanager->can_view_hidden_posts($user),
$user->id);
}
}
/**
* Fetch the page size to use when displaying the page.
*
* @param int $pagesize The number of discussions to show on the page
* @return int The normalised page size
*/
private function get_page_size(?int $pagesize) : int {
if (null === $pagesize || $pagesize <= 0) {
$pagesize = discussion_list_vault::PAGESIZE_DEFAULT;
}
return $pagesize;
}
/**
* Fetch the current page number (zero-indexed).
*
* @param int $pageno The zero-indexed page number to use
* @return int The normalised page number
*/
private function get_page_number(?int $pageno) : int {
if (null === $pageno || $pageno < 0) {
$pageno = 0;
}
return $pageno;
}
/**
* Fetch the name of the template to use for the current forum and view modes.
*
* @return string
*/
private function get_template() : string {
switch ($this->forum->get_type()) {
case 'news':
return 'mod_forum/news_discussion_list';
break;
case 'blog':
return 'mod_forum/blog_discussion_list';
break;
case 'qanda':
return 'mod_forum/qanda_discussion_list';
break;
default:
return 'mod_forum/discussion_list';
}
}
/**
* Get the list of notification for display.
*
* @param stdClass $user The viewing user
* @param int|null $groupid The forum's group id
* @return array
*/
private function get_notifications(stdClass $user, ?int $groupid) : array {
$notifications = $this->notifications;
$forum = $this->forum;
$renderer = $this->renderer;
$capabilitymanager = $this->capabilitymanager;
if ($forum->has_blocking_enabled()) {
$notifications[] = (new notification(
get_string('thisforumisthrottled', 'forum', [
'blockafter' => $forum->get_block_after(),
'blockperiod' => get_string('secondstotime' . $forum->get_block_period())
])
))->set_show_closebutton();
}
if ($forum->is_in_group_mode() && !$capabilitymanager->can_access_all_groups($user)) {
if ($groupid === null) {
if (!$capabilitymanager->can_post_to_my_groups($user)) {
$notifications[] = (new notification(
get_string('cannotadddiscussiongroup', 'mod_forum'),
\core\output\notification::NOTIFY_WARNING
))->set_show_closebutton();
} else {
$notifications[] = (new notification(
get_string('cannotadddiscussionall', 'mod_forum'),
\core\output\notification::NOTIFY_WARNING
))->set_show_closebutton();
}
} else if (!$capabilitymanager->can_access_group($user, $groupid)) {
$notifications[] = (new notification(
get_string('cannotadddiscussion', 'mod_forum'),
\core\output\notification::NOTIFY_WARNING
))->set_show_closebutton();
}
}
if ('qanda' === $forum->get_type() && !$capabilitymanager->can_manage_forum($user)) {
$notifications[] = (new notification(
get_string('qandanotify', 'forum'),
notification::NOTIFY_INFO
))->set_show_closebutton();
}
if ('eachuser' === $forum->get_type()) {
$notifications[] = (new notification(
get_string('allowsdiscussions', 'forum'),
notification::NOTIFY_INFO)
)->set_show_closebutton();
}
return array_map(function($notification) {
return $notification->export_for_template($this->renderer);
}, $notifications);
}
}
+111
View File
@@ -0,0 +1,111 @@
<?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/>.
/**
* Posts renderer.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\renderers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\builders\exported_posts as exported_posts_builder;
use renderer_base;
use stdClass;
/**
* Posts renderer class.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class posts {
/** @var renderer_base $renderer Renderer base */
private $renderer;
/** @var exported_posts_builder $exportedpostsbuilder Builder for building exported posts */
private $exportedpostsbuilder;
/** @var string $template The template to render */
private $template;
/** @var callable $postprocessfortemplate Function to process exported posts before template rendering */
private $postprocessfortemplate;
/**
* Constructor.
*
* @param renderer_base $renderer Renderer base
* @param exported_posts_builder $exportedpostsbuilder Builder for building exported posts
* @param string $template The template to render
* @param callable $postprocessfortemplate Function to process exported posts before template rendering
*/
public function __construct(
renderer_base $renderer,
exported_posts_builder $exportedpostsbuilder,
string $template,
callable $postprocessfortemplate = null
) {
$this->renderer = $renderer;
$this->exportedpostsbuilder = $exportedpostsbuilder;
$this->template = $template;
$this->postprocessfortemplate = $postprocessfortemplate;
}
/**
* Render the given posts for the forums and discussions.
*
* @param stdClass $user The user viewing the posts
* @param forum_entity[] $forums A list of all forums for these posts
* @param discussion_entity[] $discussions A list of all discussions for these posts
* @param post_entity[] $posts The posts to render
* @return string
*/
public function render(
stdClass $user,
array $forums,
array $discussions,
array $posts
) : string {
// Format the forums and discussion to make them more easily accessed later.
$forums = array_reduce($forums, function($carry, $forum) {
$carry[$forum->get_id()] = $forum;
return $carry;
}, []);
$discussions = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
$exportedposts = $this->exportedpostsbuilder->build(
$user,
$forums,
$discussions,
$posts
);
if ($this->postprocessfortemplate !== null) {
// We've got some post processing to do!
$exportedposts = ($this->postprocessfortemplate)($exportedposts, $forums, $discussions, $user);
}
return $this->renderer->render_from_template(
$this->template,
['posts' => array_values($exportedposts)]
);
}
}
+101
View File
@@ -0,0 +1,101 @@
<?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/>.
/**
* Author vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
/**
* Author vault class.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author extends db_table_vault {
/** The table for this vault */
private const TABLE = 'user';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias() : string {
return 'a';
}
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string {
$selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias();
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into author entities.
*
* @param array $results The DB records
* @return author_entity[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'record' => $record,
] = $result;
return $entityfactory->get_author_from_stdclass($record);
}, $results);
}
/**
* Get the authors for the given posts.
*
* Returns a distinct list of authors indexed by author id.
*
* @param post_entity[] $posts The list of posts
* @return author_entity[]
*/
public function get_authors_for_posts(array $posts) : array {
$authorids = array_reduce($posts, function($carry, $post) {
$carry[$post->get_author_id()] = true;
return $carry;
}, []);
$authorids = array_keys($authorids);
return $this->get_from_ids($authorids);
}
}
@@ -0,0 +1,173 @@
<?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/>.
/**
* Abstract class for loading records from the DB.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\factories\entity as entity_factory;
use moodle_database;
/**
* Abstract class for loading records from the DB.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class db_table_vault {
/** @var moodle_database $db A moodle database */
private $db;
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/**
* Constructor.
*
* @param moodle_database $db A moodle database
* @param entity_factory $entityfactory Entity factory
*/
public function __construct(
moodle_database $db,
entity_factory $entityfactory
) {
$this->db = $db;
$this->entityfactory = $entityfactory;
}
/**
* Get the table alias.
*
* @return string
*/
abstract protected function get_table_alias() : string;
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
abstract protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string;
/**
* Convert the DB records into entities. The list of records will have been
* passed through any preprocessors that may be defined before being given to
* this function.
*
* Each result will from the list will be an associative array where the key
* is set to the identifier given to the preprocessor and the result is the value
* generated by the preprocessor.
*
* All results will have a 'record' key who's value is the original DB record.
*
* @param array $results The DB records
*/
abstract protected function from_db_records(array $results);
/**
* Get the list of preprocessors to run on the DB record results. The preprocessors
* should be defined using an associative array. The key used to identify the
* preprocessor in this list will be used to identify the value of that preprocessor
* in the list of results when passed to the from_db_records function.
*
* @return array
*/
protected function get_preprocessors() : array {
return [];
}
/**
* Get the moodle database.
*
* @return moodle_database
*/
protected function get_db() : moodle_database {
return $this->db;
}
/**
* Get the entity factory.
*
* @return entity_factory
*/
protected function get_entity_factory() : entity_factory {
return $this->entityfactory;
}
/**
* Execute the defined preprocessors on the DB record results and then convert
* them into entities.
*
* @param stdClass[] $records List of DB results
* @return array
*/
protected function transform_db_records_to_entities(array $records) {
$preprocessors = $this->get_preprocessors();
$result = array_map(function($record) {
return ['record' => $record];
}, $records);
$result = array_reduce(array_keys($preprocessors), function($carry, $preprocessor) use ($records, $preprocessors) {
$step = $preprocessors[$preprocessor];
$dependencies = $step->execute($records);
foreach ($dependencies as $index => $dependency) {
// Add the new dependency to the list.
$carry[$index] = array_merge($carry[$index], [$preprocessor => $dependency]);
}
return $carry;
}, $result);
return $this->from_db_records($result);
}
/**
* Get the entity for the given id.
*
* @param int $id Identifier for the entity
* @return object|null
*/
public function get_from_id(int $id) {
$records = $this->get_from_ids([$id]);
return count($records) ? array_shift($records) : null;
}
/**
* Get the list of entities for the given ids.
*
* @param int[] $ids Identifiers
* @return array
*/
public function get_from_ids(array $ids) {
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($ids);
$wheresql = $alias . '.id ' . $insql;
$sql = $this->generate_get_records_sql($wheresql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
}
@@ -0,0 +1,128 @@
<?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/>.
/**
* Discussion vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\discussion as discussion_entity;
/**
* Discussion vault class.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_discussions';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias() : string {
return 'd';
}
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string {
$selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias();
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into discussion entities.
*
* @param array $results The DB records
* @return discussion_entity[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'record' => $record,
] = $result;
return $entityfactory->get_discussion_from_stdclass($record);
}, $results);
}
/**
* Get the first discussion in the specified forum.
*
* @param forum_entity $forum
* @return discussion_entity|null
*/
public function get_first_discussion_in_forum(forum_entity $forum) : ?discussion_entity {
$records = $this->get_db()->get_records(self::TABLE, [
'forum' => $forum->get_id(),
], 'timemodified ASC', '*', 0, 1);
$records = $this->transform_db_records_to_entities($records);
return count($records) ? array_shift($records) : null;
}
/**
* Get the last discussion in the specified forum.
*
* @param forum_entity $forum
* @return discussion_entity|null
*/
public function get_last_discussion_in_forum(forum_entity $forum) : ?discussion_entity {
$records = $this->get_db()->get_records(self::TABLE, [
'forum' => $forum->get_id(),
], 'timemodified DESC', '*', 0, 1);
$records = $this->transform_db_records_to_entities($records);
return count($records) ? array_shift($records) : null;
}
/**
* Get the count of the discussions in the specified forum.
*
* @param forum_entity $forum
* @return int
*/
public function get_count_discussions_in_forum(forum_entity $forum) : ?int {
return $this->get_db()->count_records(self::TABLE, [
'forum' => $forum->get_id()]);
}
}
@@ -0,0 +1,392 @@
<?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/>.
/**
* Vault class for a discussion list.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor;
use mod_forum\local\vaults\preprocessors\extract_user as extract_user_preprocessor;
use mod_forum\local\renderers\discussion_list as discussion_list_renderer;
use stdClass;
/**
* Discussion list vault.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_list extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_discussions';
/** Alias for first author id */
private const FIRST_AUTHOR_ID_ALIAS = 'userpictureid';
/** Alias for author fields */
private const FIRST_AUTHOR_ALIAS = 'userrecord';
/** Alias for last author id */
private const LATEST_AUTHOR_ID_ALIAS = 'userpictureid';
/** Alias for last author fields */
private const LATEST_AUTHOR_ALIAS = 'userrecord';
/** Default limit */
public const PAGESIZE_DEFAULT = 100;
/** Sort by newest first */
public const SORTORDER_NEWEST_FIRST = 1;
/** Sort by oldest first */
public const SORTORDER_OLDEST_FIRST = 2;
/** Sort by created desc */
public const SORTORDER_CREATED_DESC = 3;
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias() : string {
return 'd';
}
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, ?string $sortsql = null) : string {
$alias = $this->get_table_alias();
$db = $this->get_db();
// Fetch:
// - Discussion
// - First post
// - Author
// - Most recent editor.
$tablefields = $db->get_preload_columns(self::TABLE, $alias);
$postfields = $db->get_preload_columns('forum_posts', 'p_');
$firstauthorfields = \user_picture::fields('fa', null, self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS);
$latestuserfields = \user_picture::fields('la', null, self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS);
$fields = implode(', ', [
$db->get_preload_columns_sql($tablefields, $alias),
$db->get_preload_columns_sql($postfields, 'fp'),
$firstauthorfields,
$latestuserfields,
]);
$tables = '{' . self::TABLE . '} ' . $alias;
$tables .= ' JOIN {user} fa ON fa.id = ' . $alias . '.userid';
$tables .= ' JOIN {user} la ON la.id = ' . $alias . '.usermodified';
$tables .= ' JOIN {forum_posts} fp ON fp.id = ' . $alias . '.firstpost';
$selectsql = 'SELECT ' . $fields . ' FROM ' . $tables;
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Build the SQL to be used in count_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @return string
*/
protected function generate_count_records_sql(string $wheresql = null) : string {
$alias = $this->get_table_alias();
$db = $this->get_db();
$selectsql = "SELECT COUNT(1) FROM {" . self::TABLE . "} {$alias}";
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
return $selectsql;
}
/**
* Get a list of preprocessors to execute on the DB results before being converted
* into entities.
*
* @return array
*/
protected function get_preprocessors() : array {
return array_merge(
parent::get_preprocessors(),
[
'discussion' => new extract_record_preprocessor($this->get_db(), self::TABLE, $this->get_table_alias()),
'firstpost' => new extract_record_preprocessor($this->get_db(), 'forum_posts', 'p_'),
'firstpostauthor' => new extract_user_preprocessor(self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS),
'latestpostauthor' => new extract_user_preprocessor(self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS),
]
);
}
/**
* Convert the DB records into discussion list entities.
*
* @param array $results The DB records
* @return discussion_list[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'discussion' => $discussion,
'firstpost' => $firstpost,
'firstpostauthor' => $firstpostauthor,
'latestpostauthor' => $latestpostauthor,
] = $result;
return $entityfactory->get_discussion_summary_from_stdclass(
$discussion,
$firstpost,
$firstpostauthor,
$latestpostauthor
);
}, $results);
}
/**
* Get the sort order SQL for a sort method.
*
* @param int|null $sortmethod
*/
public function get_sort_order(?int $sortmethod) : string {
global $CFG;
$alias = $this->get_table_alias();
if ($sortmethod == self::SORTORDER_CREATED_DESC) {
$keyfield = "fp.created";
$direction = "DESC";
} else {
// TODO consider user favourites...
$keyfield = "{$alias}.timemodified";
$direction = "DESC";
if ($sortmethod == self::SORTORDER_OLDEST_FIRST) {
$direction = "ASC";
}
if (!empty($CFG->forum_enabletimedposts)) {
$keyfield = "CASE WHEN {$keyfield} < {$alias}.timestart THEN {$alias}.timestart ELSE {$keyfield} END";
}
}
return "{$alias}.pinned DESC, {$keyfield} {$direction}, {$alias}.id DESC";
}
/**
* Fetch any required SQL to respect timed posts.
*
* @param bool $includehiddendiscussions Whether to include hidden discussions or not
* @param int $includepostsforuser Which user to include posts for, if any
* @return array The SQL and parameters to include
*/
protected function get_hidden_post_sql(bool $includehiddendiscussions, ?int $includepostsforuser) {
$wheresql = '';
$params = [];
if (!$includehiddendiscussions) {
$now = time();
$wheresql = " AND ((d.timestart <= :timestart AND (d.timeend = 0 OR d.timeend > :timeend))";
$params['timestart'] = $now;
$params['timeend'] = $now;
if (null !== $includepostsforuser) {
$wheresql .= " OR d.userid = :byuser";
$params['byuser'] = $includepostsforuser;
}
$wheresql .= ")";
}
return [
'wheresql' => $wheresql,
'params' => $params,
];
}
/**
* Get each discussion, first post, first and last post author for the given forum, considering timed posts, and
* pagination.
*
* @param int $forumid The forum to fetch the discussion set for
* @param bool $includehiddendiscussions Whether to include hidden discussions or not
* @param int|null $includepostsforuser Which user to include posts for, if any
* @param int $sortorder The sort order to use
* @param int $limit The number of discussions to fetch
* @param int $offset The record offset
* @return array The set of data fetched
*/
public function get_from_forum_id(
int $forumid,
bool $includehiddendiscussions,
?int $includepostsforuser,
?int $sortorder,
int $limit,
int $offset
) {
$alias = $this->get_table_alias();
$wheresql = "{$alias}.forum = :forumid";
[
'wheresql' => $hiddensql,
'params' => $hiddenparams
] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser);
$wheresql .= $hiddensql;
$params = array_merge($hiddenparams, [
'forumid' => $forumid,
]);
$sql = $this->generate_get_records_sql($wheresql, $this->get_sort_order($sortorder));
$records = $this->get_db()->get_records_sql($sql, $params, $offset, $limit);
return $this->transform_db_records_to_entities($records);
}
/**
* Get each discussion, first post, first and last post author for the given forum, and the set of groups to display
* considering timed posts, and pagination.
*
* @param int $forumid The forum to fetch the discussion set for
* @param int[] $groupids The list of real groups to filter on
* @param bool $includehiddendiscussions Whether to include hidden discussions or not
* @param int|null $includepostsforuser Which user to include posts for, if any
* @param int $sortorder The sort order to use
* @param int $limit The number of discussions to fetch
* @param int $offset The record offset
* @return array The set of data fetched
*/
public function get_from_forum_id_and_group_id(
int $forumid,
array $groupids,
bool $includehiddendiscussions,
?int $includepostsforuser,
?int $sortorder,
int $limit,
int $offset
) {
$alias = $this->get_table_alias();
$wheresql = "{$alias}.forum = :forumid AND ";
$groupparams = [];
if (empty($groupids)) {
$wheresql .= "{$alias}.groupid = :allgroupsid";
} else {
list($insql, $groupparams) = $this->get_db()->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gid');
$wheresql .= "({$alias}.groupid = :allgroupsid OR {$alias}.groupid {$insql})";
}
[
'wheresql' => $hiddensql,
'params' => $hiddenparams
] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser);
$wheresql .= $hiddensql;
$params = array_merge($hiddenparams, $groupparams, [
'forumid' => $forumid,
'allgroupsid' => -1,
]);
$sql = $this->generate_get_records_sql($wheresql, $this->get_sort_order($sortorder));
$records = $this->get_db()->get_records_sql($sql, $params, $offset, $limit);
return $this->transform_db_records_to_entities($records);
}
/**
* Count the number of discussions in the forum.
*
* @param int $forumid Id of the forum to count discussions in
* @param bool $includehiddendiscussions Include hidden dicussions in the count?
* @param int|null $includepostsforuser Include discussions created by this user in the count
* (only works if not including hidden discussions).
* @return int
*/
public function get_total_discussion_count_from_forum_id(
int $forumid,
bool $includehiddendiscussions,
?int $includepostsforuser
) {
$alias = $this->get_table_alias();
$wheresql = "{$alias}.forum = :forumid";
[
'wheresql' => $hiddensql,
'params' => $hiddenparams
] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser);
$wheresql .= $hiddensql;
$params = array_merge($hiddenparams, [
'forumid' => $forumid,
]);
return $this->get_db()->count_records_sql($this->generate_count_records_sql($wheresql), $params);
}
/**
* Count the number of discussions in all groups and the list of groups provided.
*
* @param int $forumid Id of the forum to count discussions in
* @param int[] $groupids List of group ids to include in the count (discussions in all groups will always be counted)
* @param bool $includehiddendiscussions Include hidden dicussions in the count?
* @param int|null $includepostsforuser Include discussions created by this user in the count
* (only works if not including hidden discussions).
* @return int
*/
public function get_total_discussion_count_from_forum_id_and_group_id(
int $forumid,
array $groupids,
bool $includehiddendiscussions,
?int $includepostsforuser
) {
$alias = $this->get_table_alias();
$wheresql = "{$alias}.forum = :forumid AND ";
$groupparams = [];
if (empty($groupids)) {
$wheresql .= "{$alias}.groupid = :allgroupsid";
} else {
list($insql, $groupparams) = $this->get_db()->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gid');
$wheresql .= "({$alias}.groupid = :allgroupsid OR {$alias}.groupid {$insql})";
}
[
'wheresql' => $hiddensql,
'params' => $hiddenparams
] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser);
$wheresql .= $hiddensql;
$params = array_merge($hiddenparams, $groupparams, [
'forumid' => $forumid,
'allgroupsid' => -1,
]);
return $this->get_db()->count_records_sql($this->generate_count_records_sql($wheresql), $params);
}
}
+156
View File
@@ -0,0 +1,156 @@
<?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/>.
/**
* Forum vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\vaults\preprocessors\extract_context as extract_context_preprocessor;
use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor;
use context_helper;
/**
* Forum vault class.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias() : string {
return 'f';
}
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string {
$db = $this->get_db();
$alias = $this->get_table_alias();
$tablefields = $db->get_preload_columns(self::TABLE, $alias);
$coursemodulefields = $db->get_preload_columns('course_modules', 'cm_');
$coursefields = $db->get_preload_columns('course', 'c_');
$fields = implode(', ', [
$db->get_preload_columns_sql($tablefields, $alias),
context_helper::get_preload_record_columns_sql('ctx'),
$db->get_preload_columns_sql($coursemodulefields, 'cm'),
$db->get_preload_columns_sql($coursefields, 'c'),
]);
$tables = '{' . self::TABLE . '} ' . $alias;
$tables .= " JOIN {modules} m ON m.name = 'forum'";
$tables .= " JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = {$alias}.id";
$tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE . ' AND ctx.instanceid = cm.id';
$tables .= " JOIN {course} c ON c.id = {$alias}.course";
$selectsql = 'SELECT ' . $fields . ' FROM ' . $tables;
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Get a list of preprocessors to execute on the DB results before being converted
* into entities.
*
* @return array
*/
protected function get_preprocessors() : array {
return array_merge(
parent::get_preprocessors(),
[
'forum' => new extract_record_preprocessor($this->get_db(), self::TABLE, $this->get_table_alias()),
'course_module' => new extract_record_preprocessor($this->get_db(), 'course_modules', 'cm_'),
'course' => new extract_record_preprocessor($this->get_db(), 'course', 'c_'),
'context' => new extract_context_preprocessor(),
]
);
}
/**
* Convert the DB records into forum entities.
*
* @param array $results The DB records
* @return forum_entity[]
*/
protected function from_db_records(array $results) : array {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'forum' => $forumrecord,
'course_module' => $coursemodule,
'course' => $course,
'context' => $context,
] = $result;
return $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course);
}, $results);
}
/**
* Get the forum for the given course module id.
*
* @param int $id The course module id
* @return forum_entity|null
*/
public function get_from_course_module_id(int $id) : ?forum_entity {
$records = $this->get_from_course_module_ids([$id]);
return count($records) ? array_shift($records) : null;
}
/**
* Get the forums for the given course module ids
*
* @param int[] $ids The course module ids
* @return forum_entity[]
*/
public function get_from_course_module_ids(array $ids) : array {
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($ids);
$wheresql = 'cm.id ' . $insql;
$sql = $this->generate_get_records_sql($wheresql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
}
+271
View File
@@ -0,0 +1,271 @@
<?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/>.
/**
* Post vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\entity as entity_factory;
use stdClass;
/**
* Post vault class.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_posts';
/** Alias for user id */
private const USER_ID_ALIAS = 'userpictureid';
/** Alias for user fields */
private const USER_ALIAS = 'userrecord';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias() : string {
return 'p';
}
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string {
$table = self::TABLE;
$alias = $this->get_table_alias();
$fields = $alias . '.*';
$tables = "{{$table}} {$alias}";
$selectsql = "SELECT {$fields} FROM {$tables}";
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into post entities.
*
* @param array $results The DB records
* @return post_entity[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
['record' => $record] = $result;
return $entityfactory->get_post_from_stdclass($record);
}, $results);
}
/**
* Get the post ids for the given discussion.
*
* @param int $discussionid The discussion to load posts for
* @param string $orderby Order the results
* @return post_entity[]
*/
public function get_from_discussion_id(int $discussionid, string $orderby = 'created ASC') : array {
$alias = $this->get_table_alias();
$wheresql = $alias . '.discussion = ?';
$orderbysql = $alias . '.' . $orderby;
$sql = $this->generate_get_records_sql($wheresql, $orderbysql);
$records = $this->get_db()->get_records_sql($sql, [$discussionid]);
return $this->transform_db_records_to_entities($records);
}
/**
* Get the list of posts for the given discussions.
*
* @param int[] $discussionids The list of discussion ids to load posts for
* @return post_entity[]
*/
public function get_from_discussion_ids(array $discussionids) : array {
if (empty($discussionids)) {
return [];
}
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids);
$wheresql = "{$alias}.discussion {$insql}";
$sql = $this->generate_get_records_sql($wheresql, '');
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
/**
* Load a list of replies to the given post. This will load all descendants of the post.
* That is, all direct replies and replies to those replies etc.
*
* The return value will be a flat array of posts in the requested order.
*
* @param post_entity $post The post to load replies for
* @param string $orderby How to order the replies
* @return post_entity[]
*/
public function get_replies_to_post(post_entity $post, string $orderby = 'created ASC') : array {
$alias = $this->get_table_alias();
$params = [$post->get_discussion_id(), $post->get_time_created(), $post->get_id()];
// Unfortunately the best we can do to filter down the query is ignore all posts
// that were created before the given post (since they can't be replies).
$wheresql = "{$alias}.discussion = ? and {$alias}.created >= ? and {$alias}.id != ?";
$orderbysql = $alias . '.' . $orderby;
$sql = $this->generate_get_records_sql($wheresql, $orderbysql);
$records = $this->get_db()->get_records_sql($sql, $params);
$posts = $this->transform_db_records_to_entities($records);
$sorter = $this->get_entity_factory()->get_posts_sorter();
// We need to sort all of the values into the replies tree in order to capture
// the full list of descendants.
$sortedposts = $sorter->sort_into_children($posts);
$replies = [];
// From the sorted list we can grab the first elements and check if they are replies
// to the post we care about. If so we keep them.
foreach ($sortedposts as $candidate) {
[$candidatepost, $candidatereplies] = $candidate;
if ($candidatepost->has_parent() && $candidatepost->get_parent_id() == $post->get_id()) {
$replies[] = $candidate;
}
}
if (empty($replies)) {
return $replies;
}
$getreplypostids = function($candidates) use (&$getreplypostids) {
$ids = [];
foreach ($candidates as $candidate) {
[$reply, $replies] = $candidate;
$ids = array_merge($ids, [$reply->get_id()], $getreplypostids($replies));
}
return $ids;
};
// Recursively build a list of the ids of all posts in the full reply tree.
$replypostids = $getreplypostids($replies);
// Now go back and filter the original result set down to just the posts that
// we've flagged as in the reply tree. We need to filter the original set of values
// so that we can maintain the requested sort order.
return array_values(array_filter($posts, function($post) use ($replypostids) {
return in_array($post->get_id(), $replypostids);
}));
}
/**
* Get a mapping of replies to the specified discussions.
*
* @param int[] $discussionids The list of discussions to fetch counts for
* @return int[] The number of replies for each discussion returned in an associative array
*/
public function get_reply_count_for_discussion_ids(array $discussionids) : array {
if (empty($discussionids)) {
return [];
}
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids);
$sql = "SELECT discussion, COUNT(1) FROM {" . self::TABLE . "} p " .
"WHERE p.discussion {$insql} AND p.parent > 0 GROUP BY discussion";
return $this->get_db()->get_records_sql_menu($sql, $params);
}
/**
* Get a mapping of unread post counts for the specified discussions.
*
* @param stdClass $user The user to fetch counts for
* @param int[] $discussionids The list of discussions to fetch counts for
* @return int[] The count of unread posts for each discussion returned in an associative array
*/
public function get_unread_count_for_discussion_ids(stdClass $user, array $discussionids) : array {
global $CFG;
if (empty($discussionids)) {
return [];
}
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED);
$sql = "SELECT p.discussion, COUNT(p.id) FROM {" . self::TABLE . "} p
LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = :userid
WHERE p.discussion {$insql} AND p.modified > :cutofftime AND r.id IS NULL
GROUP BY p.discussion";
$params['userid'] = $user->id;
$params['cutofftime'] = floor((new \DateTime())
->sub(new \DateInterval("P{$CFG->forum_oldpostdays}D"))
->format('U') / 60) * 60;
return $this->get_db()->get_records_sql_menu($sql, $params);
}
/**
* Get a mapping of the most recent post in each discussion based on post creation time.
*
* @param int[] $discussionids The list of discussions to fetch counts for
* @return int[] The post id of the most recent post for each discussions returned in an associative array
*/
public function get_latest_post_id_for_discussion_ids(array $discussionids) : array {
global $CFG;
if (empty($discussionids)) {
return [];
}
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED);
$sql = "
SELECT p.discussion, MAX(p.id)
FROM {" . self::TABLE . "} p
JOIN (
SELECT mp.discussion, MAX(mp.created) AS created
FROM {" . self::TABLE . "} mp
WHERE mp.discussion {$insql}
GROUP BY mp.discussion
) lp ON lp.discussion = p.discussion AND lp.created = p.created
GROUP BY p.discussion";
return $this->get_db()->get_records_sql_menu($sql, $params);
}
}
@@ -0,0 +1,98 @@
<?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/>.
/**
* Post attachment vault class.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use context;
use file_storage;
/**
* Post attachment vault class.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_attachment {
/** The component for attachments */
private const COMPONENT = 'mod_forum';
/** File area for attachments */
private const FILE_AREA = 'attachment';
/** Sort the attachments by filename */
private const SORT = 'filename';
/** Don't include directories */
private const INCLUDE_DIRECTORIES = false;
/** @var file_storage $filestorage File storage */
private $filestorage;
/**
* Construct.
*
* @param file_storage $filestorage File storage
*/
public function __construct(file_storage $filestorage) {
$this->filestorage = $filestorage;
}
/**
* Get the attachments for the given posts. The results are indexed by
* post id.
*
* @param context $context The (forum) context that the posts are in
* @param post_entity[] $posts The list of posts to load attachments for
* @return array Post attachments indexed by post id
*/
public function get_attachments_for_posts(context $context, array $posts) {
$itemids = array_map(function($post) {
return $post->get_id();
}, $posts);
$files = $this->filestorage->get_area_files(
$context->id,
self::COMPONENT,
self::FILE_AREA,
$itemids,
self::SORT,
self::INCLUDE_DIRECTORIES
);
$filesbyid = array_reduce($posts, function($carry, $post) {
$carry[$post->get_id()] = [];
return $carry;
}, []);
return array_reduce($files, function($carry, $file) {
$itemid = $file->get_itemid();
$carry[$itemid] = array_merge($carry[$itemid], [$file]);
return $carry;
}, $filesbyid);
}
}
@@ -0,0 +1,120 @@
<?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/>.
/**
* Post read receipt collection class.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use stdClass;
/**
* Post read receipt collection class.
*
* This should be the only place that accessed the database.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_read_receipt_collection extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_read';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias() : string {
return 'fr';
}
/**
* Build the SQL to be used in get_records_sql.
*
* @param string|null $wheresql Where conditions for the SQL
* @param string|null $sortsql Order by conditions for the SQL
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string {
$selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias();
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into post_read_receipt_collection entities.
*
* @param array $results The DB records
* @return post_read_receipt_collection
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
$records = array_map(function($result) {
return $result['record'];
}, $results);
return $entityfactory->get_post_read_receipt_collection_from_stdclasses($records);
}
/**
* Load the post_read_receipt_collection for the given user and set
* of posts.
*
* @param int $userid Id of the user to load receipts for
* @param int[] $postids List of post ids to load receipts for
* @return post_read_receipt_collection
*/
public function get_from_user_id_and_post_ids(int $userid, array $postids) {
$alias = $this->get_table_alias();
[$postidinsql, $params] = $this->get_db()->get_in_or_equal($postids);
$params[] = $userid;
$wheresql = "{$alias}.postid {$postidinsql}";
$wheresql .= " AND {$alias}.userid = ?";
$sql = $this->generate_get_records_sql($wheresql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
/**
* Load the post_read_receipt_collection for the given user and set
* of posts.
*
* @param stdClass $user The user to load receipts for
* @param post_entity[] $posts List of posts to load receipts for
* @return post_read_receipt_collection
*/
public function get_from_user_and_posts(stdClass $user, array $posts) {
$postids = array_map(function($post) {
return $post->get_id();
}, $posts);
return $this->get_from_user_id_and_post_ids($user->id, $postids);
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Extract context vault preprocessor.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults\preprocessors;
defined('MOODLE_INTERNAL') || die();
use context;
use context_helper;
/**
* Extract context vault preprocessor.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extract_context {
/**
* Extract the contexts from a list of records.
*
* @param stdClass[] $records The list of records which have context properties
* @return context[] List of contexts matching the records.
*/
public function execute(array $records) : array {
return array_map(function($record) {
$contextid = $record->ctxid;
context_helper::preload_from_record($record);
$context = context::instance_by_id($contextid);
return $context;
}, $records);
}
}
@@ -0,0 +1,76 @@
<?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/>.
/**
* Extract record vault preprocessor.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults\preprocessors;
defined('MOODLE_INTERNAL') || die();
use moodle_database;
/**
* Extract record vault preprocessor.
*
* Extract record vault preprocessor. Typically used to separate out records
* when two tables have been joined together in a query.
*
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extract_record {
/** @var moodle_database $db A moodle database */
private $db;
/** @var string $table The table name where the records were loaded from */
private $table;
/** @var string $alias The table alias used as the record property prefix */
private $alias;
/**
* Constructor.
*
* @param moodle_database $db A moodle database
* @param string $table The table name where the records were loaded from
* @param string $alias The table alias used as the record property prefix
*/
public function __construct(moodle_database $db, string $table, string $alias) {
$this->db = $db;
$this->table = $table;
$this->alias = $alias;
}
/**
* Extract a record embedded in the properties of another record out into a
* separate record.
*
* @param stdClass[] $records The list of records to process
* @return stdClass[] The extracted records
*/
public function execute(array $records) : array {
$db = $this->db;
$fields = $this->db->get_preload_columns($this->table, $this->alias);
return array_map(function($record) use ($db, $fields) {
return $db->extract_fields_from_object($fields, $record);
}, $records);
}
}
@@ -0,0 +1,71 @@
<?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/>.
/**
* Extract user vault preprocessor.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults\preprocessors;
defined('MOODLE_INTERNAL') || die();
use user_picture;
/**
* Extract user vault preprocessor.
*
* Used to separate out the user record
* from a list of DB records that have been joined on the user table.
*
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extract_user {
/** @var string $idalias The alias for the id property of the user */
private $idalias;
/** @var string $alias The prefix used for each of the user properties */
private $alias;
/**
* Constructor.
*
* @param string $idalias The alias for the id property of the user
* @param string $alias The prefix used for each of the user properties
*/
public function __construct(string $idalias, string $alias) {
$this->idalias = $idalias;
$this->alias = $alias;
}
/**
* Extract the user records from a list of DB records.
*
* @param stdClass[] $records The DB records
* @return stdClass[] The list of extracted users
*/
public function execute(array $records) : array {
$idalias = $this->idalias;
$alias = $this->alias;
return array_map(function($record) use ($idalias, $alias) {
return user_picture::unalias($record, null, $idalias, $alias);
}, $records);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - https://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/>.
/**
* Defined caches used internally by the plugin.
*
* @package mod_forum
* @category cache
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$definitions = [
'forum_is_tracked' => [
'mode' => cache_store::MODE_REQUEST,
'simplekeys' => true,
'simpledata' => true,
'staticacceleration' => true
],
];
+10
View File
@@ -113,4 +113,14 @@ $functions = array(
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'mod_forum_set_subscription_state' => array(
'classname' => 'mod_forum_external',
'methodname' => 'set_subscription_state',
'classpath' => 'mod/forum/externallib.php',
'description' => 'Set the subscription state',
'type' => 'write',
'ajax' => true,
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
);
+902
View File
@@ -410,3 +410,905 @@ function forum_cron() {
debugging("forum_cron() has been deprecated and replaced with new tasks. Please uses these instead.",
DEBUG_DEVELOPER);
}
/**
* Prints a forum discussion
*
* @uses CONTEXT_MODULE
* @uses FORUM_MODE_FLATNEWEST
* @uses FORUM_MODE_FLATOLDEST
* @uses FORUM_MODE_THREADED
* @uses FORUM_MODE_NESTED
* @param stdClass $course
* @param stdClass $cm
* @param stdClass $forum
* @param stdClass $discussion
* @param stdClass $post
* @param int $mode
* @param mixed $canreply
* @param bool $canrate
* @deprecated since Moodle 3.7
*/
function forum_print_discussion($course, $cm, $forum, $discussion, $post, $mode, $canreply=NULL, $canrate=false) {
debugging('forum_print_discussion() has been deprecated, ' .
'please use \mod_forum\local\renderers\discussion instead.', DEBUG_DEVELOPER);
global $USER, $CFG;
require_once($CFG->dirroot.'/rating/lib.php');
$ownpost = (isloggedin() && $USER->id == $post->userid);
$modcontext = context_module::instance($cm->id);
if ($canreply === NULL) {
$reply = forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext);
} else {
$reply = $canreply;
}
// $cm holds general cache for forum functions
$cm->cache = new stdClass;
$cm->cache->groups = groups_get_all_groups($course->id, 0, $cm->groupingid);
$cm->cache->usersgroups = array();
$posters = array();
// preload all posts - TODO: improve...
if ($mode == FORUM_MODE_FLATNEWEST) {
$sort = "p.created DESC";
} else {
$sort = "p.created ASC";
}
$forumtracked = forum_tp_is_tracked($forum);
$posts = forum_get_all_discussion_posts($discussion->id, $sort, $forumtracked);
$post = $posts[$post->id];
foreach ($posts as $pid=>$p) {
$posters[$p->userid] = $p->userid;
}
// preload all groups of ppl that posted in this discussion
if ($postersgroups = groups_get_all_groups($course->id, $posters, $cm->groupingid, 'gm.id, gm.groupid, gm.userid')) {
foreach($postersgroups as $pg) {
if (!isset($cm->cache->usersgroups[$pg->userid])) {
$cm->cache->usersgroups[$pg->userid] = array();
}
$cm->cache->usersgroups[$pg->userid][$pg->groupid] = $pg->groupid;
}
unset($postersgroups);
}
//load ratings
if ($forum->assessed != RATING_AGGREGATE_NONE) {
$ratingoptions = new stdClass;
$ratingoptions->context = $modcontext;
$ratingoptions->component = 'mod_forum';
$ratingoptions->ratingarea = 'post';
$ratingoptions->items = $posts;
$ratingoptions->aggregate = $forum->assessed;//the aggregation method
$ratingoptions->scaleid = $forum->scale;
$ratingoptions->userid = $USER->id;
if ($forum->type == 'single' or !$discussion->id) {
$ratingoptions->returnurl = "$CFG->wwwroot/mod/forum/view.php?id=$cm->id";
} else {
$ratingoptions->returnurl = "$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id";
}
$ratingoptions->assesstimestart = $forum->assesstimestart;
$ratingoptions->assesstimefinish = $forum->assesstimefinish;
$rm = new rating_manager();
$posts = $rm->get_ratings($ratingoptions);
}
$post->forum = $forum->id; // Add the forum id to the post object, later used by forum_print_post
$post->forumtype = $forum->type;
$post->subject = format_string($post->subject);
$postread = !empty($post->postread);
forum_print_post_start($post);
forum_print_post($post, $discussion, $forum, $cm, $course, $ownpost, $reply, false,
'', '', $postread, true, $forumtracked);
switch ($mode) {
case FORUM_MODE_FLATOLDEST :
case FORUM_MODE_FLATNEWEST :
default:
forum_print_posts_flat($course, $cm, $forum, $discussion, $post, $mode, $reply, $forumtracked, $posts);
break;
case FORUM_MODE_THREADED :
forum_print_posts_threaded($course, $cm, $forum, $discussion, $post, 0, $reply, $forumtracked, $posts);
break;
case FORUM_MODE_NESTED :
forum_print_posts_nested($course, $cm, $forum, $discussion, $post, $reply, $forumtracked, $posts);
break;
}
forum_print_post_end($post);
}
/**
* Return a static array of posts that are open.
*
* @return array
* @deprecated since Moodle 3.7
*/
function forum_post_nesting_cache() {
debugging('forum_post_nesting_cache() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
static $nesting = array();
return $nesting;
}
/**
* Return true for the first time this post was started
*
* @param int $id The id of the post to start
* @return bool
* @deprecated since Moodle 3.7
*/
function forum_should_start_post_nesting($id) {
debugging('forum_should_start_post_nesting() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
$cache = forum_post_nesting_cache();
if (!array_key_exists($id, $cache)) {
$cache[$id] = 1;
return true;
} else {
$cache[$id]++;
return false;
}
}
/**
* Return true when all the opens are nested with a close.
*
* @param int $id The id of the post to end
* @return bool
* @deprecated since Moodle 3.7
*/
function forum_should_end_post_nesting($id) {
debugging('forum_should_end_post_nesting() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
$cache = forum_post_nesting_cache();
if (!array_key_exists($id, $cache)) {
return true;
} else {
$cache[$id]--;
if ($cache[$id] == 0) {
unset($cache[$id]);
return true;
}
}
return false;
}
/**
* Start a forum post container
*
* @param object $post The post to print.
* @param bool $return Return the string or print it
* @return string
* @deprecated since Moodle 3.7
*/
function forum_print_post_start($post, $return = false) {
debugging('forum_print_post_start() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
$output = '';
if (forum_should_start_post_nesting($post->id)) {
$attributes = [
'id' => 'p'.$post->id,
'tabindex' => -1,
'class' => 'relativelink'
];
$output .= html_writer::start_tag('article', $attributes);
}
if ($return) {
return $output;
}
echo $output;
return;
}
/**
* End a forum post container
*
* @param object $post The post to print.
* @param bool $return Return the string or print it
* @return string
* @deprecated since Moodle 3.7
*/
function forum_print_post_end($post, $return = false) {
debugging('forum_print_post_end() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
$output = '';
if (forum_should_end_post_nesting($post->id)) {
$output .= html_writer::end_tag('article');
}
if ($return) {
return $output;
}
echo $output;
return;
}
/**
* Print a forum post
* This function should always be surrounded with calls to forum_print_post_start
* and forum_print_post_end to create the surrounding container for the post.
* Replies can be nested before forum_print_post_end and should reflect the structure of
* thread.
*
* @global object
* @global object
* @uses FORUM_MODE_THREADED
* @uses PORTFOLIO_FORMAT_PLAINHTML
* @uses PORTFOLIO_FORMAT_FILE
* @uses PORTFOLIO_FORMAT_RICHHTML
* @uses PORTFOLIO_ADD_TEXT_LINK
* @uses CONTEXT_MODULE
* @param object $post The post to print.
* @param object $discussion
* @param object $forum
* @param object $cm
* @param object $course
* @param boolean $ownpost Whether this post belongs to the current user.
* @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
* @param boolean $link Just print a shortened version of the post as a link to the full post.
* @param string $footer Extra stuff to print after the message.
* @param string $highlight Space-separated list of terms to highlight.
* @param int $post_read true, false or -99. If we already know whether this user
* has read this post, pass that in, otherwise, pass in -99, and this
* function will work it out.
* @param boolean $dummyifcantsee When forum_user_can_see_post says that
* the current user can't see this post, if this argument is true
* (the default) then print a dummy 'you can't see this post' post.
* If false, don't output anything at all.
* @param bool|null $istracked
* @return void
* @deprecated since Moodle 3.7
*/
function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
$footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
debugging('forum_print_post() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
global $USER, $CFG, $OUTPUT;
require_once($CFG->libdir . '/filelib.php');
// String cache
static $str;
// This is an extremely hacky way to ensure we only print the 'unread' anchor
// the first time we encounter an unread post on a page. Ideally this would
// be moved into the caller somehow, and be better testable. But at the time
// of dealing with this bug, this static workaround was the most surgical and
// it fits together with only printing th unread anchor id once on a given page.
static $firstunreadanchorprinted = false;
$modcontext = context_module::instance($cm->id);
$post->course = $course->id;
$post->forum = $forum->id;
$post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir.'/plagiarismlib.php');
$post->message .= plagiarism_get_links(array('userid' => $post->userid,
'content' => $post->message,
'cmid' => $cm->id,
'course' => $post->course,
'forum' => $post->forum));
}
// caching
if (!isset($cm->cache)) {
$cm->cache = new stdClass;
}
if (!isset($cm->cache->caps)) {
$cm->cache->caps = array();
$cm->cache->caps['mod/forum:viewdiscussion'] = has_capability('mod/forum:viewdiscussion', $modcontext);
$cm->cache->caps['moodle/site:viewfullnames'] = has_capability('moodle/site:viewfullnames', $modcontext);
$cm->cache->caps['mod/forum:editanypost'] = has_capability('mod/forum:editanypost', $modcontext);
$cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
$cm->cache->caps['mod/forum:deleteownpost'] = has_capability('mod/forum:deleteownpost', $modcontext);
$cm->cache->caps['mod/forum:deleteanypost'] = has_capability('mod/forum:deleteanypost', $modcontext);
$cm->cache->caps['mod/forum:viewanyrating'] = has_capability('mod/forum:viewanyrating', $modcontext);
$cm->cache->caps['mod/forum:exportpost'] = has_capability('mod/forum:exportpost', $modcontext);
$cm->cache->caps['mod/forum:exportownpost'] = has_capability('mod/forum:exportownpost', $modcontext);
}
if (!isset($cm->uservisible)) {
$cm->uservisible = \core_availability\info_module::is_user_visible($cm, 0, false);
}
if ($istracked && is_null($postisread)) {
$postisread = forum_tp_is_post_read($USER->id, $post);
}
if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
// Do _not_ check the deleted flag - we need to display a different UI.
$output = '';
if (!$dummyifcantsee) {
if ($return) {
return $output;
}
echo $output;
return;
}
$output .= html_writer::start_tag('div', array('class' => 'forumpost clearfix',
'aria-label' => get_string('hiddenforumpost', 'forum')));
$output .= html_writer::start_tag('header', array('class' => 'row header'));
$output .= html_writer::tag('div', '', array('class' => 'left picture', 'role' => 'presentation')); // Picture.
if ($post->parent) {
$output .= html_writer::start_tag('div', array('class' => 'topic'));
} else {
$output .= html_writer::start_tag('div', array('class' => 'topic starter'));
}
$output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class' => 'subject',
'role' => 'header',
'id' => ('headp' . $post->id))); // Subject.
$authorclasses = array('class' => 'author');
$output .= html_writer::tag('address', get_string('forumauthorhidden', 'forum'), $authorclasses); // Author.
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('header'); // Header.
$output .= html_writer::start_tag('div', array('class'=>'row'));
$output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
$output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
$output .= html_writer::end_tag('div'); // row
$output .= html_writer::end_tag('div'); // forumpost
if ($return) {
return $output;
}
echo $output;
return;
}
if (!empty($post->deleted)) {
// Note: Posts marked as deleted are still returned by the above forum_user_can_post because it is required for
// nesting of posts.
$output = '';
if (!$dummyifcantsee) {
if ($return) {
return $output;
}
echo $output;
return;
}
$output .= html_writer::start_tag('div', [
'class' => 'forumpost clearfix',
'aria-label' => get_string('forumbodydeleted', 'forum'),
]);
$output .= html_writer::start_tag('header', array('class' => 'row header'));
$output .= html_writer::tag('div', '', array('class' => 'left picture', 'role' => 'presentation'));
$classes = ['topic'];
if (!empty($post->parent)) {
$classes[] = 'starter';
}
$output .= html_writer::start_tag('div', ['class' => implode(' ', $classes)]);
// Subject.
$output .= html_writer::tag('div', get_string('forumsubjectdeleted', 'forum'), [
'class' => 'subject',
'role' => 'header',
'id' => ('headp' . $post->id)
]);
// Author.
$output .= html_writer::tag('address', '', ['class' => 'author']);
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('header'); // End header.
$output .= html_writer::start_tag('div', ['class' => 'row']);
$output .= html_writer::tag('div', '&nbsp;', ['class' => 'left side']); // Groups.
$output .= html_writer::tag('div', get_string('forumbodydeleted', 'forum'), ['class' => 'content']); // Content.
$output .= html_writer::end_tag('div'); // End row.
$output .= html_writer::end_tag('div'); // End forumpost.
if ($return) {
return $output;
}
echo $output;
return;
}
if (empty($str)) {
$str = new stdClass;
$str->edit = get_string('edit', 'forum');
$str->delete = get_string('delete', 'forum');
$str->reply = get_string('reply', 'forum');
$str->parent = get_string('parent', 'forum');
$str->pruneheading = get_string('pruneheading', 'forum');
$str->prune = get_string('prune', 'forum');
$str->displaymode = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
$str->markread = get_string('markread', 'forum');
$str->markunread = get_string('markunread', 'forum');
}
$discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
// Build an object that represents the posting user
$postuser = new stdClass;
$postuserfields = explode(',', user_picture::fields());
$postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
$postuser->id = $post->userid;
$postuser->fullname = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
$postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
// Prepare the groups the posting user belongs to
if (isset($cm->cache->usersgroups)) {
$groups = array();
if (isset($cm->cache->usersgroups[$post->userid])) {
foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
$groups[$gid] = $cm->cache->groups[$gid];
}
}
} else {
$groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
}
// Prepare the attachements for the post, files then images
list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
// Determine if we need to shorten this post
$shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
// Prepare an array of commands
$commands = array();
// Add a permalink.
$permalink = new moodle_url($discussionlink);
$permalink->set_anchor('p' . $post->id);
$commands[] = array('url' => $permalink, 'text' => get_string('permalink', 'forum'), 'attributes' => ['rel' => 'bookmark']);
// SPECIAL CASE: The front page can display a news item post to non-logged in users.
// Don't display the mark read / unread controls in this case.
if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
$url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
$text = $str->markunread;
if (!$postisread) {
$url->param('mark', 'read');
$text = $str->markread;
}
if ($str->displaymode == FORUM_MODE_THREADED) {
$url->param('parent', $post->parent);
} else {
$url->set_anchor('p'.$post->id);
}
$commands[] = array('url'=>$url, 'text'=>$text, 'attributes' => ['rel' => 'bookmark']);
}
// Zoom in to the parent specifically
if ($post->parent) {
$url = new moodle_url($discussionlink);
if ($str->displaymode == FORUM_MODE_THREADED) {
$url->param('parent', $post->parent);
} else {
$url->set_anchor('p'.$post->parent);
}
$commands[] = array('url'=>$url, 'text'=>$str->parent, 'attributes' => ['rel' => 'bookmark']);
}
// Hack for allow to edit news posts those are not displayed yet until they are displayed
$age = time() - $post->created;
if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
$age = 0;
}
if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
if (has_capability('moodle/course:manageactivities', $modcontext)) {
// The first post in single simple is the forum description.
$commands[] = array('url'=>new moodle_url('/course/modedit.php', array('update'=>$cm->id, 'sesskey'=>sesskey(), 'return'=>1)), 'text'=>$str->edit);
}
} else if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
$commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
}
if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
$commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
}
if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
// Do not allow deleting of first post in single simple type.
} else if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
$commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
}
if ($reply) {
$commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
}
if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
$p = array('postid' => $post->id);
require_once($CFG->libdir.'/portfoliolib.php');
$button = new portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), 'mod_forum');
if (empty($attachments)) {
$button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
} else {
$button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
}
$porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
if (!empty($porfoliohtml)) {
$commands[] = $porfoliohtml;
}
}
// Finished building commands
// Begin output
$output = '';
if ($istracked) {
if ($postisread) {
$forumpostclass = ' read';
} else {
$forumpostclass = ' unread';
// If this is the first unread post printed then give it an anchor and id of unread.
if (!$firstunreadanchorprinted) {
$output .= html_writer::tag('a', '', array('id' => 'unread'));
$firstunreadanchorprinted = true;
}
}
} else {
// ignore trackign status if not tracked or tracked param missing
$forumpostclass = '';
}
$topicclass = '';
if (empty($post->parent)) {
$topicclass = ' firstpost starter';
}
if (!empty($post->lastpost)) {
$forumpostclass .= ' lastpost';
}
// Flag to indicate whether we should hide the author or not.
$authorhidden = forum_is_author_hidden($post, $forum);
$postbyuser = new stdClass;
$postbyuser->post = $post->subject;
$postbyuser->user = $postuser->fullname;
$discussionbyuser = get_string('postbyuser', 'forum', $postbyuser);
// Begin forum post.
$output .= html_writer::start_div('forumpost clearfix' . $forumpostclass . $topicclass,
['aria-label' => $discussionbyuser]);
// Begin header row.
$output .= html_writer::start_tag('header', ['class' => 'row header clearfix']);
// User picture.
if (!$authorhidden) {
$picture = $OUTPUT->user_picture($postuser, ['courseid' => $course->id]);
$output .= html_writer::div($picture, 'left picture', ['role' => 'presentation']);
$topicclass = 'topic' . $topicclass;
}
// Begin topic column.
$output .= html_writer::start_div($topicclass);
$postsubject = $post->subject;
if (empty($post->subjectnoformat)) {
$postsubject = format_string($postsubject);
}
$output .= html_writer::div($postsubject, 'subject', ['role' => 'heading', 'aria-level' => '1', 'id' => ('headp' . $post->id)]);
if ($authorhidden) {
$bytext = userdate_htmltime($post->created);
} else {
$by = new stdClass();
$by->date = userdate_htmltime($post->created);
$by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
$bytext = get_string('bynameondate', 'forum', $by);
}
$bytextoptions = [
'class' => 'author'
];
$output .= html_writer::tag('address', $bytext, $bytextoptions);
// End topic column.
$output .= html_writer::end_div();
// End header row.
$output .= html_writer::end_tag('header');
// Row with the forum post content.
$output .= html_writer::start_div('row maincontent clearfix');
// Show if author is not hidden or we have groups.
if (!$authorhidden || $groups) {
$output .= html_writer::start_div('left');
$groupoutput = '';
if ($groups) {
$groupoutput = print_group_picture($groups, $course->id, false, true, true);
}
if (empty($groupoutput)) {
$groupoutput = '&nbsp;';
}
$output .= html_writer::div($groupoutput, 'grouppictures');
$output .= html_writer::end_div(); // Left side.
}
$output .= html_writer::start_tag('div', array('class'=>'no-overflow'));
$output .= html_writer::start_tag('div', array('class'=>'content'));
$options = new stdClass;
$options->para = false;
$options->trusted = $post->messagetrust;
$options->context = $modcontext;
if ($shortenpost) {
// Prepare shortened version by filtering the text then shortening it.
$postclass = 'shortenedpost';
$postcontent = format_text($post->message, $post->messageformat, $options);
$postcontent = shorten_text($postcontent, $CFG->forum_shortpost);
$postcontent .= html_writer::link($discussionlink, get_string('readtherest', 'forum'));
$postcontent .= html_writer::tag('div', '('.get_string('numwords', 'moodle', count_words($post->message)).')',
array('class'=>'post-word-count'));
} else {
// Prepare whole post
$postclass = 'fullpost';
$postcontent = format_text($post->message, $post->messageformat, $options, $course->id);
if (!empty($highlight)) {
$postcontent = highlight($highlight, $postcontent);
}
if (!empty($forum->displaywordcount)) {
$postcontent .= html_writer::tag('div', get_string('numwords', 'moodle', count_words($postcontent)),
array('class'=>'post-word-count'));
}
$postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
}
if (\core_tag_tag::is_enabled('mod_forum', 'forum_posts')) {
$postcontent .= $OUTPUT->tag_list(core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id), null, 'forum-tags');
}
// Output the post content
$output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
$output .= html_writer::end_tag('div'); // Content
$output .= html_writer::end_tag('div'); // Content mask
$output .= html_writer::end_tag('div'); // Row
$output .= html_writer::start_tag('nav', array('class' => 'row side'));
$output .= html_writer::tag('div','&nbsp;', array('class'=>'left'));
$output .= html_writer::start_tag('div', array('class'=>'options clearfix'));
if (!empty($attachments)) {
$output .= html_writer::tag('div', $attachments, array('class' => 'attachments'));
}
// Output ratings
if (!empty($post->rating)) {
$output .= html_writer::tag('div', $OUTPUT->render($post->rating), array('class'=>'forum-post-rating'));
}
// Output the commands
$commandhtml = array();
foreach ($commands as $command) {
if (is_array($command)) {
$attributes = ['class' => 'nav-item nav-link'];
if (isset($command['attributes'])) {
$attributes = array_merge($attributes, $command['attributes']);
}
$commandhtml[] = html_writer::link($command['url'], $command['text'], $attributes);
} else {
$commandhtml[] = $command;
}
}
$output .= html_writer::tag('div', implode(' ', $commandhtml), array('class' => 'commands nav'));
// Output link to post if required
if ($link) {
if (forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext)) {
$langstring = 'discussthistopic';
} else {
$langstring = 'viewthediscussion';
}
if ($post->replies == 1) {
$replystring = get_string('repliesone', 'forum', $post->replies);
} else {
$replystring = get_string('repliesmany', 'forum', $post->replies);
}
if (!empty($discussion->unread) && $discussion->unread !== '-') {
$replystring .= ' <span class="sep">/</span> <span class="unread">';
$unreadlink = new moodle_url($discussionlink, null, 'unread');
if ($discussion->unread == 1) {
$replystring .= html_writer::link($unreadlink, get_string('unreadpostsone', 'forum'));
} else {
$replystring .= html_writer::link($unreadlink, get_string('unreadpostsnumber', 'forum', $discussion->unread));
}
$replystring .= '</span>';
}
$output .= html_writer::start_tag('div', array('class'=>'link'));
$output .= html_writer::link($discussionlink, get_string($langstring, 'forum'));
$output .= '&nbsp;('.$replystring.')';
$output .= html_writer::end_tag('div'); // link
}
// Output footer if required
if ($footer) {
$output .= html_writer::tag('div', $footer, array('class'=>'footer'));
}
// Close remaining open divs
$output .= html_writer::end_tag('div'); // content
$output .= html_writer::end_tag('nav'); // row
$output .= html_writer::end_tag('div'); // forumpost
// Mark the forum post as read if required
if ($istracked && !$CFG->forum_usermarksread && !$postisread) {
forum_tp_mark_post_read($USER->id, $post);
}
if ($return) {
return $output;
}
echo $output;
return;
}
/**
* @global object
* @global object
* @uses FORUM_MODE_FLATNEWEST
* @param object $course
* @param object $cm
* @param object $forum
* @param object $discussion
* @param object $post
* @param object $mode
* @param bool $reply
* @param bool $forumtracked
* @param array $posts
* @return void
* @deprecated since Moodle 3.7
*/
function forum_print_posts_flat($course, &$cm, $forum, $discussion, $post, $mode, $reply, $forumtracked, $posts) {
debugging('forum_print_posts_flat() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
global $USER, $CFG;
$link = false;
foreach ($posts as $post) {
if (!$post->parent) {
continue;
}
$post->subject = format_string($post->subject);
$ownpost = ($USER->id == $post->userid);
$postread = !empty($post->postread);
forum_print_post_start($post);
forum_print_post($post, $discussion, $forum, $cm, $course, $ownpost, $reply, $link,
'', '', $postread, true, $forumtracked);
forum_print_post_end($post);
}
}
/**
* @todo Document this function
*
* @global object
* @global object
* @uses CONTEXT_MODULE
* @return void
* @deprecated since Moodle 3.7
*/
function forum_print_posts_threaded($course, &$cm, $forum, $discussion, $parent, $depth, $reply, $forumtracked, $posts) {
debugging('forum_print_posts_threaded() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
global $USER, $CFG;
$link = false;
if (!empty($posts[$parent->id]->children)) {
$posts = $posts[$parent->id]->children;
$modcontext = context_module::instance($cm->id);
$canviewfullnames = has_capability('moodle/site:viewfullnames', $modcontext);
foreach ($posts as $post) {
echo '<div class="indent">';
if ($depth > 0) {
$ownpost = ($USER->id == $post->userid);
$post->subject = format_string($post->subject);
$postread = !empty($post->postread);
forum_print_post_start($post);
forum_print_post($post, $discussion, $forum, $cm, $course, $ownpost, $reply, $link,
'', '', $postread, true, $forumtracked);
forum_print_post_end($post);
} else {
if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, true)) {
if (forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
// This post has been deleted but still exists and may have children.
$subject = get_string('privacy:request:delete:post:subject', 'mod_forum');
$byline = '';
} else {
// The user can't see this post at all.
echo "</div>\n";
continue;
}
} else {
$by = new stdClass();
$by->name = fullname($post, $canviewfullnames);
$by->date = userdate_htmltime($post->modified);
$byline = ' ' . get_string("bynameondate", "forum", $by);
$subject = format_string($post->subject, true);
}
if ($forumtracked) {
if (!empty($post->postread)) {
$style = '<span class="forumthread read">';
} else {
$style = '<span class="forumthread unread">';
}
} else {
$style = '<span class="forumthread">';
}
echo $style;
echo "<a name='{$post->id}'></a>";
echo html_writer::link(new moodle_url('/mod/forum/discuss.php', [
'd' => $post->discussion,
'parent' => $post->id,
]), $subject);
echo $byline;
echo "</span>";
}
forum_print_posts_threaded($course, $cm, $forum, $discussion, $post, $depth-1, $reply, $forumtracked, $posts);
echo "</div>\n";
}
}
}
/**
* @todo Document this function
* @global object
* @global object
* @return void
* @deprecated since Moodle 3.7
*/
function forum_print_posts_nested($course, &$cm, $forum, $discussion, $parent, $reply, $forumtracked, $posts) {
debugging('forum_print_posts_nested() has been deprecated, ' .
'please use \mod_forum\local\renderers\posts instead.', DEBUG_DEVELOPER);
global $USER, $CFG;
$link = false;
if (!empty($posts[$parent->id]->children)) {
$posts = $posts[$parent->id]->children;
foreach ($posts as $post) {
echo '<div class="indent">';
if (!isloggedin()) {
$ownpost = false;
} else {
$ownpost = ($USER->id == $post->userid);
}
$post->subject = format_string($post->subject);
$postread = !empty($post->postread);
forum_print_post_start($post);
forum_print_post($post, $discussion, $forum, $cm, $course, $ownpost, $reply, $link,
'', '', $postread, true, $forumtracked);
forum_print_posts_nested($course, $cm, $forum, $discussion, $post, $reply, $forumtracked, $posts);
forum_print_post_end($post);
echo "</div>\n";
}
}
}
+120 -204
View File
@@ -40,42 +40,80 @@ if ($parent !== 0) {
}
$PAGE->set_url($url);
$discussion = $DB->get_record('forum_discussions', array('id' => $d), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id' => $discussion->course), '*', MUST_EXIST);
$forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
$vaultfactory = mod_forum\local\container::get_vault_factory();
$discussionvault = $vaultfactory->get_discussion_vault();
$discussion = $discussionvault->get_from_id($d);
if (!$discussion) {
throw new \moodle_exception('Unable to find discussion with id ' . $discussionid);
}
$forumvault = $vaultfactory->get_forum_vault();
$forum = $forumvault->get_from_id($discussion->get_forum_id());
if (!$forum) {
throw new \moodle_exception('Unable to find forum with id ' . $discussion->get_forum_id());
}
$managerfactory = mod_forum\local\container::get_manager_factory();
$capabilitymanager = $managerfactory->get_capability_manager($forum);
$urlfactory = mod_forum\local\container::get_url_factory();
// Make sure we can render.
if (!$capabilitymanager->can_view_discussions($USER)) {
throw new moodle_exception('noviewdiscussionspermission', 'mod_forum');
}
$datamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
$forumdatamapper = $datamapperfactory->get_forum_data_mapper();
$forumrecord = $forumdatamapper->to_legacy_object($forum);
$discussiondatamapper = $datamapperfactory->get_discussion_data_mapper();
$discussionrecord = $discussiondatamapper->to_legacy_object($discussion);
$discussionviewurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
$course = $forum->get_course_record();
$cm = $forum->get_course_module_record();
require_course_login($course, true, $cm);
// move this down fix for MDL-6926
require_once($CFG->dirroot.'/mod/forum/lib.php');
require_once($CFG->dirroot . '/mod/forum/lib.php');
$modcontext = context_module::instance($cm->id);
require_capability('mod/forum:viewdiscussion', $modcontext, NULL, true, 'noviewdiscussionspermission', 'forum');
$modcontext = $forum->get_context();
if (!empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds) && $forum->rsstype && $forum->rssarticles) {
if (
!empty($CFG->enablerssfeeds) &&
!empty($CFG->forum_enablerssfeeds) &&
$forum->get_rss_type() &&
$forum->get_rss_articles()
) {
require_once("$CFG->libdir/rsslib.php");
$rsstitle = format_string($course->shortname, true, array('context' => context_course::instance($course->id))) . ': ' . format_string($forum->name);
rss_add_http_header($modcontext, 'mod_forum', $forum, $rsstitle);
$rsstitle = format_string(
$course->shortname,
true,
['context' => context_course::instance($course->id)]
);
$rsstitle .= ': ' . format_string($forum->get_name());
rss_add_http_header($modcontext, 'mod_forum', $forumrecord, $rsstitle);
}
// Move discussion if requested.
if ($move > 0 and confirm_sesskey()) {
$return = $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
if ($move > 0 && confirm_sesskey()) {
$forumid = $forum->get_id();
$discussionid = $discussion->get_id();
$return = $discussionviewurl->out(false);
if (!$forumto = $DB->get_record('forum', array('id' => $move))) {
if (!$forumto = $DB->get_record('forum', ['id' => $move])) {
print_error('cannotmovetonotexist', 'forum', $return);
}
require_capability('mod/forum:movediscussions', $modcontext);
if ($forum->type == 'single') {
print_error('cannotmovefromsingleforum', 'forum', $return);
}
if (!$forumto = $DB->get_record('forum', array('id' => $move))) {
print_error('cannotmovetonotexist', 'forum', $return);
if (!$capabilitymanager->can_move_discussions($USER)) {
if ($forum->get_type() == 'single') {
print_error('cannotmovefromsingleforum', 'forum', $return);
} else {
print_error('nopermissions', 'error', $return, get_capability_string('mod/forum:movediscussions'));
}
}
if ($forumto->type == 'single') {
@@ -88,6 +126,7 @@ if ($move > 0 and confirm_sesskey()) {
if (!array_key_exists($forumto->id, $forums)) {
print_error('cannotmovetonotfound', 'forum', $return);
}
$cmto = $forums[$forumto->id];
if (!$cmto->uservisible) {
print_error('cannotmovenotvisible', 'forum', $return);
@@ -96,13 +135,13 @@ if ($move > 0 and confirm_sesskey()) {
$destinationctx = context_module::instance($cmto->id);
require_capability('mod/forum:startdiscussion', $destinationctx);
if (!forum_move_attachments($discussion, $forum->id, $forumto->id)) {
if (!forum_move_attachments($discussionrecord, $forumid, $forumto->id)) {
echo $OUTPUT->notification("Errors occurred while moving attachment directories - check your file permissions");
}
// For each subscribed user in this forum and discussion, copy over per-discussion subscriptions if required.
$discussiongroup = $discussion->groupid == -1 ? 0 : $discussion->groupid;
$discussiongroup = $discussion->get_group_id() == -1 ? 0 : $discussion->get_group_id();
$potentialsubscribers = \mod_forum\subscriptions::fetch_subscribed_users(
$forum,
$forumrecord,
$discussiongroup,
$modcontext,
'u.id',
@@ -113,14 +152,14 @@ if ($move > 0 and confirm_sesskey()) {
// Firstly for the forum being moved to.
\mod_forum\subscriptions::fill_subscription_cache($forumto->id);
// And also for the discussion being moved.
\mod_forum\subscriptions::fill_subscription_cache($forum->id);
$subscriptionchanges = array();
\mod_forum\subscriptions::fill_subscription_cache($forumid);
$subscriptionchanges = [];
$subscriptiontime = time();
foreach ($potentialsubscribers as $subuser) {
$userid = $subuser->id;
$targetsubscription = \mod_forum\subscriptions::is_subscribed($userid, $forumto, null, $cmto);
$discussionsubscribed = \mod_forum\subscriptions::is_subscribed($userid, $forum, $discussion->id);
$forumsubscribed = \mod_forum\subscriptions::is_subscribed($userid, $forum);
$discussionsubscribed = \mod_forum\subscriptions::is_subscribed($userid, $forumrecord, $discussionid);
$forumsubscribed = \mod_forum\subscriptions::is_subscribed($userid, $forumrecord);
if ($forumsubscribed && !$discussionsubscribed && $targetsubscription) {
// The user has opted out of this discussion and the move would cause them to receive notifications again.
@@ -133,12 +172,12 @@ if ($move > 0 and confirm_sesskey()) {
}
}
$DB->set_field('forum_discussions', 'forum', $forumto->id, array('id' => $discussion->id));
$DB->set_field('forum_read', 'forumid', $forumto->id, array('discussionid' => $discussion->id));
$DB->set_field('forum_discussions', 'forum', $forumto->id, ['id' => $discussionid]);
$DB->set_field('forum_read', 'forumid', $forumto->id, ['discussionid' => $discussionid]);
// Delete the existing per-discussion subscriptions and replace them with the newly calculated ones.
$DB->delete_records('forum_discussion_subs', array('discussion' => $discussion->id));
$newdiscussion = clone $discussion;
$DB->delete_records('forum_discussion_subs', ['discussion' => $discussionid]);
$newdiscussion = clone $discussionrecord;
$newdiscussion->forum = $forumto->id;
foreach ($subscriptionchanges as $userid => $preference) {
if ($preference != \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED) {
@@ -151,52 +190,54 @@ if ($move > 0 and confirm_sesskey()) {
}
}
$params = array(
$params = [
'context' => $destinationctx,
'objectid' => $discussion->id,
'other' => array(
'fromforumid' => $forum->id,
'objectid' => $discussionid,
'other' => [
'fromforumid' => $forumid,
'toforumid' => $forumto->id,
)
);
]
];
$event = \mod_forum\event\discussion_moved::create($params);
$event->add_record_snapshot('forum_discussions', $discussion);
$event->add_record_snapshot('forum', $forum);
$event->add_record_snapshot('forum_discussions', $discussionrecord);
$event->add_record_snapshot('forum', $forumrecord);
$event->add_record_snapshot('forum', $forumto);
$event->trigger();
// Delete the RSS files for the 2 forums to force regeneration of the feeds
require_once($CFG->dirroot.'/mod/forum/rsslib.php');
forum_rss_delete_file($forum);
require_once($CFG->dirroot . '/mod/forum/rsslib.php');
forum_rss_delete_file($forumrecord);
forum_rss_delete_file($forumto);
redirect($return.'&move=-1&sesskey='.sesskey());
redirect($return . '&move=-1&sesskey=' . sesskey());
}
// Pin or unpin discussion if requested.
if ($pin !== -1 && confirm_sesskey()) {
require_capability('mod/forum:pindiscussions', $modcontext);
if (!$capabilitymanager->can_pin_discussions($USER)) {
print_error('nopermissions', 'error', $return, get_capability_string('mod/forum:pindiscussions'));
}
$params = array('context' => $modcontext, 'objectid' => $discussion->id, 'other' => array('forumid' => $forum->id));
$params = ['context' => $modcontext, 'objectid' => $discussion->get_id(), 'other' => ['forumid' => $forum->get_id()]];
switch ($pin) {
case FORUM_DISCUSSION_PINNED:
// Pin the discussion and trigger discussion pinned event.
forum_discussion_pin($modcontext, $forum, $discussion);
forum_discussion_pin($modcontext, $forumrecord, $discussionrecord);
break;
case FORUM_DISCUSSION_UNPINNED:
// Unpin the discussion and trigger discussion unpinned event.
forum_discussion_unpin($modcontext, $forum, $discussion);
forum_discussion_unpin($modcontext, $forumrecord, $discussionrecord);
break;
default:
echo $OUTPUT->notification("Invalid value when attempting to pin/unpin discussion");
break;
}
redirect(new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)));
redirect($discussionviewurl->out(false));
}
// Trigger discussion viewed event.
forum_discussion_view($modcontext, $forum, $discussion);
forum_discussion_view($modcontext, $forumrecord, $discussionrecord);
unset($SESSION->fromdiscussion);
@@ -212,19 +253,21 @@ if ($parent) {
$displaymode = FORUM_MODE_NESTED;
}
} else {
$parent = $discussion->firstpost;
$parent = $discussion->get_first_post_id();
}
if (! $post = forum_get_post_full($parent)) {
print_error("notexists", 'forum', "$CFG->wwwroot/mod/forum/view.php?f=$forum->id");
$postvault = $vaultfactory->get_post_vault();
if (!$post = $postvault->get_from_id($parent)) {
print_error("notexists", 'forum', "$CFG->wwwroot/mod/forum/view.php?f={$forum->get_id()}");
}
if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
print_error('noviewdiscussionspermission', 'forum', "$CFG->wwwroot/mod/forum/view.php?id=$forum->id");
if (!$capabilitymanager->can_view_post($USER, $discussion, $post)) {
print_error('noviewdiscussionspermission', 'forum', "$CFG->wwwroot/mod/forum/view.php?id={$forum->get_id()}");
}
if ($mark == 'read' or $mark == 'unread') {
if ($CFG->forum_usermarksread && forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
$istracked = forum_tp_is_tracked($forumrecord, $USER);
if ($mark == 'read'|| $mark == 'unread') {
if ($CFG->forum_usermarksread && forum_tp_can_track_forums($forumrecord) && $istracked) {
if ($mark == 'read') {
forum_tp_add_read_record($USER->id, $postid);
} else {
@@ -242,163 +285,36 @@ if (empty($forumnode)) {
} else {
$forumnode->make_active();
}
$node = $forumnode->add(format_string($discussion->name), new moodle_url('/mod/forum/discuss.php', array('d'=>$discussion->id)));
$node = $forumnode->add(format_string($discussion->get_name()), $discussionviewurl);
$node->display = false;
if ($node && $post->id != $discussion->firstpost) {
$node->add(format_string($post->subject), $PAGE->url);
if ($node && $post->get_id() != $discussion->get_first_post_id()) {
$node->add(format_string($post->get_subject()), $PAGE->url);
}
$PAGE->set_title("$course->shortname: ".format_string($discussion->name));
$PAGE->set_title("$course->shortname: " . format_string($discussion->get_name()));
$PAGE->set_heading($course->fullname);
$PAGE->set_button($searchform);
$renderer = $PAGE->get_renderer('mod_forum');
$PAGE->set_button(forum_search_form($course));
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->get_name()), 2);
echo $OUTPUT->heading(format_string($discussion->get_name()), 3, 'discussionname');
echo $OUTPUT->heading(format_string($forum->name), 2);
echo $OUTPUT->heading(format_string($discussion->name), 3, 'discussionname');
// is_guest should be used here as this also checks whether the user is a guest in the current course.
// Guests and visitors cannot subscribe - only enrolled users.
if ((!is_guest($modcontext, $USER) && isloggedin()) && has_capability('mod/forum:viewdiscussion', $modcontext)) {
// Discussion subscription.
if (\mod_forum\subscriptions::is_subscribable($forum)) {
echo html_writer::div(
forum_get_discussion_subscription_icon($forum, $post->discussion, null, true),
'discussionsubscription'
);
echo forum_get_discussion_subscription_icon_preloaders();
}
}
/// Check to see if groups are being used in this forum
/// If so, make sure the current person is allowed to see this discussion
/// Also, if we know they should be able to reply, then explicitly set $canreply for performance reasons
$canreply = forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext);
if (!$canreply and $forum->type !== 'news') {
if (isguestuser() or !isloggedin()) {
$canreply = true;
}
if (!is_enrolled($modcontext) and !is_viewing($modcontext)) {
// allow guests and not-logged-in to see the link - they are prompted to log in after clicking the link
// normal users with temporary guest access see this link too, they are asked to enrol instead
$canreply = enrol_selfenrol_available($course->id);
}
}
// Output the links to neighbour discussions.
$neighbours = forum_get_discussion_neighbours($cm, $discussion, $forum);
$neighbourlinks = $renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']);
echo $neighbourlinks;
/// Print the controls across the top
echo '<div class="discussioncontrols clearfix"><div class="controlscontainer m-b-1">';
if (!empty($CFG->enableportfolios) && has_capability('mod/forum:exportdiscussion', $modcontext)) {
require_once($CFG->libdir.'/portfoliolib.php');
$button = new portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', array('discussionid' => $discussion->id), 'mod_forum');
$button = $button->to_html(PORTFOLIO_ADD_FULL_FORM, get_string('exportdiscussion', 'mod_forum'));
$buttonextraclass = '';
if (empty($button)) {
// no portfolio plugin available.
$button = '&nbsp;';
$buttonextraclass = ' noavailable';
}
echo html_writer::tag('div', $button, array('class' => 'discussioncontrol exporttoportfolio'.$buttonextraclass));
} else {
echo html_writer::tag('div', '&nbsp;', array('class'=>'discussioncontrol nullcontrol'));
}
// groups selector not needed here
echo '<div class="discussioncontrol displaymode">';
forum_print_mode_form($discussion->id, $displaymode);
echo "</div>";
if ($forum->type != 'single'
&& has_capability('mod/forum:movediscussions', $modcontext)) {
echo '<div class="discussioncontrol movediscussion">';
// Popup menu to move discussions to other forums. The discussion in a
// single discussion forum can't be moved.
$modinfo = get_fast_modinfo($course);
if (isset($modinfo->instances['forum'])) {
$forummenu = array();
// Check forum types and eliminate simple discussions.
$forumcheck = $DB->get_records('forum', array('course' => $course->id),'', 'id, type');
foreach ($modinfo->instances['forum'] as $forumcm) {
if (!$forumcm->uservisible || !has_capability('mod/forum:startdiscussion',
context_module::instance($forumcm->id))) {
continue;
}
$section = $forumcm->sectionnum;
$sectionname = get_section_name($course, $section);
if (empty($forummenu[$section])) {
$forummenu[$section] = array($sectionname => array());
}
$forumidcompare = $forumcm->instance != $forum->id;
$forumtypecheck = $forumcheck[$forumcm->instance]->type !== 'single';
if ($forumidcompare and $forumtypecheck) {
$url = "/mod/forum/discuss.php?d=$discussion->id&move=$forumcm->instance&sesskey=".sesskey();
$forummenu[$section][$sectionname][$url] = format_string($forumcm->name);
}
}
if (!empty($forummenu)) {
echo '<div class="movediscussionoption">';
$select = new url_select($forummenu, '',
array('/mod/forum/discuss.php?d=' . $discussion->id => get_string("movethisdiscussionto", "forum")),
'forummenu', get_string('move'));
echo $OUTPUT->render($select);
echo "</div>";
}
}
echo "</div>";
}
if (has_capability('mod/forum:pindiscussions', $modcontext)) {
if ($discussion->pinned == FORUM_DISCUSSION_PINNED) {
$pinlink = FORUM_DISCUSSION_UNPINNED;
$pintext = get_string('discussionunpin', 'forum');
} else {
$pinlink = FORUM_DISCUSSION_PINNED;
$pintext = get_string('discussionpin', 'forum');
}
$button = new single_button(new moodle_url('discuss.php', array('pin' => $pinlink, 'd' => $discussion->id)), $pintext, 'post');
echo html_writer::tag('div', $OUTPUT->render($button), array('class' => 'discussioncontrol pindiscussion'));
}
echo "</div></div>";
if (forum_discussion_is_locked($forum, $discussion)) {
echo $OUTPUT->notification(get_string('discussionlocked', 'forum'),
\core\output\notification::NOTIFY_INFO . ' discussionlocked');
}
if (!empty($forum->blockafter) && !empty($forum->blockperiod)) {
$a = new stdClass();
$a->blockafter = $forum->blockafter;
$a->blockperiod = get_string('secondstotime'.$forum->blockperiod);
echo $OUTPUT->notification(get_string('thisforumisthrottled','forum',$a));
}
if ($forum->type == 'qanda' && !has_capability('mod/forum:viewqandawithoutposting', $modcontext) &&
!forum_user_has_posted($forum->id,$discussion->id,$USER->id)) {
echo $OUTPUT->notification(get_string('qandanotify', 'forum'));
}
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$discussionrenderer = $rendererfactory->get_discussion_renderer($forum, $discussion, $displaymode);
$orderpostsby = $displaymode == FORUM_MODE_FLATNEWEST ? 'created DESC' : 'created ASC';
$replies = $postvault->get_replies_to_post($post, $orderpostsby);
$postids = array_map(function($post) {
return $post->get_id();
}, array_merge([$post], array_values($replies)));
if ($move == -1 and confirm_sesskey()) {
echo $OUTPUT->notification(get_string('discussionmoved', 'forum', format_string($forum->name,true)), 'notifysuccess');
$forumname = format_string($forum->get_name(), true);
echo $OUTPUT->notification(get_string('discussionmoved', 'forum', $forumname), 'notifysuccess');
}
$canrate = has_capability('mod/forum:rate', $modcontext);
forum_print_discussion($course, $cm, $forum, $discussion, $post, $displaymode, $canreply, $canrate);
echo $neighbourlinks;
// Add the subscription toggle JS.
$PAGE->requires->yui_module('moodle-mod_forum-subscriptiontoggle', 'Y.M.mod_forum.subscriptiontoggle.init');
echo $discussionrenderer->render($USER, $post, $replies);
echo $OUTPUT->footer();
if ($istracked && !$CFG->forum_usermarksread) {
forum_tp_mark_posts_read($USER, $postids);
}
+83
View File
@@ -1258,4 +1258,87 @@ class mod_forum_external extends external_api {
return new external_single_structure($structure);
}
/**
* Set the subscription state.
*
* @param int $forumid
* @param int $discussionid
* @param bool $targetstate
* @return \stdClass
*/
public static function set_subscription_state($forumid, $discussionid, $targetstate) {
global $PAGE, $USER;
$params = self::validate_parameters(self::set_subscription_state_parameters(), [
'forumid' => $forumid,
'discussionid' => $discussionid,
'targetstate' => $targetstate
]);
$vaultfactory = mod_forum\local\container::get_vault_factory();
$forumvault = $vaultfactory->get_forum_vault();
$forum = $forumvault->get_from_id($params['forumid']);
$coursemodule = $forum->get_course_module_record();
$context = $forum->get_context();
self::validate_context($context);
$discussionvault = $vaultfactory->get_discussion_vault();
$discussion = $discussionvault->get_from_id($params['discussionid']);
$legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
$forumrecord = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum);
$discussionrecord = $legacydatamapperfactory->get_discussion_data_mapper()->to_legacy_object($discussion);
if (!\mod_forum\subscriptions::is_subscribable($forumrecord)) {
// Nothing to do. We won't actually output any content here though.
throw new \moodle_exception('cannotsubscribe', 'mod_forum');
}
$issubscribed = \mod_forum\subscriptions::is_subscribed(
$USER->id,
$forumrecord,
$discussion->get_id(),
$coursemodule
);
// If the current state doesn't equal the desired state then update the current
// state to the desired state.
if ($issubscribed != (bool) $params['targetstate']) {
if ($params['targetstate']) {
\mod_forum\subscriptions::subscribe_user_to_discussion($USER->id, $discussionrecord, $context);
} else {
\mod_forum\subscriptions::unsubscribe_user_from_discussion($USER->id, $discussionrecord, $context);
}
}
$exporterfactory = mod_forum\local\container::get_exporter_factory();
$exporter = $exporterfactory->get_discussion_exporter($USER, $forum, $discussion);
return $exporter->export($PAGE->get_renderer('mod_forum'));
}
/**
* Returns description of method parameters.
*
* @return external_function_parameters
*/
public static function set_subscription_state_parameters() {
return new external_function_parameters(
[
'forumid' => new external_value(PARAM_INT, 'Forum that the discussion is in'),
'discussionid' => new external_value(PARAM_INT, 'The discussion to subscribe or unsubscribe'),
'targetstate' => new external_value(PARAM_BOOL, 'The target state')
]
);
}
/**
* Returns description of method result value.
*
* @return external_description
*/
public static function set_subscription_state_returns() {
return \mod_forum\local\exporters\discussion::get_read_structure();
}
}
+1
View File
@@ -50,6 +50,7 @@ $string['blockperiod_help'] = 'Students can be blocked from posting more than a
$string['blockperioddisabled'] = 'Don\'t block';
$string['blogforum'] = 'Standard forum displayed in a blog-like format';
$string['bynameondate'] = 'by {$a->name} - {$a->date}';
$string['cachedef_forum_is_tracked'] = 'Forum tracking status for user';
$string['cannotadd'] = 'Could not add the discussion for this forum';
$string['cannotadddiscussion'] = 'Adding discussions to this forum requires group membership.';
$string['cannotadddiscussionall'] = 'You do not have permission to add a new discussion topic for all participants.';
+50 -901
View File
File diff suppressed because it is too large Load Diff
+6 -14
View File
@@ -29,14 +29,14 @@ require_once("lib.php");
$f = required_param('f',PARAM_INT); // The forum to mark
$mark = required_param('mark',PARAM_ALPHA); // Read or unread?
$d = optional_param('d',0,PARAM_INT); // Discussion to mark.
$returnpage = optional_param('returnpage', 'index.php', PARAM_FILE); // Page to return to.
$return = optional_param('return', null, PARAM_LOCALURL); // Page to return to.
$url = new moodle_url('/mod/forum/markposts.php', array('f'=>$f, 'mark'=>$mark));
if ($d !== 0) {
$url->param('d', $d);
}
if ($returnpage !== 'index.php') {
$url->param('returnpage', $returnpage);
if (null !== $return) {
$url->param('return', $return);
}
$PAGE->set_url($url);
@@ -57,10 +57,10 @@ $user = $USER;
require_login($course, false, $cm);
require_sesskey();
if ($returnpage == 'index.php') {
$returnto = new moodle_url("/mod/forum/$returnpage", array('id' => $course->id));
if (null === $return) {
$returnto = new moodle_url("/mod/forum/index.php", ['id' => $course->id]);
} else {
$returnto = new moodle_url("/mod/forum/$returnpage", array('f' => $forum->id));
$returnto = new moodle_url($return);
}
if (isguestuser()) { // Guests can't change forum
@@ -93,14 +93,6 @@ if ($mark == 'read') {
}
forum_tp_mark_forum_read($user, $forum->id, $currentgroup);
}
/// FUTURE - Add ability to mark them as unread.
// } else { // subscribe
// if (forum_tp_start_tracking($forum->id, $user->id)) {
// redirect($returnto, get_string("nowtracking", "forum", $info), 1);
// } else {
// print_error("Could not start tracking that forum", get_local_referer());
// }
}
redirect($returnto);
+37 -11
View File
@@ -96,6 +96,9 @@ if (!isloggedin() or isguestuser()) {
require_login(0, false); // Script is useless unless they're logged in.
$entityfactory = mod_forum\local\container::get_entity_factory();
$vaultfactory = mod_forum\local\container::get_vault_factory();
if (!empty($forum)) { // User is starting a new discussion in a forum.
if (! $forum = $DB->get_record("forum", array("id" => $forum))) {
print_error('invalidforumid', 'forum');
@@ -305,6 +308,10 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
require_login($course, false, $cm);
$modcontext = context_module::instance($cm->id);
$postentity = $entityfactory->get_post_from_stdclass($post);
$discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
$forumentity = $entityfactory->get_forum_from_stdclass($forum, $modcontext, $cm, $course);
if ( !(($post->userid == $USER->id && has_capability('mod/forum:deleteownpost', $modcontext))
|| has_capability('mod/forum:deleteanypost', $modcontext)) ) {
print_error('cannotdeletepost', 'forum');
@@ -400,20 +407,27 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
"post.php?delete=$delete&confirm=$delete",
$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
$postentities = [$postentity];
if (empty($post->edit)) {
$forumtracked = forum_tp_is_tracked($forum);
$posts = forum_get_all_discussion_posts($discussion->id, "created ASC", $forumtracked);
forum_print_posts_nested($course, $cm, $forum, $discussion, $post, false, false, $forumtracked, $posts);
$postvault = $vaultfactory->get_post_vault();
$replies = $postvault->get_replies_to_post($postentity, 'created ASC');
$postentities = array_merge($postentities, $replies);
}
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(FORUM_MODE_NESTED, true);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]);
} else {
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->name), 2);
echo $OUTPUT->confirm(get_string("deletesure", "forum", $replycount),
"post.php?delete=$delete&confirm=$delete",
$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(null, true);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]);
}
}
@@ -452,7 +466,6 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$prunemform = new mod_forum_prune_form(null, array('prune' => $prune, 'confirm' => $prune));
if ($prunemform->is_cancelled()) {
redirect(forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
} else if ($fromform = $prunemform->get_data()) {
@@ -539,7 +552,12 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$prunemform->display();
forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
$postentity = $entityfactory->get_post_from_stdclass($post);
$discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
$forumentity = $entityfactory->get_forum_from_stdclass($forum, $modcontext, $cm, $course);
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(null, true);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]);
}
echo $OUTPUT->footer();
@@ -1048,14 +1066,22 @@ if (!empty($parent)) {
print_error('notpartofdiscussion', 'forum');
}
forum_print_post($parent, $discussion, $forum, $cm, $course, false, false, false);
$postentity = $entityfactory->get_post_from_stdclass($parent);
$discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
$forumentity = $entityfactory->get_forum_from_stdclass($forum, $modcontext, $cm, $course);
$postentities = [$postentity];
if (empty($post->edit)) {
if ($forum->type != 'qanda' || forum_user_can_see_discussion($forum, $discussion, $modcontext)) {
$forumtracked = forum_tp_is_tracked($forum);
$posts = forum_get_all_discussion_posts($discussion->id, "created ASC", $forumtracked);
forum_print_posts_threaded($course, $cm, $forum, $discussion, $parent, 0, false, $forumtracked, $posts);
$postvault = $vaultfactory->get_post_vault();
$replies = $postvault->get_replies_to_post($postentity, 'created ASC');
$postentities = array_merge($postentities, $replies);
}
}
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(FORUM_MODE_THREADED, true);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]);
} else {
if (!empty($forum->intro)) {
echo $OUTPUT->box(format_module_intro('forum', $forum, $cm->id), 'generalbox', 'intro');
+57 -69
View File
@@ -233,91 +233,79 @@ foreach ($searchterms as $key => $searchterm) {
}
}
$strippedsearch = implode(' ', $searchterms); // Rebuild the string
$entityfactory = mod_forum\local\container::get_entity_factory();
$vaultfactory = mod_forum\local\container::get_vault_factory();
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$managerfactory = mod_forum\local\container::get_manager_factory();
$legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
$forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
$discussionvault = $vaultfactory->get_discussion_vault();
$discussionids = array_keys(array_reduce($posts, function($carry, $post) {
$carry[$post->discussion] = true;
return $carry;
}, []));
$discussions = $discussionvault->get_from_ids($discussionids);
$discussionsbyid = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
$forumvault = $vaultfactory->get_forum_vault();
$forumids = array_keys(array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_forum_id()] = true;
return $carry;
}, []));
$forums = $forumvault->get_from_ids($forumids);
$forumsbyid = array_reduce($forums, function($carry, $forum) {
$carry[$forum->get_id()] = $forum;
return $carry;
}, []);
$postids = array_map(function($post) {
return $post->id;
}, $posts);
$poststorender = [];
foreach ($posts as $post) {
// Replace the simple subject with the three items forum name -> thread name -> subject
// (if all three are appropriate) each as a link.
if (! $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) {
if (!isset($discussionsbyid[$post->discussion])) {
print_error('invaliddiscussionid', 'forum');
}
if (! $forum = $DB->get_record('forum', array('id' => "$discussion->forum"))) {
$discussion = $discussionsbyid[$post->discussion];
if (!isset($forumsbyid[$discussion->get_forum_id()])) {
print_error('invalidforumid', 'forum');
}
if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
print_error('invalidcoursemodule');
$forum = $forumsbyid[$discussion->get_forum_id()];
$capabilitymanager = $managerfactory->get_capability_manager($forum);
$postentity = $entityfactory->get_post_from_stdclass($post);
if (!$capabilitymanager->can_view_post($USER, $discussion, $postentity)) {
// Don't render posts that the user can't view.
continue;
}
$post->subject = highlight($strippedsearch, $post->subject);
$discussion->name = highlight($strippedsearch, $discussion->name);
$fullsubject = "<a href=\"view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
if ($forum->type != 'single') {
$fullsubject .= " -> <a href=\"discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a>";
if ($post->parent != 0) {
$fullsubject .= " -> <a href=\"discuss.php?d=$post->discussion&amp;parent=$post->id\">".format_string($post->subject,true)."</a>";
}
if ($postentity->is_deleted()) {
// Don't render deleted posts.
continue;
}
$post->subject = $fullsubject;
$post->subjectnoformat = true;
//add the ratings information to the post
//Unfortunately seem to have do this individually as posts may be from different forums
if ($forum->assessed != RATING_AGGREGATE_NONE) {
$modcontext = context_module::instance($cm->id);
$ratingoptions->context = $modcontext;
$ratingoptions->items = array($post);
$ratingoptions->aggregate = $forum->assessed;//the aggregation method
$ratingoptions->scaleid = $forum->scale;
$ratingoptions->assesstimestart = $forum->assesstimestart;
$ratingoptions->assesstimefinish = $forum->assesstimefinish;
$postswithratings = $rm->get_ratings($ratingoptions);
if ($postswithratings && count($postswithratings)==1) {
$post = $postswithratings[0];
}
}
// Identify search terms only found in HTML markup, and add a warning about them to
// the start of the message text. However, do not do the highlighting here. forum_print_post
// will do it for us later.
$missing_terms = "";
$options = new stdClass();
$options->trusted = $post->messagetrust;
$post->message = highlight($strippedsearch,
format_text($post->message, $post->messageformat, $options, $course->id),
0, '<fgw9sdpq4>', '</fgw9sdpq4>');
foreach ($searchterms as $searchterm) {
if (preg_match("/$searchterm/i",$post->message) && !preg_match('/<fgw9sdpq4>'.$searchterm.'<\/fgw9sdpq4>/i',$post->message)) {
$missing_terms .= " $searchterm";
}
}
$post->message = str_replace('<fgw9sdpq4>', '<span class="highlight">', $post->message);
$post->message = str_replace('</fgw9sdpq4>', '</span>', $post->message);
if ($missing_terms) {
$strmissingsearchterms = get_string('missingsearchterms','forum');
$post->message = '<p class="highlight2">'.$strmissingsearchterms.' '.$missing_terms.'</p>'.$post->message;
}
// Prepare a link to the post in context, to be displayed after the forum post.
$fulllink = "<a href=\"discuss.php?d=$post->discussion#p$post->id\">".get_string("postincontext", "forum")."</a>";
// Message is now html format.
if ($post->messageformat != FORMAT_HTML) {
$post->messageformat = FORMAT_HTML;
}
// Now pring the post.
forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false,
$fulllink, '', -99, false);
$poststorender[] = $postentity;
}
$renderer = $rendererfactory->get_posts_search_results_renderer($searchterms);
echo $renderer->render(
$USER,
$forumsbyid,
$discussionsbyid,
$poststorender
);
echo $OUTPUT->paging_bar($totalcount, $page, $perpage, $url);
echo $OUTPUT->footer();
@@ -0,0 +1,49 @@
{{!
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/>.
}}
{{!
@template mod_forum/blog_discussion_list
Template which defines a forum post for sending in a single-post HTML email.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{< mod_forum/discussion_list }}
{{$discussion_create_text}}
{{#str}}addanewtopic, forum{{/str}}
{{/discussion_create_text}}
{{$discussion_list_output}}
{{#posts}}
{{< mod_forum/forum_discussion_post }}
{{$footer}}
<div class="link text-right">
<a href="{{urls.discuss}}">{{#str}}discussthistopic, forum{{/str}}</a>&nbsp;({{discussionrepliescount}}{{#isreplyplural}}{{#str}}repliesmany, forum{{/str}}{{/isreplyplural}}{{^isreplyplural}}{{#str}}repliesone, forum{{/str}}{{/isreplyplural}})
</div>
{{/footer}}
{{$replyoutput}}{{/replyoutput}}
{{/ mod_forum/forum_discussion_post }}
{{/posts}}
{{/discussion_list_output}}
{{/ mod_forum/discussion_list }}
@@ -0,0 +1,243 @@
{{!
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/>.
}}
{{!
@template mod_forum/discussion_list
Template to display the list of forum discussions.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* TODO
Example context (json):
{
}
}}
<div id="discussion-list-{{uniqid}}">
{{{groupchangemenu}}}
{{#notifications}}
{{> core/notification}}
{{/notifications}}
{{#forum.capabilities.create}}
<div class="p-t-1 p-b-1">
<a href="{{forum.urls.create}}" class="btn btn-primary">
{{$discussion_create_text}}
{{#str}}addanewdiscussion, forum{{/str}}
{{/discussion_create_text}}
</a>
</div>
{{/forum.capabilities.create}}
{{#state.hasdiscussions}}
{{{ pagination }}}
{{$discussion_list_output}}
<table class="table table-hover table-striped">
{{$discussion_list_header}}
<thead>
<tr>
<th scope="col">&nbsp;</th>
<th scope="col">&nbsp;</th>
<th scope="col" class="p-l-0">{{#str}}discussion, mod_forum{{/str}}</th>
<th scope="col" class="author">{{#str}}startedby, mod_forum{{/str}}</th>
{{#forum.state.groupmode}}
<th scope="col" class="group">{{#str}}group{{/str}}</th>
{{/forum.state.groupmode}}
{{#forum.capabilities.viewdiscussions}}
<th scope="col" class="text-center">{{#str}}replies, mod_forum{{/str}}</th>
{{#forum.userstate.tracked}}
<th scope="col" class="text-center">
{{#str}}unread, mod_forum{{/str}}
<a href="{{{forum.urls.markasread}}}">{{#pix}}t/markasread, core, {{#str}}markallread, mod_forum{{/str}}{{/pix}}</a>
</th>
{{/forum.userstate.tracked}}
{{/forum.capabilities.viewdiscussions}}
<th scope="col" class="lastpost">{{#str}}lastpost, mod_forum{{/str}}</th>
{{#forum.capabilities.subscribe}}
<th scope="col" class="discussionsubscription"></th>
{{/forum.capabilities.subscribe}}
</tr>
</thead>
{{/discussion_list_header}}
{{$discussion_list_body}}
<tbody>
{{#summaries}}
<!-- The discussion class is only required for behat tests to identify the row -->
<tr class="discussion {{#discussion.timed}}{{#istimed}}{{^visible}}dimmed_text{{/visible}}{{/istimed}}{{/discussion.timed}}">
<td scope="col" class="pinned p-0 text-center align-middle">
{{#discussion.pinned}}
{{#pix}}i/pinned, mod_forum, {{#str}}discussionpinned, mod_forum{{/str}}{{/pix}}
{{/discussion.pinned}}
</td>
<td scope="col" class="timed p-0 text-center align-middle">
{{#discussion.timed.istimed}}
<div class="timedpost">
{{#pix}}
i/calendar, moodle,
{{#discussion.times.start}}
{{! }}{{#str}} displaystart, mod_forum {{/str}}: {{#userdate}}{{.}}, {{#str}}strftimerecentfull {{/str}}{{/userdate}}
{{/discussion.times.start}}
{{#discussion.times.end}}
{{! }}{{#str}} displayend, mod_forum {{/str}}: {{#userdate}}{{.}}, {{#str}} strftimerecentfull {{/str}}{{/userdate}}
{{/discussion.times.end}}
{{#discussion.timed.visible}}
{{! }}{{#str}} timedvisible, mod_forum {{/str}}
{{/discussion.timed.visible}}
{{^discussion.timed.visible}}
{{! }}{{#str}} timedhidden, mod_forum {{/str}}
{{/discussion.timed.visible}}
{{/pix}}
</div>
{{/discussion.timed.istimed}}
</td>
<td scope="col" class="topic p-0 align-middle">
<a class="p-3 p-l-0 w-100 h-100 d-block" href="{{discussion.urls.view}}">{{{discussion.name}}}</a>
</td>
<td scope="col" class="author align-middle">
{{#firstpostauthor}}
<a href="{{urls.profile}}">
<div class="d-flex flex-row">
<div class="align-middle p-0">
<a href="{{urls.profile}}">
<img {{!
}} class="h-auto rounded-circle userpicture" {{!
}} src="{{urls.profileimage}}" {{!
}} alt="{{#str}}pictureof, moodle, {{fullname}}{{/str}}" {{!
}}>
</a>
</div>
<div class="align-middle p-2">
<a href="{{urls.profile}}">{{fullname}}</a>
</div>
</div>
</a>
{{/firstpostauthor}}
</td>
{{#forum.state.groupmode}}
<td scope="col" class="group">
{{#discussion.group}}
{{#urls.picture}}
{{#urls.userlist}}
<a href="{{{urls.userlist}}}">
<img class="border rounded h-auto rounded-circle" src="{{{urls.picture}}}">
</a>
{{/urls.userlist}}
{{^urls.userlist}}
<img class="border rounded h-auto rounded-circle" src="{{{urls.picture}}}">
{{/urls.userlist}}
{{/urls.picture}}
{{^urls.picture}}
{{#urls.userlist}}
<a href="{{{urls.userlist}}}">{{name}}</a>
{{/urls.userlist}}
{{^urls.userlist}}
{{name}}
{{/urls.userlist}}
{{/urls.picture}}
{{/discussion.group}}
</td>
{{/forum.state.groupmode}}
{{#forum.capabilities.viewdiscussions}}
<td scope="col" class="p-0 text-center align-middle">
<a href="{{discussion.urls.view}}" class="p-3 w-100 h-100 d-block">
{{replies}}
</a>
</td>
{{#forum.userstate.tracked}}
<td scope="col" class="p-0 text-center align-middle">
{{#unread}}
{{! TODO Rewrite as AJAX}}
<div class="p-3 w-100 h-100 d-block">
<a href="{{{discussion.urls.viewfirstunread}}}">{{unread}}</a>
<a href="{{{discussion.urls.markasread}}}">{{#pix}}t/markasread, core, {{#str}}markalldread, mod_forum{{/str}}{{/pix}}</a>
</div>
{{/unread}}
{{^unread}}
<span class="p-3 w-100 h-100 d-block">
0
</span>
{{/unread}}
</td>
{{/forum.userstate.tracked}}
{{/forum.capabilities.viewdiscussions}}
<td scope="col" class="text-left">
{{! TODO Check q&a, eachuser }}
{{#latestpostid}}
<div class="d-flex flex-row">
<div class="align-middle p-0">
<a href="{{latestpostauthor.urls.profile}}">
<img {{!
}} class="h-auto rounded-circle userpicture" {{!
}} src="{{latestpostauthor.urls.profileimage}}" {{!
}} alt="{{#str}}pictureof, moodle, {{latestpostauthor.fullname}}{{/str}}" {{!
}}>
</a>
</div>
<div class="p-2 p-t-0 p-b-0 d-inline-flex flex-column">
<div>
<a href="{{latestpostauthor.urls.profile}}">{{latestpostauthor.fullname}}</a>
</div>
<div>
<a href="{{{discussion.urls.viewlatest}}}">{{#userdate}}
{{discussion.times.modified}}, {{#str}}strftimerecentfull{{/str}}
{{/userdate}}</a>
</div>
</div>
</div>
{{/latestpostid}}
</td>
<td scope="col" class="p-0 align-middle">
{{#discussion}}
{{> mod_forum/discussion_subscription_toggle}}
{{/discussion}}
</td>
</tr>
{{/summaries}}
</tbody>
{{/discussion_list_body}}
</table>
{{/discussion_list_output}}
{{$pagination_output}}
{{{ pagination }}}
{{/pagination_output}}
{{#can_create_discussion}}
<div class="forumaddnew">
<a href="{{create_discussion_link}}" class="btn btn-primary">{{create_discussion_link_text}}</a>
</div>
{{/can_create_discussion}}
{{/state.hasdiscussions}}
{{^state.hasdiscussions}}
<div class="forumnodiscuss">
{{$no_discussions_text}}
({{#str}}nodiscussions, forum{{/str}})
{{/no_discussions_text}}
</div>
{{/state.hasdiscussions}}
</div>
{{#js}}
require(['jquery', 'mod_forum/discussion_list'], function($, View) {
var root = $('#discussion-list-{{uniqid}}');
View.init(root);
});
{{/js}}
@@ -0,0 +1,59 @@
{{!
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/>.
}}
{{!
@template mod_forum/discussion_subscription_toggle
Template to display the discussion subscription toggle.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* TODO
Example context (json):
{
}
}}
{{#capabilities.subscribe}}
<a
href="{{{urls.subscribe}}}"
class="p-3 btn btn-link"
data-type="subscription-toggle"
data-action="toggle"
data-discussionid="{{id}}"
data-forumid="{{forumid}}"
{{#userstate.subscribed}}
title="{{#str}}clicktounsubscribe, mod_forum{{/str}}"
data-targetstate="0"
{{/userstate.subscribed}}
{{^userstate.subscribed}}
title="{{#str}}clicktosubscribe, mod_forum{{/str}}"
data-targetstate="1"
{{/userstate.subscribed}}
>
{{#userstate.subscribed}}
{{#pix}}t/subscribed, mod_forum, {{#str}}clicktounsubscribe, mod_forum{{/str}}{{/pix}}
{{/userstate.subscribed}}
{{^userstate.subscribed}}
{{#pix}}t/unsubscribed, mod_forum, {{#str}}clicktosubscribe, mod_forum{{/str}}{{/pix}}
{{/userstate.subscribed}}
</a>
{{/capabilities.subscribe}}
@@ -0,0 +1,51 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion
Template for displaying a single forum discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{#html}}
{{{subscribe}}}
{{{neighbourlinks}}}
<div class="d-flex flex-wrap m-b-1">
<div>{{{exportdiscussion}}}</div>
<div class="{{#exportdiscussion}}ml-2{{/exportdiscussion}}">{{{modeselectorform}}}</div>
<div class="ml-2">{{{movediscussion}}}</div>
<div class="ml-2">{{{pindiscussion}}}</div>
</div>
{{/html}}
{{#notifications}}
{{> core/notification}}
{{/notifications}}
{{{html.posts}}}
{{#html.neighbourlinks}}{{{.}}}{{/html.neighbourlinks}}
@@ -0,0 +1,40 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion_nested_post
Template to render a post in a nested format.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{< mod_forum/forum_discussion_post }}
{{$replies}}
<div class="indent">
{{#replies}}
{{> mod_forum/forum_discussion_nested_post }}
{{/replies}}
</div>
{{/replies}}
{{/ mod_forum/forum_discussion_post }}
@@ -0,0 +1,34 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion_nested_posts
Template to render a list of posts in a nested format.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{#posts}}
{{> mod_forum/forum_discussion_nested_post }}
{{/posts}}
@@ -0,0 +1,181 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion_post
Template to render a single post from a discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
<article id="p{{id}}" class="relativelink mb-2" tabindex="-1" data-post-id="{{id}}">
<!-- The firstpost and starter classes below aren't used for anything other than to identify the first post in behat -->
<div
class="d-flex border p-2 mb-2 forumpost {{#unread}}unread{{/unread}} {{#firstpost}}firstpost starter{{/firstpost}}"
aria-label='{{#str}} postbyuser, mod_forum, {"post": "{{subject}}", "user": "{{author.fullname}}"} {{/str}}'
>
{{#isfirstunread}}<a id="unread"></a>{{/isfirstunread}}
<div style="width: 45px;">
{{^isdeleted}}
{{#author}}
{{#urls.profileimage}}
<img class="rounded-circle w-100" src="{{{.}}}">
{{/urls.profileimage}}
{{#groups}}
<img class="rounded-circle w-100" src="{{{urls.image}}}">
{{/groups}}
{{/author}}
{{/isdeleted}}
</div>
<div class="d-flex flex-column ml-2 w-100">
<header class="mb-2 header row">
<h3 class="h6 font-weight-bold mb-0">{{$subject}}{{{subject}}}{{/subject}}</h3>
{{^isdeleted}}
<address>
{{{html.authorsubheading}}}
</address>
{{/isdeleted}}
</header>
<div>
{{{message}}}
</div>
{{^isdeleted}}
{{#haswordcount}}
<p class="font-italic mb-0"><small>{{#str}} numwords, core, {{wordcount}} {{/str}}</small></p>
{{/haswordcount}}
{{#attachments}}
{{#isimage}}
<div>
<img src="{{{url}}}" alt="{{filename}}" style="max-width: 100%">
{{#urls.export}}
<a href="{{{.}}}" title="{{#str}} addtoportfolio, core_portfolio {{/str}}">
{{#pix}} t/portfolioadd, core {{/pix}}
</a>
{{/urls.export}}
{{#html.plagiarism}}
<div>{{{.}}}</div>
{{/html.plagiarism}}
</div>
{{/isimage}}
{{/attachments}}
{{{html.taglist}}}
{{#attachments}}
{{^isimage}}
<div>
<a href="{{{url}}}">{{#pix}} {{icon}}, core {{/pix}} {{filename}}</a>
{{#urls.export}}
<a href="{{{.}}}" title="{{#str}} addtoportfolio, core_portfolio {{/str}}">
{{#pix}} t/portfolioadd, core {{/pix}}
</a>
{{/urls.export}}
{{#html.plagiarism}}
<div>{{{.}}}</div>
{{/html.plagiarism}}
</div>
{{/isimage}}
{{/attachments}}
{{#html.rating}}
<div class="mt-2">{{{.}}}</div>
{{/html.rating}}
{{$actions}}
{{^readonly}}
<div class="d-flex justify-content-end">
{{#capabilities}}
{{#view}}
<a href="{{{urls.view}}}" class="btn btn-link">
{{#str}} permalink, mod_forum {{/str}}
</a>
{{/view}}
{{#controlreadstatus}}
{{#unread}}
<a href="{{{urls.markasread}}}" class="btn btn-link">
{{#str}} markread, mod_forum {{/str}}
</a>
{{/unread}}
{{^unread}}
<a href="{{{urls.markasunread}}}" class="btn btn-link">
{{#str}} markunread, mod_forum {{/str}}
</a>
{{/unread}}
{{/controlreadstatus}}
{{#urls.viewparent}}
<a href="{{{.}}}" class="btn btn-link">
{{#str}} parent, mod_forum {{/str}}
</a>
{{/urls.viewparent}}
{{#edit}}
<a href="{{{urls.edit}}}" class="btn btn-link">
{{#str}} edit, mod_forum {{/str}}
</a>
{{/edit}}
{{#split}}
<a href="{{{urls.split}}}" class="btn btn-link">
{{#str}} prune, mod_forum {{/str}}
</a>
{{/split}}
{{#delete}}
<a href="{{{urls.delete}}}" class="btn btn-link">
{{#str}} delete, mod_forum {{/str}}
</a>
{{/delete}}
{{#reply}}
{{$replyoutput}}
<a href="{{{urls.reply}}}" class="btn btn-link">
{{#str}} reply, mod_forum {{/str}}
</a>
{{/replyoutput}}
{{/reply}}
{{#export}}
<a href="{{{urls.export}}}" class="btn btn-link">
{{#str}} addtoportfolio, core_portfolio {{/str}}
</a>
{{/export}}
{{/capabilities}}
</div>
{{/readonly}}
{{/actions}}
{{$footer}}{{/footer}}
{{/isdeleted}}
</div>
</div>
{{$replies}}
{{#hasreplies}}
<div>
{{#replies}}
{{> mod_forum/forum_discussion_post }}
{{/replies}}
</div>
{{/hasreplies}}
{{/replies}}
</article>
@@ -0,0 +1,34 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion_posts
Template to render a list of posts for a discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{#posts}}
{{> mod_forum/forum_discussion_post }}
{{/posts}}
@@ -0,0 +1,45 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion_threaded_post
Template to render a single post from a discussion in a threaded format.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
<div class="mb-2" data-post-id="{{id}}">
<a href="{{{urls.viewisolated}}}">{{subject}}</a>
{{^isdeleted}}
<address class="d-inline-block mb-0">
{{{html.authorsubheading}}}
</address>
{{/isdeleted}}
<div class="indent">
{{#replies}}
{{> mod_forum/forum_discussion_threaded_post }}
{{/replies}}
</div>
</div>
@@ -0,0 +1,44 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_discussion_threaded_posts
Template to render a list of posts in a threaded format.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{#posts}}
{{< mod_forum/forum_discussion_post }}
{{$replies}}
<!-- The forumthread class is only added for behat -->
<div class="indent forumthread">
{{#replies}}
{{> mod_forum/forum_discussion_threaded_post }}
{{/replies}}
</div>
{{/replies}}
{{/ mod_forum/forum_discussion_post }}
{{/posts}}
@@ -0,0 +1,72 @@
{{!
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/>.
}}
{{!
@template mod_forum/forum_posts_with_context_links
Template to render a list of posts with breadcrumb style subject and a link
in the footer to the full discussion.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{#posts}}
{{< mod_forum/forum_discussion_post }}
{{$subject}}
{{#coursename}}
{{#urls.viewcourse}}
<a href="{{{urls.viewcourse}}}">{{{coursename}}}</a> ->
{{/urls.viewcourse}}
{{^urls.viewcourse}}
{{coursename}} ->
{{/urls.viewcourse}}
{{/coursename}}
{{#urls.viewforum}}
<a href="{{{urls.viewforum}}}">{{{forumname}}}</a>
{{/urls.viewforum}}
{{^urls.viewforum}}
{{{forumname}}}
{{/urls.viewforum}}
{{#showdiscussionname}}
{{#urls.viewdiscussion}}
-> <a href="{{{urls.viewdiscussion}}}">{{{discussionname}}}</a>
{{/urls.viewdiscussion}}
{{^urls.viewdiscussion}}
{{{discussionname}}}
{{/urls.viewdiscussion}}
{{#hasparent}}
{{#urls.viewparent}}
-> <a href="{{{urls.viewparent}}}">{{{subject}}}</a>
{{/urls.viewparent}}
{{^urls.viewparent}}
{{{subject}}}
{{/urls.viewparent}}
{{/hasparent}}
{{/showdiscussionname}}
{{/subject}}
{{$replies}}{{/replies}}
{{$footer}}
<div><a href="{{{urls.view}}}">{{#str}} postincontext, mod_forum {{/str}}</a></div>
{{/footer}}
{{/ mod_forum/forum_discussion_post }}
{{/posts}}
@@ -0,0 +1,39 @@
{{!
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/>.
}}
{{!
@template mod_forum/news_discussion_list
Template to render a list of discussions for a news forum
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{< mod_forum/discussion_list }}
{{$discussion_create_text}}
{{#str}}addanewtopic, forum{{/str}}
{{/discussion_create_text}}
{{$no_discussions_text}}
({{#str}}nonews, forum{{/str}})
{{/no_discussions_text}}
{{/ mod_forum/discussion_list }}
@@ -0,0 +1,39 @@
{{!
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/>.
}}
{{!
@template mod_forum/qanda_discussion_list
Template which defines a forum post for sending in a single-post HTML email.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{< mod_forum/discussion_list }}
{{$discussion_create_text}}
{{#str}}addanewquestion, forum{{/str}}
{{/discussion_create_text}}
{{$no_discussions_text}}
({{#str}}noquestions, forum{{/str}})
{{/no_discussions_text}}
{{/ mod_forum/discussion_list }}
@@ -0,0 +1,42 @@
{{!
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/>.
}}
{{!
@template mod_forum/single_discussion_list
Template to render a discussion for a single discussion forum.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
}
}}
{{< mod_forum/discussion_list }}
{{$discussion_create_text}}
{{#str}}addanewtopic, forum{{/str}}
{{/discussion_create_text}}
{{$discussion_list_output}}
{{#posts}}
{{> mod_forum/forum_discussion_post }}
{{/posts}}
{{/discussion_list_output}}
{{/ mod_forum/discussion_list }}
+1 -1
View File
@@ -96,7 +96,7 @@ class behat_mod_forum extends behat_base {
// Navigate to forum.
$this->execute('behat_general::click_link', $this->escape($forumname));
$this->execute('behat_forms::press_button', $buttonstr);
$this->execute('behat_general::click_link', $buttonstr);
// Fill form and post.
$this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
@@ -68,12 +68,12 @@ Feature: Students can choose from 4 discussion display options and their choice
And I follow "Discussion 1"
When I select "Display replies in threaded form" from the "mode" singleselect
Then I should see "Discussion contents 1, first message"
And I should see "Reply 1 to discussion 1" in the "span.forumthread" "css_element"
And I should see "Reply 1 to discussion 1" in the "div.forumthread" "css_element"
And I follow "Test forum name"
And I follow "Discussion 2"
And the field "Display mode" matches value "Display replies in threaded form"
And I should see "Discussion contents 2, first message"
And I should see "Reply 1 to discussion 2" in the "span.forumthread" "css_element"
And I should see "Reply 1 to discussion 2" in the "div.forumthread" "css_element"
Scenario: Display replies in nested form
Given I follow "Test forum name"
+5 -5
View File
@@ -34,7 +34,7 @@ Feature: Edited forum posts handle tags correctly
| Subject | Student post subject |
| Message | Student post message |
| Tags | Tag1 |
Then I should see "Tag1" in the ".forum-tags" "css_element"
Then I should see "Tag1" in the ".tag_list" "css_element"
And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Student 1']" "xpath_element"
Then I should see "Tag1" in the ".form-autocomplete-selection" "css_element"
@@ -50,7 +50,7 @@ Feature: Edited forum posts handle tags correctly
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Test forum"
And I click on "Add a new discussion topic" "button"
And I click on "Add a new discussion topic" "link"
And I expand all fieldsets
And I open the autocomplete suggestions list
And I should see "OT1" in the ".form-autocomplete-suggestions" "css_element"
@@ -60,9 +60,9 @@ Feature: Edited forum posts handle tags correctly
| Subject | Student post subject |
| Message | Student post message |
| Tags | OT1, OT3 |
Then I should see "OT1" in the ".forum-tags" "css_element"
And I should see "OT3" in the ".forum-tags" "css_element"
And I should not see "OT2" in the ".forum-tags" "css_element"
Then I should see "OT1" in the ".tag_list" "css_element"
And I should see "OT3" in the ".tag_list" "css_element"
And I should not see "OT2" in the ".tag_list" "css_element"
And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Teacher 1']" "xpath_element"
And I should see "OT1" in the ".form-autocomplete-selection" "css_element"
And I should see "OT3" in the ".form-autocomplete-selection" "css_element"
@@ -29,13 +29,13 @@ Feature: A user can control their default discussion subscription settings
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test forum name"
When I press "Add a new discussion topic"
When I click on "Add a new discussion topic" "link"
Then "input[name=discussionsubscribe][checked=checked]" "css_element" should exist
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test forum name"
And I press "Add a new discussion topic"
And I click on "Add a new discussion topic" "link"
And "input[name=discussionsubscribe]:not([checked=checked])" "css_element" should exist
Scenario: Replying to an existing discussion in an optional forum follows user preferences
@@ -72,13 +72,13 @@ Feature: A user can control their default discussion subscription settings
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Test forum name"
When I press "Add a new discussion topic"
When I click on "Add a new discussion topic" "link"
Then "input[name=discussionsubscribe][checked=checked]" "css_element" should exist
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I follow "Test forum name"
And I press "Add a new discussion topic"
And I click on "Add a new discussion topic" "link"
And "input[name=discussionsubscribe][checked=checked]" "css_element" should exist
Scenario: Replying to an existing discussion in an automatic forum follows forum subscription
@@ -35,7 +35,7 @@ Feature: Forums in 'No groups' mode allow posting to All participants for all us
And I follow "Standard forum name"
And I should not see "Group A"
And I should not see "Group B"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
And I should not see "Group" in the "form" "css_element"
And I set the following fields to these values:
@@ -51,7 +51,7 @@ Feature: Forums in 'No groups' mode allow posting to All participants for all us
And I follow "Standard forum name"
And I should not see "Group A"
And I should not see "Group B"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
And I should not see "Group" in the "form" "css_element"
And I set the following fields to these values:
@@ -23,7 +23,7 @@ Feature: Posting to forums in a course with no groups behaves correctly
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Standard forum"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
And I set the following fields to these values:
| Subject | Teacher -> All participants |
@@ -36,7 +36,7 @@ Feature: Posting to forums in a course with no groups behaves correctly
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Separate forum"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
And I set the following fields to these values:
| Subject | Teacher -> All participants |
@@ -49,7 +49,7 @@ Feature: Posting to forums in a course with no groups behaves correctly
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Visible forum"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
And I set the following fields to these values:
| Subject | Teacher -> All participants |
@@ -62,7 +62,7 @@ Feature: Posting to forums in a course with no groups behaves correctly
Given I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Standard forum"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
And I set the following fields to these values:
| Subject | Student -> All participants |
@@ -112,7 +112,7 @@ Feature: A user with access to multiple groups should be able to post a copy of
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "No group forum"
And I press "Add a new discussion topic"
And I click on "Add a new discussion topic" "link"
Then I should not see "Post a copy to all groups"
Scenario: Posts to all groups that have groupings should only display within the grouping and not to other groups
@@ -56,7 +56,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Standard forum name"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then the "Group" select box should contain "All participants"
And the "Group" select box should contain "Group A"
And the "Group" select box should contain "Group B"
@@ -68,7 +68,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And I am on "Course 1" course homepage
And I follow "Standard forum name"
And I select "Group A" from the "Separate groups" singleselect
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should see "Post a copy to all groups"
And I set the following fields to these values:
| Subject | Teacher 1 -> Group B |
@@ -98,7 +98,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And I am on "Course 1" course homepage
And I follow "Standard forum name"
And I select "Group A" from the "Separate groups" singleselect
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then I should see "Post a copy to all groups"
And I set the following fields to these values:
| Subject | Teacher 1 -> Group C |
@@ -127,7 +127,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Standard forum name"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
And I set the following fields to these values:
| Subject | Teacher 1 -> Post to all |
| Message | Teacher 1 -> Post to all |
@@ -155,7 +155,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And I am on "Course 1" course homepage
When I follow "Standard forum name"
Then I should see "Group A"
And I click on "Add a new discussion topic" "button"
And I click on "Add a new discussion topic" "link"
And I should see "Group A"
And I should not see "Group B"
And I should not see "Group C"
@@ -173,7 +173,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And I am on "Course 1" course homepage
When I follow "Standard forum name"
And I select "Group A" from the "Separate groups" singleselect
And I click on "Add a new discussion topic" "button"
And I click on "Add a new discussion topic" "link"
And the "Group" select box should not contain "All participants"
And the "Group" select box should contain "Group A"
And the "Group" select box should contain "Group B"
@@ -193,7 +193,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
And I should not see "Student -> B"
# Now try posting in Group A (starting at Group B)
And I select "Group B" from the "Separate groups" singleselect
And I click on "Add a new discussion topic" "button"
And I click on "Add a new discussion topic" "link"
And the "Group" select box should not contain "All participants"
And the "Group" select box should contain "Group A"
And the "Group" select box should contain "Group B"
@@ -220,7 +220,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
Given I log in as "noneditor1"
And I am on "Course 1" course homepage
And I follow "Standard forum name"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then the "Group" select box should not contain "All participants"
And the "Group" select box should contain "Group A"
And the "Group" select box should contain "Group B"
@@ -234,7 +234,7 @@ Feature: Posting to all groups in a separate group discussion is restricted to u
Given I log in as "noneditor1"
And I am on "Course 1" course homepage
And I follow "Standard forum name"
When I click on "Add a new discussion topic" "button"
When I click on "Add a new discussion topic" "link"
Then the "Group" select box should not contain "All participants"
And the "Group" select box should contain "Group A"
And the "Group" select box should contain "Group B"

Some files were not shown because too many files have changed in this diff Show More