MDL-50794 workshop: Allow restricting submitted file types

It was reported at the Open University that there have been some
problems with the formats of files submitted by students. Not all
students completed their outlines as a Word .doc (despite this being the
format of the template provided). Some formats (e.g. .pages) couldn’t be
translated by some of the reviewing students. Therefore, they were
unable to provide a review and the submitting students not receiving any
comments.

This patch allows the teacher to define list of allowed file types that
can be attached to submitted work and/or overall feedback in the
workshop.
This commit is contained in:
M Kassaei 2015-12-17 10:00:48 +00:00 committed by David Mudrák
parent 03b8b55f10
commit 1a282212c1
13 changed files with 296 additions and 11 deletions

View File

@ -53,11 +53,11 @@ class backup_workshop_activity_structure_step extends backup_activity_structure_
'instructauthorsformat', 'instructreviewers',
'instructreviewersformat', 'timemodified', 'phase', 'useexamples',
'usepeerassessment', 'useselfassessment', 'grade', 'gradinggrade',
'strategy', 'evaluation', 'gradedecimals', 'nattachments',
'strategy', 'evaluation', 'gradedecimals', 'nattachments', 'submissionfiletypes',
'latesubmissions', 'maxbytes', 'examplesmode', 'submissionstart',
'submissionend', 'assessmentstart', 'assessmentend',
'conclusion', 'conclusionformat', 'overallfeedbackmode',
'overallfeedbackfiles', 'overallfeedbackmaxbytes'));
'overallfeedbackfiles', 'overallfeedbackfiletypes', 'overallfeedbackmaxbytes'));
// assessment forms definition
$this->add_subplugin_structure('workshopform', $workshop, true);

View File

@ -26,6 +26,7 @@
<FIELD NAME="evaluation" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false" COMMENT="The recently used grading evaluation method"/>
<FIELD NAME="gradedecimals" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Number of digits that should be shown after the decimal point when displaying grades"/>
<FIELD NAME="nattachments" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Number of required submission attachments"/>
<FIELD NAME="submissionfiletypes" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="comma separated list of file extensions"/>
<FIELD NAME="latesubmissions" TYPE="int" LENGTH="2" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Allow submitting the work after the deadline"/>
<FIELD NAME="maxbytes" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="100000" SEQUENCE="false" COMMENT="Maximum size of the one attached file"/>
<FIELD NAME="examplesmode" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="0 = example assessments are voluntary, 1 = examples must be assessed before submission, 2 = examples are available after own submission and must be assessed before peer/self assessment phase"/>
@ -38,6 +39,7 @@
<FIELD NAME="conclusionformat" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="The format of the conclusion field content."/>
<FIELD NAME="overallfeedbackmode" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="1" SEQUENCE="false" COMMENT="Mode of the overall feedback support."/>
<FIELD NAME="overallfeedbackfiles" TYPE="int" LENGTH="3" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Number of allowed attachments to the overall feedback."/>
<FIELD NAME="overallfeedbackfiletypes" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="comma separated liat of file extensions"/>
<FIELD NAME="overallfeedbackmaxbytes" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="100000" SEQUENCE="false" COMMENT="Maximum size of one file attached to the overall feedback."/>
</FIELDS>
<KEYS>

View File

