Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Assignment Subtypes Combined

From MoodleDocs

Introduction

This feature is part of the assignment module redevelopment project (https://docs.moodle.org/dev/Assignment). It involves creating a new module (mod_assign) that supersedes the previous one (mod_assignment). It provides all of the features of the four standard subtypes within one module (ie - you can enable file uploads, online text, notes, feedback etc).

Examples

See the use cases in the assignment redevelopment page.

This list is not exhaustive - the new assignment module will allow combinations that were not previously possible (e.g. An online assignment that also accepts file submissions)

Requirements

Core requirements

  • No loss of functionality from old assignment module to new assignment module
  • Supports Upgrade from old assignment module to new assignment module
  • Support for Portfolio API
  • Support for Plagiarism API
  • Full backup/restore support

Optional requirements

  • Allow subclasses to extend the standard assignment module (similar to the sub-types - but as a complete new module)

Pre-requisites

Moodle 2.3 required

Community bonding period

Milestones

  • Document proposed design and get community feedback - ongoing
  • Decide list of settings - done
  • Decide code/file structure layout - done
  • Create mockups - done

Decisions

Base class

There are 3 options for creating this module. These have been proposed in the Meta-ticket (MDL-26997).

The decision was made to go with option C but it was extended to support feedback plugins as well as submission plugins. This will allow all of the existing assignment subtypes in the plugins database to be upgraded to support the new assignment module. The information on the first 2 options has been removed to make this information clearer.

Option C (Chosen)

Although this sounds similar to the old assignment module - it is different.

Allow 2 types of plugins, submission and feedback that can:

  • add additional settings to the assignment
  • add additional elements to the submission/feedback form
  • do custom saving of a submission/grading
  • present a custom summary and view of a submission/feedback

These plugins save data in their own separate tables and only the core submission/grading info would be saved to the submissions/grades table. Plugins can implement their own install/upgrade/backup/restore code through the existing subplugin mechanism in Moodle. The standard submission and feedback types provide simple examples of how to implement this logic.

Benefits:

  • All of the assignment code is in the new assign module (instead of putting an assignmentlib.php in the core lib folder)
  • Custom assignment submission types must create their own tables to save data instead of all abusing the data1 and data2 columns.
  • Custom assignment submission modules to not need to re-implement the grading interface etc.
  • Custom assignment modules can be used in combination with the built in assignment submission modules - e.g. an online text + online audio assignment.

Coding period

Code is nearing completion and we are reviewing/documenting and performing initial QA before submitting the code for review by HQ.

Code repository

Unstable code in progress is available at:

https://github.com/netspotau/moodle-mod_assign/tree/MDL-31270

Milestones

  • Implement proposed design (new module only) - done
  • Implement Backup/Restore - done
  • Implement Upgrades - done (needs some more UI features)
  • Code complete - ready for integration

File structure

Files for the new mod_assign:

lib.php - Contains the standard moodle module hooks. Most functions create an assignment instance and call the relevant function from that class.
locallib.php - Contains the assignment class. This is the core functionality of this module.
assignment_plugin.php - Abstract class that is the parent of submission_plugin and feedback_plugin. They share similar interfaces and features so it makes sense to combine the common code. 
submission_plugin.php - Abstract class that needs to be implemented by all submission plugins. 
feedback_plugin.php - Abstract class that needs to be implemented by all feedback plugins. 
admin_manage_plugins.php - Used by the admin settings to manage both the feedback and submission plugins (but they appear on separate pages). Plugins can be reordered, uninstalled and configured here.
index.php - Include view.php - Prevent directory listing of the plugin folder.
mod_form.php - Include the base form for the module settings, grading interface and submission interface. This uses callbacks to allow the plugins to extend the forms.
portfolio_callback.php - Required class for the portfolio API.
renderer.php - Used to perform common functions such as rendering a list of files (with support for plagiarism and portfolios)
settings.php - Used to add pages to the admin navigation
style.css - Some simple css rules
assign.js - Change the default behaviour of the filepicker. 
upgradelib.php - Library for upgrading from mod_assignment to mod_assign
view.php - The entry point to the module - creates and assignment instance and then calls view on it.
version.php - Version information for the plugin
lang/en/assign.php - Language file (en, mod_assign)
db/access.php - Install capabilities
db/install.xml - Install the core database tables
db/events.php - Register the events produced by this module
db/log.php - Log definitions
db/messages.php - Notification definitions
db/subplugins - Register the subplugin types
db/upgrade.php - Upgrade code - should we put upgrade code from the old mod_assignment here (I dont think so)
backup/moodle2/backup_assign_task_activity.php - Backup activity class
backup/moodle2/backup_assign_stepslib.php - List of backup steps
backup/moodle2/restore_assign_stepslib.php - List of restore steps
backup/moodle2/backup_assign_task_activity.php - Restore activity class

submission/<plugin name> - List of submission plugins
feedback/<plugin name> - List of feedback plugins

Each submission plugin has this structure (relative to submission/<plugin-name>)

settings.php (optional) Define any admin settings for this module
lang/en/submission_<pluginname>.php - Language file (en)
db/install.php (optional) Install steps
db/upgrade.php (optional) Upgrade steps
db/install.xml (optional) Database tables
db/access.php (optional) Custom capabilities
lib.php (required) Must contain a class named submission_<plugin-name> that extends submission_plugin.
version.php (required) Version information for this plugin
backup/moodle2/backup_submission_<plugin-name>_subplugin.class.php (optional) Backup this plugin
backup/moodle2/restore_submission_<plugin-name>_subplugin.class.php (optional) Restore this plugin

Each feedback plugin has this structure (relative to feedback/<plugin-name>)

settings.php (optional) Define any admin settings for this module
lang/en/feedback_<pluginname>.php - Language file (en)
db/install.php (optional) Install steps
db/upgrade.php (optional) Upgrade steps
db/install.xml (optional) Database tables
db/access.php (optional) Custom capabilities
lib.php (required) Must contain a class named feedback_<plugin-name> that extends feedback_plugin.
version.php (required) Version information for this plugin
backup/moodle2/backup_feedback_<plugin-name>_subplugin.class.php (optional) Backup this plugin
backup/moodle2/restore_feedback_<plugin-name>_subplugin.class.php (optional) Restore this plugin

Class Diagram

Assignment Class Diagram.png

Screenshots

  • Subject to change

Developer Information

Writing a submission plugin for mod_assign

Objectives

Provide a simple structure for people who want to create new types of assignments or convert assignment "subtypes" from the old mod_assignment to the newer mod_assign.

Overview

Writing a submission plugin for mod_assign allows you to optionally do a number of things:

  • Add settings to the assignment settings page
  • Add elements to the submission form (or just arbitrary html using the static element type)
  • Add html to a table cell in the submission status page
  • Include a link in the submission status page to a full page of html with a back link
  • Participate in the backup and restore process
  • Participate in the old assignment upgrade process

An example plugin

The 3 standard submission plugins included with mod_assign are good examples to look at when building your own submission plugins. We will use submission_file here as an example because it demonstrates almost all of the functionality available to a submission plugin.

Files

The files for a custom submission plugin sit under "mod/assign/submission/<pluginname>". You can find all the files for the file submission plugin under "mod/assign/submission/file" and nowhere else.

version.php

To start with we need to tell Moodle the version information for our new plugin so that it can be installed and upgraded correctly. This information is added to version.php as with any other type of Moodle plugin. The component name must begin with "submission_" to identify this as a submission plugin.

$plugin->version   = 2012011600;
$plugin->requires  = 2011110200;
$plugin->component = 'submission_file';

settings.php

The settings file allows us to add custom settings to the system wide configuration page for our plugin. The file plugin checks to see if there is a maxbytes setting for this moodle installation and if found, it adds a new admin setting to the settings page. The name of the setting should begin with the plugin component name ("submission_file") in this case. The strings are specified in this plugins language file.

if (isset($CFG->maxbytes)) {
    $settings->add(new admin_setting_configselect('submission_file_maxbytes', 
                        get_string('maximumsubmissionsize', 'submission_file'),
                        get_string('configmaxbytes', 'submission_file'), 1048576,       
                        get_max_upload_sizes($CFG->maxbytes)));
}

lang/en/submission_file.php

The language file for this plugin must have the same name component name ("submission_file.php"). It should at least define a string for "pluginname".

$string['allowfilesubmissions'] = 'Enabled';
$string['configmaxbytes'] = 'Maximum file size';
$string['file'] = 'File Submissions';
$string['maxbytes'] = 'Maximum file size';
$string['maxfilessubmission'] = 'Maximum number of uploaded files';
$string['maximumsubmissionsize'] = 'Maximum submission size';
$string['pluginname'] = 'File Submissions';
$string['submissionfilearea'] = 'Uploaded submission files';

db/access.php

This is where any additional capabilities are defined (None for this plugin).

$capabilities = array(

);

db/upgrade.php

This is where any upgrade code is defined (None for this plugin).

function xmldb_submission_file_upgrade($oldversion) {
    global $CFG, $DB, $OUTPUT;

    $dbman = $DB->get_manager();

    // do the upgrades
    return true;
}

db/install.xml

This is where any database tables required to save this plugins data are defined. In this case we define a table that links to submission and contains a column to record the number of files.

<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/assign/submission/file/db" VERSION="20090420" COMMENT="XMLDB file for Moodle mod/assign/submission/file"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
  <TABLES>
    <TABLE NAME="assign_submission_file" COMMENT="Info about submitted assignments">
      <FIELDS>
        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="assignment"/>
        <FIELD NAME="assignment" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="submission"/>
        <FIELD NAME="submission" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="assignment" NEXT="numfiles"/>
        <FIELD NAME="numfiles" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="submission" />
      </FIELDS>
      <KEYS>
        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="assignment"/>
        <KEY NAME="assignment" TYPE="foreign" FIELDS="assignment" REFTABLE="assign" REFFIELDS="id" PREVIOUS="primary" NEXT="submission"/>
        <KEY NAME="submission" TYPE="foreign" FIELDS="submission" REFTABLE="assign_submission" REFFIELDS="id" PREVIOUS="assignment"/>
      </KEYS>
    </TABLE>
  </TABLES>
</XMLDB>

db/install.php

This example is actually from the submission_comments plugin - not the submission_file plugin - but it shows how to run custom code on installation of the plugin. In this case it makes the comments plugin the last of the three submission plugins installed by default.

/**
 * Code run after the module database tables have been created.
 */
function xmldb_submission_comments_install() {
    global $CFG, $DB, $OUTPUT;

    // do the install

    require_once($CFG->dirroot . '/mod/assign/locallib.php');
    // set the correct initial order for the plugins
    $assignment = new assignment();
    $plugin = $assignment->get_submission_plugin_by_type('comments');
    if ($plugin) {
        $plugin->move('down');
        $plugin->move('down');
    }
        
    // do the upgrades
    return true;
}

lib.php

This is where all the functionality for this plugin is defined. We will step through this file and describe each part as we go.


class submission_file extends submission_plugin {

All submission plugins MUST define a class with the component name of the plugin that extends submission plugin.


    public function get_name() {
        return get_string('file', 'submission_file');
    }

Get name is abstract in submission_plugin and must be defined in your new plugin. Use the language strings to make your plugin translatable.

    public function get_settings(&$mform) {
        global $CFG, $COURSE, $DB;

        $default_maxfilesubmissions = $this->get_config('maxfilesubmissions');
        $default_maxsubmissionsizebytes = $this->get_config('maxsubmissionsizebytes');

        $settings = array();
        $options = array();
        for($i = 1; $i <= ASSIGN_MAX_SUBMISSION_FILES; $i++) {
            $options[$i] = $i;
        }

        $mform->addElement('select', 'submission_file_maxfiles', get_string('maxfilessubmission', 'submission_file'), $options);
        $mform->setDefault('submission_file_maxfiles', $default_maxfilesubmissions);

        $choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes);
        $choices[0] = get_string('courseuploadlimit') . ' ('.display_size($COURSE->maxbytes).')';
        $settings[] = array('type' => 'select',
                            'name' => 'maxsubmissionsizebytes',
                            'description' => get_string('maximumsubmissionsize', 'submission_file'),
                            'options'=>$choices,
                            'default'=>$default_maxsubmissionsizebytes);

        $mform->addElement('select', 'submission_file_maxsizebytes', get_string('maximumsubmissionsize', 'submission_file'), $choices);
        $mform->setDefault('submission_file_maxsizebytes', $default_maxsubmissionsizebytes);


    }

The "get_settings" function is called when building the settings page for the assignment. It allows this plugin to add a list of settings to the form. Notice that the settings are prefixed by the plugin name which is good practice to avoid conflicts with other plugins.

    public function save_settings($mform) {
        $this->set_config('maxfilesubmissions', $mform->submission_file_maxfiles);
        $this->set_config('maxsubmissionsizebytes', $mform->submission_file_maxsizebytes);
        return true;
    }

The "save_settings" function is called when the assignment settings page is submitted, either for a new assignment or when editing an existing one. For settings specific to a single instance of the assignment you can use the assignment_plugin::set_config function shown here to save key/value pairs against this assignment instance for this plugin.

    public function get_form_elements($submission, & $mform, & $data) {

        $elements = array();

        if ($this->get_config('maxfilesubmissions') <= 0) {
            return $elements;
        }

        $fileoptions = $this->get_file_options();
        $submissionid = $submission ? $submission->id : 0;


        $data = file_prepare_standard_filemanager($data, 'files', $fileoptions, $this->assignment->get_context(), 'mod_assign', ASSIGN_FILEAREA_SUBMISSION_FILES, $submissionid);
        $mform->addElement('filemanager', 'files_filemanager', '', null, $fileoptions);
        return true;
    }

The get_form_elements function is called when building the submission form. It functions identically to the get_settings function except that the submission object is available (if there is a submission) to associate the settings with a single submission. This function also shows how to use a filemanager within a submission plugin. The function must return true if it has modified the form otherwise the assignment will not include a header for this plugin.

    public function save($submission, $data) {

        global $USER, $DB;

        $fileoptions = $this->get_file_options();


        $data = file_postupdate_standard_filemanager($data, 'files', $fileoptions, $this->assignment->get_context(), 'mod_assign', ASSIGN_FILEAREA_SUBMISSION_FILES, $submission->id);


        $file_submission = $this->get_file_submission($submission->id);

        //plagiarism code event trigger when files are uploaded

        $fs = get_file_storage();
        $files = $fs->get_area_files($this->assignment->get_context()->id, 'mod_assign', ASSIGN_FILEAREA_SUBMISSION_FILES, $submission->id, "id", false);

        // send files to event system
        // Let Moodle know that an assessable file was uploaded (eg for plagiarism detection)
        $eventdata = new stdClass();
        $eventdata->modulename = 'assign';
        $eventdata->cmid = $this->assignment->get_course_module()->id;
        $eventdata->itemid = $submission->id;
        $eventdata->courseid = $this->assignment->get_course()->id;
        $eventdata->userid = $USER->id;
        if ($files) {
            $eventdata->files = $files;
        }
        events_trigger('assessable_file_uploaded', $eventdata);


        if ($file_submission) {
            $file_submission->numfiles = $this->count_files($submission->id);
            return $DB->update_record('assign_submission_file', $file_submission);
        } else {
            $file_submission = new stdClass();
            $file_submission->numfiles = $this->count_files($submission->id);
            $file_submission->submission = $submission->id;
            $file_submission->assignment = $this->assignment->get_instance()->id;
            return $DB->insert_record('assign_submission_file', $file_submission) > 0;
        }
    }

The "save" function is called to save a user submission. The parameters are the submission object and the data from the submission form. This example calls file_postupdate_standard_filemanager to copy the files from the draft file area to the filearea for this submission, it then uses the event api to trigger an assessable_file_uploaded event for the plagiarism api. It then records the number of files in the plugin specific "assign_submission_file" table.

    public function get_files($submission) {
        global $DB;
        $result = array();
        $fs = get_file_storage();

        $files = $fs->get_area_files($this->assignment->get_context()->id, 'mod_assign', ASSIGN_FILEAREA_SUBMISSION_FILES, $submission->id, "timemodified", false);

        foreach ($files as $file) {
            $result[$file->get_filename()] = $file;
        }
        return $result;
    }

If this submission plugin produces one or more files, it should implement "get_files" so that the portfolio API can export a list of all the files from all of the plugins for this assignment submission.

    public function view_summary($submission) {
        $count = $this->count_files($submission->id);
        if ($count <= ASSIGN_SUBMISSION_FILE_MAX_SUMMARY_FILES) {
            return $this->assignment->render_area_files(ASSIGN_FILEAREA_SUBMISSION_FILES, $submission->id);
        } else {
            return get_string('countfiles', 'submission_file', $count);
        }
    }

The view_summary function is called to display a summary of the submission to both markers and students. It counts the numbre of files submitted and if it is more that a set number (5) it only displays a count of how many files have been updated - otherwise it uses a helper function to write the entire list of files. This is because we want to keep the summaries really short so they can be displayed in a table. There will be a link to view the full submission on the submission status page.

    public function view($submission) {
        return $this->assignment->render_area_files(ASSIGN_FILEAREA_SUBMISSION_FILES, $submission->id);
    }

The view function is called to display the entire submission to both markers and students. In this case it uses the helper function in the assignment class to write the list of files.

    public function show_view_link($submission) {
        $count = $this->count_files($submission->id);
        return $count > ASSIGN_SUBMISSION_FILE_MAX_SUMMARY_FILES;
    }

The show_view_link function allows the plugin to control whether the assignment module renders a link to show the full submission or not. This is because some submission types can display the entire submission in the summary and some cannot.

    public function can_upgrade($type, $version) {

        $uploadsingle_type ='uploadsingle';
        $upload_type ='upload';

        if (($type == $uploadsingle_type || $type == $upload_type) && $version >= 2011112900) {
            return true;
        }
        return false;
    }

The can_upgrade function is used to identify old assignment subtypes that can be upgraded by this plugin. This plugin supports upgrades from the old "upload" and "uploadsingle" assignment subtypes.

    
    public function upgrade_settings($oldassignment, & $log) {
        // TODO: get the maxbytes and maxfiles settings from the old assignment and set them for this plugin
        return true;
    }

This function is called once per assignment instance to upgrade the settings from the old assignment to the new mod_assign. In this case it should be setting the maxbytes and maxfiles configuration settings (but it isn't yet!).


    public function upgrade($oldcontext,$oldassignment, $oldsubmission, $submission, & $log) {
        global $DB;

        $file_submission = new stdClass();



        $file_submission->numfiles = $oldsubmission->numfiles;
        $file_submission->submission = $submission->id;
        $file_submission->assignment = $this->assignment->get_instance()->id;

        if (!$DB->insert_record('assign_submission_file', $file_submission) > 0) {
            $log .= get_string('couldnotconvertsubmission', 'mod_assign', $submission->userid);
            return false;
        }




        // now copy the area files
        $this->assignment->copy_area_files_for_upgrade($oldcontext->id,
                                                        'mod_assignment',
                                                        'submission',
                                                        $oldsubmission->id,
                                                        // New file area
                                                        $this->assignment->get_context()->id,
                                                        'mod_assign',
                                                        ASSIGN_FILEAREA_SUBMISSION_FILES,
                                                        $submission->id);





        return true;
    }

The "upgrade" function upgrades a single submission from the old assignment type to the new one. In this case it involves copying all the files from the old filearea to the new one. There is a helper function available in the assignment class for this (Note: the copy will be fast as it is just adding rows to the files table).

    public function get_editor_text($name, $submissionid) {
        if ($name == 'onlinetext') {
            $onlinetext_submission = $this->get_onlinetext_submission($submissionid);
            if ($onlinetext_submission) {
                return $onlinetext_submission->onlinetext;
            }
        }

        return '';
    }

This example is from submission_onlinetext - not submission_file. If the plugin uses a text-editor it is ideal if the plugin implements "get_editor_text". This allows the portfolio to retrieve the text from the plugin when exporting the list of files for a submission. This is required because the text is stored in the plugin specific table that is only known to the plugin itself. The name is used to distinguish between multiple text areas in the one plugin.

    /**
     * get the content format for the editor 
     * @param string $name
     * @param int $submissionid
     * @return bool
     */
    public function get_editor_format($name, $submissionid) {
        if ($name == 'onlinetext') {
            $onlinetext_submission = $this->get_onlinetext_submission($submissionid);
            if ($onlinetext_submission) {
                return $onlinetext_submission->onlineformat;
            }
        }


         return 0;
    }

This example is from submission_onlinetext - not submission_file. For the same reason as the previous function, if the plugin uses a text editor, it is ideal if the plugin implements "get_editor_format". This allows the portfolio to retrieve the text from the plugin when exporting the list of files for a submission. This is required because the text is stored in the plugin specific table that is only known to the plugin itself. The name is used to distinguish between multiple text areas in the one plugin.

Backup and Restore

Submission plugins fully support backup and restore. Continuing the submission_file example from above, in the backup/moodle2/ folder there are 2 files, one for backup and one for restore.

backup/moodle2/backup_submission_file_subplugin.class.php

This class gets called during the backup process to backup a plugin submission. (the settings are backed up automatically). This example adds the number of files to the XML and then calls annotate_files to add the list of submission files to the backup.

class backup_submission_file_subplugin extends backup_subplugin {

    protected function define_submission_subplugin_structure() {

        // create XML elements
        $subplugin = $this->get_subplugin_element(); // virtual optigroup element
        $subplugin_wrapper = new backup_nested_element($this->get_recommended_name());
        $subplugin_element = new backup_nested_element('submission_file', null, array('numfiles', 'submission'));

        // connect XML elements into the tree
        $subplugin->add_child($subplugin_wrapper);
        $subplugin_wrapper->add_child($subplugin_element);

        // set source to populate the data
        $subplugin_element->set_source_table('assign_submission_file', array('submission' => backup::VAR_PARENTID));

        $subplugin_element->annotate_files('mod_assign', 'submission_files', 'submission');// The parent is the submission
        return $subplugin;
    }
}

backup/moodle2/restore_submission_file_subplugin.class.php

This class gets called during the restore process. The old submission id is mapped to a new submission id through "get_mappingid". This example sets the numfiles setting and restores all the submission files.

class restore_submission_file_subplugin extends restore_subplugin {

      protected function define_submission_subplugin_structure() {

        $paths = array();

        $elename = $this->get_namefor('submission');
        $elepath = $this->get_pathfor('/submission_file'); // we used get_recommended_name() so this works
        $paths[] = new restore_path_element($elename, $elepath);

        return $paths; // And we return the interesting paths
    }

    public function process_submission_file_submission($data) {
        global $DB;

        $data = (object)$data;
        $data->assignment = $this->get_new_parentid('assign');
        $oldsubmissionid = $data->submission;
        // the mapping is set in the restore for the core assign activity. When a submission node is processed
        $data->submission = $this->get_mappingid('submission', $data->submission);

        $DB->insert_record('assign_submission_file', $data);

        $this->add_related_files('mod_assign', 'submission_files', 'submission', null, $oldsubmissionid);
    }
}

Writing a feedback plugin for mod_assign

A feedback plugin is almost identical to a submission plugin from the code point of view. Feedback plugins sit in the feedback folder under mod/assign. Feedback plugins must extend the feedback_plugin class instead of the submission_plugin class but both inherit from assignment_plugin and support the same set of features. There is a separate admin page to manage feedback plugins. Instead of adding settings to the submission form they add their custom settings to the grading form and are visible in the feedback table on the assignment view page. The 2 standard feedback plugins are feedback files and feedback comments both of which are good examples to use as a reference.