├── README.txt ├── pix ├── icon.gif ├── icon.png ├── icon.svg └── sprites.svg ├── db ├── uninstall.php ├── tag.php ├── install.php ├── upgrade.php ├── access.php └── install.xml ├── locallib.php ├── version.php ├── classes ├── event │ ├── course_module_instance_list_viewed.php │ └── course_module_viewed.php ├── post_form.php ├── output │ └── listing.php ├── listing.php └── post.php ├── grade.php ├── tests ├── behat │ ├── basic.feature │ ├── installed.feature │ └── behat_mod_expertforum.php ├── generator │ └── lib.php └── lib_test.php ├── templates ├── thread.mustache ├── listing.mustache └── post.mustache ├── backup └── moodle2 │ ├── backup_expertforum_stepslib.php │ ├── backup_expertforum_activity_task.class.php │ ├── restore_expertforum_stepslib.php │ └── restore_expertforum_activity_task.class.php ├── viewpost.php ├── post.php ├── lang └── en │ └── expertforum.php ├── mod_form.php ├── view.php ├── index.php ├── styles.css └── lib.php /README.txt: -------------------------------------------------------------------------------- 1 | Expert forum module 2 | =================== 3 | -------------------------------------------------------------------------------- /pix/icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_expertforum/master/pix/icon.gif -------------------------------------------------------------------------------- /pix/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_expertforum/master/pix/icon.png -------------------------------------------------------------------------------- /db/uninstall.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides code to be executed during the module uninstallation 19 | * 20 | * @see uninstall_plugin() 21 | * 22 | * @package mod_expertforum 23 | * @copyright 2015 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | 27 | /** 28 | * Custom uninstallation procedure 29 | */ 30 | function xmldb_expertforum_uninstall() { 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /db/tag.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Tag area definitions 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2016 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $tagareas = array( 28 | array( 29 | 'itemtype' => 'expertforum_post', 30 | 'component' => 'mod_expertforum', 31 | 'callback' => 'mod_expertforum_listing::tagged_posts_index', 32 | ), 33 | ); 34 | -------------------------------------------------------------------------------- /locallib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Internal library of functions for module expertforum 19 | * 20 | * All the expertforum specific functions, needed to implement the module 21 | * logic, should go here. Never include this file from your lib.php! 22 | * 23 | * @package mod_expertforum 24 | * @copyright 2015 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | /* 31 | * Does something really useful with the passed things 32 | * 33 | * @param array $things 34 | * @return object 35 | *function expertforum_do_something_useful(array $things) { 36 | * return new stdClass(); 37 | *} 38 | */ 39 | 40 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the version and other meta-info about the plugin 19 | * 20 | * Setting the $plugin->version to 0 prevents the plugin from being installed. 21 | * See https://docs.moodle.org/dev/version.php for more info. 22 | * 23 | * @package mod_expertforum 24 | * @copyright 2015 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | $plugin->component = 'mod_expertforum'; 31 | $plugin->version = 2016060603; 32 | $plugin->release = 'v1.1'; 33 | $plugin->requires = 2016052300; 34 | $plugin->maturity = MATURITY_ALPHA; 35 | $plugin->dependencies = array(); 36 | -------------------------------------------------------------------------------- /classes/event/course_module_instance_list_viewed.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * The mod_expertforum instance list viewed event. 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace mod_expertforum\event; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * The mod_expertforum instance list viewed event class. 31 | * 32 | * @package mod_expertforum 33 | * @copyright 2015 Marina Glancy 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { 37 | } 38 | -------------------------------------------------------------------------------- /grade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Redirect the user to the appropriate submission related page 19 | * 20 | * @package mod_expertforum 21 | * @category grade 22 | * @copyright 2015 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | require_once(__DIR__ . "../../../config.php"); 27 | 28 | $id = required_param('id', PARAM_INT);// Course module ID. 29 | // Item number may be != 0 for activities that allow more than one grade per user. 30 | $itemnumber = optional_param('itemnumber', 0, PARAM_INT); 31 | $userid = optional_param('userid', 0, PARAM_INT); // Graded user ID (optional). 32 | 33 | // In the simplest case just redirect to the view page. 34 | redirect('view.php?id='.$id); 35 | -------------------------------------------------------------------------------- /tests/behat/basic.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_expertforum 2 | Feature: Add forum activities and posts 3 | In order to discuss topics with other users 4 | As a user 5 | I need to add expertforum activities to moodle courses 6 | 7 | @javascript 8 | Scenario: Add an expertforum 9 | Given the following "users" exist: 10 | | username | firstname | lastname | email | 11 | | teacher1 | Teacher | 1 | teacher1@example.com | 12 | | student1 | Student | 1 | student1@example.com | 13 | | student2 | Student | 2 | student2@example.com | 14 | And the following "courses" exist: 15 | | fullname | shortname | category | 16 | | Course 1 | C1 | 0 | 17 | And the following "course enrolments" exist: 18 | | user | course | role | 19 | | teacher1 | C1 | editingteacher | 20 | | student1 | C1 | student | 21 | | student2 | C1 | student | 22 | And I log in as "teacher1" 23 | And I follow "Course 1" 24 | And I turn editing mode on 25 | And I add a "Expert forum" to section "1" and I fill the form with: 26 | | Expert forum name | Test expertforum name | 27 | | Description | Test expertforum description | 28 | And I log out 29 | And I log in as "student1" 30 | And I follow "Course 1" 31 | And I add a new question to "Test expertforum name" expertforum with: 32 | | Title | Forum post 1 | 33 | | Question | This is the body | 34 | | Tags | Php, programming | 35 | And I log out 36 | -------------------------------------------------------------------------------- /db/install.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides code to be executed during the module installation 19 | * 20 | * This file replaces the legacy STATEMENTS section in db/install.xml, 21 | * lib.php/modulename_install() post installation hook and partially defaults.php. 22 | * 23 | * @package mod_expertforum 24 | * @copyright 2015 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | /** 29 | * Post installation procedure 30 | * 31 | * @see upgrade_plugins_modules() 32 | */ 33 | function xmldb_expertforum_install() { 34 | } 35 | 36 | /** 37 | * Post installation recovery procedure 38 | * 39 | * @see upgrade_plugins_modules() 40 | */ 41 | function xmldb_expertforum_install_recovery() { 42 | } 43 | -------------------------------------------------------------------------------- /classes/event/course_module_viewed.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the view event. 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace mod_expertforum\event; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * The mod_expertforum instance list viewed event class 31 | * 32 | * If the view mode needs to be stored as well, you may need to 33 | * override methods get_url() and get_legacy_log_data(), too. 34 | * 35 | * @package mod_expertforum 36 | * @copyright 2015 Marina Glancy 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class course_module_viewed extends \core\event\course_module_viewed { 40 | 41 | /** 42 | * Initialize the event 43 | */ 44 | protected function init() { 45 | $this->data['objecttable'] = 'expertforum'; 46 | parent::init(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /db/upgrade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * This file keeps track of upgrades to the expertforum module 19 | * 20 | * Sometimes, changes between versions involve alterations to database 21 | * structures and other major things that may break installations. The upgrade 22 | * function in this file will attempt to perform all the necessary actions to 23 | * upgrade your older installation to the current version. If there's something 24 | * it cannot do itself, it will tell you what you need to do. The commands in 25 | * here will all be database-neutral, using the functions defined in DLL libraries. 26 | * 27 | * @package mod_expertforum 28 | * @copyright 2015 Marina Glancy 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | 32 | defined('MOODLE_INTERNAL') || die(); 33 | 34 | /** 35 | * Execute expertforum upgrade from the given old version 36 | * 37 | * @param int $oldversion 38 | * @return bool 39 | */ 40 | function xmldb_expertforum_upgrade($oldversion) { 41 | global $DB; 42 | 43 | $dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes. 44 | 45 | return true; 46 | } 47 | -------------------------------------------------------------------------------- /templates/thread.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template mod_expertforum/thread 19 | 20 | Displays one post (question with answers) 21 | 22 | Classes required for JS: 23 | * none 24 | 25 | Data attributes required for JS: 26 | * none 27 | 28 | Context variables required for this template: 29 | * none 30 | 31 | Example context (json): 32 | { 33 | "id":1, 34 | "votes":4, 35 | "message":"My message", 36 | "answersheader":"2 answers", 37 | "answers":[ 38 | {"id":2, 39 | "votes":3, 40 | "message":"My answer1"}, 41 | {"id":3, 42 | "votes":1, 43 | "message":"My answer2"} 44 | ] 45 | } 46 | }} 47 |
48 | {{> mod_expertforum/post}} 49 |
50 |
51 | 52 |
53 |

{{{answersheader}}}

54 |
55 | {{#answers}} 56 | 57 |
58 |
59 | {{> mod_expertforum/post}} 60 |
61 | {{/answers}} 62 |
63 | -------------------------------------------------------------------------------- /backup/moodle2/backup_expertforum_stepslib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Define all the backup steps that will be used by the backup_expertforum_activity_task 19 | * 20 | * @package mod_expertforum 21 | * @category backup 22 | * @copyright 2015 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die; 27 | 28 | /** 29 | * Define the complete expertforum structure for backup, with file and id annotations 30 | * 31 | * @package mod_expertforum 32 | * @category backup 33 | * @copyright 2015 Marina Glancy 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class backup_expertforum_activity_structure_step extends backup_activity_structure_step { 37 | 38 | /** 39 | * Defines the backup structure of the module 40 | * 41 | * @return backup_nested_element 42 | */ 43 | protected function define_structure() { 44 | 45 | // Get know if we are including userinfo. 46 | $userinfo = $this->get_setting_value('userinfo'); 47 | 48 | // Define the root element describing the expertforum instance. 49 | $expertforum = new backup_nested_element('expertforum', array('id'), array( 50 | 'name', 'intro', 'introformat', 'grade')); 51 | 52 | // If we had more elements, we would build the tree here. 53 | 54 | // Define data sources. 55 | $expertforum->set_source_table('expertforum', array('id' => backup::VAR_ACTIVITYID)); 56 | 57 | // If we were referring to other tables, we would annotate the relation 58 | // with the element's annotate_ids() method. 59 | 60 | // Define file annotations (we do not use itemid in this example). 61 | $expertforum->annotate_files('mod_expertforum', 'intro', null); 62 | 63 | // Return the root element (expertforum), wrapped into standard activity structure. 64 | return $this->prepare_activity_structure($expertforum); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backup/moodle2/backup_expertforum_activity_task.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines backup_expertforum_activity_task class 19 | * 20 | * @package mod_expertforum 21 | * @category backup 22 | * @copyright 2015 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die; 27 | 28 | require_once($CFG->dirroot . '/mod/expertforum/backup/moodle2/backup_expertforum_stepslib.php'); 29 | 30 | /** 31 | * Provides the steps to perform one complete backup of the expertforum instance 32 | * 33 | * @package mod_expertforum 34 | * @category backup 35 | * @copyright 2015 Marina Glancy 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class backup_expertforum_activity_task extends backup_activity_task { 39 | 40 | /** 41 | * No specific settings for this activity 42 | */ 43 | protected function define_my_settings() { 44 | } 45 | 46 | /** 47 | * Defines a backup step to store the instance data in the expertforum.xml file 48 | */ 49 | protected function define_my_steps() { 50 | $this->add_step(new backup_expertforum_activity_structure_step('expertforum_structure', 'expertforum.xml')); 51 | } 52 | 53 | /** 54 | * Encodes URLs to the index.php and view.php scripts 55 | * 56 | * @param string $content some HTML text that eventually contains URLs to the activity instance scripts 57 | * @return string the content with the URLs encoded 58 | */ 59 | static public function encode_content_links($content) { 60 | global $CFG; 61 | 62 | $base = preg_quote($CFG->wwwroot, '/'); 63 | 64 | // Link to the list of expertforums. 65 | $search = '/('.$base.'\/mod\/expertforum\/index.php\?id\=)([0-9]+)/'; 66 | $content = preg_replace($search, '$@EXPERTFORUMINDEX*$2@$', $content); 67 | 68 | // Link to expertforum view by moduleid. 69 | $search = '/('.$base.'\/mod\/expertforum\/view.php\?id\=)([0-9]+)/'; 70 | $content = preg_replace($search, '$@EXPERTFORUMVIEWBYID*$2@$', $content); 71 | 72 | return $content; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /viewpost.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Views a forum post with answers 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once(__DIR__ . '/../../config.php'); 26 | 27 | $instanceid = required_param('e', PARAM_INT); 28 | $postid = optional_param('parent', null, PARAM_INT); 29 | if (!$postid) { 30 | $postid = required_param('id', PARAM_INT); 31 | } 32 | 33 | list($course, $cm) = get_course_and_cm_from_instance($instanceid, 'expertforum'); 34 | 35 | require_login($course, true, $cm); 36 | $expertforum = $DB->get_record('expertforum', array('id' => $cm->instance), '*', MUST_EXIST); 37 | $post = mod_expertforum_post::get($postid, $cm, MUST_EXIST); 38 | 39 | if ($upvote = optional_param('upvote', null, PARAM_INT)) { 40 | require_sesskey(); 41 | $post->vote($upvote, 1); 42 | redirect($post->get_url()); 43 | } 44 | if ($downvote = optional_param('downvote', null, PARAM_INT)) { 45 | require_sesskey(); 46 | $post->vote($downvote, -1); 47 | redirect($post->get_url()); 48 | } 49 | 50 | $PAGE->set_url($post->get_url()); 51 | $PAGE->set_title($post->get_formatted_subject()); 52 | $PAGE->set_heading(format_string($course->fullname)); 53 | 54 | $form = null; 55 | if ($post->can_answer()) { 56 | $form = new mod_expertforum_post_form(null, 57 | array('expertforum' => $expertforum, 'parent' => $post, 'cm' => $cm), 58 | 'post', '', array('class' => 'mod_expertforum_post')); 59 | 60 | if ($form->is_cancelled()) { 61 | redirect($PAGE->url); 62 | } else if ($data = $form->get_data()) { 63 | $post = mod_expertforum_post::create($data, $cm); 64 | redirect($post->get_url()); 65 | } 66 | } 67 | 68 | $PAGE->navbar->add($post->get_formatted_subject(), $post->get_url()); 69 | 70 | echo $OUTPUT->header(); 71 | echo $OUTPUT->heading($post->get_formatted_subject()); 72 | 73 | echo $OUTPUT->render_from_template('mod_expertforum/thread', $post->export_for_template($OUTPUT)); 74 | 75 | if ($form) { 76 | $form->display(); 77 | } 78 | 79 | // Finish the page. 80 | echo $OUTPUT->footer(); 81 | -------------------------------------------------------------------------------- /post.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adds a post to the forum 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once(__DIR__ . '/../../config.php'); 26 | 27 | $instanceid = required_param('e', PARAM_INT); 28 | $editpostid = optional_param('edit', null, PARAM_INT); 29 | 30 | list($course, $cm) = get_course_and_cm_from_instance($instanceid, 'expertforum'); 31 | 32 | require_login($course, true, $cm); 33 | $expertforum = $DB->get_record('expertforum', array('id' => $cm->instance), '*', MUST_EXIST); 34 | if ($editpostid) { 35 | $postrecord = $DB->get_record('expertforum_post', 36 | array('id' => $editpostid, 'expertforumid' => $expertforum->id), '*', MUST_EXIST); 37 | $post = new mod_expertforum_post($postrecord, $cm); 38 | if (!$post->can_update()) { 39 | throw new moodle_exception('notallowededit', 'mod_expertforum'); 40 | } 41 | $PAGE->navbar->add($post->get_formatted_subject(), $post->get_url()); 42 | $PAGE->navbar->add(get_string('editlink', 'mod_expertforum')); 43 | } else { 44 | $post = null; 45 | if (!mod_expertforum_post::can_create($cm)) { 46 | throw new moodle_exception('notallowedcreate', 'mod_expertforum'); 47 | } 48 | } 49 | 50 | // Print the page header. 51 | 52 | $PAGE->set_url('/mod/expertforum/post.php', array('e' => $cm->instance)); 53 | $PAGE->set_title(format_string($expertforum->name)); 54 | $PAGE->set_heading(format_string($course->fullname)); 55 | 56 | $form = new mod_expertforum_post_form(null, 57 | array('expertforum' => $expertforum, 'post' => $post, 'cm' => $cm), 58 | 'post', '', array('class' => 'mod_expertforum_post')); 59 | 60 | if ($form->is_cancelled()) { 61 | redirect(new moodle_url('/mod/expertforum/view.php', array('id' => $cm->id))); 62 | } else if ($data = $form->get_data()) { 63 | if ($post) { 64 | $post->update($data); 65 | } else { 66 | $post = mod_expertforum_post::create($data, $cm); 67 | } 68 | redirect($post->get_url()); 69 | } 70 | 71 | 72 | // Output starts here. 73 | echo $OUTPUT->header(); 74 | 75 | $form->display(); 76 | 77 | // Finish the page. 78 | echo $OUTPUT->footer(); 79 | -------------------------------------------------------------------------------- /db/access.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Capability definitions for the expertforum module 19 | * 20 | * The capabilities are loaded into the database table when the module is 21 | * installed or updated. Whenever the capability definitions are updated, 22 | * the module version number should be bumped up. 23 | * 24 | * The system has four possible values for a capability: 25 | * CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT, and inherit (not set). 26 | * 27 | * It is important that capability names are unique. The naming convention 28 | * for capabilities that are specific to modules and blocks is as follows: 29 | * [mod/block]/: 30 | * 31 | * component_name should be the same as the directory name of the mod or block. 32 | * 33 | * Core moodle capabilities are defined thus: 34 | * moodle/: 35 | * 36 | * Examples: mod/forum:viewpost 37 | * block/recent_activity:view 38 | * moodle/site:deleteuser 39 | * 40 | * The variable name for the capability definitions array is $capabilities 41 | * 42 | * @package mod_expertforum 43 | * @copyright 2015 Marina Glancy 44 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 45 | */ 46 | 47 | defined('MOODLE_INTERNAL') || die(); 48 | 49 | // Modify capabilities as needed and remove this comment. 50 | $capabilities = array( 51 | 'mod/expertforum:addinstance' => array( 52 | 'riskbitmask' => RISK_XSS, 53 | 'captype' => 'write', 54 | 'contextlevel' => CONTEXT_COURSE, 55 | 'archetypes' => array( 56 | 'editingteacher' => CAP_ALLOW, 57 | 'manager' => CAP_ALLOW 58 | ), 59 | 'clonepermissionsfrom' => 'moodle/course:manageactivities' 60 | ), 61 | 62 | 'mod/expertforum:view' => array( 63 | 'captype' => 'read', 64 | 'contextlevel' => CONTEXT_MODULE, 65 | 'legacy' => array( 66 | 'guest' => CAP_ALLOW, 67 | 'student' => CAP_ALLOW, 68 | 'teacher' => CAP_ALLOW, 69 | 'editingteacher' => CAP_ALLOW, 70 | 'manager' => CAP_ALLOW 71 | ) 72 | ), 73 | 74 | 'mod/expertforum:submit' => array( 75 | 'riskbitmask' => RISK_SPAM, 76 | 'captype' => 'write', 77 | 'contextlevel' => CONTEXT_MODULE, 78 | 'legacy' => array( 79 | 'student' => CAP_ALLOW 80 | ) 81 | ), 82 | ); 83 | -------------------------------------------------------------------------------- /backup/moodle2/restore_expertforum_stepslib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Define all the restore steps that will be used by the restore_expertforum_activity_task 19 | * 20 | * @package mod_expertforum 21 | * @category backup 22 | * @copyright 2015 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | /** 27 | * Structure step to restore one expertforum activity 28 | * 29 | * @package mod_expertforum 30 | * @category backup 31 | * @copyright 2015 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class restore_expertforum_activity_structure_step extends restore_activity_structure_step { 35 | 36 | /** 37 | * Defines structure of path elements to be processed during the restore 38 | * 39 | * @return array of {@link restore_path_element} 40 | */ 41 | protected function define_structure() { 42 | 43 | $paths = array(); 44 | $paths[] = new restore_path_element('expertforum', '/activity/expertforum'); 45 | 46 | // Return the paths wrapped into standard activity structure. 47 | return $this->prepare_activity_structure($paths); 48 | } 49 | 50 | /** 51 | * Process the given restore path element data 52 | * 53 | * @param array $data parsed element data 54 | */ 55 | protected function process_expertforum($data) { 56 | global $DB; 57 | 58 | $data = (object)$data; 59 | $oldid = $data->id; 60 | $data->course = $this->get_courseid(); 61 | 62 | if (empty($data->timecreated)) { 63 | $data->timecreated = time(); 64 | } 65 | 66 | if (empty($data->timemodified)) { 67 | $data->timemodified = time(); 68 | } 69 | 70 | if ($data->grade < 0) { 71 | // Scale found, get mapping. 72 | $data->grade = -($this->get_mappingid('scale', abs($data->grade))); 73 | } 74 | 75 | // Create the expertforum instance. 76 | $newitemid = $DB->insert_record('expertforum', $data); 77 | $this->apply_activity_instance($newitemid); 78 | } 79 | 80 | /** 81 | * Post-execution actions 82 | */ 83 | protected function after_execute() { 84 | // Add expertforum related files, no need to match by itemname (just internally handled context). 85 | $this->add_related_files('mod_expertforum', 'intro', null); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lang/en/expertforum.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | 18 | /** 19 | * English strings for expertforum 20 | * 21 | * You can have a rather longer description of the file as well, 22 | * if you like, and it can span multiple lines. 23 | * 24 | * @package mod_expertforum 25 | * @copyright 2015 Marina Glancy 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | 29 | defined('MOODLE_INTERNAL') || die(); 30 | 31 | $string['answerdownvote'] = 'This answer is not useful'; 32 | $string['answeredago'] = 'answered {$a} ago'; 33 | $string['answerscount'] = '{$a} answers'; 34 | $string['answerupvote'] = 'This answer is useful'; 35 | $string['askedago'] = 'asked {$a} ago'; 36 | $string['downvote'] = 'down vote'; 37 | $string['editlink'] = 'edit'; 38 | $string['edittitle'] = 'revise and improve this post'; 39 | $string['expertforum:addinstance'] = 'Add instance of Expert forum'; 40 | $string['expertforum:submit'] = 'Post to Expert forum'; 41 | $string['expertforum:view'] = 'View Expert forum'; 42 | $string['expertforumname'] = 'Expert forum name'; 43 | $string['favourite'] = 'favourite'; 44 | $string['favouriteoff'] = 'This is a favourite question (click to undo)'; 45 | $string['favouriteon'] = 'Click to mark as favourite'; 46 | $string['modulename'] = 'Expert forum'; 47 | $string['modulename_help'] = 'The Expert forum module allows to create forums where answers can be up/down voted and automatically sorted with the most voted first'; 48 | $string['modulenameplural'] = 'Expert forums'; 49 | $string['notallowedcreate'] = 'You are not allowed to post here'; 50 | $string['notallowededit'] = 'You are not allowed to edit this'; 51 | $string['pluginadministration'] = 'Expert forum administration'; 52 | $string['pluginname'] = 'Expert forum'; 53 | $string['postnotfound'] = 'Post not found'; 54 | $string['postsubject'] = 'Title'; 55 | $string['questiondownvote'] = 'This question does not show any research effort; it is unclear or not useful'; 56 | $string['questionupvote'] = 'This question shows research effort; it is useful and clear'; 57 | $string['reputation'] = 'reputation score'; 58 | $string['tagarea_expertforum_post'] = 'Expertforum posts'; 59 | $string['tagcollection_expertforum'] = 'Expertforum posts'; 60 | $string['upvote'] = 'up vote'; 61 | $string['viewscount'] = '{$a} views'; 62 | $string['votescount'] = '{$a} votes'; 63 | $string['votescountshort'] = '{$a}'; 64 | $string['youranswer'] = 'Your answer'; 65 | $string['yourquestion'] = 'Question'; 66 | -------------------------------------------------------------------------------- /mod_form.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * The main expertforum configuration form 19 | * 20 | * It uses the standard core Moodle formslib. For more info about them, please 21 | * visit: http://docs.moodle.org/en/Development:lib/formslib.php 22 | * 23 | * @package mod_expertforum 24 | * @copyright 2015 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | require_once($CFG->dirroot.'/course/moodleform_mod.php'); 31 | 32 | /** 33 | * Module instance settings form 34 | * 35 | * @package mod_expertforum 36 | * @copyright 2015 Marina Glancy 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class mod_expertforum_mod_form extends moodleform_mod { 40 | 41 | /** 42 | * Defines forms elements 43 | */ 44 | public function definition() { 45 | global $CFG; 46 | 47 | $mform = $this->_form; 48 | 49 | // Adding the "general" fieldset, where all the common settings are showed. 50 | $mform->addElement('header', 'general', get_string('general', 'form')); 51 | 52 | // Adding the standard "name" field. 53 | $mform->addElement('text', 'name', get_string('expertforumname', 'expertforum'), array('size' => '64')); 54 | if (!empty($CFG->formatstringstriptags)) { 55 | $mform->setType('name', PARAM_TEXT); 56 | } else { 57 | $mform->setType('name', PARAM_CLEANHTML); 58 | } 59 | $mform->addRule('name', null, 'required', null, 'client'); 60 | $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); 61 | 62 | // Adding the standard "intro" and "introformat" fields. 63 | if ($CFG->branch >= 29) { 64 | $this->standard_intro_elements(); 65 | } else { 66 | $this->add_intro_editor(); 67 | } 68 | 69 | // Adding the rest of expertforum settings, spreading all them into this fieldset 70 | // ... or adding more fieldsets ('header' elements) if needed for better logic. 71 | //$mform->addElement('static', 'label1', 'expertforumsetting1', 'Your expertforum fields go here. Replace me!'); 72 | 73 | //$mform->addElement('header', 'expertforumfieldset', get_string('expertforumfieldset', 'expertforum')); 74 | //$mform->addElement('static', 'label2', 'expertforumsetting2', 'Your expertforum fields go here. Replace me!'); 75 | 76 | // Add standard grading elements. 77 | $this->standard_grading_coursemodule_elements(); 78 | 79 | // Add standard elements, common to all modules. 80 | $this->standard_coursemodule_elements(); 81 | 82 | // Add standard buttons, common to all modules. 83 | $this->add_action_buttons(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/behat/installed.feature: -------------------------------------------------------------------------------- 1 | @mod @mod_expertforum 2 | Feature: Installation succeeds 3 | In order to use this plugin 4 | As a user 5 | I need the installation to work 6 | 7 | Scenario: Check the Plugins overview for the name of this plugin 8 | Given I log in as "admin" 9 | And I navigate to "Plugins overview" node in "Site administration > Plugins" 10 | Then the following should exist in the "plugins-control-panel" table: 11 | |Plugin name| 12 | |mod_expertforum| 13 | 14 | @javascript 15 | Scenario: Add an expertforum 16 | Given the following "users" exist: 17 | | username | firstname | lastname | email | 18 | | teacher1 | Teacher | 1 | teacher1@example.com | 19 | | student1 | Student | 1 | student1@example.com | 20 | | student2 | Student | 2 | student2@example.com | 21 | | student3 | Student | 3 | student3@example.com | 22 | And the following "courses" exist: 23 | | fullname | shortname | category | 24 | | Course 1 | C1 | 0 | 25 | And the following "course enrolments" exist: 26 | | user | course | role | 27 | | teacher1 | C1 | editingteacher | 28 | | student1 | C1 | student | 29 | | student2 | C1 | student | 30 | | student3 | C1 | student | 31 | And I log in as "teacher1" 32 | And I follow "Course 1" 33 | And I turn editing mode on 34 | And I add a "Expert forum" to section "1" and I fill the form with: 35 | | Expert forum name | Questions about everything | 36 | And I log out 37 | And I log in as "student1" 38 | And I follow "Course 1" 39 | #And I add a new question to "Questions about everything" expertforum with: 40 | # | Title | Forum post 1 | 41 | # | Question | This is the body | 42 | # | Tags | Php, programming | 43 | And I follow "Questions about everything" 44 | And I press "Ask question" 45 | And I set the following fields to these values: 46 | | Title | Question about stars | 47 | | Question | How can I count the stars? | 48 | | Tags | Astronomy, Counting | 49 | And I press "Save changes" 50 | And I wait to be redirected 51 | And I log out 52 | And I log in as "student2" 53 | And I follow "Course 1" 54 | And I follow "Questions about everything" 55 | And I follow "Question about stars" 56 | And I set the following fields to these values: 57 | | Your answer | It is impossible | 58 | And I press "Save changes" 59 | And I wait to be redirected 60 | And I should see "1 answers" 61 | And I log out 62 | And I log in as "student3" 63 | And I follow "Course 1" 64 | And I follow "Questions about everything" 65 | And I follow "Question about stars" 66 | And I set the following fields to these values: 67 | | Your answer | Use the telescope | 68 | And I press "Save changes" 69 | And I wait to be redirected 70 | And I should see "2 answers" 71 | And I log out 72 | And I log in as "teacher1" 73 | And I follow "Course 1" 74 | And I follow "Questions about everything" 75 | And I follow "Question about stars" 76 | And "Use the telescope" "text" should appear after "It is impossible" "text" 77 | And I click on "This answer is useful" "link" in the "//div[contains(@class,'answer') and contains(.,'Use the telescope')]" "xpath_element" 78 | And I follow "This question does not show any research effort" 79 | And I follow "Question about stars" 80 | And "Use the telescope" "text" should appear before "It is impossible" "text" 81 | And I log out 82 | 83 | -------------------------------------------------------------------------------- /templates/listing.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template mod_expertforum/listing 19 | 20 | Displays the list of posts (questions) 21 | 22 | Classes required for JS: 23 | * none 24 | 25 | Data attributes required for JS: 26 | * none 27 | 28 | Context variables required for this template: 29 | * none 30 | 31 | Example context (json): 32 | { 33 | "posts": [ {"subject":"Hello", 34 | "excerpt": "This is my very important question", 35 | "votes":2, 36 | "answers":1, 37 | "views":4, 38 | "viewurl":"http://moodle.org", 39 | "timestamp":"asked 15h ago", 40 | "userpicture": "", 41 | "username": "User X", 42 | "userreputation": 15} ] 43 | } 44 | }} 45 |
46 | {{#posts}} 47 |
48 |
49 |
50 |
51 |
52 |
53 | {{#str}}votescount,mod_expertforum,{{votes}}{{/str}} 54 |
55 |
56 | {{#str}}answerscount,mod_expertforum,{{answers}}{{/str}} 57 |
58 |
59 |
60 |
61 | 62 |
63 |
64 |

{{subject}}

65 |
{{{excerpt}}}
66 |
67 | {{#tags}} 68 | 69 | {{/tags}} 70 |
71 |
72 | 85 |
86 |
87 |
88 | {{/posts}} 89 |
-------------------------------------------------------------------------------- /view.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Prints a particular instance of expertforum 19 | * 20 | * You can have a rather longer description of the file as well, 21 | * if you like, and it can span multiple lines. 22 | * 23 | * @package mod_expertforum 24 | * @copyright 2015 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | // Replace expertforum with the name of your module and remove this line. 29 | 30 | require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); 31 | require_once(dirname(__FILE__).'/lib.php'); 32 | 33 | $id = optional_param('id', 0, PARAM_INT); // Course_module ID, or 34 | $instanceid = optional_param('e', 0, PARAM_INT); // ... expertforum instance ID - it should be named as the first character of the module. 35 | $tag = optional_param('tag', null, PARAM_TAG); 36 | 37 | if ($id) { 38 | list($course, $cm) = get_course_and_cm_from_cmid($id, 'expertforum'); 39 | } else if ($instanceid) { 40 | list($course, $cm) = get_course_and_cm_from_instance($instanceid, 'expertforum'); 41 | } else { 42 | print_error('missingparameter'); 43 | } 44 | 45 | require_login($course, true, $cm); 46 | $expertforum = $DB->get_record('expertforum', array('id' => $cm->instance), '*', MUST_EXIST); 47 | 48 | $event = \mod_expertforum\event\course_module_viewed::create(array( 49 | 'objectid' => $PAGE->cm->instance, 50 | 'context' => $PAGE->context, 51 | )); 52 | $event->add_record_snapshot('course', $PAGE->course); 53 | $event->add_record_snapshot($PAGE->cm->modname, $expertforum); 54 | $event->trigger(); 55 | 56 | // Print the page header. 57 | 58 | $PAGE->set_url('/mod/expertforum/view.php', array('id' => $cm->id)); 59 | $PAGE->set_title(format_string($expertforum->name)); 60 | $PAGE->set_heading(format_string($course->fullname)); 61 | 62 | $tagobject = null; 63 | if ($tag) { 64 | $tagcollid = core_tag_area::get_collection('mod_expertforum', 'expertforum_post'); 65 | if ($tagobject = core_tag_tag::get_by_name($tagcollid, $tag)) { 66 | $PAGE->navbar->add($tagobject->get_display_name()); 67 | } 68 | } 69 | 70 | /* 71 | * Other things you may want to set - remove if not needed. 72 | * $PAGE->set_cacheable(false); 73 | * $PAGE->set_focuscontrol('some-html-id'); 74 | * $PAGE->add_body_class('expertforum-'.$somevar); 75 | */ 76 | 77 | // Output starts here. 78 | echo $OUTPUT->header(); 79 | 80 | // Conditions to show the intro can change to look for own settings or whatever. 81 | if ($expertforum->intro) { 82 | echo $OUTPUT->box(format_module_intro('expertforum', $expertforum, $cm->id), 'generalbox mod_introbox', 'expertforumintro'); 83 | } 84 | 85 | echo $OUTPUT->single_button( 86 | new moodle_url('/mod/expertforum/post.php', array('e' => $expertforum->id)), 87 | 'Ask question' // TODO string 88 | ); 89 | 90 | $page = optional_param('page', 0, PARAM_INT); 91 | echo mod_expertforum_listing::module_posts_display($cm, $tagobject, $page); 92 | 93 | // Finish the page. 94 | echo $OUTPUT->footer(); 95 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * This is a one-line short description of the file 19 | * 20 | * You can have a rather longer description of the file as well, 21 | * if you like, and it can span multiple lines. 22 | * 23 | * @package mod_expertforum 24 | * @copyright 2015 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | // Replace expertforum with the name of your module and remove this line. 29 | 30 | require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); 31 | require_once(dirname(__FILE__).'/lib.php'); 32 | 33 | $id = required_param('id', PARAM_INT); // Course. 34 | 35 | $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); 36 | 37 | require_course_login($course); 38 | 39 | $params = array( 40 | 'context' => context_course::instance($course->id) 41 | ); 42 | $event = \mod_expertforum\event\course_module_instance_list_viewed::create($params); 43 | $event->add_record_snapshot('course', $course); 44 | $event->trigger(); 45 | 46 | $strname = get_string('modulenameplural', 'mod_expertforum'); 47 | $PAGE->set_url('/mod/expertforum/index.php', array('id' => $id)); 48 | $PAGE->navbar->add($strname); 49 | $PAGE->set_title("$course->shortname: $strname"); 50 | $PAGE->set_heading($course->fullname); 51 | $PAGE->set_pagelayout('incourse'); 52 | 53 | echo $OUTPUT->header(); 54 | echo $OUTPUT->heading($strname); 55 | 56 | if (! $expertforums = get_all_instances_in_course('expertforum', $course)) { 57 | notice(get_string('noexpertforums', 'expertforum'), new moodle_url('/course/view.php', array('id' => $course->id))); 58 | } 59 | 60 | $usesections = course_format_uses_sections($course->format); 61 | 62 | $table = new html_table(); 63 | $table->attributes['class'] = 'generaltable mod_index'; 64 | 65 | if ($usesections) { 66 | $strsectionname = get_string('sectionname', 'format_'.$course->format); 67 | $table->head = array ($strsectionname, $strname); 68 | $table->align = array ('center', 'left'); 69 | } else { 70 | $table->head = array ($strname); 71 | $table->align = array ('left'); 72 | } 73 | 74 | $modinfo = get_fast_modinfo($course); 75 | $currentsection = ''; 76 | foreach ($modinfo->instances['expertforum'] as $cm) { 77 | $row = array(); 78 | if ($usesections) { 79 | if ($cm->sectionnum !== $currentsection) { 80 | if ($cm->sectionnum) { 81 | $row[] = get_section_name($course, $cm->sectionnum); 82 | } 83 | if ($currentsection !== '') { 84 | $table->data[] = 'hr'; 85 | } 86 | $currentsection = $cm->sectionnum; 87 | } 88 | } 89 | 90 | $class = $cm->visible ? null : array('class' => 'dimmed'); 91 | 92 | $row[] = html_writer::link(new moodle_url('view.php', array('id' => $cm->id)), 93 | $cm->get_formatted_name(), $class); 94 | $table->data[] = $row; 95 | } 96 | 97 | echo html_writer::table($table); 98 | 99 | echo $OUTPUT->footer(); 100 | -------------------------------------------------------------------------------- /classes/post_form.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the class 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | require_once($CFG->libdir . '/formslib.php'); 28 | 29 | /** 30 | * The mod_expertforum instance list viewed event class 31 | * 32 | * If the view mode needs to be stored as well, you may need to 33 | * override methods get_url() and get_legacy_log_data(), too. 34 | * 35 | * @package mod_expertforum 36 | * @copyright 2015 Marina Glancy 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class mod_expertforum_post_form extends moodleform { 40 | 41 | public function definition() { 42 | $expertforum = $this->_customdata['expertforum']; 43 | $cm = $this->_customdata['cm']; 44 | $parent = null; 45 | $post = null; 46 | if (!empty($this->_customdata['post'])) { 47 | $post = $this->_customdata['post']; 48 | if ($post->parent) { 49 | $parent = (object)array('id' => $post->parent); 50 | } 51 | } else if (!empty($this->_customdata['parent'])) { 52 | $parent = $this->_customdata['parent']; 53 | } 54 | 55 | $mform = $this->_form; 56 | 57 | $mform->addElement('hidden', 'e'); 58 | $mform->setType('e', PARAM_INT); 59 | 60 | $mform->addElement('hidden', 'edit'); 61 | $mform->setType('edit', PARAM_INT); 62 | 63 | $mform->addElement('hidden', 'parent'); 64 | $mform->setType('parent', PARAM_INT); 65 | 66 | if (!$parent) { 67 | $mform->addElement('text', 'subject', get_string('postsubject', 'mod_expertforum')); 68 | $mform->setType('subject', PARAM_NOTAGS); 69 | // TODO limit length 70 | } 71 | 72 | $mform->addElement('editor', 'message_editor', 73 | $parent ? get_string('youranswer', 'mod_expertforum') : 74 | get_string('yourquestion', 'mod_expertforum'), 75 | mod_expertforum_post::editoroptions($cm)); 76 | 77 | if (!$parent) { 78 | $mform->addElement('tags', 'tags', get_string('tags'), 79 | ['component' => 'mod_expertforum', 'itemtype' => 'expertforum_post']); 80 | } 81 | 82 | $data = (object)array('e' => $expertforum->id); 83 | if ($parent) { 84 | $data->parent = $parent->id; 85 | } 86 | if ($post) { 87 | $data->subject = $post->subject; 88 | $data->edit = $post->id; 89 | $data->tags = core_tag_tag::get_item_tags_array('mod_expertforum', 'expertforum_post', $post->id); 90 | $data->message = $post->message; 91 | $data->messageformat = $post->messageformat; 92 | $data = file_prepare_standard_editor($data, 'message', mod_expertforum_post::editoroptions($cm), 93 | $cm->context, 'mod_expertforum', 'post', $post->id); 94 | } 95 | $this->set_data($data); 96 | 97 | $this->add_action_buttons(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /classes/output/listing.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Contains class mod_expertforum\output\listing 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace mod_expertforum\output; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | use templatable, renderer_base, stdClass, user_picture, cm_info; 30 | 31 | /** 32 | * Class mod_expertforum\output\listing 33 | * 34 | * @package mod_expertforum 35 | * @copyright 2015 Marina Glancy 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class listing implements templatable { 39 | protected $records; 40 | protected $cm; 41 | protected $fetchedtags; 42 | 43 | /** 44 | * Constructor 45 | * 46 | */ 47 | public function __construct($records) { 48 | $this->records = $records; 49 | } 50 | 51 | /** 52 | * Function to export the renderer data in a format that is suitable for a 53 | * mustache template. This means: 54 | * 1. No complex types - only stdClass, array, int, string, float, bool 55 | * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). 56 | * 57 | * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. 58 | * @return stdClass|array 59 | */ 60 | public function export_for_template(\renderer_base $output) { 61 | $posts = array(); 62 | foreach ($this->records as $id => $record) { 63 | //$cm = get_fast_modinfo($record->courseid)->cms[$record->cmid]; 64 | $cm = (object)['id' => $record->cmid, 'instance' => $record->expertforumid, 'course' => $record->courseid]; 65 | $tags = empty($record->tags) ? array() : $record->tags; 66 | unset($record->tags); 67 | $post = new \mod_expertforum_post($record, $cm, $tags); 68 | $user = \user_picture::unalias($record, array('deleted'), 'useridx', 'user'); 69 | 70 | $r = new stdClass(); 71 | 72 | // Post information. 73 | $r->viewurl = $post->get_url()->out(false); 74 | $r->subject = $post->get_formatted_subject(); 75 | $r->timecreated = userdate($record->timecreated, get_string('strftimedatetime', 'core_langconfig')); 76 | $r->votes = $record->votes; 77 | $r->answers = isset($record->answers) ? $record->answers : 0; 78 | $r->excerpt = $post->get_excerpt(); 79 | $r->views = 0; // TODO 80 | $r->timestamp = $post->get_timestamp(); 81 | 82 | // User information. 83 | $userpicture = new user_picture($user); 84 | $userpicture->size = 32; 85 | $userpicture->courseid = $cm->course; 86 | $context = \context_course::instance($cm->course); 87 | $r->username = fullname($user, has_capability('moodle/site:viewfullnames', $context)); 88 | if (has_capability('moodle/user:viewdetails', $context)) { 89 | $r->username = \html_writer::link(new \moodle_url('/user/view.php', 90 | array('id' => $user->id, 'course' => $cm->course)), $r->username); 91 | } else { 92 | $userpicture->link = false; 93 | } 94 | $r->userpicture = $output->render($userpicture); 95 | $r->userreputation = 15; // TODO 96 | 97 | $r->tags = $post->get_tags(); 98 | 99 | $posts[] = $r; 100 | } 101 | return array('posts' => $posts); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /backup/moodle2/restore_expertforum_activity_task.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the restore activity task class 19 | * 20 | * @package mod_expertforum 21 | * @category backup 22 | * @copyright 2015 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once($CFG->dirroot . '/mod/expertforum/backup/moodle2/restore_expertforum_stepslib.php'); 29 | 30 | /** 31 | * Restore task for the expertforum activity module 32 | * 33 | * Provides all the settings and steps to perform complete restore of the activity. 34 | * 35 | * @package mod_expertforum 36 | * @category backup 37 | * @copyright 2015 Marina Glancy 38 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 | */ 40 | class restore_expertforum_activity_task extends restore_activity_task { 41 | 42 | /** 43 | * Define (add) particular settings this activity can have 44 | */ 45 | protected function define_my_settings() { 46 | // No particular settings for this activity. 47 | } 48 | 49 | /** 50 | * Define (add) particular steps this activity can have 51 | */ 52 | protected function define_my_steps() { 53 | // We have just one structure step here. 54 | $this->add_step(new restore_expertforum_activity_structure_step('expertforum_structure', 'expertforum.xml')); 55 | } 56 | 57 | /** 58 | * Define the contents in the activity that must be 59 | * processed by the link decoder 60 | */ 61 | static public function define_decode_contents() { 62 | $contents = array(); 63 | 64 | $contents[] = new restore_decode_content('expertforum', array('intro'), 'expertforum'); 65 | 66 | return $contents; 67 | } 68 | 69 | /** 70 | * Define the decoding rules for links belonging 71 | * to the activity to be executed by the link decoder 72 | */ 73 | static public function define_decode_rules() { 74 | $rules = array(); 75 | 76 | $rules[] = new restore_decode_rule('EXPERTFORUMVIEWBYID', '/mod/expertforum/view.php?id=$1', 'course_module'); 77 | $rules[] = new restore_decode_rule('EXPERTFORUMINDEX', '/mod/expertforum/index.php?id=$1', 'course'); 78 | 79 | return $rules; 80 | 81 | } 82 | 83 | /** 84 | * Define the restore log rules that will be applied 85 | * by the {@link restore_logs_processor} when restoring 86 | * expertforum logs. It must return one array 87 | * of {@link restore_log_rule} objects 88 | */ 89 | static public function define_restore_log_rules() { 90 | $rules = array(); 91 | 92 | $rules[] = new restore_log_rule('expertforum', 'add', 'view.php?id={course_module}', '{expertforum}'); 93 | $rules[] = new restore_log_rule('expertforum', 'update', 'view.php?id={course_module}', '{expertforum}'); 94 | $rules[] = new restore_log_rule('expertforum', 'view', 'view.php?id={course_module}', '{expertforum}'); 95 | 96 | return $rules; 97 | } 98 | 99 | /** 100 | * Define the restore log rules that will be applied 101 | * by the {@link restore_logs_processor} when restoring 102 | * course logs. It must return one array 103 | * of {@link restore_log_rule} objects 104 | * 105 | * Note this rules are applied when restoring course logs 106 | * by the restore final task, but are defined here at 107 | * activity level. All them are rules not linked to any module instance (cmid = 0) 108 | */ 109 | static public function define_restore_log_rules_for_course() { 110 | $rules = array(); 111 | 112 | $rules[] = new restore_log_rule('expertforum', 'view all', 'index.php?id={course}', null); 113 | 114 | return $rules; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/generator/lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * mod_expertforum data generator 19 | * 20 | * @package mod_expertforum 21 | * @category test 22 | * @copyright 2016 Marina Galncy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | 29 | /** 30 | * Expertforum module data generator class 31 | * 32 | * @package mod_expertforum 33 | * @category test 34 | * @copyright 2016 Marina Galncy 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class mod_expertforum_generator extends testing_module_generator { 38 | 39 | /** 40 | * @var int keep track of how many posts have been created. 41 | */ 42 | protected $postcount = 0; 43 | 44 | /** 45 | * To be called from data reset code only, 46 | * do not use in tests. 47 | * @return void 48 | */ 49 | public function reset() { 50 | $this->postcount = 0; 51 | parent::reset(); 52 | } 53 | 54 | public function create_instance($record = null, array $options = null) { 55 | $record = (object)(array)$record; 56 | return parent::create_instance($record, (array)$options); 57 | } 58 | 59 | /** 60 | * Function to create a dummy post. 61 | * 62 | * @param array|stdClass $record 63 | * @return mod_expertforum_post the post object 64 | */ 65 | public function create_post($record, cm_info $cm = null) { 66 | global $DB, $USER; 67 | 68 | // Increment the forum post count. 69 | $this->postcount++; 70 | 71 | // Variable to store time. 72 | $time = time() + $this->postcount; 73 | 74 | $record = (array) $record + 75 | array( 76 | 'userid' => $USER->id, 77 | 'parent' => null, 78 | 'subject' => 'Forum post subject ' . $this->postcount, 79 | 'message' => html_writer::tag('p', 'Forum message post ' . $this->postcount), 80 | 'messageformat' => FORMAT_HTML, 81 | 'messagetrust' => 1, 82 | 'timecreated' => $time, 83 | 'timemodified' => $time 84 | ); 85 | 86 | if (!empty($record['parent']) && 87 | (empty($record['expertforumid']) || empty($record['courseid']))) { 88 | $parent = $DB->get_record_sql("SELECT courseid, expertforumid FROM {expertforum_post} WHERE id = ?", 89 | array($record['parent']), MUST_EXIST); 90 | $record['expertforumid'] = $parent->expertforumid; 91 | $record['courseid'] = $parent->courseid; 92 | } 93 | 94 | if (empty($record['courseid']) && !empty($record['expertforumid'])) { 95 | $record['courseid'] = $DB->get_field('expertforum', 'course', 96 | array('id' => $record['expertforumid']), MUST_EXIST); 97 | } 98 | 99 | if ($cm) { 100 | $record['courseid'] = $cm->course; 101 | $record['expertforumid'] = $cm->instance; 102 | } 103 | 104 | if (!isset($record['expertforumid'])) { 105 | throw new coding_exception('expertforumid must be present in mod_expertforum_generator::create_post() $record'); 106 | } 107 | 108 | if (!isset($record['courseid'])) { 109 | throw new coding_exception('courseid must be present in mod_expertforum_generator::create_post() $record'); 110 | } 111 | 112 | if (!$cm) { 113 | $modinfo = get_fast_modinfo($record['courseid']); 114 | $instances = $modinfo->get_instances_of('expertforum'); 115 | $cm = $instances[$record['expertforumid']]; 116 | } 117 | return mod_expertforum_post::create($record, $cm); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/behat/behat_mod_expertforum.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Steps definitions related with the forum activity. 19 | * 20 | * @package mod_expertforum 21 | * @category test 22 | * @copyright 2016 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. 27 | 28 | require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); 29 | 30 | use Behat\Behat\Context\Step\Given as Given, 31 | Behat\Gherkin\Node\TableNode as TableNode; 32 | /** 33 | * Forum-related steps definitions. 34 | * 35 | * @package mod_expertforum 36 | * @category test 37 | * @copyright 2016 Marina Glancy 38 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 | */ 40 | class behat_mod_expertforum extends behat_base { 41 | 42 | /** 43 | * Adds a topic to the forum specified by it's name. Useful for the News forum and blog-style forums. 44 | * 45 | * @Given /^I add a new topic to "(?P(?:[^"]|\\")*)" forum with:$/ 46 | * @param string $forumname 47 | * @param TableNode $table 48 | */ 49 | //public function i_add_a_new_topic_to_forum_with($forumname, TableNode $table) { 50 | // return $this->add_new_discussion($forumname, $table, get_string('addanewtopic', 'forum')); 51 | //} 52 | 53 | /** 54 | * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message). The step begins from the forum's course page. 55 | * 56 | * @Given /^I add a new question to "(?P(?:[^"]|\\")*)" expertforum with:$/ 57 | * @param string $forumname 58 | * @param TableNode $table 59 | */ 60 | public function i_add_a_new_question_to_expertforum_with($forumname, TableNode $table) { 61 | return $this->add_new_post($forumname, $table); 62 | } 63 | 64 | /** 65 | * Adds a reply to the specified post of the specified forum. The step begins from the forum's page or from the forum's course page. 66 | * 67 | * @Given /^I reply "(?P(?:[^"]|\\")*)" post from "(?P(?:[^"]|\\")*)" forum with:$/ 68 | * @param string $postname The subject of the post 69 | * @param string $forumname The forum name 70 | * @param TableNode $table 71 | */ 72 | /*public function i_reply_post_from_forum_with($postsubject, $forumname, TableNode $table) { 73 | 74 | return array( 75 | new Given('I follow "' . $this->escape($forumname) . '"'), 76 | new Given('I follow "' . $this->escape($postsubject) . '"'), 77 | new Given('I follow "' . get_string('reply', 'forum') . '"'), 78 | new Given('I set the following fields to these values:', $table), 79 | new Given('I press "' . get_string('posttoforum', 'forum') . '"'), 80 | new Given('I wait to be redirected') 81 | ); 82 | 83 | }*/ 84 | 85 | /** 86 | * Returns the steps list to add a new discussion to a forum. 87 | * 88 | * Abstracts add a new topic and add a new discussion, as depending 89 | * on the forum type the button string changes. 90 | * 91 | * @param string $forumname 92 | * @param TableNode $table 93 | * @return Given[] 94 | */ 95 | protected function add_new_post($forumname, TableNode $table) { 96 | 97 | $buttonstr = "Ask question"; // TODO string. 98 | $savechanges = "Save changes"; // TODO string. 99 | return array( 100 | new Given('I follow "' . $this->escape($forumname) . '"'), 101 | new Given('I press "' . $buttonstr . '"'), 102 | new Given('I set the following fields to these values:', $table), 103 | new Given('I press "' . $savechanges . '"'), 104 | new Given('I wait to be redirected') 105 | ); 106 | 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /db/install.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 |
-------------------------------------------------------------------------------- /templates/post.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template mod_expertforum/post 19 | 20 | Displays one answer 21 | 22 | Classes required for JS: 23 | * none 24 | 25 | Data attributes required for JS: 26 | * none 27 | 28 | Context variables required for this template: 29 | * none 30 | 31 | Example context (json): 32 | { 33 | "id":1, 34 | "votes":4, 35 | "message":"My message", 36 | "parent":0, 37 | "isfavourite":0, 38 | "favouritecount":5, 39 | "votes":2, 40 | "timestamp":"asked 10 hours ago", 41 | "userpicture":"", 42 | "username":"User X", 43 | "userreputation":15 44 | } 45 | }} 46 | 47 | 48 | 49 | 71 | 72 | 73 | 117 | 118 | 119 | 121 | 142 | 143 | 144 |
50 |
51 | 52 | {{#str}}upvote,mod_expertforum{{/str}} 54 | {{#str}}votescountshort,mod_expertforum,{{votes}}{{/str}} 55 | {{#str}}downvote,mod_expertforum{{/str}} 57 | {{^parent}} 58 | 59 | {{#isfavourite}} 60 | {{#str}}favourite,mod_expertforum{{/str}} 61 | {{/isfavourite}} 62 | {{^isfavourite}} 63 | {{#str}}favourite,mod_expertforum{{/str}} 64 | {{/isfavourite}} 65 | {{#favouritecount}} 66 |
{{favouritecount}}
67 | {{/favouritecount}} 68 | {{/parent}} 69 |
70 |
74 |
75 |
76 | {{{message}}} 77 |
78 | {{^parent}} 79 |
80 | {{#tags}} 81 | 82 | {{/tags}} 83 |
84 | {{/parent}} 85 | 86 | 87 | 88 | 97 | 112 | 113 | 114 |
89 |
90 | {{#editurl}} 91 | {{#str}}editlink,mod_expertforum{{/str}} 92 | {{/editurl}} 93 |
94 | 95 | 96 |
98 | 111 |
115 |
116 |
120 | 122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 137 | 138 | 139 |
134 | 135 | 136 |
140 |
141 |
145 | -------------------------------------------------------------------------------- /pix/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 34 | 37 | 41 | 45 | 46 | 49 | 53 | 57 | 58 | 60 | 64 | 68 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 102 | 105 | 108 | 111 | 114 | 115 | 124 | 126 | 130 | 134 | 135 | 144 | 153 | 162 | 163 | 184 | 191 | 192 | 194 | 195 | 197 | image/svg+xml 198 | 200 | 201 | 202 | 203 | 204 | 209 | 215 | 222 | 229 | 235 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /pix/sprites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .mform.mod_expertforum_post .fitem div.fitemtitle { 2 | display: block; 3 | width: 100%; 4 | text-align: left; 5 | margin-top: 4px; 6 | margin-bottom: 4px; 7 | } 8 | .mform.mod_expertforum_post .fitem .felement { 9 | float: left; 10 | width: 100%; 11 | padding-right: 0px; 12 | padding-left: 0px; 13 | margin-left: 0px; 14 | } 15 | 16 | .mod-expertforum-questionlist { 17 | clear: both; 18 | float: left; 19 | width: 728px; 20 | margin-bottom: 20px; 21 | } 22 | .question-summary { 23 | overflow: hidden; 24 | float: left; 25 | width: 728px; 26 | padding: 12px 0px 10px; 27 | border-bottom: 1px solid #E0E0E0; 28 | } 29 | 30 | .statscontainer { 31 | width: 78px; 32 | float: left; 33 | margin-right: 8px; 34 | margin-left: 8px; 35 | } 36 | 37 | .statsarrow { 38 | float: right; 39 | height: 13px; 40 | width: 7px; 41 | margin-top: 12px; 42 | } 43 | 44 | .stats { 45 | margin: 0px; 46 | width: 58px; 47 | } 48 | 49 | .vote { 50 | text-align: center; 51 | } 52 | .votes { 53 | padding: 0px; 54 | margin-bottom: 8px; 55 | text-align: center; 56 | } 57 | 58 | .statscontainer .votes { 59 | color: #777; 60 | font-size: 11px; 61 | } 62 | 63 | .votes strong { 64 | display: block; 65 | font-size: 20px; 66 | margin: 0px 0px 3px; 67 | font-weight: normal; 68 | color: #777; 69 | } 70 | .status { 71 | padding: 0px; 72 | margin-bottom: 8px; 73 | text-align: center; 74 | } 75 | 76 | .statscontainer .status { 77 | padding: 7px 0px 5px; 78 | font-size: 11px; 79 | } 80 | 81 | .statscontainer .status strong { 82 | font-size: 18px; 83 | font-weight: normal; 84 | display: block; 85 | } 86 | .answered { 87 | background-color: #75845C; 88 | color: #FFF; 89 | } 90 | .statscontainer .views { 91 | width: 58px; 92 | } 93 | .views { 94 | padding-top: 4px; 95 | text-align: center; 96 | } 97 | 98 | .summary { 99 | float: left; 100 | width: 630px; 101 | } 102 | 103 | .summary h3 { 104 | font-size: 15px; 105 | line-height: 1.4; 106 | margin-bottom: 0.5em; 107 | margin-top: 0; 108 | } 109 | 110 | answer-hyperlink, .question-hyperlink { 111 | color: #07C; 112 | line-height: 1.3; 113 | margin-bottom: 1.2em; 114 | } 115 | .question-hyperlink { 116 | font-size: 16px; 117 | font-weight: 400; 118 | } 119 | 120 | .excerpt { 121 | padding: 0px 0px 5px; 122 | margin: 0px; 123 | color: #444; 124 | } 125 | .tags { 126 | line-height: 18px; 127 | float: left; 128 | } 129 | .post-tag { 130 | color: #566E76; 131 | background: #F7FDFF none repeat scroll 0% 0%; 132 | border: 1px solid #C0D4DB; 133 | padding: 0.4em 0.5em; 134 | border-radius: 15px; 135 | margin: 2px 2px 2px 0px; 136 | text-decoration: none; 137 | text-align: center; 138 | font-size: 11px; 139 | line-height: 1; 140 | white-space: nowrap; 141 | display: inline-block; 142 | } 143 | .post-tag { 144 | color: #3E6D8E; 145 | font-size: 12px; 146 | white-space: nowrap; 147 | background: #E4EDF4 none repeat scroll 0% 0%; 148 | border: 1px solid #E4EDF4; 149 | display: inline-block; 150 | margin: 2px 2px 2px 0px; 151 | border-radius: 0px; 152 | transition: color 0.15s ease 0s, background 0.15s ease 0s, border 0.15s ease 0s; 153 | } 154 | 155 | .fr { 156 | float: right; 157 | } 158 | .started { 159 | color: #999; 160 | width: 185px; 161 | float: right; 162 | line-height: 18px; 163 | } 164 | .user-info { 165 | height: 35px; 166 | width: 190px; 167 | } 168 | 169 | .user-info .user-action-time { 170 | margin-top: 2px; 171 | margin-bottom: 4px; 172 | } 173 | .relativetime { 174 | text-decoration: none; 175 | } 176 | 177 | .user-info .user-gravatar32 { 178 | float: left; 179 | width: 32px; 180 | height: 32px; 181 | border-radius: 1px; 182 | } 183 | 184 | .user-details { 185 | color: #888; 186 | line-height: 17px; 187 | } 188 | .user-info .user-details { 189 | float: left; 190 | margin-left: 5px; 191 | width: 150px; 192 | overflow: hidden; 193 | white-space: nowrap; 194 | } 195 | .started a:not(.started-link), .started .mod-flair { 196 | font-size: 12px; 197 | color: #07C; 198 | } 199 | .reputation-score { 200 | font-weight: bold; 201 | font-size: 12px; 202 | margin-right: 2px; 203 | } 204 | .started .reputation-score { 205 | font-weight: normal; 206 | color: #777; 207 | margin-left: 1px; 208 | } 209 | 210 | 211 | /* View single thread */ 212 | .question { 213 | clear: both; 214 | } 215 | .votecell { 216 | vertical-align: top; 217 | padding-right: 15px; 218 | } 219 | .vote { 220 | text-align: center; 221 | } 222 | .votecell .vote { 223 | min-width: 46px; 224 | } 225 | 226 | 227 | .envelope-on, .envelope-off, .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .comment-up-off, .comment-up-on, .comment-flag, .edited-yes, .feed-icon, .vote-accepted-off, .vote-accepted-on, .vote-accepted-bounty, .badge-earned-check, .delete-tag, .grippie, .expander-arrow-hide, .expander-arrow-show, .expander-arrow-small-hide, .expander-arrow-small-show, .anonymous-gravatar, .badge1, .badge2, .badge3 { 228 | background-image: url([[pix:mod_expertforum|sprites]]), none; 229 | background-repeat: no-repeat; 230 | overflow: hidden; 231 | } 232 | .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .vote-accepted-off, .vote-accepted-on { 233 | display: block; 234 | margin: 0px auto 2px; 235 | text-indent: -999em; 236 | width: 40px; 237 | height: 40px; 238 | } 239 | .vote-up-off, .vote-down-off, .vote-accepted-off, .star-off, .comment-up-off, .flag-off { 240 | cursor: pointer; 241 | } 242 | .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .comment-up-off, .comment-up-on, .comment-flag, .flag-off, .vote-accepted-off, .vote-accepted-on { 243 | text-indent: -9999em; 244 | font-size: 1px; 245 | } 246 | .vote-up-off { 247 | background-position: 0px -170px; 248 | } 249 | .envelope-on, .envelope-off, .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .comment-up-off, .comment-up-on, .comment-flag, .edited-yes, .feed-icon, .vote-accepted-off, .vote-accepted-on, .vote-accepted-bounty, .badge-earned-check, .delete-tag, .grippie, .expander-arrow-hide, .expander-arrow-show, .expander-arrow-small-hide, .expander-arrow-small-show, .anonymous-gravatar, .badge1, .badge2, .badge3, .gp-share, .fb-share, .twitter-share, #notify-container span.notify-close, .migrated.to, .migrated.from { 250 | background-size: initial; 251 | } 252 | .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on { 253 | height: 30px; 254 | } 255 | 256 | 257 | .vote-count-post { 258 | display: block; 259 | font-size: 20px; 260 | margin: 0px 0px 3px; 261 | } 262 | .vote span { 263 | display: block; 264 | color: #777; 265 | } 266 | .votecell .vote-count-post { 267 | margin: 8px 0px; 268 | } 269 | 270 | .vote-down-off { 271 | background-position: 0px -220px; 272 | margin-bottom: 8px; 273 | } 274 | .vote-down-off, .vote-down-on { 275 | margin-bottom: 10px; 276 | } 277 | 278 | .star-off { 279 | background-position: 0px -120px; 280 | } 281 | .star-on, .star-off { 282 | height: 30px; 283 | } 284 | .star-on { 285 | background-position: -40px -120px; 286 | } 287 | 288 | .favouritecount { 289 | text-align: center; 290 | color: #777; 291 | font-weight: normal; 292 | } 293 | .favouritecount.favouritecount-selected { 294 | color: #D4A849; 295 | } 296 | 297 | .question .postcell { 298 | vertical-align: top; 299 | } 300 | 301 | .post-text, .wmd-preview { 302 | width: 660px; 303 | margin-bottom: 5px; 304 | word-wrap: break-word; 305 | font-size: 15px; 306 | line-height: 1.3; 307 | } 308 | 309 | .post-taglist { 310 | margin-bottom: 10px; 311 | clear: both; 312 | } 313 | 314 | .fw { 315 | margin-bottom: 4px; 316 | width: 100%; 317 | } 318 | 319 | .vt { 320 | vertical-align: top; 321 | } 322 | .post-menu { 323 | padding-top: 2px; 324 | } 325 | .post-menu > a { 326 | padding: 0px 3px 2px; 327 | color: #888; 328 | } 329 | .lsep { 330 | margin: 0px 2px; 331 | color: #1B4072; 332 | font-size: 1px; 333 | visibility: hidden; 334 | } 335 | .post-menu .lsep { 336 | margin: 0px; 337 | padding: 0px; 338 | } 339 | 340 | .post-signature { 341 | text-align: left; 342 | vertical-align: top; 343 | width: 175px; 344 | height: 58px; 345 | padding: 5px; 346 | } 347 | .owner { 348 | background-color: #E0EAF1; 349 | } 350 | 351 | 352 | #answers { 353 | clear: both; 354 | padding-top: 10px; 355 | width: 728px; 356 | } 357 | 358 | #answers-header { 359 | margin-top: 10px; 360 | width: 728px; 361 | } 362 | 363 | .subheader { 364 | clear: both; 365 | margin-bottom: 15px; 366 | height: 40px; 367 | border-bottom: 1px solid #E0E0E0; 368 | } 369 | .subheader { 370 | height: 34px; 371 | border-bottom: 1px solid #E0E0E0; 372 | } 373 | 374 | .answer { 375 | padding-bottom: 20px; 376 | padding-top: 20px; 377 | width: 728px; 378 | } 379 | .question-page #answers .answer { 380 | border-bottom: 1px solid #F0F0F0; 381 | } 382 | -------------------------------------------------------------------------------- /tests/lib_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * The module expertforums tests 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2016 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | class mod_expertforum_lib_testcase extends advanced_testcase { 28 | 29 | /** 30 | * Create a course, expertforum, three users, one question and two answers 31 | * by different users. 32 | * @return mod_expertforum_post the question post 33 | */ 34 | protected function prepare() { 35 | $this->resetAfterTest(); 36 | $user1 = $this->getDataGenerator()->create_user(); 37 | $user2 = $this->getDataGenerator()->create_user(); 38 | $user3 = $this->getDataGenerator()->create_user(); 39 | 40 | $course = $this->getDataGenerator()->create_course(); 41 | $ef = $this->getDataGenerator()->create_module('expertforum', 42 | array('course' => $course->id, 'grade' => 100)); 43 | $cm = get_fast_modinfo($course)->get_cm($ef->cmid); 44 | 45 | $post = mod_expertforum_post::create( 46 | array('userid' => $user1->id, 'subject' => 'Question', 47 | 'message' => 'Question text'), 48 | $cm); 49 | 50 | $post2 = mod_expertforum_post::create( 51 | array('parent' => $post->id, 'userid' => $user2->id, 52 | 'message' => 'Answer text'), $cm); 53 | 54 | $post3 = mod_expertforum_post::create( 55 | array('parent' => $post->id, 'userid' => $user3->id, 56 | 'message' => 'Answer text'), $cm); 57 | 58 | return $post; 59 | } 60 | 61 | public function test_generator() { 62 | global $DB; 63 | $this->resetAfterTest(); 64 | $user1 = $this->getDataGenerator()->create_user(); 65 | $user2 = $this->getDataGenerator()->create_user(); 66 | 67 | $this->assertEquals(0, $DB->count_records('expertforum')); 68 | 69 | $course = $this->getDataGenerator()->create_course(); 70 | $ef = $this->getDataGenerator()->create_module('expertforum', 71 | array('course' => $course->id, 'grade' => 100)); 72 | 73 | $this->assertEquals(1, $DB->count_records('expertforum')); 74 | 75 | /** @var mod_expertforum_generator $generator */ 76 | $generator = $this->getDataGenerator()->get_plugin_generator('mod_expertforum'); 77 | 78 | $this->assertEquals(0, $DB->count_records('expertforum_post')); 79 | $post = $generator->create_post( 80 | array('expertforumid' => $ef->id, 'courseid' => $course->id, 81 | 'userid' => $user1->id)); 82 | $this->assertEquals(1, $DB->count_records('expertforum_post')); 83 | $this->assertEquals(array('courseid' => $course->id, 84 | 'expertforumid' => $ef->id, 85 | 'parent' => 0, 86 | 'userid' => $user1->id), 87 | (array)$DB->get_record('expertforum_post', 88 | array('id' => $post->id), 'courseid,expertforumid,parent,userid')); 89 | 90 | $post2 = $generator->create_post( 91 | array('parent' => $post->id, 'userid' => $user2->id)); 92 | $this->assertEquals(2, $DB->count_records('expertforum_post')); 93 | $this->assertEquals(array('courseid' => $course->id, 94 | 'expertforumid' => $ef->id, 95 | 'parent' => $post->id, 96 | 'userid' => $user2->id), 97 | (array)$DB->get_record('expertforum_post', 98 | array('id' => $post2->id), 'courseid,expertforumid,parent,userid')); 99 | } 100 | 101 | public function test_create() { 102 | global $DB; 103 | $this->resetAfterTest(); 104 | $user1 = $this->getDataGenerator()->create_user(); 105 | $user2 = $this->getDataGenerator()->create_user(); 106 | 107 | $course = $this->getDataGenerator()->create_course(); 108 | $ef = $this->getDataGenerator()->create_module('expertforum', 109 | array('course' => $course->id, 'grade' => 100)); 110 | $cm = get_fast_modinfo($course)->get_cm($ef->cmid); 111 | 112 | $this->assertEquals(0, $DB->count_records('expertforum_post')); 113 | $post = mod_expertforum_post::create( 114 | array('userid' => $user1->id, 'subject' => 'Question', 115 | 'message' => 'Question text'), 116 | $cm); 117 | $this->assertEquals(1, $DB->count_records('expertforum_post')); 118 | $this->assertEquals(array('courseid' => $course->id, 119 | 'expertforumid' => $ef->id, 120 | 'parent' => 0, 121 | 'userid' => $user1->id), 122 | (array)$DB->get_record('expertforum_post', 123 | array('id' => $post->id), 'courseid,expertforumid,parent,userid')); 124 | 125 | $post2 = mod_expertforum_post::create( 126 | array('parent' => $post->id, 'userid' => $user2->id, 127 | 'message' => 'Answer text'), $cm); 128 | $this->assertEquals(2, $DB->count_records('expertforum_post')); 129 | $this->assertEquals(array('courseid' => $course->id, 130 | 'expertforumid' => $ef->id, 131 | 'parent' => $post->id, 132 | 'userid' => $user2->id), 133 | (array)$DB->get_record('expertforum_post', 134 | array('id' => $post2->id), 'courseid,expertforumid,parent,userid')); 135 | 136 | // Test mod_expertforum_post::get(). 137 | $fetchedpost = mod_expertforum_post::get($post->id, $cm); 138 | $this->assertEquals($post->id, $fetchedpost->id); 139 | 140 | // One can not use method get() to retrieve answer. 141 | $this->assertNull(mod_expertforum_post::get($post2->id, $cm)); 142 | 143 | // Test method get_answers(). 144 | $answers = array_values($fetchedpost->get_answers()); 145 | $this->assertCount(1, $answers); 146 | $this->assertEquals($post2->id, $answers[0]->id); 147 | } 148 | 149 | public function test_update() { 150 | global $DB; 151 | $this->resetAfterTest(); 152 | $user1 = $this->getDataGenerator()->create_user(); 153 | 154 | $course = $this->getDataGenerator()->create_course(); 155 | $ef = $this->getDataGenerator()->create_module('expertforum', 156 | array('course' => $course->id, 'grade' => 100)); 157 | $cm = get_fast_modinfo($course)->get_cm($ef->cmid); 158 | 159 | $post = mod_expertforum_post::create( 160 | array('userid' => $user1->id, 'subject' => 'Question', 161 | 'message' => 'Question text'), 162 | $cm); 163 | 164 | $post->update((object)array('message' => 'New question text')); 165 | $record = $DB->get_record('expertforum_post', array('id' => $post->id)); 166 | $this->assertEquals('New question text', $record->message); 167 | 168 | $post->update((object)array('message_editor' => 169 | array('text' => 'Another edit', 'format' => FORMAT_HTML))); 170 | $record = $DB->get_record('expertforum_post', array('id' => $post->id)); 171 | $this->assertEquals('Another edit', $record->message); 172 | } 173 | 174 | public function test_update_tags() { 175 | global $DB; 176 | $this->resetAfterTest(); 177 | $user1 = $this->getDataGenerator()->create_user(); 178 | 179 | $course = $this->getDataGenerator()->create_course(); 180 | $ef = $this->getDataGenerator()->create_module('expertforum', 181 | array('course' => $course->id, 'grade' => 100)); 182 | $cm = get_fast_modinfo($course)->get_cm($ef->cmid); 183 | 184 | $post = mod_expertforum_post::create( 185 | array('userid' => $user1->id, 'subject' => 'Question', 186 | 'message' => 'Question text', 'tags' => array('programming')), 187 | $cm); 188 | 189 | $postfetched = mod_expertforum_post::get($post->id, $cm); 190 | $tags = $postfetched->get_tags(); 191 | $this->assertCount(1, $tags); 192 | $this->assertEquals('programming', $tags[0]['tagname']); 193 | 194 | $post->update((object)array('tags' => array('Php', 'programming'))); 195 | 196 | $postfetched = mod_expertforum_post::get($post->id, $cm); 197 | $tags = $postfetched->get_tags(); 198 | $this->assertCount(2, $tags); 199 | $this->assertEquals('Php', $tags[0]['tagname']); 200 | $this->assertEquals('programming', $tags[1]['tagname']); 201 | } 202 | 203 | public function test_voting() { 204 | $post = $this->prepare(); 205 | $answers = array_values($post->get_answers()); 206 | 207 | $this->assertEquals(0, $post->votes); 208 | $this->assertEquals(0, $answers[0]->votes); 209 | $this->assertEquals(0, $answers[1]->votes); 210 | 211 | // User can not vote for his own question but other users can. 212 | $this->setUser($post->userid); 213 | $this->assertFalse($post->can_upvote()); 214 | $this->assertFalse($post->can_downvote()); 215 | 216 | $this->setUser($answers[0]->userid); 217 | $this->assertTrue($post->can_upvote()); 218 | $this->assertTrue($post->can_downvote()); 219 | 220 | // Voting on own posts has no effect. 221 | $this->setUser($post->userid); 222 | $post->vote($post->id, 1); 223 | $this->assertEquals(0, $post->votes); 224 | $fetchedpost = mod_expertforum_post::get($post->id, $post->cm); 225 | $this->assertEquals(0, $fetchedpost->votes); 226 | 227 | // Users can vote on other users' posts but only once. 228 | $this->setUser($answers[0]->userid); 229 | $post->vote($post->id, 1); 230 | $this->assertEquals(1, $post->votes); 231 | $fetchedpost = mod_expertforum_post::get($post->id, $post->cm); 232 | $this->assertEquals(1, $fetchedpost->votes); 233 | // Voting again makes no effect. 234 | $post->vote($post->id, 1); 235 | $this->assertEquals(1, $post->votes); 236 | $fetchedpost = mod_expertforum_post::get($post->id, $post->cm); 237 | $this->assertEquals(1, $fetchedpost->votes); 238 | // Same user downvoting. 239 | $post->vote($post->id, -1); 240 | $this->assertEquals(-1, $post->votes); 241 | $fetchedpost = mod_expertforum_post::get($post->id, $post->cm); 242 | $this->assertEquals(-1, $fetchedpost->votes); 243 | 244 | // Another user downvoting. 245 | $this->setUser($answers[1]->userid); 246 | $post->vote($post->id, -1); 247 | $this->assertEquals(-2, $post->votes); 248 | $fetchedpost = mod_expertforum_post::get($post->id, $post->cm); 249 | $this->assertEquals(-2, $fetchedpost->votes); 250 | 251 | // Two users voting for an answer. 252 | $post->vote($answers[0]->id, 1); 253 | $this->setUser($post->userid); 254 | $post->vote($answers[0]->id, 1); 255 | $this->assertEquals(2, $answers[0]->votes); 256 | $fetchedpost = mod_expertforum_post::get($post->id, $post->cm); 257 | $this->assertEquals(2, $post->find_answer($answers[0]->id)->votes); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /classes/listing.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Contains class mod_expertforum_listing 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2016 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * Class mod_expertforum_listing 29 | * 30 | * @package mod_expertforum 31 | * @copyright 2016 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class mod_expertforum_listing { 35 | 36 | const PERPAGE = 20; 37 | 38 | /** 39 | * Returns posts tagged with a specified tag. 40 | * 41 | * This is a callback used by the tag area mod_expertforum/expertforum_post to search for posts 42 | * tagged with a specific tag. 43 | * 44 | * @global core_renderer $OUTPUT 45 | * @param core_tag_tag $tag 46 | * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag 47 | * are displayed on the page and the per-page limit may be bigger 48 | * @param int $fromctx context id where the link was displayed, may be used by callbacks 49 | * to display items in the same context first 50 | * @param int $ctx context id where to search for records 51 | * @param bool $rec search in subcontexts as well 52 | * @param int $page 0-based number of page being displayed 53 | * @return \core_tag\output\tagindex 54 | */ 55 | protected static function tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { 56 | $perpage = $exclusivemode ? self::PERPAGE : 5; 57 | 58 | // Basic precheck. 59 | $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); 60 | if (!$rec && $context->contextlevel != CONTEXT_MODULE) { 61 | // Non-recursive search in any context except for the module context will not return anything anyway. 62 | return array(); 63 | } 64 | if ($context->contextlevel == CONTEXT_BLOCK || $context->contextlevel == CONTEXT_USER) { 65 | // Nothing can be tagged 66 | return array(); 67 | } 68 | 69 | // Build the SQL query. 70 | $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); 71 | $userfields = user_picture::fields('u', array('deleted'), 'useridx', 'user'); 72 | $params = array('itemtype' => 'expertforum_post', 'component' => 'mod_expertforum', 73 | 'coursemodulecontextlevel' => CONTEXT_MODULE); 74 | $fromtag = $wheretag = ''; 75 | if ($tag) { 76 | $fromtag = 'JOIN {tag_instance} tt ON ep.id = tt.itemid '; 77 | $wheretag = 'tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND '; 78 | $params['tagid'] = $tag->id; 79 | } 80 | $query = "SELECT ep.id, ep.groupid, ep.subject, e.id AS expertforumid, $userfields, 81 | cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect 82 | FROM {expertforum_post} ep 83 | JOIN {expertforum} e ON e.id = ep.expertforumid 84 | JOIN {modules} m ON m.name = 'expertforum' 85 | JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = e.id 86 | $fromtag 87 | JOIN {course} c ON cm.course = c.id 88 | JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel 89 | LEFT OUTER JOIN {user} u ON u.deleted = 0 AND u.id = ep.userid 90 | WHERE $wheretag ep.parent is NULL AND ep.id %ITEMFILTER% AND c.id %COURSEFILTER%"; 91 | 92 | if ($context->contextlevel == CONTEXT_COURSE) { 93 | $query .= ' AND c.id = :courseid'; 94 | $params['courseid'] = $context->instanceid; 95 | } else if ($context->contextlevel == CONTEXT_MODULE) { 96 | $query .= ' AND cm.id = :cmid'; 97 | $params['cmid'] = $context->instanceid; 98 | } else if ($context->contextlevel != CONTEXT_SYSTEM) { 99 | $query .= ' AND (ctx.id = :contextid OR ctx.path LIKE :path)'; 100 | $params['contextid'] = $context->id; 101 | $params['path'] = $context->path.'/%'; 102 | } 103 | 104 | $query .= " ORDER BY "; 105 | if ($fromctx) { 106 | // In order-clause specify that modules from inside "fromctx" context should be returned first. 107 | $fromcontext = context::instance_by_id($fromctx); 108 | $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; 109 | $params['fromcontextid'] = $fromcontext->id; 110 | $params['frompath'] = $fromcontext->path.'/%'; 111 | } 112 | $query .= ' c.sortorder, cm.id, ep.id'; 113 | 114 | // Use core_tag_index_builder to build and filter the list of items. 115 | $builder = new core_tag_index_builder('mod_expertforum', 'expertforum_post', $query, $params, $page * $perpage, $perpage + 1); 116 | while ($item = $builder->has_item_that_needs_access_check()) { 117 | context_helper::preload_from_record($item); 118 | $courseid = $item->courseid; 119 | if (!$builder->can_access_course($courseid)) { 120 | $builder->set_accessible($item, false); 121 | if ($context->contextlevel >= CONTEXT_COURSE) { 122 | // No need to check anything else - everything is not accessible. 123 | return array(); 124 | } 125 | continue; 126 | } 127 | $modinfo = get_fast_modinfo($builder->get_course($courseid)); 128 | // Set accessibility of this item and all other items in the same course. 129 | $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { 130 | if ($taggeditem->courseid == $courseid) { 131 | $accessible = false; 132 | if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) { 133 | // TODO check group access. 134 | $accessible = true; 135 | } else if ($context->contextlevel >= CONTEXT_MODULE) { 136 | // No need to check anything else - everything is not accessible. 137 | return array(); 138 | } 139 | $builder->set_accessible($taggeditem, $accessible); 140 | } 141 | }); 142 | } 143 | 144 | $items = $builder->get_items(); 145 | 146 | return $items; 147 | } 148 | 149 | 150 | /** 151 | * Returns posts tagged with a specified tag. 152 | * 153 | * This is a callback used by the tag area mod_expertforum/expertforum_post to search for posts 154 | * tagged with a specific tag. 155 | * 156 | * @global core_renderer $OUTPUT 157 | * @param core_tag_tag $tag 158 | * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag 159 | * are displayed on the page and the per-page limit may be bigger 160 | * @param int $fromctx context id where the link was displayed, may be used by callbacks 161 | * to display items in the same context first 162 | * @param int $ctx context id where to search for records 163 | * @param bool $rec search in subcontexts as well 164 | * @param int $page 0-based number of page being displayed 165 | * @return \core_tag\output\tagindex 166 | */ 167 | public static function tagged_posts_index($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { 168 | global $OUTPUT; 169 | $perpage = $exclusivemode ? self::PERPAGE : 5; 170 | $items = self::tagged_posts($tag, $exclusivemode , $fromctx , $ctx , $rec, $page); 171 | $totalpages = $page + 1; 172 | if (count($items) > $perpage) { 173 | $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. 174 | array_pop($items); 175 | } 176 | 177 | // Build the display contents. 178 | if ($items) { 179 | if ($exclusivemode) { 180 | $content = self::display_items_full($OUTPUT, $items, true); 181 | } else { 182 | $content = self::display_items_tagfeed($OUTPUT, $items); 183 | } 184 | 185 | return new core_tag\output\tagindex($tag, 'mod_expertforum', 'expertforum_post', $content, 186 | $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); 187 | } 188 | } 189 | 190 | /** 191 | * 192 | * @global moodle_database $DB 193 | * @param cm_info $cm 194 | * @param type $page 195 | * @return type 196 | */ 197 | /*public static function module_posts_display_OLD(cm_info $cm, $page = 0) { 198 | // TODO: filter visible groups. 199 | global $DB, $OUTPUT; 200 | $perpage = 20; 201 | 202 | $params = array('expertforumid' => $cm->instance, 'courseid' => $cm->course, 'cmid' => $cm->id); 203 | $userfields = user_picture::fields('u', array('deleted'), 'useridx', 'user'); 204 | $sql = "SELECT p.id, p.subject, p.expertforumid, $userfields, :cmid AS cmid, p.courseid 205 | FROM {expertforum_post} p 206 | LEFT OUTER JOIN {user} u ON u.deleted = 0 AND u.id = p.userid 207 | WHERE p.parent is null 208 | AND p.expertforumid = :expertforumid 209 | AND p.courseid = :courseid 210 | ORDER BY p.timecreated DESC 211 | "; 212 | $items = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage); 213 | 214 | $content = self::display_items_full($OUTPUT, $items); 215 | return $content; // TODO display paging bar 216 | 217 | //$fromctx = $ctx = context_module::instance($cm->id)->id; 218 | //$totalpages = 1; // TODO. 219 | //$tagindex = new core_tag\output\tagindex($tag, 'mod_expertforum', 'expertforum_post', $content, 220 | // true, $fromctx, $ctx, false, $page, $totalpages); 221 | //return $OUTPUT->render_from_template('core_tag/index', $tagindex->export_for_template($OUTPUT)); 222 | }*/ 223 | 224 | public static function module_posts_display($cm, $tag, $page = 0) { 225 | global $OUTPUT; 226 | $perpage = self::PERPAGE; 227 | $context = context_module::instance($cm->id); 228 | $items = self::tagged_posts($tag, true, 0, $context->id, false, $page); 229 | $hasnextpage = count($items) > $perpage; 230 | if ($hasnextpage) { 231 | array_pop($items); 232 | } 233 | $content = self::display_items_full($OUTPUT, $items, false); 234 | if ($page || $hasnextpage) { 235 | $content .= self::paging_bar($cm, $tag, $page, $hasnextpage); 236 | } 237 | return $content; 238 | } 239 | 240 | protected static function paging_bar($cm, $tag, $page, $hasnextpage) { 241 | return ''; // TODO display paging bar 242 | } 243 | 244 | protected static function display_items_tagfeed(core_renderer $output, $items) { 245 | $tagfeed = new core_tag\output\tagfeed(); 246 | foreach ($items as $item) { 247 | $user = \user_picture::unalias($item, array('deleted'), 'useridx', 'user'); 248 | context_helper::preload_from_record($item); 249 | $modinfo = get_fast_modinfo($item->courseid); 250 | $cm = $modinfo->get_cm($item->cmid); 251 | $pageurl = new moodle_url('/mod/expertforum/viewpost.php', 252 | array('e' => $item->expertforumid, 'id' => $item->id)); 253 | $pagename = format_string($item->subject, true, array('context' => context_module::instance($item->cmid))); 254 | $pagename = html_writer::link($pageurl, $pagename); 255 | $courseurl = course_get_url($item->courseid, $cm->sectionnum); 256 | $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); 257 | $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); 258 | $coursename = html_writer::link($courseurl, $coursename); 259 | if (isset($user->id)) { 260 | $icon = $output->user_picture($user, ['courseid' => $item->courseid]); 261 | } else { 262 | $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); 263 | } 264 | $tagfeed->add($icon, $pagename, $cmname.'
'.$coursename); 265 | } 266 | 267 | return $output->render_from_template('core_tag/tagfeed', 268 | $tagfeed->export_for_template($output)); 269 | } 270 | 271 | protected static function fetch_additional_data(&$items) { 272 | global $DB; 273 | 274 | if (empty($items)) { 275 | return; 276 | } 277 | 278 | $fetchedtags = array(); 279 | list($itemsql, $itemparams) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED); 280 | 281 | // Get additional details about items (message, timecreated, etc.) 282 | $sql = "SELECT p.id, timecreated, message, messageformat, messagetrust, votes 283 | FROM {expertforum_post} p 284 | WHERE p.id $itemsql"; 285 | $records = $DB->get_records_sql($sql, $itemparams); 286 | foreach ($records as $record) { 287 | $items[$record->id]->timecreated = $record->timecreated; 288 | $items[$record->id]->message = $record->message; 289 | $items[$record->id]->messageformat = $record->messageformat; 290 | $items[$record->id]->messagetrust = $record->messagetrust; 291 | $items[$record->id]->votes = $record->votes; 292 | } 293 | 294 | // Get number of answers for each post. 295 | $sql = "SELECT parent, COUNT(id) AS answers 296 | FROM {expertforum_post} 297 | WHERE parent $itemsql 298 | GROUP BY parent"; 299 | $records = $DB->get_records_sql($sql, $itemparams); 300 | foreach ($records as $record) { 301 | $items[$record->parent]->answers = $record->answers; 302 | } 303 | 304 | // Fetch items tags. 305 | $sql = "SELECT ti.itemid, tg.id, tg.name, tg.rawname 306 | FROM {tag_instance} ti 307 | JOIN {tag} tg ON tg.id = ti.tagid 308 | WHERE ti.component = :component AND ti.itemtype = :recordtype AND ti.itemid $itemsql 309 | ORDER BY ti.ordering ASC"; 310 | $itemparams['component'] = 'mod_expertforum'; 311 | $itemparams['recordtype'] = 'expertforum_post'; 312 | $rs = $DB->get_recordset_sql($sql, $itemparams); 313 | foreach ($rs as $record) { 314 | if (!isset($items[$record->itemid]->tags)) { 315 | $items[$record->itemid]->tags = []; 316 | } 317 | $items[$record->itemid]->tags[] = $record; 318 | } 319 | $rs->close(); 320 | 321 | return $fetchedtags; 322 | } 323 | 324 | protected static function display_items_full(core_renderer $output, $items, $showmoduleinfo) { 325 | self::fetch_additional_data($items); 326 | $listing = new \mod_expertforum\output\listing($items); 327 | return $output->render_from_template('mod_expertforum/listing', 328 | $listing->export_for_template($output)); 329 | } 330 | } 331 | 332 | -------------------------------------------------------------------------------- /lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Library of interface functions and constants for module expertforum 19 | * 20 | * All the core Moodle functions, neeeded to allow the module to work 21 | * integrated in Moodle should be placed here. 22 | * 23 | * All the expertforum specific functions, needed to implement all the module 24 | * logic, should go to locallib.php. This will help to save some memory when 25 | * Moodle is performing actions across all modules. 26 | * 27 | * @package mod_expertforum 28 | * @copyright 2015 Marina Glancy 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | 32 | defined('MOODLE_INTERNAL') || die(); 33 | 34 | /** 35 | * Example constant, you probably want to remove this :-) 36 | */ 37 | define('EXPERTFORUM_ULTIMATE_ANSWER', 42); 38 | 39 | /* Moodle core API */ 40 | 41 | /** 42 | * Returns the information on whether the module supports a feature 43 | * 44 | * See {@link plugin_supports()} for more info. 45 | * 46 | * @param string $feature FEATURE_xx constant for requested feature 47 | * @return mixed true if the feature is supported, null if unknown 48 | */ 49 | function expertforum_supports($feature) { 50 | 51 | switch($feature) { 52 | case FEATURE_MOD_INTRO: 53 | return true; 54 | case FEATURE_SHOW_DESCRIPTION: 55 | return true; 56 | case FEATURE_GRADE_HAS_GRADE: 57 | return true; 58 | case FEATURE_BACKUP_MOODLE2: 59 | return true; 60 | default: 61 | return null; 62 | } 63 | } 64 | 65 | /** 66 | * Saves a new instance of the expertforum into the database 67 | * 68 | * Given an object containing all the necessary data, 69 | * (defined by the form in mod_form.php) this function 70 | * will create a new instance and return the id number 71 | * of the new instance. 72 | * 73 | * @param stdClass $expertforum Submitted data from the form in mod_form.php 74 | * @param mod_expertforum_mod_form $mform The form instance itself (if needed) 75 | * @return int The id of the newly inserted expertforum record 76 | */ 77 | function expertforum_add_instance(stdClass $expertforum, mod_expertforum_mod_form $mform = null) { 78 | global $DB; 79 | 80 | $expertforum->timecreated = time(); 81 | 82 | // You may have to add extra stuff in here. 83 | 84 | $expertforum->id = $DB->insert_record('expertforum', $expertforum); 85 | 86 | expertforum_grade_item_update($expertforum); 87 | 88 | return $expertforum->id; 89 | } 90 | 91 | /** 92 | * Updates an instance of the expertforum in the database 93 | * 94 | * Given an object containing all the necessary data, 95 | * (defined by the form in mod_form.php) this function 96 | * will update an existing instance with new data. 97 | * 98 | * @param stdClass $expertforum An object from the form in mod_form.php 99 | * @param mod_expertforum_mod_form $mform The form instance itself (if needed) 100 | * @return boolean Success/Fail 101 | */ 102 | function expertforum_update_instance(stdClass $expertforum, mod_expertforum_mod_form $mform = null) { 103 | global $DB; 104 | 105 | $expertforum->timemodified = time(); 106 | $expertforum->id = $expertforum->instance; 107 | 108 | // You may have to add extra stuff in here. 109 | 110 | $result = $DB->update_record('expertforum', $expertforum); 111 | 112 | expertforum_grade_item_update($expertforum); 113 | 114 | return $result; 115 | } 116 | 117 | /** 118 | * Removes an instance of the expertforum from the database 119 | * 120 | * Given an ID of an instance of this module, 121 | * this function will permanently delete the instance 122 | * and any data that depends on it. 123 | * 124 | * @param int $id Id of the module instance 125 | * @return boolean Success/Failure 126 | */ 127 | function expertforum_delete_instance($id) { 128 | global $DB; 129 | 130 | if (! $expertforum = $DB->get_record('expertforum', array('id' => $id))) { 131 | return false; 132 | } 133 | 134 | $DB->delete_records('expertforum_post', array('expertforumid' => $expertforum->id)); 135 | $DB->delete_records('expertforum_vote', array('expertforumid' => $expertforum->id)); 136 | $DB->delete_records('expertforum', array('id' => $expertforum->id)); 137 | 138 | expertforum_grade_item_delete($expertforum); 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * Returns a small object with summary information about what a 145 | * user has done with a given particular instance of this module 146 | * Used for user activity reports. 147 | * 148 | * $return->time = the time they did it 149 | * $return->info = a short text description 150 | * 151 | * @param stdClass $course The course record 152 | * @param stdClass $user The user record 153 | * @param cm_info|stdClass $mod The course module info object or record 154 | * @param stdClass $expertforum The expertforum instance record 155 | * @return stdClass|null 156 | */ 157 | function expertforum_user_outline($course, $user, $mod, $expertforum) { 158 | 159 | $return = new stdClass(); 160 | $return->time = 0; 161 | $return->info = ''; 162 | return $return; 163 | } 164 | 165 | /** 166 | * Prints a detailed representation of what a user has done with 167 | * a given particular instance of this module, for user activity reports. 168 | * 169 | * It is supposed to echo directly without returning a value. 170 | * 171 | * @param stdClass $course the current course record 172 | * @param stdClass $user the record of the user we are generating report for 173 | * @param cm_info $mod course module info 174 | * @param stdClass $expertforum the module instance record 175 | */ 176 | function expertforum_user_complete($course, $user, $mod, $expertforum) { 177 | } 178 | 179 | /** 180 | * Given a course and a time, this module should find recent activity 181 | * that has occurred in expertforum activities and print it out. 182 | * 183 | * @param stdClass $course The course record 184 | * @param bool $viewfullnames Should we display full names 185 | * @param int $timestart Print activity since this timestamp 186 | * @return boolean True if anything was printed, otherwise false 187 | */ 188 | function expertforum_print_recent_activity($course, $viewfullnames, $timestart) { 189 | return false; 190 | } 191 | 192 | /** 193 | * Prepares the recent activity data 194 | * 195 | * This callback function is supposed to populate the passed array with 196 | * custom activity records. These records are then rendered into HTML via 197 | * {@link expertforum_print_recent_mod_activity()}. 198 | * 199 | * Returns void, it adds items into $activities and increases $index. 200 | * 201 | * @param array $activities sequentially indexed array of objects with added 'cmid' property 202 | * @param int $index the index in the $activities to use for the next record 203 | * @param int $timestart append activity since this time 204 | * @param int $courseid the id of the course we produce the report for 205 | * @param int $cmid course module id 206 | * @param int $userid check for a particular user's activity only, defaults to 0 (all users) 207 | * @param int $groupid check for a particular group's activity only, defaults to 0 (all groups) 208 | */ 209 | function expertforum_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) { 210 | } 211 | 212 | /** 213 | * Prints single activity item prepared by {@link expertforum_get_recent_mod_activity()} 214 | * 215 | * @param stdClass $activity activity record with added 'cmid' property 216 | * @param int $courseid the id of the course we produce the report for 217 | * @param bool $detail print detailed report 218 | * @param array $modnames as returned by {@link get_module_types_names()} 219 | * @param bool $viewfullnames display users' full names 220 | */ 221 | function expertforum_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) { 222 | } 223 | 224 | /** 225 | * Function to be run periodically according to the moodle cron 226 | * 227 | * This function searches for things that need to be done, such 228 | * as sending out mail, toggling flags etc ... 229 | * 230 | * Note that this has been deprecated in favour of scheduled task API. 231 | * 232 | * @return boolean 233 | */ 234 | function expertforum_cron () { 235 | return true; 236 | } 237 | 238 | /** 239 | * Returns all other caps used in the module 240 | * 241 | * For example, this could be array('moodle/site:accessallgroups') if the 242 | * module uses that capability. 243 | * 244 | * @return array 245 | */ 246 | function expertforum_get_extra_capabilities() { 247 | return array(); 248 | } 249 | 250 | /* Gradebook API */ 251 | 252 | /** 253 | * Is a given scale used by the instance of expertforum? 254 | * 255 | * This function returns if a scale is being used by one expertforum 256 | * if it has support for grading and scales. 257 | * 258 | * @param int $expertforumid ID of an instance of this module 259 | * @param int $scaleid ID of the scale 260 | * @return bool true if the scale is used by the given expertforum instance 261 | */ 262 | function expertforum_scale_used($expertforumid, $scaleid) { 263 | global $DB; 264 | 265 | if ($scaleid and $DB->record_exists('expertforum', array('id' => $expertforumid, 'grade' => -$scaleid))) { 266 | return true; 267 | } else { 268 | return false; 269 | } 270 | } 271 | 272 | /** 273 | * Checks if scale is being used by any instance of expertforum. 274 | * 275 | * This is used to find out if scale used anywhere. 276 | * 277 | * @param int $scaleid ID of the scale 278 | * @return boolean true if the scale is used by any expertforum instance 279 | */ 280 | function expertforum_scale_used_anywhere($scaleid) { 281 | global $DB; 282 | 283 | if ($scaleid and $DB->record_exists('expertforum', array('grade' => -$scaleid))) { 284 | return true; 285 | } else { 286 | return false; 287 | } 288 | } 289 | 290 | /** 291 | * Creates or updates grade item for the given expertforum instance 292 | * 293 | * Needed by {@link grade_update_mod_grades()}. 294 | * 295 | * @param stdClass $expertforum instance object with extra cmidnumber and modname property 296 | * @param bool $reset reset grades in the gradebook 297 | * @return void 298 | */ 299 | function expertforum_grade_item_update(stdClass $expertforum, $reset=false) { 300 | global $CFG; 301 | require_once($CFG->libdir.'/gradelib.php'); 302 | 303 | $item = array(); 304 | $item['itemname'] = clean_param($expertforum->name, PARAM_NOTAGS); 305 | $item['gradetype'] = GRADE_TYPE_VALUE; 306 | 307 | if ($expertforum->grade > 0) { 308 | $item['gradetype'] = GRADE_TYPE_VALUE; 309 | $item['grademax'] = $expertforum->grade; 310 | $item['grademin'] = 0; 311 | } else if ($expertforum->grade < 0) { 312 | $item['gradetype'] = GRADE_TYPE_SCALE; 313 | $item['scaleid'] = -$expertforum->grade; 314 | } else { 315 | $item['gradetype'] = GRADE_TYPE_NONE; 316 | } 317 | 318 | if ($reset) { 319 | $item['reset'] = true; 320 | } 321 | 322 | grade_update('mod/expertforum', $expertforum->course, 'mod', 'expertforum', 323 | $expertforum->id, 0, null, $item); 324 | } 325 | 326 | /** 327 | * Delete grade item for given expertforum instance 328 | * 329 | * @param stdClass $expertforum instance object 330 | * @return grade_item 331 | */ 332 | function expertforum_grade_item_delete($expertforum) { 333 | global $CFG; 334 | require_once($CFG->libdir.'/gradelib.php'); 335 | 336 | return grade_update('mod/expertforum', $expertforum->course, 'mod', 'expertforum', 337 | $expertforum->id, 0, null, array('deleted' => 1)); 338 | } 339 | 340 | /** 341 | * Update expertforum grades in the gradebook 342 | * 343 | * Needed by {@link grade_update_mod_grades()}. 344 | * 345 | * @param stdClass $expertforum instance object with extra cmidnumber and modname property 346 | * @param int $userid update grade of specific user only, 0 means all participants 347 | */ 348 | function expertforum_update_grades(stdClass $expertforum, $userid = 0) { 349 | global $CFG, $DB; 350 | require_once($CFG->libdir.'/gradelib.php'); 351 | 352 | // Populate array of grade objects indexed by userid. 353 | $grades = array(); 354 | 355 | grade_update('mod/expertforum', $expertforum->course, 'mod', 'expertforum', $expertforum->id, 0, $grades); 356 | } 357 | 358 | /* File API */ 359 | 360 | /** 361 | * Returns the lists of all browsable file areas within the given module context 362 | * 363 | * The file area 'intro' for the activity introduction field is added automatically 364 | * by {@link file_browser::get_file_info_context_module()} 365 | * 366 | * @param stdClass $course 367 | * @param stdClass $cm 368 | * @param stdClass $context 369 | * @return array of [(string)filearea] => (string)description 370 | */ 371 | function expertforum_get_file_areas($course, $cm, $context) { 372 | return array(); 373 | } 374 | 375 | /** 376 | * File browsing support for expertforum file areas 377 | * 378 | * @package mod_expertforum 379 | * @category files 380 | * 381 | * @param file_browser $browser 382 | * @param array $areas 383 | * @param stdClass $course 384 | * @param stdClass $cm 385 | * @param stdClass $context 386 | * @param string $filearea 387 | * @param int $itemid 388 | * @param string $filepath 389 | * @param string $filename 390 | * @return file_info instance or null if not found 391 | */ 392 | function expertforum_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { 393 | return null; 394 | } 395 | 396 | /** 397 | * Serves the files from the expertforum file areas 398 | * 399 | * @package mod_expertforum 400 | * @category files 401 | * 402 | * @param stdClass $course the course object 403 | * @param stdClass $cm the course module object 404 | * @param stdClass $context the expertforum's context 405 | * @param string $filearea the name of the file area 406 | * @param array $args extra arguments (itemid, path) 407 | * @param bool $forcedownload whether or not force download 408 | * @param array $options additional options affecting the file serving 409 | */ 410 | function expertforum_pluginfile($course, $cm, $context, $filearea, array $args, $forcedownload, array $options=array()) { 411 | global $DB, $CFG; 412 | 413 | if ($context->contextlevel != CONTEXT_MODULE) { 414 | send_file_not_found(); 415 | } 416 | 417 | require_login($course, true, $cm); 418 | 419 | send_file_not_found(); 420 | } 421 | 422 | /* Navigation API */ 423 | 424 | /** 425 | * Extends the global navigation tree by adding expertforum nodes if there is a relevant content 426 | * 427 | * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. 428 | * 429 | * @param navigation_node $navref An object representing the navigation tree node of the expertforum module instance 430 | * @param stdClass $course current course record 431 | * @param stdClass $module current expertforum instance record 432 | * @param cm_info $cm course module information 433 | */ 434 | function expertforum_extend_navigation(navigation_node $navref, stdClass $course, stdClass $module, cm_info $cm) { 435 | // TODO Delete this function and its docblock, or implement it. 436 | } 437 | 438 | /** 439 | * Extends the settings navigation with the expertforum settings 440 | * 441 | * This function is called when the context for the page is a expertforum module. This is not called by AJAX 442 | * so it is safe to rely on the $PAGE. 443 | * 444 | * @param settings_navigation $settingsnav complete settings navigation tree 445 | * @param navigation_node $expertforumnode expertforum administration node 446 | */ 447 | function expertforum_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $expertforumnode=null) { 448 | // TODO Delete this function and its docblock, or implement it. 449 | } 450 | -------------------------------------------------------------------------------- /classes/post.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Contains class mod_expertforum_post 19 | * 20 | * @package mod_expertforum 21 | * @copyright 2015 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | /** 28 | * Class to manage one post 29 | * 30 | * @package mod_expertforum 31 | * @copyright 2015 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | * 34 | * @property-read int id 35 | * @property-read int courseid 36 | * @property-read int expertforumid 37 | * @property-read int parent 38 | * @property-read int userid 39 | * @property-read int groupid 40 | * @property-read string subject 41 | * @property-read string message 42 | * @property-read int messageformat 43 | * @property-read int messagetrust 44 | * @property-read int timecreated 45 | * @property-read int timemodified 46 | * @property-read int votes 47 | */ 48 | class mod_expertforum_post implements templatable { 49 | 50 | protected $record; 51 | protected $cm; 52 | protected $cachedtags = null; 53 | protected $cachedparent = null; 54 | protected $answers = null; 55 | 56 | /** 57 | * 58 | * @param type $record 59 | * @param cm_info|stdClass $cm must have at least properties: id, course, instance 60 | * @param type $fetchedtags 61 | */ 62 | public function __construct($record, $cm, $fetchedtags = null) { 63 | $this->record = $record; 64 | $this->cm = $cm; 65 | $this->cachedtags = $fetchedtags; 66 | } 67 | 68 | public function __get($name) { 69 | if ($name === 'cm') { 70 | return $this->cm; 71 | } 72 | return $this->record->$name; 73 | } 74 | 75 | public function __isset($name) { 76 | return ($name === 'cm') || isset($this->record->$name); 77 | } 78 | 79 | public static function editoroptions($cm) { 80 | $context = context_module::instance($cm->id); 81 | return array('trusttext' => 1, 'context' => $context); // TODO maxbytes,maxfiles. 82 | } 83 | 84 | public static function can_create($cm) { 85 | global $USER; 86 | // TODO 87 | return isloggedin() && !isguestuser(); 88 | } 89 | 90 | public static function create($data, $cm) { 91 | global $USER, $DB; 92 | 93 | $context = context_module::instance($cm->id); 94 | $time = time(); 95 | $data = (object)((array)$data + array( 96 | 'groupid' => 0, 97 | 'votes' => 0, 98 | 'timecreated' => $time, 99 | 'timemodified' => $time, 100 | 'userid' => $USER->id, 101 | 'message' => '', 102 | 'messageformat' => FORMAT_PLAIN, 103 | )); 104 | unset($data->id); 105 | $data->courseid = $cm->course; 106 | $data->expertforumid = $cm->instance; 107 | if (empty($data->parent)) { 108 | $data->parent = null; 109 | $parent = null; 110 | if (empty($data->subject)) { 111 | throw new coding_exception('Caller must verify that subject is set'); 112 | } 113 | } else { 114 | $parent = $DB->get_record('expertforum_post', array('id' => $data->parent, 115 | 'courseid' => $data->courseid, 'expertforumid' => $data->expertforumid), 116 | 'id, subject, groupid', MUST_EXIST); 117 | $data->groupid = $parent->groupid; 118 | $data->subject = null; 119 | } 120 | $data->messagetrust = trusttext_trusted($context); 121 | 122 | $data->id = $DB->insert_record('expertforum_post', $data); 123 | 124 | if (isset($data->message_editor)) { 125 | $data = file_postupdate_standard_editor($data, 'message', self::editoroptions($cm), 126 | $context, 'mod_expertforum', 'post', $data->id); 127 | unset($data->message_editor); 128 | $DB->update_record('expertforum_post', $data); 129 | } 130 | 131 | if (isset($data->tags)) { 132 | core_tag_tag::set_item_tags('mod_expertforum', 'expertforum_post', $data->id, $context, $data->tags); 133 | } 134 | 135 | return new mod_expertforum_post($data, $cm); 136 | } 137 | 138 | public function can_update() { 139 | global $USER; 140 | // TODO 141 | return isloggedin() && !isguestuser() && ($USER->id == $this->record->userid); 142 | } 143 | 144 | /** 145 | * 146 | * @param stdClass $formdata data from the form, allowed keys: 147 | * message, messageformat, message_editor, tags 148 | */ 149 | public function update($formdata) { 150 | global $DB; 151 | 152 | $allowedkeys = array('message' => 1, 'messageformat' => 1, 'message_editor' => 1); 153 | if (!$this->record->parent) { 154 | $allowedkeys['subject'] = 1; 155 | } 156 | $data = (object)array_intersect_key((array)$formdata, $allowedkeys); 157 | $data->id = $this->record->id; 158 | $data->timemodified = time(); 159 | 160 | $context = context_module::instance($this->cm->id); 161 | if (isset($data->message_editor)) { 162 | $data = file_postupdate_standard_editor($data, 'message', self::editoroptions($this->cm), 163 | $context, 'mod_expertforum', 'post', $data->id); 164 | unset($data->message_editor); 165 | } 166 | $DB->update_record('expertforum_post', $data); 167 | 168 | if (!$this->record->parent && isset($formdata->tags)) { 169 | core_tag_tag::set_item_tags('mod_expertforum', 'expertforum_post', 170 | $data->id, $context, $formdata->tags); 171 | } 172 | } 173 | 174 | public function get_url($params = null) { 175 | $id = empty($this->record->parent) ? $this->record->id : $this->record->parent; 176 | $urlparams = array('e' => $this->cm->instance, 'id' => $id) + ($params ? $params : array()); 177 | $url = new moodle_url('/mod/expertforum/viewpost.php', $urlparams); 178 | return $url; 179 | } 180 | 181 | public function get_parent() { 182 | global $DB; 183 | if (empty($this->record->parent)) { 184 | return null; 185 | } 186 | if ($this->cachedparent === null) { 187 | $this->cachedparent = new self( 188 | $DB->get_record('expertforum_post', array('id' => $this->record->parent)), 189 | $this->cm); 190 | } 191 | return $this->cachedparent; 192 | } 193 | 194 | public function get_formatted_subject() { 195 | global $CFG; 196 | require_once($CFG->libdir.'/externallib.php'); 197 | if ($parent = $this->get_parent()) { 198 | return $parent->get_formatted_subject(); 199 | } 200 | $context = context_module::instance($this->cm->id); 201 | return external_format_string($this->record->subject, $context->id); 202 | } 203 | 204 | public function get_formatted_message() { 205 | global $CFG; 206 | require_once($CFG->libdir.'/externallib.php'); 207 | // TODO not possible to set trusttext! 208 | //return $this->record->message; 209 | $context = context_module::instance($this->cm->id); 210 | list($text, $textformat) = external_format_text($this->record->message, 211 | $this->record->messageformat, 212 | $context->id, 213 | 'mod_expertforum', 214 | 'post', 215 | $this->record->id); 216 | return $text; 217 | } 218 | 219 | public function get_excerpt($maxlength = 250) { 220 | $message = $this->get_formatted_message(); 221 | return html_to_text(shorten_text($message, $maxlength), $maxlength + 100); 222 | } 223 | 224 | public static function get($id, $cm, $strictness = IGNORE_MISSING) { 225 | global $DB; 226 | $userfields = \user_picture::fields('u', array('deleted'), 'useridx', 'user'); 227 | $record = $DB->get_record_sql('SELECT p.*, ' . $userfields . ' 228 | FROM {expertforum_post} p 229 | LEFT JOIN {user} u ON u.id = p.userid AND u.deleted = 0 230 | WHERE p.id = ? AND p.expertforumid = ? AND p.parent IS NULL', 231 | array($id, $cm->instance), 232 | $strictness); 233 | if ($record) { 234 | return new self($record, $cm); 235 | } 236 | return null; 237 | } 238 | 239 | public function can_answer() { 240 | global $USER; 241 | // TODO 242 | return isloggedin() && !isguestuser(); 243 | } 244 | 245 | public function get_answers($usecache = true) { 246 | global $DB; 247 | if ($this->record->parent) { 248 | // Answers do not have sub-answers. 249 | return array(); 250 | } 251 | if ($usecache && $this->answers !== null) { 252 | return $this->answers; 253 | } 254 | $userfields = \user_picture::fields('u', array('deleted'), 'useridx', 'user'); 255 | $records = $DB->get_records_sql('SELECT a.*, ' . $userfields . ' 256 | FROM {expertforum_post} a 257 | LEFT JOIN {user} u ON u.id = a.userid AND u.deleted = 0 258 | WHERE a.parent = ? AND a.expertforumid = ? 259 | ORDER BY a.votes DESC, a.timecreated, a.id', 260 | array($this->record->id, $this->cm->instance)); 261 | $this->answers = array(); 262 | foreach ($records as $record) { 263 | $answer = new self($record, $this->cm); 264 | $answer->cachedparent = $this; 265 | $this->answers[$answer->id] = $answer; 266 | } 267 | return $this->answers; 268 | } 269 | 270 | public function can_upvote() { 271 | global $USER; 272 | // TODO 273 | return isloggedin() && !isguestuser() && $USER->id != $this->record->userid; 274 | } 275 | 276 | public function can_downvote() { 277 | global $USER; 278 | // TODO 279 | return isloggedin() && !isguestuser() && $USER->id != $this->record->userid; 280 | } 281 | 282 | public function find_answer($answerid, $strictness = IGNORE_MISSING) { 283 | $answers = $this->get_answers(); 284 | if (isset($answers[$answerid])) { 285 | return $answers[$answerid]; 286 | } else if ($strictness == MUST_EXIST) { 287 | throw new moodle_exception('postnotfound', 'mod_expertforum'); 288 | } 289 | return null; 290 | } 291 | 292 | public function vote($postid, $vote) { 293 | if ($postid == $this->record->id) { 294 | $this->voteme($vote); 295 | } else if ($answer = $this->find_answer($postid)) { 296 | $answer->voteme($vote); 297 | } 298 | } 299 | 300 | protected function voteme($vote) { 301 | global $DB, $USER; 302 | if (!isloggedin() || isguestuser()) { 303 | return; 304 | } 305 | $params = array('userid' => $USER->id, 'postid' => $this->record->id); 306 | $answer = $DB->get_record_sql('SELECT p.id, p.parent, p.userid AS author, v.id AS voteid, v.vote 307 | FROM {expertforum_post} p 308 | LEFT JOIN {expertforum_vote} v ON v.postid = p.id AND v.userid = :userid 309 | WHERE p.id = :postid', 310 | $params); 311 | if (!$answer) { 312 | return; 313 | } 314 | if ($answer->author == $USER->id) { 315 | // Can't vote on own posts. 316 | return; 317 | } 318 | if ($answer->voteid) { 319 | if ($answer->vote != $vote) { 320 | $DB->update_record('expertforum_vote', array( 321 | 'vote' => $vote, 322 | 'id' => $answer->voteid, 323 | 'timemodified' => time())); 324 | } else { 325 | return; 326 | } 327 | } else { 328 | $DB->insert_record('expertforum_vote', array( 329 | 'userid' => $USER->id, 330 | 'courseid' => $this->cm->course, 331 | 'expertforumid' => $this->cm->instance, 332 | 'parent' => $this->record->parent ?: $this->record->id, // TODO field not needed. 333 | 'postid' => $this->record->id, 334 | 'timemodified' => time(), 335 | 'vote' => $vote 336 | )); 337 | } 338 | $votes = $DB->get_field_sql("SELECT SUM(v.vote) " 339 | . "FROM {expertforum_vote} v WHERE v.postid = ?", 340 | array($this->record->id)); 341 | $DB->update_record('expertforum_post', array('id' => $this->record->id, 342 | 'votes' => $votes)); 343 | $this->record->votes = $votes; 344 | /*$DB->execute("UPDATE {expertforum_post} p SET votes = 345 | (SELECT SUM(v.vote) FROM {expertforum_vote} v WHERE v.parent = p.id) 346 | WHERE p.id = :parent", $params);*/ 347 | } 348 | 349 | public function get_timestamp() { 350 | $now = time(); 351 | $timestamp = format_time($now - $this->record->timecreated); 352 | $timestamp = html_writer::span($timestamp, 'relativetime', 353 | array('title' => userdate($this->record->timecreated, get_string('strftimedatetime', 'core_langconfig')))); 354 | if (empty($this->record->parent)) { 355 | return get_string('askedago', 'mod_expertforum', $timestamp); 356 | } else { 357 | return get_string('answeredago', 'mod_expertforum', $timestamp); 358 | } 359 | } 360 | 361 | /** 362 | * 363 | * @return array list of tags associated with this post, each element is array with 364 | * two keys - tagname and tagurl 365 | */ 366 | public function get_tags() { 367 | $rv = array(); 368 | if (empty($this->record->parent)) { 369 | if ($this->cachedtags === null) { 370 | $this->cachedtags = core_tag_tag::get_item_tags('mod_expertforum', 'expertforum_post', $this->id); 371 | } 372 | $url = new moodle_url('/mod/expertforum/view.php', array('id' => $this->cm->id)); 373 | foreach ($this->cachedtags as $tag) { 374 | $url->param('tag', $tag->name); 375 | $rv[] = array('tagname' => core_tag_tag::make_display_name($tag), 376 | 'tagurl' => $url->out(false)); 377 | } 378 | } 379 | return $rv; 380 | } 381 | 382 | /** 383 | * Function to export the renderer data in a format that is suitable for a 384 | * mustache template. This means: 385 | * 1. No complex types - only stdClass, array, int, string, float, bool 386 | * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). 387 | * 388 | * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. 389 | * @return stdClass|array 390 | */ 391 | public function export_for_template(renderer_base $output) { 392 | $posts = array(); 393 | $record = new stdClass(); 394 | $record->id = $this->record->id; 395 | $record->subject = $this->get_formatted_subject(); 396 | $record->message = $this->get_formatted_message(); 397 | $record->votes = empty($this->votes) ? 0 : $this->votes; 398 | $record->userpicture = ''; 399 | $record->username = ''; 400 | $record->userreputation = ''; 401 | $record->isfavourite = 1; // TODO 402 | $record->favouritecount = 5; // TODO 403 | $record->tags = $this->get_tags(); 404 | 405 | $user = \user_picture::unalias($this->record, array('deleted'), 'useridx', 'user'); 406 | if (isset($user->id)) { 407 | $record->userpicture = $output->user_picture($user, array('size' => 35)); 408 | $record->username = fullname($user); 409 | if (user_can_view_profile($user)) { 410 | $profilelink = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $this->cm->course)); 411 | $record->username = html_writer::link($profilelink, $record->username); 412 | } 413 | $record->userreputation = 15; // TODO 414 | } 415 | 416 | $record->timestamp = $this->get_timestamp(); 417 | if (empty($this->record->parent)) { 418 | $record->answers = array(); 419 | $answers = $this->get_answers(); 420 | foreach ($answers as $answer) { 421 | $record->answers[] = $answer->export_for_template($output); 422 | } 423 | if ($answers) { 424 | $record->answersheader = get_string('answerscount', 'mod_expertforum', count($record->answers)); 425 | } 426 | } else { 427 | $record->parent = $this->record->parent; 428 | } 429 | 430 | $record->upvoteurl = ''; 431 | $record->downvoteurl = ''; 432 | if ($this->can_upvote()) { 433 | $record->upvoteurl = $this->get_url()->out(false, 434 | array('upvote' => $this->record->id, 'sesskey' => sesskey())); 435 | } 436 | if ($this->can_downvote()) { 437 | $record->downvoteurl = $this->get_url()->out(false, 438 | array('downvote' => $this->record->id, 'sesskey' => sesskey())); 439 | } 440 | 441 | $record->editurl = ''; 442 | if ($this->can_update()) { 443 | $url = new moodle_url('/mod/expertforum/post.php', array('e' => $this->cm->instance, 'edit' => $this->record->id)); 444 | $record->editurl = $url->out(false); 445 | } 446 | 447 | return $record; 448 | } 449 | } --------------------------------------------------------------------------------