@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
* @return bool result
*/
function xmldb_workshop_upgrade($oldversion) {
global $CFG;
global $CFG, $DB;
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
@ -46,5 +46,30 @@ function xmldb_workshop_upgrade($oldversion) {
// Moodle v3.0.0 release upgrade line.
// Put any upgrade step following this.
$dbman = $DB->get_manager();
if ($oldversion < 2016022200) {
// Define field submissionfiletypes to be added to workshop.
$table = new xmldb_table('workshop');
$field = new xmldb_field('submissionfiletypes', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'nattachments');
// Conditionally launch add field submissionfiletypes.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field overallfeedbackfiletypes to be added to workshop.
$field = new xmldb_field('overallfeedbackfiletypes',
XMLDB_TYPE_CHAR, '255', null, null, null, null, 'overallfeedbackfiles');
// Conditionally launch add field overallfeedbackfiletypes.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Workshop savepoint reached.
upgrade_mod_savepoint(true, 2016022200, 'workshop');
}
return true;
}

View File

@ -121,6 +121,7 @@ if ($edit and $canmanage) {
'trusttext' => true,
'subdirs' => false,
'maxfiles' => $maxfiles,
'filetypes' => $workshop->submissionfiletypes,
'maxbytes' => $maxbytes,
'context' => $workshop->context
);

View File

@ -120,4 +120,52 @@ class workshop_assessment_form extends moodleform {
public function is_editable() {
return !$this->_form->isFrozen();
}
/**
* Validate incoming data.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (isset($data['feedbackauthorattachment_filemanager'])) {
$draftitemid = $data['feedbackauthorattachment_filemanager'];
// If we have draft files, then make sure they are the correct ones.
if ($draftfiles = file_get_drafarea_files($draftitemid)) {
if (!$validfileextensions = workshop::get_array_of_file_extensions($this->workshop->overallfeedbackfiletypes)) {
return $errors;
}
$wrongfileextensions = null;
$bigfiles = null;
// Check the size of each file.
foreach ($draftfiles->list as $file) {
$a = new stdClass();
$a->maxbytes = $this->workshop->overallfeedbackmaxbytes;
$a->currentbytes = $file->size;
$a->filename = $file->filename;
$a->validfileextensions = implode(',', $validfileextensions);
// Check whether the extension of uploaded file is in the list.
$thisextension = substr(strrchr($file->filename, '.'), 1);
if (!in_array($thisextension, $validfileextensions)) {
$wrongfileextensions .= get_string('err_wrongfileextension', 'workshop', $a) . '<br/>';
}
if ($file->size > $this->workshop->overallfeedbackmaxbytes) {
$bigfiles .= get_string('err_maxbytes', 'workshop', $a) . '<br/>';
}
}
if ($bigfiles || $wrongfileextensions) {
$errors['feedbackauthorattachment_filemanager'] = $bigfiles . $wrongfileextensions;
}
}
}
return $errors;
}
}

View File

@ -30,6 +30,10 @@ $string['allocation'] = 'Submission allocation';
$string['allocationdone'] = 'Allocation done';
$string['allocationerror'] = 'Allocation error';
$string['allocationconfigured'] = 'Allocation configured';
$string['allowedfiletypesforoverallfeedback'] = 'Feedback attachment allowed file types';
$string['allowedfiletypesforoverallfeedback_help'] = 'Feedback attachment allowed file types can be restricted by entering a comma-separated list of file extensions, for example \'mp4, mp3, jpg, jpeg\'. If the field is left empty, then all file types are allowed.';
$string['allowedfiletypesforsubmission'] = 'Submission attachment allowed file types';
$string['allowedfiletypesforsubmission_help'] = 'Submission attachment allowed file types can be restricted by entering a comma-separated list of file extensions, for example \'mp4, mp3, jpg, jpeg\'. If the field is left empty, then all file types are allowed.';
$string['allsubmissions'] = 'All submissions ({$a})';
$string['alreadygraded'] = 'Already graded';
$string['areaconclusion'] = 'Conclusion text';
@ -100,6 +104,9 @@ $string['editingsubmission'] = 'Editing submission';
$string['editsubmission'] = 'Edit submission';
$string['err_multiplesubmissions'] = 'While editing this form, another version of the submission has been saved. Multiple submissions per user are not allowed.';
$string['err_removegrademappings'] = 'Unable to remove the unused grade mappings';
$string['err_maxbytes'] = 'The attachment "{$a->filename} ({$a->currentbytes} bytes)" exeeds the maximum allowed file size ({$a->maxbytes} bytes)';
$string['err_notallowedfiletype'] = 'The file extension "{$a}" is not allowed';
$string['err_wrongfileextension'] = 'The file "{$a->filename}" cannot be uploaded, only file types "{$a->validfileextensions}" are allowed';
$string['evaluategradeswait'] = 'Please wait until the assessments are evaluated and the grades are calculated';
$string['evaluation'] = 'Grading evaluation';
$string['evaluationmethod'] = 'Grading evaluation method';

View File

@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@ -124,6 +123,9 @@ class workshop {
/** @var int number of allowed submission attachments and the files embedded into submission */
public $nattachments;
/** @var string list of allowed file types that are allowed to be embedded into submission */
public $submissionfiletypes = null;
/** @var bool allow submitting the work after the deadline */
public $latesubmissions;
@ -160,6 +162,9 @@ class workshop {
/** @var int maximum number of overall feedback attachments */
public $overallfeedbackfiles;
/** @var string list of allowed file types that can be attached to the overall feedback */
public $overallfeedbackfiletypes = null;
/** @var int maximum size of one file attached to the overall feedback */
public $overallfeedbackmaxbytes;
@ -412,6 +417,40 @@ class workshop {
return $a;
}
/**
* Split a list of file extensions to an array
* @param string $listofextensions
* @return array of extensions
*/
public static function get_array_of_file_extensions($listofextensions) {
return preg_split("/[\s,;:\"']+/", strtolower($listofextensions), null, PREG_SPLIT_NO_EMPTY);
}
/**
* Check allowed file types and return an error when invalid file extensions found in the list
*
* @param string $extensionlist
*/
public static function check_allowed_file_types($extensionlist) {
if (!$extensionlist) {
return '';
}
if ($extensions = self::get_array_of_file_extensions($extensionlist)) {
$coreextensions = array_keys(get_mimetypes_array());
foreach ($extensions as $ext) {
$ext = ltrim($ext, '.');
if (!$ext) {
continue;
}
// Use strtolower(), because all extensions are in lower case.
if (!in_array($ext, $coreextensions)) {
return get_string('err_notallowedfiletype', 'workshop', $ext);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Workshop API //
////////////////////////////////////////////////////////////////////////////////
@ -2371,6 +2410,7 @@ class workshop {
return array(
'subdirs' => 0,
'maxbytes' => $this->overallfeedbackmaxbytes,
'filetypes' => $this->overallfeedbackfiletypes,
'maxfiles' => $this->overallfeedbackfiles,
'changeformat' => 1,
'context' => $this->context,
@ -2386,6 +2426,7 @@ class workshop {
return array(
'subdirs' => 1,
'maxbytes' => $this->overallfeedbackmaxbytes,
'filetypes' => $this->overallfeedbackfiletypes,
'maxfiles' => $this->overallfeedbackfiles,
'return_types' => FILE_INTERNAL,
);

View File

@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@ -65,7 +64,7 @@ class mod_workshop_mod_form extends moodleform_mod {
// Workshop name
$label = get_string('workshopname', 'workshop');
$mform->addElement('text', 'name', $label, array('size'=>'64'));
$mform->addElement('text', 'name', $label, array('size' => '64'));
if (!empty($CFG->formatstringstriptags)) {
$mform->setType('name', PARAM_TEXT);
} else {
@ -118,7 +117,7 @@ class mod_workshop_mod_form extends moodleform_mod {
$mform->addRule('gradinggradepass', null, 'numeric', null, 'client');
$options = array();
for ($i=5; $i>=0; $i--) {
for ($i = 5; $i >= 0; $i--) {
$options[$i] = $i;
}
$label = get_string('gradedecimals', 'workshop');
@ -133,16 +132,24 @@ class mod_workshop_mod_form extends moodleform_mod {
workshop::instruction_editors_options($this->context));
$options = array();
for ($i=7; $i>=0; $i--) {
for ($i = 7; $i >= 0; $i--) {
$options[$i] = $i;
}
$label = get_string('nattachments', 'workshop');
$mform->addElement('select', 'nattachments', $label, $options);
$mform->setDefault('nattachments', 1);
$label = get_string('allowedfiletypesforsubmission', 'workshop');
$mform->addElement('text', 'submissionfiletypes', $label, array('maxlength' => 255, 'size' => 64));
$mform->addHelpButton('submissionfiletypes', 'allowedfiletypesforsubmission', 'workshop');
$mform->setType('submissionfiletypes', PARAM_TEXT);
$mform->addRule('submissionfiletypes', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->disabledIf('submissionfiletypes', 'nattachments', 'eq', 0);
$options = get_max_upload_sizes($CFG->maxbytes, $this->course->maxbytes, 0, $workshopconfig->maxbytes);
$mform->addElement('select', 'maxbytes', get_string('maxbytes', 'workshop'), $options);
$mform->setDefault('maxbytes', $workshopconfig->maxbytes);
$mform->disabledIf('maxbytes', 'nattachments', 'eq', 0);
$label = get_string('latesubmissions', 'workshop');
$text = get_string('latesubmissions_desc', 'workshop');
@ -179,6 +186,13 @@ class mod_workshop_mod_form extends moodleform_mod {
$mform->setDefault('overallfeedbackfiles', 0);
$mform->disabledIf('overallfeedbackfiles', 'overallfeedbackmode', 'eq', 0);
$label = get_string('allowedfiletypesforoverallfeedback', 'workshop');
$mform->addElement('text', 'overallfeedbackfiletypes', $label, array('maxlength' => 255, 'size' => 64));
$mform->addHelpButton('overallfeedbackfiletypes', 'allowedfiletypesforoverallfeedback', 'workshop');
$mform->setType('overallfeedbackfiletypes', PARAM_TEXT);
$mform->addRule('overallfeedbackfiletypes', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->disabledIf('overallfeedbackfiletypes', 'overallfeedbackfiles', 'eq', 0);
$options = get_max_upload_sizes($CFG->maxbytes, $this->course->maxbytes);
$mform->addElement('select', 'overallfeedbackmaxbytes', get_string('overallfeedbackmaxbytes', 'workshop'), $options);
$mform->setDefault('overallfeedbackmaxbytes', $workshopconfig->maxbytes);
@ -229,7 +243,7 @@ class mod_workshop_mod_form extends moodleform_mod {
// Common module settings, Restrict availability, Activity completion etc. ----
$features = array('groups' => true, 'groupings' => true,
'outcomes'=>true, 'gradecat'=>false, 'idnumber'=>false);
'outcomes' => true, 'gradecat' => false, 'idnumber' => false);
$this->standard_coursemodule_elements();
@ -347,6 +361,22 @@ class mod_workshop_mod_form extends moodleform_mod {
public function validation($data, $files) {
$errors = parent::validation($data, $files);
// Check the input of submissionfiletypes field.
if ($data['submissionfiletypes']) {
$invalidextension = workshop::check_allowed_file_types($data['submissionfiletypes']);
if ($invalidextension) {
$errors['submissionfiletypes'] = $invalidextension;
}
}
// Check the input of overallfeedbackfiletypes field.
if ($data['overallfeedbackfiletypes']) {
$invalidextension = workshop::check_allowed_file_types($data['overallfeedbackfiletypes']);
if ($invalidextension) {
$errors['overallfeedbackfiletypes'] = $invalidextension;
}
}
// check the phases borders are valid
if ($data['submissionstart'] > 0 and $data['submissionend'] > 0 and $data['submissionstart'] >= $data['submissionend']) {
$errors['submissionend'] = get_string('submissionendbeforestart', 'mod_workshop');

View File

@ -184,17 +184,20 @@ if ($edit) {
require_once(dirname(__FILE__).'/submission_form.php');
$maxfiles = $workshop->nattachments;
$filetypes = $workshop->submissionfiletypes;
$maxbytes = $workshop->maxbytes;
$contentopts = array(
'trusttext' => true,
'subdirs' => false,
'maxfiles' => $maxfiles,
'filetypes' => $filetypes,
'maxbytes' => $maxbytes,
'context' => $workshop->context,
'return_types' => FILE_INTERNAL | FILE_EXTERNAL
);
$attachmentopts = array('subdirs' => true, 'maxfiles' => $maxfiles, 'maxbytes' => $maxbytes, 'return_types' => FILE_INTERNAL);
$attachmentopts = array('subdirs' => true, 'maxfiles' => $maxfiles, 'filetypes' => $filetypes, 'maxbytes' => $maxbytes,
'return_types' => FILE_INTERNAL);
$submission = file_prepare_standard_editor($submission, 'content', $contentopts, $workshop->context,
'mod_workshop', 'submission_content', $submission->id);
$submission = file_prepare_standard_filemanager($submission, 'attachment', $attachmentopts, $workshop->context,

View File

@ -37,6 +37,8 @@ class workshop_submission_form extends moodleform {
$contentopts = $this->_customdata['contentopts'];
$attachmentopts = $this->_customdata['attachmentopts'];
$this->attachmentopts = $attachmentopts;
$mform->addElement('header', 'general', get_string('submission', 'workshop'));
$mform->addElement('text', 'title', get_string('submissiontitle', 'workshop'));
@ -88,6 +90,42 @@ class workshop_submission_form extends moodleform {
}
}
if (isset ($data['attachment_filemanager'])) {
$draftitemid = $data['attachment_filemanager'];
// If we have draft files, then make sure they are the correct ones.
if ($draftfiles = file_get_drafarea_files($draftitemid)) {
if (!$validfileextensions = workshop::get_array_of_file_extensions($this->attachmentopts['filetypes'])) {
return $errors;
}
$wrongfileextensions = null;
$bigfiles = null;
// Check the size and type of each file.
foreach ($draftfiles->list as $file) {
$a = new stdClass();
$a->maxbytes = $this->attachmentopts['maxbytes'];
$a->currentbytes = $file->size;
$a->filename = $file->filename;
$a->validfileextensions = implode(',', $validfileextensions);
// Check whether the extension of uploaded file is in the list.
$thisextension = substr(strrchr($file->filename, '.'), 1);
if (!in_array($thisextension, $validfileextensions)) {
$wrongfileextensions .= get_string('err_wrongfileextension', 'workshop', $a) . '<br/>';
}
// Check whether the file size exceeds the maximum submission attachment size.
if ($file->size > $this->attachmentopts['maxbytes']) {
$bigfiles .= get_string('err_maxbytes', 'workshop', $a) . '<br/>';
}
}
if ($bigfiles || $wrongfileextensions) {
$errors['attachment_filemanager'] = $bigfiles . $wrongfileextensions;
}
}
}
return $errors;
}
}

View File

@ -48,11 +48,13 @@ class mod_workshop_generator extends testing_module_generator {
'gradinggrade' => $workshopconfig->gradinggrade,
'gradedecimals' => $workshopconfig->gradedecimals,
'nattachments' => 1,
'submissionfiletypes' => null,
'maxbytes' => $workshopconfig->maxbytes,
'latesubmissions' => 0,
'useselfassessment' => 0,
'overallfeedbackmode' => 1,
'overallfeedbackfiles' => 0,
'overallfeedbackfiletypes' => null,
'overallfeedbackmaxbytes' => $workshopconfig->maxbytes,
'useexamples' => 0,
'examplesmode' => $workshopconfig->examplesmode,

View File

@ -622,4 +622,92 @@ class mod_workshop_internal_api_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('workshop_submissions', array('workshopid' => $this->workshop->id)));
$this->assertEquals(0, $DB->count_records('workshop_assessments'));
}
/**
* Test converting the string to array.
*/
public function test_get_array_of_file_extensions() {
$this->resetAfterTest(true);
$listofextensions = 'doc, jpg, mp3';
$actual = workshop::get_array_of_file_extensions($listofextensions);
$expected = array('doc', 'jpg', 'mp3');
$this->assertEquals($expected, $actual);
$listofextensions = 'mp4,; docx,; gif';
$actual = workshop::get_array_of_file_extensions($listofextensions);
$expected = array('mp4', 'docx', 'gif');
$this->assertEquals($expected, $actual);
$listofextensions = 'mp4 docx gif';
$actual = workshop::get_array_of_file_extensions($listofextensions);
$expected = array('mp4', 'docx', 'gif');
$this->assertEquals($expected, $actual);
$listofextensions = 'MP4 DOCx Gif';
$actual = workshop::get_array_of_file_extensions($listofextensions);
$expected = array('mp4', 'docx', 'gif');
$this->assertEquals($expected, $actual);
$listofextensions = '.doc; .jpg; Mp4 "mp3"';
$actual = workshop::get_array_of_file_extensions($listofextensions);
$expected = array('.doc', '.jpg', 'mp4', 'mp3');
$this->assertEquals($expected, $actual);
$listofextensions = '.doc,;.jpg; .Mp3, ".Avi"';
$actual = workshop::get_array_of_file_extensions($listofextensions);
$expected = array('.doc', '.jpg', '.mp3', '.avi');
$this->assertEquals($expected, $actual);
}
/**
* Test the list of allowed file extensions.
*/
public function test_check_allowed_file_types() {
$this->resetAfterTest(true);
// Valid file extensions.
$listofextensions = '';
$expected = '';
// The function returns '' when file extensions are valid or the input field is empty.
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
$listofextensions = 'doc, jpg, mp3';
$expected = '';
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
$listofextensions = 'doc; ".jpg"; mp4 ...mp3';
$expected = '';
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
// Error handling.
$listofextensions = 'doc.jpg .mp3 .avi';
$expected = get_string('err_notallowedfiletype', 'workshop', 'doc.jpg');
// The function returns and error on the form-field: 'The file extension "doc.jpg" is not allowed'.
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
$listofextensions = 'doc, jpg, mp3, unusual';
$expected = get_string('err_notallowedfiletype', 'workshop', 'unusual');
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
$listofextensions = 'doc,; unusual1, unusual2';
$expected = get_string('err_notallowedfiletype', 'workshop', 'unusual1');
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
$listofextensions = 'unusual1,; unsusual2, doc, jpg';
$expected = get_string('err_notallowedfiletype', 'workshop', 'unusual1');
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
$listofextensions = 'unusual1; unusual2; mp4';
$expected = get_string('err_notallowedfiletype', 'workshop', 'unusual1');
$actual = workshop::check_allowed_file_types($listofextensions);
$this->assertEquals($expected, $actual);
}
}

View File

@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2016012101; // The current module version (YYYYMMDDXX)
$plugin->version = 2016022200; // The current module version (YYYYMMDDXX)
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'mod_workshop';
$plugin->cron = 60; // Give as a chance every minute.