<PHP_EXTENSION name="json" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="hash" level="required"/>
+ <PHP_EXTENSION name="fileinfo" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
$mform->addElement('checkbox', 'badges', '',
" " . get_string('badgesnumber', 'hub', $badges));
$mform->setDefault('badges', $badgesnumber != -1);
- $mform->setType('resources', PARAM_INT);
+ $mform->setType('badges', PARAM_INT);
$mform->addElement('checkbox', 'issuedbadges', '',
" " . get_string('issuedbadgesnumber', 'hub', $issuedbadges));
$mform->setDefault('issuedbadges', $issuedbadgesnumber != -1);
- $mform->setType('resources', PARAM_INT);
+ $mform->setType('issuedbadges', PARAM_INT);
$mform->addElement('checkbox', 'participantnumberaverage', '',
" " . get_string('participantnumberaverage', 'hub', $participantnumberaverage));
* autostart
* canpost
* cid
- * collapsediconurl
+ * collapsediconkey
* commentarea
* component
* contextid
$string['configuredstatus'] = 'Configured';
$string['connectsystemaccount'] = 'Connect to a system account';
$string['createfromtemplate'] = 'Create an OAuth 2 service from a template';
-$string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service template below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, but you will still need to enter the client ID and secret for the new service before it can be used.';
+$string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service templates below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, though you will still need to enter the client ID and secret for the new service before it can be used.';
$string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"';
$string['createnewfacebookissuer'] = 'Create new Facebook service';
$string['createnewgoogleissuer'] = 'Create new Google service';
$string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.';
$string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.';
$string['deleteuserfieldmappingconfirm'] = 'Are you sure you want to delete the user field mapping for issuer "{$a}"?';
-$string['discovered_help'] = 'Discovery means that the OAuth2 endpoints could be automatically determined from the base url for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.';
+$string['discovered_help'] = 'Discovery means that the OAuth 2 endpoints could be automatically determined from the base URL for the OAuth service. Not all services are required to be "discovered", but if they are not, then the endpoints and user mapping information will need to be entered manually.';
$string['discovered'] = 'Service discovery successful';
$string['discoverystatus'] = 'Discovery';
$string['editendpoint'] = 'Edit endpoint: {$a->endpoint} for issuer {$a->issuer}';
$string['endpointname'] = 'Name';
$string['endpointsforissuer'] = 'Endpoints for issuer: {$a}';
$string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.';
-$string['endpointurl'] = 'Url';
-$string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 Services';
+$string['endpointurl'] = 'URL';
+$string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 services';
$string['issuersetuptype'] = 'Detailed instructions on setting up the {$a} OAuth 2 provider';
$string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.';
$string['issueralloweddomains_link'] = 'OAuth_2_login_domains';
$string['issueralloweddomains'] = 'Login domains';
-$string['issuerbaseurl_help'] = 'Base url used to access the service.';
-$string['issuerbaseurl'] = 'Service base url';
-$string['issuerclientid'] = 'Client Id';
+$string['issuerbaseurl_help'] = 'Base URL used to access the service.';
+$string['issuerbaseurl'] = 'Service base URL';
+$string['issuerclientid'] = 'Client ID';
$string['issuerclientid_help'] = 'The OAuth client ID for this issuer.';
-$string['issuerclientsecret'] = 'Client Secret';
+$string['issuerclientsecret'] = 'Client secret';
$string['issuerclientsecret_help'] = 'The OAuth client secret for this issuer.';
$string['issuerdeleted'] = 'Identity issuer deleted';
$string['issuerdisabled'] = 'Identity issuer disabled';
$string['issuerenabled'] = 'Identity issuer enabled';
-$string['issuerimage_help'] = 'An image url used to show a logo for this issuer. May be displayed on login page.';
+$string['issuerimage_help'] = 'An image URL used to show a logo for this issuer. May be displayed on login page.';
$string['issuerimage'] = 'Logo URL';
$string['issuerloginparams'] = 'Additional parameters included in a login request.';
-$string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the users basic profile.';
+$string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the user\'s basic profile.';
$string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.';
$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.';
$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the users basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".';
$string['issuerloginscopes'] = 'Scopes included in a login request.';
$string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.';
$string['issuername'] = 'Name';
-$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.';
+$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to log in with accounts from this issuer.';
$string['issuershowonloginpage'] = 'Show on login page.';
$string['issuers'] = 'Issuers';
$string['loginissuer'] = 'Allow login';
$string['notconfigured'] = 'Not configured';
$string['notdiscovered'] = 'Service discovery not successful';
$string['notloginissuer'] = 'Do not allow login';
-$string['pluginname'] = 'OAuth 2 Services';
+$string['pluginname'] = 'OAuth 2 services';
$string['savechanges'] = 'Save changes';
$string['serviceshelp'] = 'Service provider setup instructions.';
$string['systemaccountconnected_help'] = 'System accounts are used to provide advanced functionality for plugins. They are not required for login functionality only, but other plugins using the OAuth service may offer a reduced set of features if the system account has not been connected. For example repositories cannot support "controlled links" without a system account to perform file operations.';
// Boost - administrator tour.
$string['tour1_title_welcome'] = 'Welcome';
-$string['tour1_content_welcome'] = 'Welcome to the Boost theme for Moodle 3.2. If you\'ve used Moodle before you might find some things look a bit different.';
+$string['tour1_content_welcome'] = 'Welcome to the Boost theme. If you\'ve upgraded from an earlier version, you might find some things look a bit different with this theme.';
$string['tour1_title_navigation'] = 'Navigation';
$string['tour1_content_navigation'] = 'Major navigation is now through this nav drawer. The contents update depending on where you are in the site. Use the button at the top to hide or show it.';
$string['tour1_title_customisation'] = 'Customisation';
$string['tour1_title_addingblocks'] = 'Adding blocks';
$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
$string['tour1_title_end'] = 'End of tour';
-$string['tour1_content_end'] = 'This has been a user tour, a new feature in Moodle 3.2. It won\'t show again unless you reset it using the link in the footer. As an admin you can also create your own tours like this!';
+$string['tour1_content_end'] = 'This is the end of your user tour. It won\'t show again unless you reset it using the link in the footer. As an admin you can also create your own tours like this!';
// Boost - course view tour.
$string['tour2_title_welcome'] = 'Welcome';
-$string['tour2_content_welcome'] = 'Welcome to the Boost theme for Moodle 3.2. If you\'ve used Moodle before you might find things look a bit different here on the course page.';
+$string['tour2_content_welcome'] = 'Welcome to the Boost theme. If your site has been upgraded from an earlier version, you might find things look a bit different here on the course page.';
$string['tour2_title_customisation'] = 'Customisation';
$string['tour2_content_customisation'] = 'To change any course settings, use the settings menu in the corner of this header. You will find a similar settings menu on the home page of every activity, too. Try turning editing on right now.';
$string['tour2_title_navigation'] = 'Navigation';
$string['tour2_title_addingblocks'] = 'Adding blocks';
$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
$string['tour2_title_end'] = 'End of tour';
-$string['tour2_content_end'] = 'This has been a user tour, a new feature in Moodle 3.2. It won\'t show again unless you reset it using the link in the footer. The site admin can also create further tours for this site if required.';
+$string['tour2_content_end'] = 'This is the end of your user tour. It won\'t show again unless you reset it using the link in the footer. The site admin can also create further tours for this site if required.';
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/cas to auth_cas.
- $DB->set_field('config_plugins', 'plugin', 'auth_cas', array('plugin' => 'auth/cas'));
+ upgrade_fix_config_auth_plugin_names('cas');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'cas');
}
if ($oldversion < 2017032800) {
// Convert info in config plugins from auth/db to auth_db
- $DB->set_field('config_plugins', 'plugin', 'auth_db', array('plugin' => 'auth/db'));
+ upgrade_fix_config_auth_plugin_names('db');
upgrade_plugin_savepoint(true, 2017032800, 'auth', 'db');
}
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/email to auth_email.
- $DB->set_field('config_plugins', 'plugin', 'auth_email', array('plugin' => 'auth/email'));
+ upgrade_fix_config_auth_plugin_names('email');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'email');
}
return true;
}
-
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/fc to auth_fc.
- $DB->set_field('config_plugins', 'plugin', 'auth_fc', array('plugin' => 'auth/fc'));
+ upgrade_fix_config_auth_plugin_names('fc');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'fc');
}
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/imap to auth_imap.
- $DB->set_field('config_plugins', 'plugin', 'auth_imap', array('plugin' => 'auth/imap'));
+ upgrade_fix_config_auth_plugin_names('imap');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'imap');
}
return true;
}
-
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/ldap to auth_ldap.
- $DB->set_field('config_plugins', 'plugin', 'auth_ldap', array('plugin' => 'auth/ldap'));
+ upgrade_fix_config_auth_plugin_names('ldap');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'ldap');
}
$string['auth_ldap_passtype_key'] = 'Password format';
$string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiration settings';
$string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.';
-$string['auth_ldap_preventpassindb_key'] = 'Don\'t cache passwords';
+$string['auth_ldap_preventpassindb_key'] = 'Prevent password caching';
$string['auth_ldap_search_sub'] = 'Search users from subcontexts.';
$string['auth_ldap_search_sub_key'] = 'Search subcontexts';
$string['auth_ldap_server_settings'] = 'LDAP server settings';
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/manual to auth_manual.
- $DB->set_field('config_plugins', 'plugin', 'auth_manual', array('plugin' => 'auth/manual'));
+ upgrade_fix_config_auth_plugin_names('manual');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'manual');
}
// Put any upgrade step following this.
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/mnet to auth_mnet.
- $DB->set_field('config_plugins', 'plugin', 'auth_mnet', array('plugin' => 'auth/mnet'));
+ upgrade_fix_config_auth_plugin_names('mnet');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'mnet');
}
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/nntp to auth_nntp.
- $DB->set_field('config_plugins', 'plugin', 'auth_nntp', array('plugin' => 'auth/nntp'));
+ upgrade_fix_config_auth_plugin_names('nntp');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'nntp');
}
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/none to auth_none.
- $DB->set_field('config_plugins', 'plugin', 'auth_none', array('plugin' => 'auth/none'));
+ upgrade_fix_config_auth_plugin_names('none');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'none');
}
return true;
}
-
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$string['accountexists'] = 'A user already exists on this site with this username. If this is your account, login manually and link this link from your preferences page.';
+$string['accountexists'] = 'A user already exists on this site with this username. If this is your account, log in by entering your username and password and add it as a linked login via your preferences page.';
$string['auth_oauth2description'] = 'OAuth 2 standards based authentication';
$string['auth_oauth2settings'] = 'OAuth 2 authentication settings.';
$string['confirmaccountemail'] = 'Hi {$a->fullname},
$string['createnewlinkedlogin'] = 'Link a new account ({$a})';
$string['emailconfirmlink'] = 'Link your accounts';
$string['emailconfirmlinksent'] = '<p>An existing account was found with this email address but it is not linked yet.</p>
- <p>The accounts must be linked before you can login.</p>
- <p>An email should have been sent to your address at <b>{$a}</b></p>
+ <p>The accounts must be linked before you can log in.</p>
+ <p>An email should have been sent to your address at <b>{$a}</b>.</p>
<p>It contains easy instructions to link your accounts.</p>
- <p>If you continue to have difficulty, contact the site administrator.</p>';
+ <p>If you have any difficulty, contact the site administrator.</p>';
$string['info'] = 'External account';
$string['issuer'] = 'OAuth 2 Service';
$string['linkedlogins'] = 'Linked logins';
-$string['linkedloginshelp'] = 'Help with linked logins.';
+$string['linkedloginshelp'] = 'Help with linked logins';
$string['loginerror_userincomplete'] = 'The user information returned did not contain a username and email address. The OAuth 2 service may be configured incorrectly.';
$string['loginerror_nouserinfo'] = 'No user information was returned. The OAuth 2 service may be configured incorrectly.';
$string['loginerror_invaliddomain'] = 'The email address is not allowed at this site.';
$string['notloggedindebug'] = 'The login attempt failed. Reason: {$a}';
$string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.';
$string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts';
-$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.';
+$string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.';
$string['pluginname'] = 'OAuth 2';
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/pam to auth_pam.
- $DB->set_field('config_plugins', 'plugin', 'auth_pam', array('plugin' => 'auth/pam'));
+ upgrade_fix_config_auth_plugin_names('pam');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'pam');
}
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/pop3 to auth_pop3.
- $DB->set_field('config_plugins', 'plugin', 'auth_pop3', array('plugin' => 'auth/pop3'));
+ upgrade_fix_config_auth_plugin_names('pop3');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'pop3');
}
if ($oldversion < 2017020700) {
// Convert info in config plugins from auth/shibboleth to auth_shibboleth.
- $DB->set_field('config_plugins', 'plugin', 'auth_shibboleth', array('plugin' => 'auth/shibboleth'));
+ upgrade_fix_config_auth_plugin_names('shibboleth');
upgrade_plugin_savepoint(true, 2017020700, 'auth', 'shibboleth');
}
* Authentication plugins have been migrated to use the admin settings API.
Plugins should use a settings.php file to manage configurations rather than using the config.html files.
+ See how the helper function upgrade_fix_config_auth_plugin_names() can be used to convert the legacy settings to the
+ new ones.
* The function 'print_auth_lock_options' has been replaced by 'display_auth_lock_options' which uses the admin settings API.
See auth_manual as an exmple of how it can be used. More information can be found in MDL-12689.
* The list of supported identity providers (SSO IdP) returned by the 'loginpage_idp_list' method (used to render the
protected $visibility; // visibility of the setting (setting_base::VISIBLE/setting_base::HIDDEN)
protected $status; // setting_base::NOT_LOCKED/setting_base::LOCKED_BY_PERMISSION...
+ /** @var setting_dependency[] */
protected $dependencies = array(); // array of dependent (observer) objects (usually setting_base ones)
protected $dependenton = array();
public function set_status($status) {
$status = $this->validate_status($status);
+ if (($this->status == base_setting::LOCKED_BY_PERMISSION || $this->status == base_setting::LOCKED_BY_CONFIG)
+ && $status == base_setting::LOCKED_BY_HIERARCHY) {
+ // Lock by permission or config can not be overriden by lock by hierarchy.
+ return;
+ }
+
// If the setting is being unlocked first check whether an other settings
// this setting is dependent on are locked. If they are then we still don't
// want to lock this setting.
* @return bool
*/
protected function process_value_change($oldvalue) {
+ if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_PERMISSION ||
+ $this->dependentsetting->get_status() == base_setting::LOCKED_BY_CONFIG) {
+ // When setting is locked by permission or config do not apply dependencies.
+ return false;
+ }
$prevalue = $this->dependentsetting->get_value();
// If the setting is the desired value enact the dependency
if ($this->setting->get_value() == $this->value) {
$string['sortbydates'] = 'Sort by dates';
$string['timeline'] = 'Timeline';
$string['viewcourse'] = 'View course';
+$string['viewcoursename'] = 'View course {$a}';
</button>
</div>
</div>
- <div class="hidden text-xs-center text-center" data-region="empty-message">
+ <div class="hidden text-xs-center text-center m-y-3" data-region="empty-message">
<img class="empty-placeholder-image-sm"
src="{{urls.noevents}}"
- alt="{{#str}} noevents, block_myoverview {{/str}}">
+ alt="{{#str}} noevents, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
- <a href="{{viewurl}}" class="btn btn-secondary text-primary">
+ <a href="{{viewurl}}" class="btn btn-secondary text-primary"
+ aria-label="{{#str}} viewcoursename, block_myoverview, {{fullnamedisplay}} {{/str}}">
{{#str}} viewcourse, block_myoverview {{/str}}
</a>
</div>
<div class="text-xs-center text-center m-t-3">
<img class="empty-placeholder-image-lg"
src="{{urls.nocourses}}"
- alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+ alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
</div>
{{/inprogress}}
<div class="text-xs-center text-center m-t-3">
<img class="empty-placeholder-image-lg"
src="{{urls.nocourses}}"
- alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}">
+ alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} nocoursesfuture, block_myoverview {{/str}}</p>
</div>
{{/future}}
<div class="text-xs-center text-center m-t-3">
<img class="empty-placeholder-image-lg"
src="{{urls.nocourses}}"
- alt="{{#str}} nocoursespast, block_myoverview {{/str}}">
+ alt="{{#str}} nocoursespast, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} nocoursespast, block_myoverview {{/str}}</p>
</div>
{{/past}}
<div class="text-xs-center text-center m-t-3">
<img class="empty-placeholder-image-lg"
src="{{urls.nocourses}}"
- alt="{{#str}} nocourses, block_myoverview {{/str}}">
+ alt="{{#str}} nocourses, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} nocourses, block_myoverview {{/str}}</p>
</div>
{{/hascourses}}
<div class="hidden text-xs-center text-center m-t-3" data-region="empty-message">
<img class="empty-placeholder-image-lg"
src="{{urls.noevents}}"
- alt="{{#str}} noevents, block_myoverview {{/str}}">
+ alt="{{#str}} noevents, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
</div>
</div>
</a>
</li>
</ul>
- <div class="tab-content">
+ <div class="tab-content content-centred">
<div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
{{> block_myoverview/timeline-view }}
</div>
<div class="progress-indicator">
<svg xmlns="http://www.w3.org/2000/svg">
<g>
- <title>{{progress}}%</title>
+ <title aria-hidden="true">{{progress}}%</title>
<circle class="circle percent-{{progress}}"
r="27.5"
cx="35"
<div class="text-xs-center text-center m-t-3">
<img class="empty-placeholder-image-lg"
src="{{urls.noevents}}"
- alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+ alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
</div>
{{/haspages}}
<div class="text-xs-center text-center m-t-3">
<img class="empty-placeholder-image-lg"
src="{{urls.noevents}}"
- alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+ alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+ role="presentation">
<p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
</div>
{{/inprogress}}
$external = new stdClass();
-// Check that this id exists.
-if (!empty($id) && !$DB->record_exists('blog_external', array('id' => $id))) {
- print_error('wrongexternalid', 'blog');
-} else if (!empty($id)) {
- $external = $DB->get_record('blog_external', array('id' => $id));
+// Retrieve the external blog record.
+if (!empty($id)) {
+ if (!$external = $DB->get_record('blog_external', array('id' => $id, 'userid' => $USER->id))) {
+ print_error('wrongexternalid', 'blog');
+ }
$external->autotags = core_tag_tag::get_item_tags_array('core', 'blog_external', $id);
}
}
// Append Search info.
- if (!empty($search)) {
+ if (!empty($search) && has_capability('moodle/blog:search', $sitecontext)) {
$headers['filters']['search'] = $search;
$blogurl->param('search', $search);
$PAGE->navbar->add(get_string('searchterm', 'blog', $search), $blogurl->out());
$version = phpversion('memcached');
$this->candeletemulti = ($version && version_compare($version, self::REQUIRED_VERSION, '>='));
- // Test the connection to the main connection.
- $this->isready = @$this->connection->set("ping", 'ping', 1);
+ $this->isready = $this->is_connection_ready();
+ }
+
+ /**
+ * Confirm whether the connection is ready and usable.
+ *
+ * @return boolean
+ */
+ public function is_connection_ready() {
+ if (!@$this->connection->set("ping", 'ping', 1)) {
+ // Test the connection to the server.
+ return false;
+ }
+
+ if ($this->isshared) {
+ // There is a bug in libmemcached which means that it is not possible to purge the cache in a shared cache
+ // configuration.
+ // This issue currently affects:
+ // - memcached 1.4.23+ with php-memcached <= 2.2.0
+ // The following combinations are not affected:
+ // - memcached <= 1.4.22 with any version of php-memcached
+ // - any version of memcached with php-memcached >= 3.0.1
+
+
+ // This check is cheapest as it does not involve connecting to the server at all.
+ $safecombination = false;
+ $extension = new ReflectionExtension('memcached');
+ if ((version_compare($extension->getVersion(), '3.0.1') >= 0)) {
+ // This is php-memcached version >= 3.0.1 which is a safe combination.
+ $safecombination = true;
+ }
+
+ if (!$safecombination) {
+ $allsafe = true;
+ foreach ($this->connection->getVersion() as $version) {
+ $allsafe = $allsafe && (version_compare($version, '1.4.22') <= 0);
+ }
+ // All memcached servers connected are version <= 1.4.22 which is a safe combination.
+ $safecombination = $allsafe;
+ }
+
+ if (!$safecombination) {
+ // This is memcached 1.4.23+ and php-memcached < 3.0.1.
+ // The issue may have been resolved in a subsequent update to any of the three libraries.
+ // The only way to safely determine if the combination is safe is to call getAllKeys.
+ // A safe combination will return an array, whilst an affected combination will return false.
+ // This is the most expensive check.
+ if (!is_array($this->connection->getAllKeys())) {
+ return false;
+ }
+ }
+ }
+
+ return true;
}
/**
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
$cachestore = $this->create_test_cache_with_config($definition, array('isshared' => true));
+ if (!$cachestore->is_connection_ready()) {
+ $this->markTestSkipped('Could not test cachestore_memcached. Connection is not ready.');
+ }
+
$connection = new Memcached(crc32(__METHOD__));
$connection->addServers($this->get_servers(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
$connection->setOptions(array(
event_interface $afterevent = null,
$limitnum = 20
) {
+ $courseids = array_map(function($course) {
+ return $course->id;
+ }, enrol_get_all_users_courses($user->id));
+
+ $groupids = array_reduce($courseids, function($carry, $courseid) use ($user) {
+ $groupings = groups_get_user_groups($courseid, $user->id);
+ // Grouping 0 is all groups.
+ return array_merge($carry, $groupings[0]);
+ }, []);
+
return $this->get_events(
null,
null,
$limitnum,
CALENDAR_EVENT_TYPE_ACTION,
[$user->id],
- null,
- null,
+ $groupids ? $groupids : null,
+ $courseids ? $courseids : null,
true,
true,
function ($event) {
event_interface $afterevent = null,
$limitnum = 20
) {
+ $groupings = groups_get_user_groups($course->id, $user->id);
return array_values(
$this->get_events(
null,
$limitnum,
CALENDAR_EVENT_TYPE_ACTION,
[$user->id],
- null,
+ $groupings[0] ? $groupings[0] : null,
[$course->id],
true,
true,
* @return array $events of selected events or an empty array if there aren't any (or there was an error)
*/
function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration=true, $ignorehidden=true) {
- // We have a new implementation of this function in the calendar API class, which has slightly different behaviour
- // so the old implementation must remain here.
global $DB;
- $params = array();
+ $whereclause = '';
+ $params = array();
// Quick test.
if (empty($users) && empty($groups) && empty($courses)) {
return array();
}
- // Array of filter conditions. To be concatenated by the OR operator.
- $filters = [];
-
- // User filter.
if ((is_array($users) && !empty($users)) or is_numeric($users)) {
- // Events from a number of users.
+ // Events from a number of users
+ if(!empty($whereclause)) $whereclause .= ' OR';
list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
- $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
+ $whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
$params = array_merge($params, $inparamsusers);
- } else if ($users === true) {
- // Events from ALL users.
- $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)";
+ } else if($users === true) {
+ // Events from ALL users
+ if(!empty($whereclause)) $whereclause .= ' OR';
+ $whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)';
+ } else if($users === false) {
+ // No user at all, do nothing
}
- // Boolean false (no users at all): We don't need to do anything.
- // Group filter.
if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) {
- // Events from a number of groups.
+ // Events from a number of groups
+ if(!empty($whereclause)) $whereclause .= ' OR';
list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED);
- $filters[] = "e.groupid $insqlgroups";
+ $whereclause .= " e.groupid $insqlgroups ";
$params = array_merge($params, $inparamsgroups);
- } else if ($groups === true) {
- // Events from ALL groups.
- $filters[] = "e.groupid != 0";
+ } else if($groups === true) {
+ // Events from ALL groups
+ if(!empty($whereclause)) $whereclause .= ' OR ';
+ $whereclause .= ' e.groupid != 0';
}
+ // boolean false (no groups at all): we don't need to do anything
- // Boolean false (no groups at all): We don't need to do anything.
- // Course filter.
if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) {
+ if(!empty($whereclause)) $whereclause .= ' OR';
list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
- $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)";
+ $whereclause .= " (e.groupid = 0 AND e.courseid $insqlcourses)";
$params = array_merge($params, $inparamscourses);
} else if ($courses === true) {
- // Events from ALL courses.
- $filters[] = "(e.groupid = 0 AND e.courseid != 0)";
+ // Events from ALL courses
+ if(!empty($whereclause)) $whereclause .= ' OR';
+ $whereclause .= ' (e.groupid = 0 AND e.courseid != 0)';
}
// Security check: if, by now, we have NOTHING in $whereclause, then it means
// that NO event-selecting clauses were defined. Thus, we won't be returning ANY
// events no matter what. Allowing the code to proceed might return a completely
// valid query with only time constraints, thus selecting ALL events in that time frame!
- if (empty($filters)) {
+ if(empty($whereclause)) {
return array();
}
- // Build our clause for the filters.
- $filterclause = implode(' OR ', $filters);
-
- // Array of where conditions for our query. To be concatenated by the AND operator.
- $whereconditions = ["($filterclause)"];
-
- // Time clause.
- if ($withduration) {
- $timeclause = "((e.timestart >= :tstart1 OR e.timestart + e.timeduration > :tstart2) AND e.timestart <= :tend)";
- $params['tstart1'] = $tstart;
- $params['tstart2'] = $tstart;
- $params['tend'] = $tend;
- } else {
- $timeclause = "(e.timestart >= :tstart AND e.timestart <= :tend)";
- $params['tstart'] = $tstart;
- $params['tend'] = $tend;
- }
- $whereconditions[] = $timeclause;
-
- // Show visible only.
- if ($ignorehidden) {
- $whereconditions[] = "(e.visible = 1)";
- }
-
- // Build the main query's WHERE clause.
- $whereclause = implode(' AND ', $whereconditions);
-
- // Build SQL subquery and conditions for filtered events based on priorities.
- $subquerywhere = '';
- $subqueryconditions = [];
-
- // Get the user's courses. Otherwise, get the default courses being shown by the calendar.
- $usercourses = calendar_get_default_courses();
-
- // Set calendar filters.
- list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true);
- $subqueryparams = [];
-
- // Flag to indicate whether the query needs to exclude group overrides.
- $viewgroupsonly = false;
- if ($user) {
- // Set filter condition for the user's events.
- $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)";
- $subqueryparams['user'] = $user;
- foreach ($usercourses as $courseid) {
- if (has_capability('moodle/site:accessallgroups', context_course::instance($courseid))) {
- $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id');
- if (count($usergroupmembership) == 0) {
- $viewgroupsonly = true;
- break;
- }
- }
- }
- }
-
- // Set filter condition for the user's group events.
- if ($usergroups === true || $viewgroupsonly) {
- // Fetch group events, but not group overrides.
- $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')";
- } else if (!empty($usergroups)) {
- // Fetch group events and group overrides.
- list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
- $subqueryconditions[] = "(ev.groupid $inusergroups)";
- $subqueryparams = array_merge($subqueryparams, $inusergroupparams);
+ if($withduration) {
+ $timeclause = '(e.timestart >= '.$tstart.' OR e.timestart + e.timeduration > '.$tstart.') AND e.timestart <= '.$tend;
}
-
- // Get courses to be used for the subquery.
- $subquerycourses = [];
- if (is_array($courses)) {
- $subquerycourses = $courses;
- } else if (is_numeric($courses)) {
- $subquerycourses[] = $courses;
+ else {
+ $timeclause = 'e.timestart >= '.$tstart.' AND e.timestart <= '.$tend;
}
-
- // Merge with user courses, if necessary.
- if (!empty($usercourses)) {
- $subquerycourses = array_merge($subquerycourses, $usercourses);
- // Make sure we remove duplicate values.
- $subquerycourses = array_unique($subquerycourses);
+ if(!empty($whereclause)) {
+ // We have additional constraints
+ $whereclause = $timeclause.' AND ('.$whereclause.')';
}
-
- // Set subquery filter condition for the courses.
- if (!empty($subquerycourses)) {
- list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED);
- $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)";
- $subqueryparams = array_merge($subqueryparams, $incoursesparams);
+ else {
+ // Just basic time filtering
+ $whereclause = $timeclause;
}
- // Build the WHERE condition for the sub-query.
- if (!empty($subqueryconditions)) {
- $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions);
- }
-
- // Merge subquery parameters to the parameters of the main query.
- if (!empty($subqueryparams)) {
- $params = array_merge($params, $subqueryparams);
+ if ($ignorehidden) {
+ $whereclause .= ' AND e.visible = 1';
}
- // Sub-query that fetches the list of unique events that were filtered based on priority.
- $subquery = "SELECT ev.modulename,
- ev.instance,
- ev.eventtype,
- MIN(ev.priority) as priority
- FROM {event} ev
- $subquerywhere
- GROUP BY ev.modulename, ev.instance, ev.eventtype";
-
- // Build the main query.
$sql = "SELECT e.*
- FROM {event} e
- INNER JOIN ($subquery) fe
- ON e.modulename = fe.modulename
- AND e.instance = fe.instance
- AND e.eventtype = fe.eventtype
- AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL))
- LEFT JOIN {modules} m
- ON e.modulename = m.name
- WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause
- ORDER BY e.timestart";
+ FROM {event} e
+ LEFT JOIN {modules} m ON e.modulename = m.name
+ -- Non visible modules will have a value of 0.
+ WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause
+ ORDER BY e.timestart";
$events = $DB->get_records_sql($sql, $params);
if ($events === false) {
$events = array();
}
-
return $events;
}
if (!empty($event->properties['RRULE'])) {
// Repeating events.
date_default_timezone_set($tz); // Change time zone to parse all events.
- $rrule = new rrule_manager($event->properties['RRULE'][0]->value);
+ $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value);
$rrule->parse_rrule();
$rrule->create_events($createdevent);
\core_date::set_default_server_timezone(); // Change time zone back to what it was.
$this->assertEmpty($events);
}
+ /**
+ * There are subtle cases where the priority of an event override may be identical to another.
+ * For example, if you duplicate a group override, but make it apply to a different group. Now
+ * there are two overrides with exactly the same overridden dates. In this case the priority of
+ * both is 1.
+ *
+ * In this situation:
+ * - A user in group A should see only the A override
+ * - A user in group B should see only the B override
+ * - A user in both A and B should see both
+ */
+ public function test_get_action_events_by_timesort_with_identical_group_override_priorities() {
+ $this->resetAfterTest();
+ $this->setAdminuser();
+
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create an assign instance.
+ $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $assigninstance = $assigngenerator->create_instance(['course' => $course->id]);
+
+ // Create users.
+ $users = [
+ 'Only in group A' => $this->getDataGenerator()->create_user(),
+ 'Only in group B' => $this->getDataGenerator()->create_user(),
+ 'In group A and B' => $this->getDataGenerator()->create_user(),
+ 'In no groups' => $this->getDataGenerator()->create_user()
+ ];
+
+ // Enrol users.
+ foreach ($users as $user) {
+ $this->getDataGenerator()->enrol_user($user->id, $course->id);
+ }
+
+ // Create groups.
+ $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+ $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+ // Add members to groups.
+ // Group A.
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]);
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['In group A and B']->id]);
+
+ // Group B.
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]);
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['In group A and B']->id]);
+
+ // Events with the same module name, instance and event type.
+ $events = [
+ [
+ 'name' => 'Assignment 1 due date - Group A override',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => $groupa->id,
+ 'userid' => 2,
+ 'modulename' => 'assign',
+ 'instance' => $assigninstance->id,
+ 'eventtype' => 'due',
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => 1,
+ 'timeduration' => 0,
+ 'visible' => 1,
+ 'priority' => 1
+ ],
+ [
+ 'name' => 'Assignment 1 due date - Group B override',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => $groupb->id,
+ 'userid' => 2,
+ 'modulename' => 'assign',
+ 'instance' => $assigninstance->id,
+ 'eventtype' => 'due',
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => 1,
+ 'timeduration' => 0,
+ 'visible' => 1,
+ 'priority' => 1
+ ],
+ [
+ 'name' => 'Assignment 1 due date',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'assign',
+ 'instance' => $assigninstance->id,
+ 'eventtype' => 'due',
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => 1,
+ 'timeduration' => 0,
+ 'visible' => 1,
+ 'priority' => null,
+ ]
+ ];
+
+ foreach ($events as $event) {
+ calendar_event::create($event, false);
+ }
+
+ $factory = new action_event_test_factory();
+ $strategy = new raw_event_retrieval_strategy();
+ $vault = new event_vault($factory, $strategy);
+
+ $usersevents = array_reduce(array_keys($users), function($carry, $description) use ($users, $vault) {
+ // NB: This is currently needed to make get_action_events_by_timesort return the right thing.
+ // It needs to be fixed, see MDL-58736.
+ $this->setUser($users[$description]);
+ return $carry + ['For user ' . lcfirst($description) => $vault->get_action_events_by_timesort($users[$description])];
+ }, []);
+
+ foreach ($usersevents as $description => $userevents) {
+ if ($description == 'For user in group A and B') {
+ // User is in both A and B, so they should see the override for both
+ // given that the priority is the same.
+ $this->assertCount(2, $userevents);
+ continue;
+ }
+
+ // Otherwise there should be only one assign event for each user.
+ $this->assertCount(1, $userevents);
+ }
+
+ // User in only group A should see the group A override.
+ $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user only in group A'][0]->get_name());
+
+ // User in only group B should see the group B override.
+ $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user only in group B'][0]->get_name());
+
+ // User in group A and B should see see both overrides since the priorities are the same.
+ $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user in group A and B'][0]->get_name());
+ $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user in group A and B'][1]->get_name());
+
+ // User in no groups should see the plain assignment event.
+ $this->assertEquals('Assignment 1 due date', $usersevents['For user in no groups'][0]->get_name());
+ }
+
/**
* Test that get_action_events_by_course returns events after the
* provided timesort value.
$this->assertEmpty($events);
}
+
+ /**
+ * There are subtle cases where the priority of an event override may be identical to another.
+ * For example, if you duplicate a group override, but make it apply to a different group. Now
+ * there are two overrides with exactly the same overridden dates. In this case the priority of
+ * both is 1.
+ *
+ * In this situation:
+ * - A user in group A should see only the A override
+ * - A user in group B should see only the B override
+ * - A user in both A and B should see both
+ */
+ public function test_get_action_events_by_course_with_identical_group_override_priorities() {
+ $this->resetAfterTest();
+ $this->setAdminuser();
+
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create an assign instance.
+ $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $assigninstance = $assigngenerator->create_instance(['course' => $course->id]);
+
+ // Create users.
+ $users = [
+ 'Only in group A' => $this->getDataGenerator()->create_user(),
+ 'Only in group B' => $this->getDataGenerator()->create_user(),
+ 'In group A and B' => $this->getDataGenerator()->create_user(),
+ 'In no groups' => $this->getDataGenerator()->create_user()
+ ];
+
+ // Enrol users.
+ foreach ($users as $user) {
+ $this->getDataGenerator()->enrol_user($user->id, $course->id);
+ }
+
+ // Create groups.
+ $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+ $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+ // Add members to groups.
+ // Group A.
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]);
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['In group A and B']->id]);
+
+ // Group B.
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]);
+ $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['In group A and B']->id]);
+
+ // Events with the same module name, instance and event type.
+ $events = [
+ [
+ 'name' => 'Assignment 1 due date - Group A override',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => $groupa->id,
+ 'userid' => 2,
+ 'modulename' => 'assign',
+ 'instance' => $assigninstance->id,
+ 'eventtype' => 'due',
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => 1,
+ 'timeduration' => 0,
+ 'visible' => 1,
+ 'priority' => 1
+ ],
+ [
+ 'name' => 'Assignment 1 due date - Group B override',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => $groupb->id,
+ 'userid' => 2,
+ 'modulename' => 'assign',
+ 'instance' => $assigninstance->id,
+ 'eventtype' => 'due',
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => 1,
+ 'timeduration' => 0,
+ 'visible' => 1,
+ 'priority' => 1
+ ],
+ [
+ 'name' => 'Assignment 1 due date',
+ 'description' => '',
+ 'format' => 1,
+ 'courseid' => $course->id,
+ 'groupid' => 0,
+ 'userid' => 2,
+ 'modulename' => 'assign',
+ 'instance' => $assigninstance->id,
+ 'eventtype' => 'due',
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => 1,
+ 'timeduration' => 0,
+ 'visible' => 1,
+ 'priority' => null,
+ ]
+ ];
+
+ foreach ($events as $event) {
+ calendar_event::create($event, false);
+ }
+
+ $factory = new action_event_test_factory();
+ $strategy = new raw_event_retrieval_strategy();
+ $vault = new event_vault($factory, $strategy);
+
+ $usersevents = array_reduce(array_keys($users), function($carry, $description) use ($users, $course, $vault) {
+ // NB: This is currently needed to make get_action_events_by_timesort return the right thing.
+ // It needs to be fixed, see MDL-58736.
+ $this->setUser($users[$description]);
+ return $carry + [
+ 'For user ' . lcfirst($description) => $vault->get_action_events_by_course($users[$description], $course)
+ ];
+ }, []);
+
+ foreach ($usersevents as $description => $userevents) {
+ if ($description == 'For user in group A and B') {
+ // User is in both A and B, so they should see the override for both
+ // given that the priority is the same.
+ $this->assertCount(2, $userevents);
+ continue;
+ }
+
+ // Otherwise there should be only one assign event for each user.
+ $this->assertCount(1, $userevents);
+ }
+
+ // User in only group A should see the group A override.
+ $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user only in group A'][0]->get_name());
+
+ // User in only group B should see the group B override.
+ $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user only in group B'][0]->get_name());
+
+ // User in group A and B should see see both overrides since the priorities are the same.
+ $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user in group A and B'][0]->get_name());
+ $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user in group A and B'][1]->get_name());
+
+ // User in no groups should see the plain assignment event.
+ $this->assertEquals('Assignment 1 due date', $usersevents['For user in no groups'][0]->get_name());
+ }
}
$this->assertEquals('assign', $event->modulename);
}
- /**
- * Test for calendar_get_events() when there are user and group overrides.
- */
- public function test_calendar_get_events_with_overrides() {
- global $DB;
- $generator = $this->getDataGenerator();
- $course = $generator->create_course();
- $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
- if (!isset($params['course'])) {
- $params['course'] = $course->id;
- }
- $instance = $plugingenerator->create_instance($params);
- // Create users.
- $useroverridestudent = $generator->create_user();
- $group1student = $generator->create_user();
- $group2student = $generator->create_user();
- $group12student = $generator->create_user();
- $nogroupstudent = $generator->create_user();
- // Enrol users.
- $generator->enrol_user($useroverridestudent->id, $course->id, 'student');
- $generator->enrol_user($group1student->id, $course->id, 'student');
- $generator->enrol_user($group2student->id, $course->id, 'student');
- $generator->enrol_user($group12student->id, $course->id, 'student');
- $generator->enrol_user($nogroupstudent->id, $course->id, 'student');
- // Create groups.
- $group1 = $generator->create_group(['courseid' => $course->id]);
- $group2 = $generator->create_group(['courseid' => $course->id]);
- // Add members to groups.
- $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]);
- $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]);
- $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]);
- $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]);
- $now = time();
- // Events with the same module name, instance and event type.
- $events = [
- [
- 'name' => 'Assignment 1 due date',
- 'description' => '',
- 'format' => 0,
- 'courseid' => $course->id,
- 'groupid' => 0,
- 'userid' => 2,
- 'modulename' => 'assign',
- 'instance' => $instance->id,
- 'eventtype' => 'due',
- 'timestart' => $now,
- 'timeduration' => 0,
- 'visible' => 1
- ], [
- 'name' => 'Assignment 1 due date - User override',
- 'description' => '',
- 'format' => 1,
- 'courseid' => 0,
- 'groupid' => 0,
- 'userid' => $useroverridestudent->id,
- 'modulename' => 'assign',
- 'instance' => $instance->id,
- 'eventtype' => 'due',
- 'timestart' => $now + 86400,
- 'timeduration' => 0,
- 'visible' => 1,
- 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY
- ], [
- 'name' => 'Assignment 1 due date - Group A override',
- 'description' => '',
- 'format' => 1,
- 'courseid' => $course->id,
- 'groupid' => $group1->id,
- 'userid' => 2,
- 'modulename' => 'assign',
- 'instance' => $instance->id,
- 'eventtype' => 'due',
- 'timestart' => $now + (2 * 86400),
- 'timeduration' => 0,
- 'visible' => 1,
- 'priority' => 1,
- ], [
- 'name' => 'Assignment 1 due date - Group B override',
- 'description' => '',
- 'format' => 1,
- 'courseid' => $course->id,
- 'groupid' => $group2->id,
- 'userid' => 2,
- 'modulename' => 'assign',
- 'instance' => $instance->id,
- 'eventtype' => 'due',
- 'timestart' => $now + (3 * 86400),
- 'timeduration' => 0,
- 'visible' => 1,
- 'priority' => 2,
- ],
- ];
- foreach ($events as $event) {
- calendar_event::create($event, false);
- }
- $timestart = $now - 100;
- $timeend = $now + (3 * 86400);
- $groups = [$group1->id, $group2->id];
- // Get user override events.
- $this->setUser($useroverridestudent);
- $events = calendar_get_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id);
- $this->assertCount(1, $events);
- $event = reset($events);
- $this->assertEquals('Assignment 1 due date - User override', $event->name);
- // Get event for user with override but with the timestart and timeend parameters only covering the original event.
- $events = calendar_get_events($timestart, $now, $useroverridestudent->id, $groups, $course->id);
- $this->assertCount(0, $events);
- // Get events for user that does not belong to any group and has no user override events.
- $this->setUser($nogroupstudent);
- $events = calendar_get_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id);
- $this->assertCount(1, $events);
- $event = reset($events);
- $this->assertEquals('Assignment 1 due date', $event->name);
- // Get events for user that belongs to groups A and B and has no user override events.
- $this->setUser($group12student);
- $events = calendar_get_events($timestart, $timeend, $group12student->id, $groups, $course->id);
- $this->assertCount(1, $events);
- $event = reset($events);
- $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
- // Get events for user that belongs to group A and has no user override events.
- $this->setUser($group1student);
- $events = calendar_get_events($timestart, $timeend, $group1student->id, $groups, $course->id);
- $this->assertCount(1, $events);
- $event = reset($events);
- $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
- // Add repeating events.
- $repeatingevents = [
- [
- 'name' => 'Repeating site event',
- 'description' => '',
- 'format' => 1,
- 'courseid' => SITEID,
- 'groupid' => 0,
- 'userid' => 2,
- 'repeatid' => $event->id,
- 'modulename' => '0',
- 'instance' => 0,
- 'eventtype' => 'site',
- 'timestart' => $now + 86400,
- 'timeduration' => 0,
- 'visible' => 1,
- ],
- [
- 'name' => 'Repeating site event',
- 'description' => '',
- 'format' => 1,
- 'courseid' => SITEID,
- 'groupid' => 0,
- 'userid' => 2,
- 'repeatid' => $event->id,
- 'modulename' => '0',
- 'instance' => 0,
- 'eventtype' => 'site',
- 'timestart' => $now + (2 * 86400),
- 'timeduration' => 0,
- 'visible' => 1,
- ],
- ];
- foreach ($repeatingevents as $event) {
- calendar_event::create($event, false);
- }
- // Make sure repeating events are not filtered out.
- $events = calendar_get_events($timestart, $timeend, true, true, true);
- $this->assertCount(3, $events);
- }
-
public function test_get_course_cached() {
// Setup some test courses.
$course1 = $this->getDataGenerator()->create_course();
public static function share_same_framework(array $ids) {
global $DB;
list($insql, $params) = $DB->get_in_or_equal($ids);
- return $DB->count_records_select(self::TABLE, "id $insql", $params, "COUNT(DISTINCT(competencyframeworkid))") == 1;
+ $sql = "SELECT COUNT('x') FROM (SELECT DISTINCT(competencyframeworkid) FROM {" . self::TABLE . "} WHERE id {$insql}) f";
+ return $DB->count_records_sql($sql, $params) == 1;
}
/**
$modinfosections = $modinfo->get_sections();
foreach ($sections as $key => $section) {
- if (!$section->uservisible) {
+ // Show the section if the user is permitted to access it, OR if it's not available
+ // but there is some available info text which explains the reason & should display.
+ $showsection = $section->uservisible ||
+ ($section->visible && !$section->available &&
+ !empty($section->availableinfo));
+
+ if (!$showsection) {
continue;
}
$context->id, 'course', 'section', $section->id, $options);
$sectionvalues['section'] = $section->section;
$sectionvalues['hiddenbynumsections'] = $section->section > $coursenumsections ? 1 : 0;
+ $sectionvalues['uservisible'] = $section->uservisible;
+ if (!empty($section->availableinfo)) {
+ $sectionvalues['availabilityinfo'] = \core_availability\info::format_info($section->availableinfo, $course);
+ }
+
$sectioncontents = array();
- //for each module of the section
- if (empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
+ // For each module of the section (if it is visible).
+ if ($section->uservisible and empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
foreach ($modinfosections[$section->section] as $cmid) {
$cm = $modinfo->cms[$cmid];
- // stop here if the module is not visible to the user
- if (!$cm->uservisible) {
+ // Stop here if the module is not visible to the user on the course main page:
+ // The user can't access the module and the user can't view the module on the course page.
+ if (!$cm->uservisible && !$cm->is_visible_on_course_page()) {
continue;
}
//user that can view hidden module should know about the visibility
$module['visible'] = $cm->visible;
$module['visibleoncoursepage'] = $cm->visibleoncoursepage;
+ $module['uservisible'] = $cm->uservisible;
+ if (!empty($cm->availableinfo)) {
+ $module['availabilityinfo'] = \core_availability\info::format_info($cm->availableinfo, $course);
+ }
// Availability date (also send to user who can see hidden module).
if ($CFG->enableavailability && ($canviewhidden || $canupdatecourse)) {
$module['availability'] = $cm->availability;
}
- $baseurl = 'webservice/pluginfile.php';
-
- //call $modulename_export_contents
- //(each module callback take care about checking the capabilities)
+ // Return contents only if the user can access to the module.
+ if ($cm->uservisible) {
+ $baseurl = 'webservice/pluginfile.php';
- require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php');
- $getcontentfunction = $cm->modname.'_export_contents';
- if (function_exists($getcontentfunction)) {
- if (empty($filters['excludecontents']) and $contents = $getcontentfunction($cm, $baseurl)) {
- $module['contents'] = $contents;
- } else {
- $module['contents'] = array();
+ // Call $modulename_export_contents (each module callback take care about checking the capabilities).
+ require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php');
+ $getcontentfunction = $cm->modname.'_export_contents';
+ if (function_exists($getcontentfunction)) {
+ if (empty($filters['excludecontents']) and $contents = $getcontentfunction($cm, $baseurl)) {
+ $module['contents'] = $contents;
+ } else {
+ $module['contents'] = array();
+ }
}
}
'section' => new external_value(PARAM_INT, 'Section number inside the course', VALUE_OPTIONAL),
'hiddenbynumsections' => new external_value(PARAM_INT, 'Whether is a section hidden in the course format',
VALUE_OPTIONAL),
+ 'uservisible' => new external_value(PARAM_BOOL, 'Is the section visible for the user?', VALUE_OPTIONAL),
+ 'availabilityinfo' => new external_value(PARAM_RAW, 'Availability information.', VALUE_OPTIONAL),
'modules' => new external_multiple_structure(
new external_single_structure(
array(
'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
+ 'uservisible' => new external_value(PARAM_BOOL, 'Is the module visible for the user?',
+ VALUE_OPTIONAL),
+ 'availabilityinfo' => new external_value(PARAM_RAW, 'Availability information.',
+ VALUE_OPTIONAL),
'visibleoncoursepage' => new external_value(PARAM_INT, 'is the module visible on course page',
VALUE_OPTIONAL),
'modicon' => new external_value(PARAM_URL, 'activity icon url'),
if (isset($option['type'])) {
$mform->setType($optionname, $option['type']);
}
- if (is_null($mform->getElementValue($optionname)) && isset($option['default'])) {
+ if (is_null($mform->getElementValue('id')) && isset($option['default'])) {
$mform->setDefault($optionname, $option['default']);
}
}
*/
private function prepare_get_course_contents_test() {
global $DB;
- $course = self::getDataGenerator()->create_course(['numsections' => 2]);
+ $course = self::getDataGenerator()->create_course(['numsections' => 3]);
$forumdescription = 'This is the forum description';
$forum = $this->getDataGenerator()->create_module('forum',
array('course' => $course->id, 'intro' => $forumdescription),
$label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
'intro' => $labeldescription));
$labelcm = get_coursemodule_from_instance('label', $label->id);
- $url = $this->getDataGenerator()->create_module('url', array('course' => $course->id,
- 'name' => 'URL: % & $ ../', 'section' => 2));
+ $tomorrow = time() + DAYSECS;
+ // Module with availability restrictions not met.
+ $url = $this->getDataGenerator()->create_module('url',
+ array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2),
+ array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}'));
$urlcm = get_coursemodule_from_instance('url', $url->id);
+ // Module for the last section.
+ $this->getDataGenerator()->create_module('url',
+ array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
+ // Module for section 1 with availability restrictions met.
+ $yesterday = time() - DAYSECS;
+ $this->getDataGenerator()->create_module('url',
+ array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
+ array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
// Set the required capabilities by the external function.
$context = context_course::instance($course->id);
$conditions = array('course' => $course->id, 'section' => 2);
$DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
+
+ // Add date availability condition not met for last section.
+ $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
+ $DB->set_field('course_sections', 'availability', $availability,
+ array('course' => $course->id, 'section' => 3));
rebuild_course_cache($course->id, true);
return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
// We need to execute the return values cleaning process to simulate the web service server.
$sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
- // Check that forum and label descriptions are correctly returned.
- $firstsection = array_shift($sections);
- $lastsection = array_pop($sections);
-
$modinfo = get_fast_modinfo($course);
$testexecuted = 0;
- foreach ($firstsection['modules'] as $module) {
+ foreach ($sections[0]['modules'] as $module) {
if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
$cm = $modinfo->cms[$forumcm->id];
$formattedtext = format_text($cm->content, FORMAT_HTML,
}
}
$this->assertEquals(2, $testexecuted);
- $this->assertEquals(0, $firstsection['section']);
+ $this->assertEquals(0, $sections[0]['section']);
// Check that the only return section has the 5 created modules.
- $this->assertCount(4, $firstsection['modules']);
- $this->assertCount(1, $lastsection['modules']);
- $this->assertEquals(2, $lastsection['section']);
- $this->assertContains('<iframe', $lastsection['summary']);
- $this->assertContains('</iframe>', $lastsection['summary']);
-
+ $this->assertCount(4, $sections[0]['modules']);
+ $this->assertCount(1, $sections[1]['modules']);
+ $this->assertCount(1, $sections[2]['modules']);
+ $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+ $this->assertNotEmpty($sections[3]['availabilityinfo']);
+ $this->assertEquals(1, $sections[1]['section']);
+ $this->assertEquals(2, $sections[2]['section']);
+ $this->assertEquals(3, $sections[3]['section']);
+ $this->assertContains('<iframe', $sections[2]['summary']);
+ $this->assertContains('</iframe>', $sections[2]['summary']);
+ // The module with the availability restriction met is returning contents.
+ $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
+ // The module with the availability restriction not met is not returning contents.
+ $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
+ $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
try {
$sections = core_course_external::get_course_contents($course->id,
array(array("name" => "invalid", "value" => 1)));
// We need to execute the return values cleaning process to simulate the web service server.
$sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
- $firstsection = array_shift($sections);
- $lastsection = array_pop($sections);
-
- $this->assertEmpty($firstsection['modules']);
- $this->assertEmpty($lastsection['modules']);
+ $this->assertEmpty($sections[0]['modules']);
+ $this->assertEmpty($sections[1]['modules']);
}
/**
// We need to execute the return values cleaning process to simulate the web service server.
$sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
- $this->assertCount(3, $sections);
+ $this->assertCount(4, $sections);
$this->assertCount(1, $sections[0]['modules']);
$this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
}
// We need to execute the return values cleaning process to simulate the web service server.
$sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
- $this->assertCount(3, $sections);
+ $this->assertCount(4, $sections);
$this->assertCount(1, $sections[0]['modules']);
$this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
}
// We need to execute the return values cleaning process to simulate the web service server.
$sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
- $this->assertCount(3, $sections);
+ $this->assertCount(4, $sections);
$this->assertCount(1, $sections[0]['modules']);
$this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
$this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
- isexternalfile (if is a file reference to a external repository)
- repositorytype (the repository name in case is a external file)
Those fields are VALUE_OPTIONAL for backwards compatibility.
+ * External function core_course_external::get_course_contents now return the following fields for section and modules:
+ - uservisible (whether the section or module is visible by the user)
+ - availabilityinfo (availability information if the course or module has any access restriction set
=== 3.2 ===
$string['course_summary_key'] = 'Summary';
$string['course_summary_updateonsync'] = 'Update summary during synchronisation script';
$string['course_summary_updateonsync_key'] = 'Update summary';
-$string['createcourseextid'] = 'CREATE User enrolled to a nonexistant course \'{$a->courseextid}\'';
-$string['createnotcourseextid'] = 'User enrolled to a nonexistant course \'{$a->courseextid}\'';
+$string['createcourseextid'] = 'CREATE User enrolled to a non-existing course \'{$a->courseextid}\'';
+$string['createnotcourseextid'] = 'User enrolled to a non-existing course \'{$a->courseextid}\'';
$string['creatingcourse'] = 'Creating course \'{$a}\'...';
$string['duplicateshortname'] = "Course creation failed. Duplicate short name. Skipping course with idnumber '{\$a->idnumber}'...";
$string['editlock'] = 'Lock value';
$string['pluginname'] = 'Google Drive';
$string['disabled'] = 'Disabled';
-$string['issuer'] = 'OAuth 2 Service';
-$string['issuer_help'] = 'The OAuth 2 service used to access google drive.';
+$string['issuer'] = 'OAuth 2 service';
+$string['issuer_help'] = 'The OAuth 2 service used to access Google Drive.';
$string['test_converter'] = 'Test this converter is working properly.';
$string['test_conversion'] = 'Test document conversion';
$string['test_conversionready'] = 'This document converter is configured properly.';
}
$PAGE->set_url('/', $urlparams);
$PAGE->set_course($SITE);
+$PAGE->set_pagelayout('frontpage');
$PAGE->set_other_editing_capability('moodle/course:update');
$PAGE->set_other_editing_capability('moodle/course:manageactivities');
$PAGE->set_other_editing_capability('moodle/course:activityvisibility');
$PAGE->set_pagetype('site-index');
$PAGE->set_docs_path('');
-$PAGE->set_pagelayout('frontpage');
$editing = $PAGE->user_is_editing();
$PAGE->set_title($SITE->fullname);
$PAGE->set_heading($SITE->fullname);
$string['pathswrongadmindir'] = 'El directorio admin no existe';
$string['phpextension'] = 'Extensión PHP {$a}';
$string['phpversion'] = 'Versión PHP';
-$string['phpversionhelp'] = '<p>Moodle requiere una versión de PHP de al menos 5.6.5 (7.x tiene algunas limitaciones del motor).</p>
+$string['phpversionhelp'] = '<p>Moodle requiere una versión de PHP de al menos 5.6.5 o 7.1 (7.0.x tiene algunas limitaciones del motor).</p>
<p>En este momento está ejecutando la versión {$a}</p>
-<p>Usted debe actualizar PHP o trasladarse a otro servidor con una versión más reciente de PHP!<br />';
+<p>¡Usted debe actualizar PHP o trasladarse a otro servidor con una versión más reciente de PHP!<br />';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Si está viendo esta página es porque ha podido instalar y ejecutar exitosamente el paquete <strong>{$a->packname} {$a->packversion}</strong> en su computadora. !Enhorabuena!';
$string['welcomep30'] = 'Esta versión de <strong>{$a->installername}</strong> incluye las aplicaciones necesarias para que <strong>Moodle</strong> funcione en su computadora, principalmente:';
-Moodle development|https://moodle.org/development
Moodle.com|http://moodle.com/
</pre>';
-$string['configcustomusermenuitems'] = 'You can configure the contents of the user menu (with the exception of the log out link, which is automatically added). Each line is separated by | characters and consists of 1) a string in "langstringname, componentname" form or as plain text, 2) a URL, and 3) an icon either as a pix icon or as a URL. Dividers can be used by adding a line of one or more # characters where desired.';
+$string['configcustomusermenuitems'] = 'You can configure the contents of the user menu (with the exception of the log out link, which is automatically added). Each line is separated by pipe characters and consists of 1) a string in "langstringname, componentname" form or as plain text, 2) a URL, and 3) an icon either as a pix icon (in the folder pix/t, or prefix the icon name with ../ if icon is in another pix folder) or as a URL. Dividers can be used by adding a line of one or more # characters where desired.';
$string['configdbsessions'] = 'If enabled, this setting will use the database to store information about current sessions. Note that changing this setting now will log out all current users (including you). If you are using MySQL please make sure that \'max_allowed_packet\' in my.cnf (or my.ini) is at least 4M. Other session drivers can be configured directly in config.php, see config-dist.php for more information. This option disappears if you specify session driver in config.php file.';
$string['configdebug'] = 'If you turn this on, then PHP\'s error_reporting will be increased so that more warnings are printed. This is only useful for developers.';
$string['configdebugdisplay'] = 'Set to on, the error reporting will go to the HTML page. This is practical, but breaks XHTML, JS, cookies and HTTP headers in general. Set to off, it will send the output to your server logs, allowing better debugging. The PHP setting error_log controls which log this goes to.';
$string['creatornewroleid'] = 'Creators\' role in new courses';
$string['creatornewroleid_help'] = 'If the user does not already have the permission to manage the new course, the user is automatically enrolled using this role.';
$string['cron'] = 'Cron';
-$string['cron_help'] = 'The cron.php maintenance script assists some of Moodle\'s modules to perform tasks on a scheduled basis, such as mailing out copies of new forum posts. A mechanism is required to run the script regularly e.g. every 5 minutes.';
+$string['cron_help'] = 'The cron.php script runs a number of tasks at different scheduled intervals, such as sending forum post notification emails. The script should be run regularly - ideally every minute.';
$string['cron_link'] = 'admin/cron';
$string['cronclionly'] = 'Cron execution via command line only';
$string['cronerrorclionly'] = 'Sorry, internet access to this page has been disabled by the administrator.';
It is recommended to install local copy of free GeoLite2 City database from MaxMind.<br />
IP address location is displayed on simple map or using Google Maps. Please note that you need to have a Google account and apply for free Google Maps API key to enable interactive maps.';
$string['iplookupmaxmindnote'] = 'This product includes GeoLite2 data created by MaxMind, available from <a href="http://www.maxmind.com">http://www.maxmind.com</a>.';
-$string['ishttpswarning'] = 'It has been detected that your site is not secured using HTTPS. For increased security and improved integrations with other systems is highly recommended to migrate your site to HTTPS.';
+$string['ishttpswarning'] = 'It has been detected that your site is not secured using HTTPS. It is strongly recommended to migrate your site to HTTPS for increased security and improved integration with other systems.';
$string['keeptagnamecase'] = 'Keep tag name casing';
$string['lang'] = 'Default language';
$string['langcache'] = 'Cache language menu';
$string['security_question'] = 'Security question';
$string['selfregistration'] = 'Self registration';
$string['selfregistration_help'] = 'If an authentication plugin, such as email-based self-registration, is selected, then it enables potential users to register themselves and create accounts. This results in the possibility of spammers creating accounts in order to use forum posts, blog entries etc. for spam. To avoid this risk, self-registration should be disabled or limited by <em>Allowed email domains</em> setting.';
+$string['settingmigrationmismatch'] = 'Values mismatch detected while correcting the plugin setting names! The authentication plugin \'{$a->plugin}\' had the setting \'{$a->setting}\' configured to \'{$a->legacy}\' under the legacy name and to \'{$a->current}\' under the current name. The latter value has been set as the valid one but you should check and confirm that it is expected.';
$string['sha1'] = 'SHA-1 hash';
$string['showguestlogin'] = 'You can hide or show the guest login button on the login page.';
$string['stdchangepassword'] = 'Use standard page for changing password';
Currently, only <a href="http://backpack.openbadges.org">Mozilla OpenBadges Backpack</a> is supported. You need to sign up for a backpack service before trying to set up backpack connection on this page.';
$string['backpackconnectioncancelattempt'] = 'Connect using a different email address';
-$string['backpackconnectionconnect'] = 'Connect to Backpack';
+$string['backpackconnectionconnect'] = 'Connect to backpack';
$string['backpackconnectionresendemail'] = 'Resend verification email';
-$string['backpackconnectionunexpectedresult'] = 'There was a problem contacting the Backpack. Please try again.<br><br>If this problem persists, please contact your system administrator.';
+$string['backpackconnectionunexpectedresult'] = 'There was a problem connecting to your backpack. Please try again.<br><br>If the problem persists, contact your administrator.';
$string['backpackdetails'] = 'Backpack settings';
$string['backpackemail'] = 'Email address';
$string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.';
A new connection to your OpenBadges backpack has been requested from \'{$a->sitename}\' using your email address.
-To confirm and activate the connection to your backpack, please click the link below.
+To confirm and activate the connection to your backpack, please go to
{$a->link}
{$a->admin}';
$string['backpackemailverifyemailsubject'] = '{$a}: OpenBadges Backpack email verification';
$string['backpackemailverifypending'] = 'A verification email has been sent to <strong>{$a}</strong>. Click on the verification link in the email to activate your Backpack connection.';
-$string['backpackemailverifysuccess'] = 'Thanks for verifying your email address. You are now connected to your Backpack.';
+$string['backpackemailverifysuccess'] = 'Thanks for verifying your email address. You are now connected to your backpack.';
$string['backpackemailverifytokenmismatch'] = 'The token in the link you clicked does not match the stored token. Make sure you clicked the link in most recent email you received.';
$string['backpackimport'] = 'Badge import settings';
$string['backpackimport_help'] = 'After the backpack connection is successfully established, badges from your backpack can be displayed on your badges page and your profile page.
$string['viewsiteentries'] = 'View all entries';
$string['viewuserentries'] = 'View all entries by {$a}';
$string['worldblogs'] = 'The world can read entries set to be world-accessible';
+$string['wrongexternalid'] = 'Wrong external blog id';
$string['wrongpostid'] = 'Wrong blog post id';
$string['page-blog-edit'] = 'Blog editing pages';
$string['page-blog-index'] = 'Blog listing pages';
$string['errorhasuntilandcount'] = 'Either UNTIL or COUNT may appear in a recurrence rule, but UNTIL and COUNT MUST NOT occur in the same recurrence rule.';
$string['errorinvalidbydaysuffix'] = 'Valid values for the day of the week parts of the BYDAY rule are MO, TU, WE, TH, FR, SA and SU';
$string['errorinvalidbydayprefix'] = 'Integer values preceding BYDAY rules can only be present for MONTHLY or YEARLY RRULE.';
-$string['errorinvalidbyhour'] = 'Valid values for the BYHOUR rule are 0 to 59.';
+$string['errorinvalidbyhour'] = 'Valid values for the BYHOUR rule are 0 to 23.';
$string['errorinvalidinterval'] = 'The value for the INTERVAL rule must be a positive integer.';
$string['errorinvalidbyminute'] = 'Valid values for the BYMINUTE rule are 0 to 59.';
$string['errorinvalidbymonth'] = 'Valid values for the BYMONTH rule are 1 to 12.';
$string['achievinggrade'] = 'Achieving grade';
$string['activities'] = 'Activities';
-$string['activitieslabel'] = 'Activities / Resources';
+$string['activitieslabel'] = 'Activities / resources';
$string['activityaggregation'] = 'Condition requires';
$string['activityaggregation_all'] = 'ALL selected activities to be completed';
$string['activityaggregation_any'] = 'ANY selected activities to be completed';
$string['activitiescompletednote'] = 'Note: Activity completion must be set for an activity to appear in the above list.';
$string['activitycompletion'] = 'Activity completion';
$string['activitycompletionupdated'] = 'Changes saved';
-$string['affectedactivities'] = 'The changes will affect the following <b>{$a}</b> Activities/Resources';
+$string['affectedactivities'] = 'The changes will affect the following <b>{$a}</b> activities or resources:';
$string['aggregationmethod'] = 'Aggregation method';
$string['all'] = 'All';
$string['any'] = 'Any';
$string['err_system'] = 'An internal error occurred in the completion system. (System administrators can enable debugging information to see more detail.)';
$string['eventcoursecompleted'] = 'Course completed';
$string['eventcoursecompletionupdated'] = 'Course completion updated';
-$string['eventcoursemodulecompletionupdated'] = 'Course module completion updated';
-$string['eventdefaultcompletionupdated'] = 'Default for course module completion updated';
+$string['eventcoursemodulecompletionupdated'] = 'Course activity completion updated';
+$string['eventdefaultcompletionupdated'] = 'Default for course activity completion updated';
$string['excelcsvdownload'] = 'Download in Excel-compatible format (.csv)';
$string['fraction'] = 'Fraction';
$string['graderequired'] = 'Required course grade';
$string['modifybulkactions'] = 'Modify the actions you wish to bulk edit';
$string['moredetails'] = 'More details';
$string['nocriteriaset'] = 'No completion criteria set for this course';
-$string['nogradeitem'] = 'Require grade can not be enabled for <b>{$a}</b> because grades are not available there';
+$string['nogradeitem'] = 'Require grade can\'t be enabled for <b>{$a}</b> because the activity is not graded.';
$string['notcompleted'] = 'Not completed';
$string['notenroled'] = 'You are not enrolled in this course';
$string['nottracked'] = 'You are currently not being tracked by completion in this course';
$string['invalidroleid'] = 'Invalid role ID';
$string['invalidscaleid'] = 'Incorrect scale id';
$string['invalidsection'] = 'Course module record contains invalid section';
-$string['invalidsesskey'] = 'Incorrect sesskey submitted, form not accepted!';
+$string['invalidsesskey'] = 'Your session has most likely timed out. Please log in again.';
$string['invalidshortname'] = 'That\'s an invalid short course name';
$string['invalidstatedetected'] = 'Something has gone wrong: {$a}. This should never normally happen.';
$string['invalidsourcefield'] = 'Draft file\'s source field is invalid';
$string['moduledoesnotexist'] = 'This module does not exist';
$string['moduleinstancedoesnotexist'] = 'The instance of this module does not exist';
$string['modulemissingcode'] = 'Module {$a} is missing the code needed to perform this function';
-$string['movecatcontentstoroot'] = 'Moving the category content to root is not allowed. You must move the contents to an existant category!';
+$string['movecatcontentstoroot'] = 'Moving the category content to root is not allowed. You must move the contents to an existing category!';
$string['movecategorynotpossible'] = 'You cannot move category \'{$a}\' into the selected category.';
$string['movecategoryownparent'] = 'You cannot make category \'{$a}\' a parent of itself.';
$string['movecategoryparentconflict'] = 'You cannot make category \'{$a}\' a subcategory of one of its own subcategories.';
$string['pgsqlextensionisnotpresentinphp'] = 'PHP has not been properly configured with the PGSQL extension so that it can communicate with PostgreSQL. Please check your php.ini file or recompile PHP.';
$string['phpextension'] = '{$a} PHP extension';
$string['phpversion'] = 'PHP version';
-$string['phpversionhelp'] = '<p>Moodle requires a PHP version of at least 5.6.5 (7.x has some engine limitations).</p>
+$string['phpversionhelp'] = '<p>Moodle requires a PHP version of at least 5.6.5 or 7.1 (7.0.x has some engine limitations).</p>
<p>You are currently running version {$a}.</p>
<p>You must upgrade PHP or move to a host with a newer version of PHP.</p>';
$string['releasenoteslink'] = 'For information about this version of Moodle, please see the release notes at {$a}';
$string['allmods'] = 'All {$a}';
$string['allow'] = 'Allow';
$string['allowinternal'] = 'Allow internal methods as well';
-$string['allowstealthmodules'] = 'Allow activities to be available but not shown in visible sections of course page';
-$string['allowstealthmodules_help'] = 'If enabled, the Availability setting in Common module settings may have three options, rather than two - \'Show on course page\', \'Hide from students\' and \'Make available but not shown on course page\'. If an activity or resource is made available but not shown on the course page, a link to it must be provided from elsewhere, such as from a page resource. The activity would still be listed in the gradebook and other reports.';
+$string['allowstealthmodules'] = 'Allow stealth activities';
+$string['allowstealthmodules_help'] = 'If enabled, activities can be made available but not shown in visible sections of the course page. If so, links to stealth activities must be provided from elsewhere, such as from a page resource. Stealth activities are still listed in the gradebook and other reports.';
$string['allownone'] = 'Allow none';
$string['allownot'] = 'Do not allow';
$string['allparticipants'] = 'All participants';
$string['helpwiththis'] = 'Help with this';
$string['hiddenassign'] = 'Hidden assignment';
$string['hiddenfromstudents'] = 'Hidden from students';
-$string['hiddenoncoursepage'] = 'Available but not displayed on course page';
+$string['hiddenoncoursepage'] = 'Available but not shown on course page';
$string['hiddensections'] = 'Hidden sections';
$string['hiddensections_help'] = 'This setting determines whether hidden sections are displayed to students in collapsed form (perhaps for a course in weekly format to indicate holidays) or are completely hidden.';
$string['hiddensectionscollapsed'] = 'Hidden sections are shown in collapsed form';
$string['todaylogs'] = 'Today\'s logs';
$string['toeveryone'] = 'to everyone';
$string['toomanybounces'] = 'That email address has had too many bounces. You <b>must</b> change it to continue.';
-$string['toomanytags'] = 'This search included too many tags, some will have been ignored.';
+$string['toomanytags'] = 'This search included too many tags; some will have been ignored.';
$string['toomanytoshow'] = 'There are too many users to show.';
$string['toomanyusersmatchsearch'] = 'Too many users ({$a->count}) match \'{$a->search}\'';
$string['toomanyuserstoshow'] = 'Too many users ({$a}) to show';
var searchKey = this.currentThemeName + '/' + templateName;
var template = templateCache[searchKey];
+ // The key might have been escaped by the JS Mustache engine which
+ // converts forward slashes to HTML entities. Let us undo that here.
+ key = key.replace(///gi, '/');
+
return iconSystem.renderIcon(key, component, text, template);
};
return $savemessage->id;
}
- $processors = get_message_processors(true);
-
$failed = false;
foreach ($processorlist as $procname) {
// Let new messaging class add custom content based on the processor.
$proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
- if (!$processors[$procname]->object->send_message($proceventdata)) {
+ $stdproc = new \stdClass();
+ $stdproc->name = $procname;
+ $processor = \core_message\api::get_processed_processor_object($stdproc);
+ if (!$processor->object->send_message($proceventdata)) {
debugging('Error calling message processor ' . $procname);
$failed = true;
// Previously the $messageid = false here was overridden
require_once($CFG->libdir . '/filelib.php');
use moodle_url;
+use moodle_exception;
use curl;
use stdClass;
return false;
}
- if (isset($r->refresh_token)) {
- $systemaccount->set('refreshtoken', $r->refresh_token);
- $systemaccount->update();
- $this->refreshtoken = $r->refresh_token;
- }
-
// Store the token an expiry time.
$accesstoken = new stdClass;
$accesstoken->token = $r->access_token;
// Expires 10 seconds before actual expiry.
$accesstoken->expires = (time() + ($r->expires_in - 10));
}
- if (isset($r->scope)) {
- $accesstoken->scope = $r->scope;
- } else {
- $accesstoken->scope = $this->scope;
- }
+ $accesstoken->scope = $this->scope;
// Also add the scopes.
$this->store_token($accesstoken);
+ if (isset($r->refresh_token)) {
+ $userinfo = $this->get_userinfo();
+
+ if ($userinfo['email'] == $systemaccount->get('email')) {
+ $systemaccount->set('refreshtoken', $r->refresh_token);
+ $systemaccount->update();
+ $this->refreshtoken = $r->refresh_token;
+ } else {
+ throw new moodle_exception('Attempt to store refresh token for non-system user.');
+ }
+ }
+
return true;
}
'core:e/visual_aid' => 'fa-universal-access',
'core:e/visual_blocks' => 'fa-audio-description',
'theme:fp/add_file' => 'fa-file-o',
- 'theme:fp/alias' => 'fa-link',
+ 'theme:fp/alias' => 'fa-share',
+ 'theme:fp/alias_sm' => 'fa-share',
'theme:fp/check' => 'fa-check',
'theme:fp/create_folder' => 'fa-folder-o',
'theme:fp/cross' => 'fa-remove',
defined('MOODLE_INTERNAL') || die();
-require_once($CFG->libdir . '/cronlib.php');
+require_once($CFG->dirroot . '/calendar/lib.php');
/**
* Simple task to run the calendar cron.
upgrade_main_savepoint(true, 2017042600.01);
}
+ if ($oldversion < 2017050300.01) {
+ // MDL-58684:
+ // Remove all portfolio_tempdata records as these may contain serialized \file_system type objects, which are now unable to
+ // be unserialized because of changes to the file storage API made in MDL-46375. Portfolio now stores an id reference to
+ // files instead of the object.
+ // These records are normally removed after a successful export, however, can be left behind if the user abandons the
+ // export attempt (a stale record). Additionally, each stale record cannot be reused and is normally cleaned up by the cron
+ // task core\task\portfolio_cron_task. Since the cron task tries to unserialize them, and generates a warning, we'll remove
+ // all records here.
+ $DB->delete_records_select('portfolio_tempdata', 'id > ?', [0]);
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2017050300.01);
+ }
+
return true;
}
global $CFG, $DB;
+ require_once($CFG->dirroot . '/calendar/lib.php');
// In order to execute this we need bennu.
require_once($CFG->libdir.'/bennu/bennu.inc.php');
3) Move the source code directory into place
mv LTI-Tool-Provider-Library-PHP-3.0.3/* lib/ltiprovider/
-4) Run unit tests on enrol_lti_testsuite
+4) Updates
+Check that the following pull request is included in the release.
+Then remove this step from this file.
+https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/13
+If not, apply manually.
+
+5) Run unit tests on enrol_lti_testsuite
Upgrading Notes
---------------
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_HEADER, true);
- curl_setopt($ch, CURLOPT_SSLVERSION,3);
$chResp = curl_exec($ch);
$this->ok = $chResp !== false;
if ($this->ok) {
}
}
- // Fetch enabled processors
- $processors = get_message_processors(true);
+ // Fetch enabled processors.
+ // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
+ if (empty($savemessage->notification)) {
+ $processors = array_filter(get_message_processors(false), function($processor) {
+ if ($processor->object->force_process_messages()) {
+ return true;
+ }
+
+ return ($processor->enabled && $processor->configured);
+ });
+ } else {
+ $processors = get_message_processors(true);
+ }
// Preset variables
$processorlist = array();
}
// Populate the list of processors we will be using
- if ($permitted == 'forced' && $userisconfigured) {
+ if (empty($savemessage->notification) && $processor->object->force_process_messages()) {
+ $processorlist[] = $processor->name;
+ } else if ($permitted == 'forced' && $userisconfigured) {
// An admin is forcing users to use this message processor. Use this processor unconditionally.
$processorlist[] = $processor->name;
} else if ($permitted == 'permitted' && $userisconfigured && !$eventdata->userto->emailstop) {
/** @var stdClass $accesstoken access token object */
private $accesstoken = null;
/** @var string $refreshtoken refresh token string */
- private $refreshtoken = '';
+ protected $refreshtoken = '';
/** @var string $mocknextresponse string */
private $mocknextresponse = '';
/** @var array $upgradedcodes list of upgraded codes in this request */
* @return array|string|int|boolean value of the field
*/
public final function get($field) {
+ // This is a legacy change to the way files are get/set.
+ // We now only set $this->file to the id of the \stored_file. So, we need to convert that id back to a \stored_file here.
+ if ($field === 'file') {
+ return $this->get_file();
+ }
if (property_exists($this, $field)) {
return $this->{$field};
}
* @return bool
*/
public final function set($field, $value) {
+ // This is a legacy change to the way files are get/set.
+ // Make sure we never save the \stored_file object. Instead, use the id from $file->get_id() - set_file() does this for us.
+ if ($field === 'file') {
+ $this->set_file($value);
+ return true;
+ }
if (property_exists($this, $field)) {
$this->{$field} =& $value;
$this->dirty = true;
*/
abstract class portfolio_plugin_pull_base extends portfolio_plugin_base {
- /** @var stdclass single file */
+ /** @var int $file the id of a single file */
protected $file;
/**
$this->get('exporter')->log_transfer();
}
+ /**
+ * Sets the $file instance var to the id of the supplied \stored_file.
+
+ * This helper allows the $this->get('file') call to return a \stored_file, but means that we only ever record an id reference
+ * in the $file instance var.
+ *
+ * @param \stored_file $file The stored_file instance.
+ * @return void
+ */
+ protected function set_file(\stored_file $file) {
+ $fileid = $file->get_id();
+ if (empty($fileid)) {
+ debugging('stored_file->id should not be empty');
+ $this->file = null;
+ } else {
+ $this->file = $fileid;
+ }
+ }
+
+ /**
+ * Gets the \stored_file object from the file id in the $file instance var.
+ *
+ * @return stored_file|null the \stored_file object if it exists, null otherwise.
+ */
+ protected function get_file() {
+ if (!$this->file) {
+ return null;
+ }
+ // The get_file_by_id call can return false, so normalise to null.
+ $file = get_file_storage()->get_file_by_id($this->file);
+ return ($file) ? $file : null;
+ }
}
defined('MOODLE_INTERNAL') || die();
+global $CFG;
+require_once($CFG->dirroot . '/calendar/lib.php');
+
/**
* Class containing unit tests for the calendar cron task.
*
$emails = $sink->get_messages();
$this->assertCount(1, $emails);
$email = reset($emails);
- $recordexists = $DB->record_exists('message_read', array('id' => $messageid));
+ $recordexists = $DB->record_exists('message', array('id' => $messageid));
$this->assertSame(true, $recordexists);
$this->assertSame($user1->email, $email->from);
$this->assertSame($user2->email, $email->to);
$emails = $sink->get_messages();
$this->assertCount(1, $emails);
$email = reset($emails);
- $recordexists = $DB->record_exists('message_read', array('id' => $messageid));
+ $recordexists = $DB->record_exists('message', array('id' => $messageid));
$this->assertSame(true, $recordexists);
$this->assertSame($user1->email, $email->from);
$this->assertSame($user2->email, $email->to);
$eventsink = $this->redirectEvents();
+ // Will always use the pop-up processor.
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'none', $user2);
$message = new \core\message\message();
$this->assertInstanceOf('\core\event\message_viewed', $events[1]);
$eventsink->clear();
+ // Will always use the pop-up processor.
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
$message = new \core\message\message();
$eventsink->clear();
$user2->emailstop = '0';
+ // Will always use the pop-up processor.
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
$message = new \core\message\message();
$emails = $sink->get_messages();
$this->assertCount(1, $emails);
$email = reset($emails);
- $savedmessage = $DB->get_record('message_read', array('id' => $messageid), '*', MUST_EXIST);
+ $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
$this->assertSame($user1->email, $email->from);
$this->assertSame($user2->email, $email->to);
$this->assertSame($message->subject, $email->subject);
$this->assertNotEmpty($email->header);
$this->assertNotEmpty($email->body);
$sink->clear();
- $this->assertFalse($DB->record_exists('message', array()));
+ $this->assertFalse($DB->record_exists('message_read', array()));
$DB->delete_records('message_read', array());
$events = $eventsink->get_events();
- $this->assertCount(2, $events);
+ $this->assertCount(1, $events);
$this->assertInstanceOf('\core\event\message_sent', $events[0]);
- $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
$eventsink->clear();
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email,popup', $user2);
}
$transaction->allow_commit();
+ // Will always use the pop-up processor.
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'none', $user2);
$message = new \core\message\message();
$this->assertFalse($DB->record_exists('message_read', array()));
$DB->delete_records('message', array());
$events = $eventsink->get_events();
- $this->assertCount(1, $events);
- $this->assertInstanceOf('\core\event\message_sent', $events[0]);
+ $this->assertCount(0, $events);
$eventsink->clear();
$transaction->allow_commit();
$events = $eventsink->get_events();
- $this->assertCount(0, $events);
+ $this->assertCount(1, $events);
+ $this->assertInstanceOf('\core\event\message_sent', $events[0]);
+ // Will always use the pop-up processor.
set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
$message = new \core\message\message();
$sink->clear();
$this->assertFalse($DB->record_exists('message_read', array()));
$events = $eventsink->get_events();
- $this->assertCount(0, $events);
+ $this->assertCount(1, $events);
+ $this->assertInstanceOf('\core\event\message_sent', $events[0]);
$transaction->allow_commit();
$events = $eventsink->get_events();
$this->assertCount(2, $events);
- $this->assertInstanceOf('\core\event\message_sent', $events[0]);
- $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
+ $this->assertInstanceOf('\core\event\message_sent', $events[1]);
$eventsink->clear();
$transaction = $DB->start_delegated_transaction();
message_send($message);
message_send($message);
- $this->assertCount(2, $DB->get_records('message'));
- $this->assertCount(1, $DB->get_records('message_read'));
+ $this->assertCount(3, $DB->get_records('message'));
+ $this->assertFalse($DB->record_exists('message_read', array()));
$events = $eventsink->get_events();
$this->assertCount(0, $events);
$transaction->allow_commit();
$events = $eventsink->get_events();
- $this->assertCount(4, $events);
+ $this->assertCount(2, $events);
$this->assertInstanceOf('\core\event\message_sent', $events[0]);
- $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
- $this->assertInstanceOf('\core\event\message_sent', $events[2]);
- $this->assertInstanceOf('\core\event\message_viewed', $events[3]);
+ $this->assertInstanceOf('\core\event\message_sent', $events[1]);
$eventsink->clear();
$DB->delete_records('message', array());
$DB->delete_records('message_read', array());
$this->assertCount(0, $DB->get_records('message'));
$this->assertCount(0, $DB->get_records('message_read'));
message_send($message);
- $this->assertCount(0, $DB->get_records('message'));
- $this->assertCount(1, $DB->get_records('message_read'));
+ $this->assertCount(1, $DB->get_records('message'));
+ $this->assertCount(0, $DB->get_records('message_read'));
$events = $eventsink->get_events();
- $this->assertCount(2, $events);
+ $this->assertCount(1, $events);
+ $this->assertInstanceOf('\core\event\message_sent', $events[0]);
$sink->clear();
$DB->delete_records('message_read', array());
}
$this->assertEquals(count($blockinstances), $DB->count_records('block_positions', ['subpage' => $page1->id, 'pagetype' => 'my-index', 'contextid' => $context1->id]));
$this->assertEquals(0, $DB->count_records('block_positions', ['subpage' => $page2->id, 'pagetype' => 'my-index']));
}
+
+ /**
+ * Test the conversion of auth plugin settings names.
+ */
+ public function test_upgrade_fix_config_auth_plugin_names() {
+ $this->resetAfterTest();
+
+ // Let the plugin auth_foo use legacy format only.
+ set_config('name1', 'val1', 'auth/foo');
+ set_config('name2', 'val2', 'auth/foo');
+
+ // Let the plugin auth_bar use new format only.
+ set_config('name1', 'val1', 'auth_bar');
+ set_config('name2', 'val2', 'auth_bar');
+
+ // Let the plugin auth_baz use a mix of legacy and new format, with no conflicts.
+ set_config('name1', 'val1', 'auth_baz');
+ set_config('name1', 'val1', 'auth/baz');
+ set_config('name2', 'val2', 'auth/baz');
+ set_config('name3', 'val3', 'auth_baz');
+
+ // Let the plugin auth_qux use a mix of legacy and new format, with conflicts.
+ set_config('name1', 'val1', 'auth_qux');
+ set_config('name1', 'val2', 'auth/qux');
+
+ // Execute the migration.
+ upgrade_fix_config_auth_plugin_names('foo');
+ upgrade_fix_config_auth_plugin_names('bar');
+ upgrade_fix_config_auth_plugin_names('baz');
+ upgrade_fix_config_auth_plugin_names('qux');
+
+ // Assert that legacy settings are gone and no new were introduced.
+ $this->assertEmpty((array) get_config('auth/foo'));
+ $this->assertEmpty((array) get_config('auth/bar'));
+ $this->assertEmpty((array) get_config('auth/baz'));
+ $this->assertEmpty((array) get_config('auth/qux'));
+
+ // Assert values were simply kept where there was no conflict.
+ $this->assertSame('val1', get_config('auth_foo', 'name1'));
+ $this->assertSame('val2', get_config('auth_foo', 'name2'));
+
+ $this->assertSame('val1', get_config('auth_bar', 'name1'));
+ $this->assertSame('val2', get_config('auth_bar', 'name2'));
+
+ $this->assertSame('val1', get_config('auth_baz', 'name1'));
+ $this->assertSame('val2', get_config('auth_baz', 'name2'));
+ $this->assertSame('val3', get_config('auth_baz', 'name3'));
+
+ // Assert the new format took precedence in case of conflict.
+ $this->assertSame('val1', get_config('auth_qux', 'name1'));
+ }
}
=== 3.3 ===
+* Behat compatibility changes are now being documented at
+ https://docs.moodle.org/dev/Acceptance_testing/Compatibility_changes
* PHPUnit's bootstrap has been changed to use HTTPS wwwroot (https://www.example.com/moodle) from previous HTTP version. Any
existing test expecting the old HTTP URLs will need to be switched to the new HTTPS value (reference: MDL-54901).
* The information returned by the idp list has changed. This is usually only rendered by the login page and login block.
return null;
}
+
+/**
+ * Fix how auth plugins are called in the 'config_plugins' table.
+ *
+ * For legacy reasons, the auth plugins did not always use their frankenstyle
+ * component name in the 'plugin' column of the 'config_plugins' table. This is
+ * a helper function to correctly migrate the legacy settings into the expected
+ * and consistent way.
+ *
+ * @param string $plugin the auth plugin name such as 'cas', 'manual' or 'mnet'
+ */
+function upgrade_fix_config_auth_plugin_names($plugin) {
+ global $CFG, $DB, $OUTPUT;
+
+ $legacy = (array) get_config('auth/'.$plugin);
+ $current = (array) get_config('auth_'.$plugin);
+
+ // I don't want to rely on array_merge() and friends here just in case
+ // there was some crazy setting with a numerical name.
+
+ if ($legacy) {
+ $new = $legacy;
+ } else {
+ $new = [];
+ }
+
+ if ($current) {
+ foreach ($current as $name => $value) {
+ if (isset($legacy[$name]) && ($legacy[$name] !== $value)) {
+ // No need to pollute the output during unit tests.
+ if (!empty($CFG->upgraderunning)) {
+ $message = get_string('settingmigrationmismatch', 'core_auth', [
+ 'plugin' => 'auth_'.$plugin,
+ 'setting' => s($name),
+ 'legacy' => s($legacy[$name]),
+ 'current' => s($value),
+ ]);
+ echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_ERROR);
+
+ upgrade_log(UPGRADE_LOG_NOTICE, 'auth_'.$plugin, 'Setting values mismatch detected',
+ 'SETTING: '.$name. ' LEGACY: '.$legacy[$name].' CURRENT: '.$value);
+ }
+ }
+
+ $new[$name] = $value;
+ }
+ }
+
+ foreach ($new as $name => $value) {
+ set_config($name, $value, 'auth_'.$plugin);
+ unset_config($name, 'auth/'.$plugin);
+ }
+}
public function has_message_preferences() {
return true;
}
+
+ /**
+ * Determines if this processor should process a message regardless of user preferences or site settings.
+ *
+ * @return bool
+ */
+ public function force_process_messages() {
+ return false;
+ }
}
$DB->update_record('message_popup', $record);
}
}
+
+ /**
+ * Determines if this processor should process a message regardless of user preferences or site settings.
+ *
+ * @return bool
+ */
+ public function force_process_messages() {
+ global $CFG;
+
+ return !empty($CFG->messaging);
+ }
}
new external_single_structure(
array(
'name' => new external_value(PARAM_TEXT, 'field name'),
- 'description' => new external_value(PARAM_TEXT, 'field description'),
+ 'description' => new external_value(PARAM_RAW, 'field description'),
'text' => new external_value (PARAM_RAW, 'field value'),
'format' => new external_format_value ('text')
)
* @param string $filearea
* @param array $args
* @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
* @return bool false if file not found, does not return if found - just send the file
*/
function assignfeedback_editpdf_pluginfile($course,
context $context,
$filearea,
$args,
- $forcedownload) {
+ $forcedownload,
+ array $options=array()) {
global $USER, $DB, $CFG;
if ($context->contextlevel == CONTEXT_MODULE) {
return false;
}
// Download MUST be forced - security!
- send_stored_file($file, 0, 0, true);// Check if we want to retrieve the stamps.
+ send_stored_file($file, 0, 0, true, $options);// Check if we want to retrieve the stamps.
}
}
* @param string $filearea
* @param array $args
* @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
* @return bool false if file not found, does not return if found - just send the file
*/
function assignfeedback_file_pluginfile($course,
context $context,
$filearea,
$args,
- $forcedownload) {
+ $forcedownload,
+ array $options=array()) {
global $USER, $DB;
if ($context->contextlevel != CONTEXT_MODULE) {
return false;
}
// Download MUST be forced - security!
- send_stored_file($file, 0, 0, true);
+ send_stored_file($file, 0, 0, true, $options);
}
* Returns user override
*
* Algorithm: For each assign setting, if there is a matching user-specific override,
- * then use that otherwise, if there are group-specific overrides, return the most
- * lenient combination of them. If neither applies, leave the assign setting unchanged.
+ * then use that otherwise, if there are group-specific overrides, use the one with the
+ * lowest sort order. If neither applies, leave the assign setting unchanged.
*
* @param int $userid The userid.
- * @return override if exist
+ * @return stdClass The override
*/
public function override_exists($userid) {
global $DB;
- // Check for user override.
- $override = $DB->get_record('assign_overrides', array('assignid' => $this->get_instance()->id, 'userid' => $userid));
+ // Gets an assoc array containing the keys for defined user overrides only.
+ $getuseroverride = function($userid) use ($DB) {
+ $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
+ return $useroverride ? get_object_vars($useroverride) : [];
+ };
- if (!$override) {
- $override = new stdClass();
- $override->duedate = null;
- $override->cutoffdate = null;
- $override->allowsubmissionsfromdate = null;
- }
+ // Gets an assoc array containing the keys for defined group overrides only.
+ $getgroupoverride = function($userid) use ($DB) {
+ $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
- // Check for group overrides.
- $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
+ if (empty($groupings[0])) {
+ return [];
+ }
- if (!empty($groupings[0])) {
// Select all overrides that apply to the User's groups.
list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
$sql = "SELECT * FROM {assign_overrides}
- WHERE groupid $extra AND assignid = ?";
+ WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
$params[] = $this->get_instance()->id;
- $records = $DB->get_records_sql($sql, $params);
-
- // Combine the overrides.
- $duedates = array();
- $cutoffdates = array();
- $allowsubmissionsfromdates = array();
-
- foreach ($records as $gpoverride) {
- if (isset($gpoverride->duedate)) {
- $duedates[] = $gpoverride->duedate;
- }
- if (isset($gpoverride->cutoffdate)) {
- $cutoffdates[] = $gpoverride->cutoffdate;
- }
- if (isset($gpoverride->allowsubmissionsfromdate)) {
- $allowsubmissionsfromdates[] = $gpoverride->allowsubmissionsfromdate;
- }
- }
- // If there is a user override for a setting, ignore the group override.
- if (is_null($override->allowsubmissionsfromdate) && count($allowsubmissionsfromdates)) {
- $override->allowsubmissionsfromdate = min($allowsubmissionsfromdates);
- }
- if (is_null($override->cutoffdate) && count($cutoffdates)) {
- if (in_array(0, $cutoffdates)) {
- $override->cutoffdate = 0;
- } else {
- $override->cutoffdate = max($cutoffdates);
- }
- }
- if (is_null($override->duedate) && count($duedates)) {
- if (in_array(0, $duedates)) {
- $override->duedate = 0;
- } else {
- $override->duedate = max($duedates);
- }
- }
-
- }
-
- return $override;
+ $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
+
+ return $groupoverride ? get_object_vars($groupoverride) : [];
+ };
+
+ // Later arguments clobber earlier ones with array_merge. The two helper functions
+ // return arrays containing keys for only the defined overrides. So we get the
+ // desired behaviour as per the algorithm.
+ return (object)array_merge(
+ ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
+ $getgroupoverride($userid),
+ $getuseroverride($userid)
+ );
}
/**
* @param string $filearea
* @param array $args
* @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
* @return bool false if file not found, does not return if found - just send the file
*/
function assignsubmission_file_pluginfile($course,
context $context,
$filearea,
$args,
- $forcedownload) {
+ $forcedownload,
+ array $options=array()) {
global $DB, $CFG;
if ($context->contextlevel != CONTEXT_MODULE) {
}
// Download MUST be forced - security!
- send_stored_file($file, 0, 0, true);
+ send_stored_file($file, 0, 0, true, $options);
}
private function get_nonexistent_file_types($types) {
$nonexistent = [];
foreach ($this->get_typesets($types) as $type) {
- $coretypes = core_filetypes::get_types();
- // We can allow any extension, but validate groups & mimetypes.
- if (strpos($type, '.') === false) {
- // If there's no dot, check if it's a group.
- $extensions = file_get_typegroup('extension', [$type]);
- if (empty($extensions)) {
- // If there's no extensions under that group, it doesn't exist.
- $nonexistent[$type] = true;
- }
+ // If there's no extensions under that group, it doesn't exist.
+ $extensions = file_get_typegroup('extension', [$type]);
+ if (empty($extensions)) {
+ $nonexistent[$type] = true;
}
}
return array_keys($nonexistent);
And I navigate to "Edit settings" in current page administration
When I set the field "Accepted file types" to "image/png;doesntexist;.anything;unreal/mimetype;nodot"
And I press "Save and display"
- And I should see "The following file types were not recognised: doesntexist unreal/mimetype nodot"
+ And I should see "The following file types were not recognised: doesntexist .anything unreal/mimetype nodot"
And I set the field "Accepted file types" to "image/png;spreadsheet"
And I press "Save and display"
And I navigate to "Edit settings" in current page administration
*/
public function get_nonexistent_file_types_provider() {
return [
- 'Nonexistent extensions are allowed' => [
+ 'Nonexistent extensions are not allowed' => [
'filetypes' => '.rat',
- 'expected' => []
+ 'expected' => ['.rat']
],
- 'Multiple nonexistent extensions are allowed' => [
+ 'Multiple nonexistent extensions are not allowed' => [
'filetypes' => '.ricefield .rat',
- 'expected' => []
+ 'expected' => ['.ricefield', '.rat']
],
'Existent extension is allowed' => [
'filetypes' => '.xml',
* @param string $filearea
* @param array $args
* @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
* @return bool false if file not found, does not return if found - just send the file
*/
-function assignsubmission_onlinetext_pluginfile($course, $cm, context $context, $filearea, $args, $forcedownload) {
+function assignsubmission_onlinetext_pluginfile($course,
+ $cm,
+ context $context,
+ $filearea,
+ $args,
+ $forcedownload,
+ array $options=array()) {
global $DB, $CFG;
if ($context->contextlevel != CONTEXT_MODULE) {
}
// Download MUST be forced - security!
- send_stored_file($file, 0, 0, true);
+ send_stored_file($file, 0, 0, true, $options);
}
* @return array An array of field names and descriptions. (name=>description, ...)
*/
public function get_editor_fields() {
- return array('onlinetext' => get_string('pluginname', 'assignsubmission_comments'));
+ return array('onlinetext' => get_string('pluginname', 'assignsubmission_onlinetext'));
}
/**
$this->assertEquals(2, $usingfilearea);
}
+ /**
+ * Test override exists
+ *
+ * This function needs to obey the group override logic as per the assign grading table and
+ * the overview block.
+ */
+ public function test_override_exists() {
+ global $DB;
+
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course();
+
+ // Create an assign instance.
+ $assign = $this->create_instance(['course' => $course]);
+ $assigninstance = $assign->get_instance();
+
+ // Create users.
+ $users = [
+ 'Only in group A' => $this->getDataGenerator()->create_user(),
+ 'Only in group B' => $this->getDataGenerator()->create_user(),
+ 'In group A and B (no user override)' => $this->getDataGenerator()->create_user(),
+ 'In group A and B (user override)' => $this->getDataGenerator()->create_user(),
+ 'In no groups' => $this->getDataGenerator()->create_user()
+ ];
+
+ // Enrol users.
+ foreach ($users as $user) {
+ $this->getDataGenerator()->enrol_user($user->id, $course->id);
+ }
+
+ // Create groups.
+ $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+ $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+ // Add members to groups.
+ // Group A.
+ $this->getDataGenerator()->create_group_member(
+ ['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]);
+ $this->getDataGenerator()->create_group_member(
+ ['groupid' => $groupa->id, 'userid' => $users['In group A and B (no user override)']->id]);
+ $this->getDataGenerator()->create_group_member(
+ ['groupid' => $groupa->id, 'userid' => $users['In group A and B (user override)']->id]);
+
+ // Group B.
+ $this->getDataGenerator()->create_group_member(
+ ['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]);
+ $this->getDataGenerator()->create_group_member(
+ ['groupid' => $groupb->id, 'userid' => $users['In group A and B (no user override)']->id]);
+ $this->getDataGenerator()->create_group_member(
+ ['groupid' => $groupb->id, 'userid' => $users['In group A and B (user override)']->id]);
+
+ // Overrides for each of the groups, and a user override.
+ $overrides = [
+ // Override for group A, highest priority (numerically lowest sortorder).
+ [
+ 'assignid' => $assigninstance->id,
+ 'groupid' => $groupa->id,
+ 'userid' => null,
+ 'sortorder' => 1,
+ 'allowsubmissionsfromdate' => 1,
+ 'duedate' => 2,
+ 'cutoffdate' => 3
+ ],
+ // Override for group B, lower priority (numerically higher sortorder).
+ [
+ 'assignid' => $assigninstance->id,
+ 'groupid' => $groupb->id,
+ 'userid' => null,
+ 'sortorder' => 2,
+ 'allowsubmissionsfromdate' => 5,
+ 'duedate' => 6,
+ 'cutoffdate' => 6
+ ],
+ // User override.
+ [
+ 'assignid' => $assigninstance->id,
+ 'groupid' => null,
+ 'userid' => $users['In group A and B (user override)']->id,
+ 'sortorder' => null,
+ 'allowsubmissionsfromdate' => 7,
+ 'duedate' => 8,
+ 'cutoffdate' => 9
+ ],
+ ];
+
+ // Kinda hacky, need to add the ID to the overrides in the above array
+ // for later.
+ foreach ($overrides as &$override) {
+ $override['id'] = $DB->insert_record('assign_overrides', $override);
+ }
+
+ $returnedoverrides = array_reduce(array_keys($users), function($carry, $description) use ($users, $assign) {
+ return $carry + ['For user ' . lcfirst($description) => $assign->override_exists($users[$description]->id)];
+ }, []);
+
+ // Test we get back the correct override from override_exists (== checks all object members match).
+ // User only in group A should see the group A override.
+ $this->assertTrue($returnedoverrides['For user only in group A'] == (object)$overrides[0]);
+ // User only in group B should see the group B override.
+ $this->assertTrue($returnedoverrides['For user only in group B'] == (object)$overrides[1]);
+ // User in group A and B, with no user override should see the group A override
+ // as it has higher priority (numerically lower sortorder).
+ $this->assertTrue($returnedoverrides['For user in group A and B (no user override)'] == (object)$overrides[0]);
+ // User in group A and B, with a user override should see the user override
+ // as it has higher priority (numerically lower sortorder).
+ $this->assertTrue($returnedoverrides['For user in group A and B (user override)'] == (object)$overrides[2]);
+ // User with no overrides should get nothing.
+ $this->assertNull($returnedoverrides['For user in no groups']->duedate);
+ $this->assertNull($returnedoverrides['For user in no groups']->cutoffdate);
+ $this->assertNull($returnedoverrides['For user in no groups']->allowsubmissionsfromdate);
+ }
+
/**
* Test the quicksave grades processor
*/
This files describes API changes in the assign code.
=== 3.3 ===
+* All pluginfile file serving functions now pass through the options to send_stored_file() (all assignment plugins should do
+ the same).
* Fixed calendar event types for overridden due dates from 'close' to 'due'.
* Removed calendar event type of 'open', since mod_assign only has the 'due' event type. No point in creating an override event
for an event type that does not exist.
return null;
}
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
*/
function mod_choice_core_calendar_provide_event_action(calendar_event $event,
\core_calendar\action_factory $factory) {
- global $DB;
$cm = get_fast_modinfo($event->courseid)->instances['choice'][$event->instance];
- $choice = $DB->get_record('choice', array('id' => $event->instance), 'id, timeopen, timeclose');
$now = time();
- if ($choice->timeclose && $choice->timeclose < $now) {
+ if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < $now) {
// The choice has closed so the user can no longer submit anything.
return null;
}
// The choice is actionable if we don't have a start time or the start time is
// in the past.
- $actionable = (!$choice->timeopen || $choice->timeopen <= $now);
+ $actionable = (empty($cm->customdata['timeopen']) || $cm->customdata['timeopen'] <= $now);
- if ($actionable && choice_get_my_response($choice)) {
+ if ($actionable && choice_get_my_response((object)['id' => $event->instance])) {
// There is no action if the user has already submitted their choice.
return null;
}
global $DB;
$dbparams = ['id' => $coursemodule->instance];
- $fields = 'id, name, intro, introformat, completionsubmit';
+ $fields = 'id, name, intro, introformat, completionsubmit, timeopen, timeclose';
if (!$choice = $DB->get_record('choice', $dbparams, $fields)) {
return false;
}
if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
$result->customdata['customcompletionrules']['completionsubmit'] = $choice->completionsubmit;
}
+ // Populate some other values that can be used in calendar or on dashboard.
+ if ($choice->timeopen) {
+ $result->customdata['timeopen'] = $choice->timeopen;
+ }
+ if ($choice->timeclose) {
+ $result->customdata['timeclose'] = $choice->timeclose;
+ }
return $result;
}
$users = choice_get_response_data($choice, $cm, $groupmode, $onlyactive);
+ $extrafields = get_extra_user_fields($context);
+
if ($download == "ods" && has_capability('mod/choice:downloadresponses', $context)) {
require_once("$CFG->libdir/odslib.class.php");
$myxls = $workbook->add_worksheet($strresponses);
/// Print names of all the fields
- $myxls->write_string(0,0,get_string("lastname"));
- $myxls->write_string(0,1,get_string("firstname"));
- $myxls->write_string(0,2,get_string("idnumber"));
- $myxls->write_string(0,3,get_string("group"));
- $myxls->write_string(0,4,get_string("choice","choice"));
+ $i = 0;
+ $myxls->write_string(0, $i++, get_string("lastname"));
+ $myxls->write_string(0, $i++, get_string("firstname"));
- /// generate the data for the body of the spreadsheet
- $i=0;
- $row=1;
+ // Add headers for extra user fields.
+ foreach ($extrafields as $field) {
+ $myxls->write_string(0, $i++, get_user_field_name($field));
+ }
+
+ $myxls->write_string(0, $i++, get_string("group"));
+ $myxls->write_string(0, $i++, get_string("choice", "choice"));
+
+ // Generate the data for the body of the spreadsheet.
+ $row = 1;
if ($users) {
foreach ($users as $option => $userid) {
$option_text = choice_get_option_text($choice, $option);
- foreach($userid as $user) {
- $myxls->write_string($row,0,$user->lastname);
- $myxls->write_string($row,1,$user->firstname);
- $studentid=(!empty($user->idnumber) ? $user->idnumber : " ");
- $myxls->write_string($row,2,$studentid);
+ foreach ($userid as $user) {
+ $i = 0;
+ $myxls->write_string($row, $i++, $user->lastname);
+ $myxls->write_string($row, $i++, $user->firstname);
+ foreach ($extrafields as $field) {
+ $myxls->write_string($row, $i++, $user->$field);
+ }
$ug2 = '';
if ($usergrps = groups_get_all_groups($course->id, $user->id)) {
foreach ($usergrps as $ug) {
- $ug2 = $ug2. $ug->name;
+ $ug2 = $ug2 . $ug->name;
}
}
- $myxls->write_string($row,3,$ug2);
+ $myxls->write_string($row, $i++, $ug2);
if (isset($option_text)) {
- $myxls->write_string($row,4,format_string($option_text,true));
+ $myxls->write_string($row, $i++, format_string($option_text, true));
}
$row++;
- $pos=4;
}
}
}
$myxls = $workbook->add_worksheet($strresponses);
/// Print names of all the fields
- $myxls->write_string(0,0,get_string("lastname"));
- $myxls->write_string(0,1,get_string("firstname"));
- $myxls->write_string(0,2,get_string("idnumber"));
- $myxls->write_string(0,3,get_string("group"));
- $myxls->write_string(0,4,get_string("choice","choice"));
+ $i = 0;
+ $myxls->write_string(0, $i++, get_string("lastname"));
+ $myxls->write_string(0, $i++, get_string("firstname"));
+ // Add headers for extra user fields.
+ foreach ($extrafields as $field) {
+ $myxls->write_string(0, $i++, get_user_field_name($field));
+ }
- /// generate the data for the body of the spreadsheet
- $i=0;
- $row=1;
+ $myxls->write_string(0, $i++, get_string("group"));
+ $myxls->write_string(0, $i++, get_string("choice", "choice"));
+
+ // Generate the data for the body of the spreadsheet.
+ $row = 1;
if ($users) {
foreach ($users as $option => $userid) {
+ $i = 0;
$option_text = choice_get_option_text($choice, $option);
foreach($userid as $user) {
- $myxls->write_string($row,0,$user->lastname);
- $myxls->write_string($row,1,$user->firstname);
- $studentid=(!empty($user->idnumber) ? $user->idnumber : " ");
- $myxls->write_string($row,2,$studentid);
+ $i = 0;
+ $myxls->write_string($row, $i++, $user->lastname);
+ $myxls->write_string($row, $i++, $user->firstname);
+ foreach ($extrafields as $field) {
+ $myxls->write_string($row, $i++, $user->$field);
+ }
$ug2 = '';
if ($usergrps = groups_get_all_groups($course->id, $user->id)) {
foreach ($usergrps as $ug) {
- $ug2 = $ug2. $ug->name;
+ $ug2 = $ug2 . $ug->name;
}
}
- $myxls->write_string($row,3,$ug2);
+ $myxls->write_string($row, $i++, $ug2);
if (isset($option_text)) {
- $myxls->write_string($row,4,format_string($option_text,true));
+ $myxls->write_string($row, $i++, format_string($option_text, true));
}
$row++;
}
}
- $pos=4;
}
/// Close the workbook
$workbook->close();
/// Print names of all the fields
- echo get_string("lastname")."\t".get_string("firstname") . "\t". get_string("idnumber") . "\t";
+ echo get_string("lastname") . "\t" . get_string("firstname") . "\t";
+
+ // Add headers for extra user fields.
+ foreach ($extrafields as $field) {
+ echo get_user_field_name($field) . "\t";
+ }
+
echo get_string("group"). "\t";
echo get_string("choice","choice"). "\n";
foreach ($users as $option => $userid) {
$option_text = choice_get_option_text($choice, $option);
foreach($userid as $user) {
- echo $user->lastname;
- echo "\t".$user->firstname;
- $studentid = " ";
- if (!empty($user->idnumber)) {
- $studentid = $user->idnumber;
+ echo $user->lastname . "\t";
+ echo $user->firstname . "\t";
+ foreach ($extrafields as $field) {
+ echo $user->$field . "\t";
}
- echo "\t". $studentid."\t";
$ug2 = '';
if ($usergrps = groups_get_all_groups($course->id, $user->id)) {
foreach ($usergrps as $ug) {
*/
function mod_data_core_calendar_provide_event_action(calendar_event $event,
\core_calendar\action_factory $factory) {
- global $DB;
$cm = get_fast_modinfo($event->courseid)->instances['data'][$event->instance];
- $data = $DB->get_record('data', array('id' => $event->instance), 'id, timeavailablefrom, timeavailableto');
-
- if ($data->timeavailablefrom && $data->timeavailableto) {
- $actionable = (time() >= $data->timeavailablefrom) && (time() <= $data->timeavailableto);
- } else if ($data->timeavailableto) {
- $actionable = time() < $data->timeavailableto;
- } else if ($data->timeavailablefrom) {
- $actionable = time() >= $data->timeavailablefrom;
- } else {
- $actionable = true;
+ $now = time();
+
+ if (!empty($cm->customdata['timeavailableto']) && $cm->customdata['timeavailableto'] < $now) {
+ // The module has closed so the user can no longer submit anything.
+ return null;
}
+ // The module is actionable if we don't have a start time or the start time is
+ // in the past.
+ $actionable = (empty($cm->customdata['timeavailablefrom']) || $cm->customdata['timeavailablefrom'] <= $now);
+
return $factory->create_instance(
get_string('add', 'data'),
new \moodle_url('/mod/data/view.php', array('id' => $cm->id)),
global $DB;
$dbparams = ['id' => $coursemodule->instance];
- $fields = 'id, name, intro, introformat, completionentries';
+ $fields = 'id, name, intro, introformat, completionentries, timeavailablefrom, timeavailableto';
if (!$data = $DB->get_record('data', $dbparams, $fields)) {
return false;
}
if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
$result->customdata['customcompletionrules']['completionentries'] = $data->completionentries;
}
+ // Other properties that may be used in calendar or on dashboard.
+ if ($data->timeavailablefrom) {
+ $result->customdata['timeavailablefrom'] = $data->timeavailablefrom;
+ }
+ if ($data->timeavailableto) {
+ $result->customdata['timeavailableto'] = $data->timeavailableto;
+ }
return $result;
}
// Decorate action event.
$actionevent = mod_data_core_calendar_provide_event_action($event, $factory);
- // Confirm the event was decorated.
- $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
- $this->assertEquals(get_string('add', 'data'), $actionevent->get_name());
- $this->assertInstanceOf('moodle_url', $actionevent->get_url());
- $this->assertEquals(1, $actionevent->get_item_count());
- $this->assertFalse($actionevent->is_actionable());
+ // No event on the dashboard if module is closed.
+ $this->assertNull($actionevent);
}
public function test_data_core_calendar_provide_event_action_open_in_future() {
$mygroupid = groups_get_activity_group($cm, true);
groups_print_activity_menu($cm, $url);
+// Button "Export to excel".
+if (has_capability('mod/feedback:viewreports', $context) && $feedbackstructure->get_items()) {
+ echo $OUTPUT->container_start('form-buttons');
+ $aurl = new moodle_url('/mod/feedback/analysis_to_excel.php', ['sesskey' => sesskey(), 'id' => $id]);
+ echo $OUTPUT->single_button($aurl, get_string('export_to_excel', 'feedback'));
+ echo $OUTPUT->container_end();
+}
+
// Show the summary.
$summary = new mod_feedback\output\summary($feedbackstructure, $mygroupid);
echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT));
$courseselectform->display();
+// Button "Export to excel".
+if (has_capability('mod/feedback:viewreports', $context) && $feedbackstructure->get_items()) {
+ echo $OUTPUT->container_start('form-buttons');
+ $aurl = new moodle_url('/mod/feedback/analysis_to_excel.php',
+ ['sesskey' => sesskey(), 'id' => $id, 'courseid' => (int)$courseid]);
+ echo $OUTPUT->single_button($aurl, get_string('export_to_excel', 'feedback'));
+ echo $OUTPUT->container_end();
+}
+
// Show the summary.
$summary = new mod_feedback\output\summary($feedbackstructure);
echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT));
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * prints an analysed excel-spreadsheet of the feedback
+ *
+ * @copyright Andreas Grabs
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package mod_feedback
+ */
+
+require_once("../../config.php");
+require_once("lib.php");
+require_once("$CFG->libdir/excellib.class.php");
+
+$id = required_param('id', PARAM_INT); // Course module id.
+$courseid = optional_param('courseid', '0', PARAM_INT);
+
+$url = new moodle_url('/mod/feedback/analysis_to_excel.php', array('id' => $id));
+if ($courseid) {
+ $url->param('courseid', $courseid);
+}
+$PAGE->set_url($url);
+
+list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback');
+require_login($course, false, $cm);
+$context = context_module::instance($cm->id);
+require_capability('mod/feedback:viewreports', $context);
+
+$feedback = $PAGE->activityrecord;
+
+// Buffering any output. This prevents some output before the excel-header will be send.
+ob_start();
+ob_end_clean();
+
+// Get the questions (item-names).
+$feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
+if (!$items = $feedbackstructure->get_items(true)) {
+ print_error('no_items_available_yet', 'feedback', $cm->url);
+}
+
+$mygroupid = groups_get_activity_group($cm);
+
+// Creating a workbook.
+$filename = "feedback_" . clean_filename($cm->get_formatted_name()) . ".xls";
+$workbook = new MoodleExcelWorkbook($filename);
+
+// Creating the worksheet.
+error_reporting(0);
+$worksheet1 = $workbook->add_worksheet();
+error_reporting($CFG->debug);
+$worksheet1->hide_gridlines();
+$worksheet1->set_column(0, 0, 10);
+$worksheet1->set_column(1, 1, 30);
+$worksheet1->set_column(2, 20, 15);
+
+// Creating the needed formats.
+$xlsformats = new stdClass();
+$xlsformats->head1 = $workbook->add_format(['bold' => 1, 'size' => 12]);
+$xlsformats->head2 = $workbook->add_format(['align' => 'left', 'bold' => 1, 'bottum' => 2]);
+$xlsformats->default = $workbook->add_format(['align' => 'left', 'v_align' => 'top']);
+$xlsformats->value_bold = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top']);
+$xlsformats->procent = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top', 'num_format' => '#,##0.00%']);
+
+// Writing the table header.
+$rowoffset1 = 0;
+$worksheet1->write_string($rowoffset1, 0, userdate(time()), $xlsformats->head1);
+
+// Get the completeds.
+$completedscount = feedback_get_completeds_group_count($feedback, $mygroupid, $courseid);
+if ($completedscount > 0) {
+ // Write the count of completeds.
+ $rowoffset1++;
+ $worksheet1->write_string($rowoffset1,
+ 0,
+ $cm->get_module_type_name(true).': '.strval($completedscount),
+ $xlsformats->head1);
+}
+
+$rowoffset1++;
+$worksheet1->write_string($rowoffset1,
+ 0,
+ get_string('questions', 'feedback').': '. strval(count($items)),
+ $xlsformats->head1);
+
+$rowoffset1 += 2;
+$worksheet1->write_string($rowoffset1, 0, get_string('item_label', 'feedback'), $xlsformats->head1);
+$worksheet1->write_string($rowoffset1, 1, get_string('question', 'feedback'), $xlsformats->head1);
+$worksheet1->write_string($rowoffset1, 2, get_string('responses', 'feedback'), $xlsformats->head1);
+$rowoffset1++;
+
+foreach ($items as $item) {
+ // Get the class of item-typ.
+ $itemobj = feedback_get_item_class($item->typ);
+ $rowoffset1 = $itemobj->excelprint_item($worksheet1,
+ $rowoffset1,
+ $xlsformats,
+ $item,
+ $mygroupid,
+ $courseid);
+}
+
+$workbook->close();
/**
* Constructor
*
- * @param stdClass $feedback feedback object, in case of the template
- * this is the current feedback the template is accessed from
+ * @param stdClass $feedback feedback object
* @param cm_info $cm course module object corresponding to the $feedback
+ * (at least one of $feedback or $cm is required)
* @param int $courseid current course (for site feedbacks only)
* @param bool $iscompleted has feedback been already completed? If yes either completedid or userid must be specified.
* @param int $completedid id in the table feedback_completed, may be omitted if userid is specified
*/
public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null, $userid = null) {
global $DB;
- // Make sure courseid is always set for site feedback and never for course feedback.
- if ($feedback->course == SITEID) {
- $courseid = $courseid ?: SITEID;
- } else {
- $courseid = 0;
- }
parent::__construct($feedback, $cm, $courseid, 0);
+ // Make sure courseid is always set for site feedback.
+ if ($this->feedback->course == SITEID && !$this->courseid) {
+ $this->courseid = SITEID;
+ }
if ($iscompleted) {
// Retrieve information about the completion.
$this->iscompleted = true;
- $params = array('feedback' => $feedback->id);
+ $params = array('feedback' => $this->feedback->id);
if (!$userid && !$completedid) {
throw new coding_exception('Either $completedid or $userid must be specified for completed feedbacks');
}
// Not possible to retrieve completed anonymous feedback.
return false;
}
- $params = array('feedback' => $this->feedback->id, 'userid' => $USER->id);
+ $params = array('feedback' => $this->feedback->id, 'userid' => $USER->id, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO);
if ($this->get_courseid()) {
$params['courseid'] = $this->get_courseid();
}
$this->jumpto = $nextpage;
} else {
$this->save_response();
- if (!$this->feedback->page_after_submit) {
+ if (!$this->get_feedback()->page_after_submit) {
\core\notification::success(get_string('entries_saved', 'feedback'));
}
$this->justcompleted = true;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_feedback_structure {
- /** @var stdClass */
+ /** @var stdClass record from 'feedback' table.
+ * Reliably has fields: id, course, timeopen, timeclose, anonymous, completionsubmit.
+ * For full object or to access any other field use $this->get_feedback()
+ */
protected $feedback;
/** @var cm_info */
protected $cm;
*
* @param stdClass $feedback feedback object, in case of the template
* this is the current feedback the template is accessed from
- * @param cm_info $cm course module object corresponding to the $feedback
+ * @param stdClass|cm_info $cm course module object corresponding to the $feedback
+ * (at least one of $feedback or $cm is required)
* @param int $courseid current course (for site feedbacks only)
* @param int $templateid template id if this class represents the template structure
*/
public function __construct($feedback, $cm, $courseid = 0, $templateid = null) {
- $this->feedback = $feedback;
- $this->cm = $cm;
- $this->courseid = ($feedback->course == SITEID) ? $courseid : 0;
+ if ((empty($feedback->id) || empty($feedback->course)) && (empty($cm->instance) || empty($cm->course))) {
+ throw new coding_exception('Either $feedback or $cm must be passed to constructor');
+ }
+ $this->feedback = $feedback ?: (object)['id' => $cm->instance, 'course' => $cm->course];
+ $this->cm = ($cm && $cm instanceof cm_info) ? $cm :
+ get_fast_modinfo($this->feedback->course)->instances['feedback'][$this->feedback->id];
$this->templateid = $templateid;
+ $this->courseid = ($this->feedback->course == SITEID) ? $courseid : 0;
+
+ if (!$feedback) {
+ // If feedback object was not specified, populate object with fields required for the most of methods.
+ // These fields were added to course module cache in feedback_get_coursemodule_info().
+ // Full instance record can be retrieved by calling mod_feedback_structure::get_feedback().
+ $customdata = ($this->cm->customdata ?: []) + ['timeopen' => 0, 'timeclose' => 0, 'anonymous' => 0];
+ $this->feedback->timeopen = $customdata['timeopen'];
+ $this->feedback->timeclose = $customdata['timeclose'];
+ $this->feedback->anonymous = $customdata['anonymous'];
+ $this->feedback->completionsubmit = empty($this->cm->customdata['customcompletionrules']['completionsubmit']) ? 0 : 1;
+ }
}
/**
* @return stdClass
*/
public function get_feedback() {
+ global $DB;
+ if (!isset($this->feedback->publish_stats) || !isset($this->feedback->name)) {
+ // Make sure the full object is retrieved.
+ $this->feedback = $DB->get_record('feedback', ['id' => $this->feedback->id], '*', MUST_EXIST);
+ }
return $this->feedback;
}
return true;
}
- if (intval($this->feedback->publish_stats) != 1 ||
+ if (intval($this->get_feedback()->publish_stats) != 1 ||
!has_capability('mod/feedback:viewanalysepage', $context)) {
return false;
}
global $DB;
$cm = get_fast_modinfo($event->courseid)->instances['feedback'][$event->instance];
- $feedback = $DB->get_record('feedback', ['id' => $event->instance]);
- $feedbackcompletion = new mod_feedback_completion($feedback, $cm, 0);
+ $feedbackcompletion = new mod_feedback_completion(null, $cm, 0);
// The event is only visible if the user can submit it.
return $feedbackcompletion->can_complete();
*/
function mod_feedback_core_calendar_provide_event_action(calendar_event $event,
\core_calendar\action_factory $factory) {
- global $DB;
$cm = get_fast_modinfo($event->courseid)->instances['feedback'][$event->instance];
- $feedback = $DB->get_record('feedback', ['id' => $event->instance]);
- $feedbackcompletion = new mod_feedback_completion($feedback, $cm, 0);
+ $feedbackcompletion = new mod_feedback_completion(null, $cm, 0);
- if ($feedbackcompletion->is_already_submitted()) {
- // There is no action if the user has already submitted the feedback.
+ if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
+ // Feedback is already closed, do not display it even if it was never submitted.
return null;
}
- $now = time();
- if ($feedback->timeopen && $feedback->timeclose) {
- $actionable = ($now >= $feedback->timeopen) && ($now <= $feedback->timeclose);
- } else if ($feedback->timeclose) {
- $actionable = $now < $feedback->timeclose;
- } else if ($feedback->timeopen) {
- $actionable = $now >= $feedback->timeopen;
- } else {
- $actionable = true;
+ // The feedback is actionable if it does not have timeopen or timeopen is in the past.
+ $actionable = $feedbackcompletion->is_open();
+
+ if ($actionable && $feedbackcompletion->is_already_submitted()) {
+ // There is no need to display anything if the user has already submitted the feedback.
+ return null;
}
return $factory->create_instance(
global $DB;
$dbparams = ['id' => $coursemodule->instance];
- $fields = 'id, name, intro, introformat, completionsubmit';
+ $fields = 'id, name, intro, introformat, completionsubmit, timeopen, timeclose, anonymous';
if (!$feedback = $DB->get_record('feedback', $dbparams, $fields)) {
return false;
}
if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
$result->customdata['customcompletionrules']['completionsubmit'] = $feedback->completionsubmit;
}
+ // Populate some other values that can be used in calendar or on dashboard.
+ if ($feedback->timeopen) {
+ $result->customdata['timeopen'] = $feedback->timeopen;
+ }
+ if ($feedback->timeclose) {
+ $result->customdata['timeclose'] = $feedback->timeclose;
+ }
+ if ($feedback->anonymous) {
+ $result->customdata['anonymous'] = $feedback->anonymous;
+ }
return $result;
}
And I should not see "Response number: 1"
And I should see "Response number: 2"
And I log out
+
+ Scenario: Collecting new non-anonymous feedback from a previously anonymous feedback activity
+ When I log in as "teacher"
+ And I follow "Course 1"
+ And I follow "Course feedback"
+ And I click on "Edit settings" "link" in the "Administration" "block"
+ And I set the following fields to these values:
+ | Allow multiple submissions | Yes |
+ And I press "Save and display"
+ And I follow "Edit questions"
+ And I add a "Short text answer" question to the feedback with:
+ | Question | this is a short text answer |
+ | Label | shorttext |
+ | Maximum characters accepted | 200 |
+ And I log out
+ When I log in as "user1"
+ And I follow "Course 1"
+ And I follow "Course feedback"
+ And I follow "Answer the questions..."
+ And I set the following fields to these values:
+ | this is a short text answer | anontext |
+ And I press "Submit your answers"
+ And I log out
+ # Switch to non-anon responses.
+ And I log in as "teacher"
+ And I follow "Course 1"
+ And I follow "Course feedback"
+ And I click on "Edit settings" "link" in the "Administration" "block"
+ And I set the following fields to these values:
+ | Record user names | User's name will be logged and shown with answers |
+ And I press "Save and display"
+ And I log out
+ # Now leave a non-anon feedback as user1
+ When I log in as "user1"
+ And I follow "Course 1"
+ And I follow "Course feedback"
+ And I follow "Answer the questions..."
+ And I set the following fields to these values:
+ | this is a short text answer | usertext |
+ And I press "Submit your answers"
+ And I log out
+ # Now check the responses are correct.
+ When I log in as "teacher"
+ And I follow "Course 1"
+ And I follow "Course feedback"
+ And I follow "Show responses"
+ And I should see "Anonymous entries (1)"
+ And I should see "Non anonymous entries (1)"
+ And I click on "," "link" in the "Username 1" "table_row"
+ And I should see "(Username 1)"
+ And I should see "usertext"
+ And I follow "Back"
+ And I follow "Response number: 1"
+ And I should see "Response number: 1 (Anonymous)"
+ Then I should see "anontext"
*/
class mod_feedback_lib_testcase extends advanced_testcase {
+ public function test_feedback_initialise() {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ $course = $this->getDataGenerator()->create_course();
+ $params['course'] = $course->id;
+ $params['timeopen'] = time() - 5 * MINSECS;
+ $params['timeclose'] = time() + DAYSECS;
+ $params['anonymous'] = 1;
+ $params['intro'] = 'Some introduction text';
+ $feedback = $this->getDataGenerator()->create_module('feedback', $params);
+
+ // Test different ways to construct the structure object.
+ $pseudocm = get_coursemodule_from_instance('feedback', $feedback->id); // Object similar to cm_info.
+ $cm = get_fast_modinfo($course)->instances['feedback'][$feedback->id]; // Instance of cm_info.
+
+ $constructorparams = [
+ [$feedback, null],
+ [null, $pseudocm],
+ [null, $cm],
+ [$feedback, $pseudocm],
+ [$feedback, $cm],
+ ];
+
+ foreach ($constructorparams as $params) {
+ $structure = new mod_feedback_completion($params[0], $params[1], 0);
+ $this->assertTrue($structure->is_open());
+ $this->assertTrue($structure->get_cm() instanceof cm_info);
+ $this->assertEquals($feedback->cmid, $structure->get_cm()->id);
+ $this->assertEquals($feedback->intro, $structure->get_feedback()->intro);
+ }
+ }
+
/**
* Tests for mod_feedback_refresh_events.
*/
$factory = new \core_calendar\action_factory();
$actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
- $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
- $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
- $this->assertInstanceOf('moodle_url', $actionevent->get_url());
- $this->assertEquals(1, $actionevent->get_item_count());
- $this->assertFalse($actionevent->is_actionable());
+ // No event on the dashboard if feedback is closed.
+ $this->assertNull($actionevent);
}
/**
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['folder'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
$string['completiondiscussionsgroup'] = 'Require discussions';
$string['completiondiscussionshelp'] = 'requiring discussions to complete';
$string['completionposts'] = 'Student must post discussions or replies:';
-$string['completionpostsdesc'] = 'Student must post at least {$a} discussion(s) or reply(s)';
+$string['completionpostsdesc'] = 'Student must post at least {$a} discussion(s) or reply/replies';
$string['completionpostsgroup'] = 'Require posts';
$string['completionpostshelp'] = 'requiring discussions or replies to complete';
$string['completionreplies'] = 'Student must post replies:';
-$string['completionrepliesdesc'] = 'Student must post at least {$a} reply(s)';
+$string['completionrepliesdesc'] = 'Student must post at least {$a} reply/replies';
$string['completionrepliesgroup'] = 'Require replies';
$string['completionreplieshelp'] = 'requiring replies to complete';
$string['configcleanreadtime'] = 'The hour of the day to clean old posts from the \'read\' table.';
* @return bool
*/
function mod_forum_core_calendar_event_action_shows_item_count(calendar_event $event, $itemcount = 0) {
- // Always show item count for forums if item count is greater than 0.
- return $itemcount > 0;
+ // Always show item count for forums if item count is greater than 1.
+ // If only one action is required than it is obvious and we don't show it for other modules.
+ return $itemcount > 1;
}
/**
*/
function mod_forum_core_calendar_provide_event_action(calendar_event $event,
\core_calendar\action_factory $factory) {
+ global $DB, $USER;
+
$cm = get_fast_modinfo($event->courseid)->instances['forum'][$event->instance];
$context = context_module::instance($cm->id);
return null;
}
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
return null;
}
+ // Get action itemcount.
+ $itemcount = 0;
+ $forum = $DB->get_record('forum', array('id' => $cm->instance));
+ $postcountsql = "
+ SELECT
+ COUNT(1)
+ FROM
+ {forum_posts} fp
+ INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
+ WHERE
+ fp.userid=:userid AND fd.forum=:forumid";
+ $postcountparams = array('userid' => $USER->id, 'forumid' => $forum->id);
+
+ if ($forum->completiondiscussions) {
+ $count = $DB->count_records('forum_discussions', array('forum' => $forum->id, 'userid' => $USER->id));
+ $itemcount += ($forum->completiondiscussions >= $count) ? ($forum->completiondiscussions - $count) : 0;
+ }
+
+ if ($forum->completionreplies) {
+ $count = $DB->get_field_sql( $postcountsql.' AND fp.parent<>0', $postcountparams);
+ $itemcount += ($forum->completionreplies >= $count) ? ($forum->completionreplies - $count) : 0;
+ }
+
+ if ($forum->completionposts) {
+ $count = $DB->get_field_sql($postcountsql, $postcountparams);
+ $itemcount += ($forum->completionposts >= $count) ? ($forum->completionposts - $count) : 0;
+ }
+
+ // Well there is always atleast one actionable item (view forum, etc).
+ $itemcount = $itemcount > 0 ? $itemcount : 1;
+
return $factory->create_instance(
get_string('view'),
new \moodle_url('/mod/forum/view.php', ['id' => $cm->id]),
- 1,
+ $itemcount,
true
);
}
// Create the activity.
$course = $this->getDataGenerator()->create_course();
- $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+ $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id,
+ 'completionreplies' => 5, 'completiondiscussions' => 2));
// Create a calendar event.
$event = $this->create_action_event($course->id, $forum->id,
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('view'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
- $this->assertEquals(1, $actionevent->get_item_count());
+ $this->assertEquals(7, $actionevent->get_item_count());
$this->assertTrue($actionevent->is_actionable());
}
$string['commentson'] = 'Comments on';
$string['commentupdated'] = 'The comment has been updated.';
$string['completionentries'] = 'Student must create entries:';
-$string['completionentriesdesc'] = 'Student must create at least {$a} entry(s)';
+$string['completionentriesdesc'] = 'Student must create at least {$a} entry/entries';
$string['completionentriesgroup'] = 'Require entries';
$string['concept'] = 'Concept';
$string['concepts'] = 'Concepts';
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['glossary'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['imscp'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['label'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
$retries = 0;
}
+ // First record this page in lesson_branch. This record may be needed by lesson_unseen_branch_jump.
+ $branch = new stdClass;
+ $branch->lessonid = $this->lesson->id;
+ $branch->userid = $USER->id;
+ $branch->pageid = $this->properties->id;
+ $branch->retry = $retries;
+ $branch->flag = $branchflag;
+ $branch->timeseen = time();
+ $branch->nextpageid = 0; // Next page id will be set later.
+ $branch->id = $DB->insert_record("lesson_branch", $branch);
+
// this is called when jumping to random from a branch table
$context = context_module::instance($PAGE->cm->id);
if($newpageid == LESSON_UNSEENBRANCHPAGE) {
$newpageid = lesson_unseen_branch_jump($this->lesson, $USER->id);
}
- // Record this page in lesson_branch.
- $branch = new stdClass;
- $branch->lessonid = $this->lesson->id;
- $branch->userid = $USER->id;
- $branch->pageid = $this->properties->id;
- $branch->retry = $retries;
- $branch->flag = $branchflag;
- $branch->timeseen = time();
+ // Update record to set nextpageid.
$branch->nextpageid = $newpageid;
- $DB->insert_record("lesson_branch", $branch);
+ $DB->update_record("lesson_branch", $branch);
// This will force to redirect to the newpageid.
$result->inmediatejump = true;
$answerdata->answers[] = array(get_string("nooneansweredthisquestion", "lesson"), " ");
}
$i++;
- } else if ($useranswer != null && ($answer->id == $useranswer->answerid || ($answer == end($answers) && empty($answerdata)))) {
- // get in here when what the user entered is not one of the answers
+ } else if ($useranswer != null && ($answer->id == $useranswer->answerid || ($answer == end($answers) &&
+ empty($answerdata->answers)))) {
+ // Get in here when the user answered or for the last answer.
$data = '<input class="form-control" type="text" size="50" ' .
'disabled="disabled" readonly="readonly" value="'.s($useranswer->useranswer).'">';
if (isset($pagestats[$this->properties->id][$useranswer->useranswer])) {
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['lti'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['page'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['resource'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
*/
function mod_scorm_core_calendar_provide_event_action(calendar_event $event,
\core_calendar\action_factory $factory) {
- global $CFG, $DB;
+ global $CFG;
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
$cm = get_fast_modinfo($event->courseid)->instances['scorm'][$event->instance];
- $scorm = $DB->get_record('scorm', array('id' => $event->instance));
+
+ if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
+ // The scorm has closed so the user can no longer submit anything.
+ return null;
+ }
+
+ // Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen.
+ $customdata = $cm->customdata ?: [];
+ $customdata['id'] = $cm->instance;
+ $scorm = (object)($customdata + ['timeclose' => 0, 'timeopen' => 0]);
// Check that the SCORM activity is open.
list($actionable, $warnings) = scorm_get_availability_status($scorm);
global $DB;
$dbparams = ['id' => $coursemodule->instance];
- $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos';
+ $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos, '.
+ 'timeopen, timeclose';
if (!$scorm = $DB->get_record('scorm', $dbparams, $fields)) {
return false;
}
$result->customdata['customcompletionrules']['completionscorerequired'] = $scorm->completionscorerequired;
$result->customdata['customcompletionrules']['completionstatusallscos'] = $scorm->completionstatusallscos;
}
+ // Populate some other values that can be used in calendar or on dashboard.
+ if ($scorm->timeopen) {
+ $result->customdata['timeopen'] = $scorm->timeopen;
+ }
+ if ($scorm->timeclose) {
+ $result->customdata['timeclose'] = $scorm->timeclose;
+ }
return $result;
}
// Decorate action event.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
- // Confirm the event was decorated.
- $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
- $this->assertEquals(get_string('enter', 'scorm'), $actionevent->get_name());
- $this->assertInstanceOf('moodle_url', $actionevent->get_url());
- $this->assertEquals(1, $actionevent->get_item_count());
- $this->assertFalse($actionevent->is_actionable());
+ // No event on the dashboard if module is closed.
+ $this->assertNull($actionevent);
}
public function test_scorm_core_calendar_provide_event_action_open_in_future() {
return null;
}
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['url'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
$string['invalidlock'] = 'This page is already locked by another user.';
$string['invalidparameters'] = 'Invalid parameters have been given.';
$string['invalidsection'] = 'Invalid section.';
-$string['invalidsesskey'] = 'The given sesskey is not valid. Please resend data again';
+$string['invalidsesskey'] = 'Your session has most likely timed out. Please make a note of your edit then log in again.';
$string['individualpagedoesnotexist'] = 'Individual wiki page doesn\'t exist';
$string['javascriptdisabledlocks'] = 'Javascript is disabled on your browser and locks are not working. The changes you make may not be saved correctly.';
$string['lockingajaxtimeout'] = 'Edit page locking refresh time';
\core_calendar\action_factory $factory) {
$cm = get_fast_modinfo($event->courseid)->instances['wiki'][$event->instance];
- $course = new stdClass();
- $course->id = $event->courseid;
- $completion = new \completion_info($course);
+ $completion = new \completion_info($cm->get_course());
$completiondata = $completion->get_data($cm, false);
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key linking to the question table."/>
<FIELD NAME="responseformat" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="editor" SEQUENCE="false" COMMENT="The type of input area students should be given for their response."/>
- <FIELD NAME="responserequired" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Nonzero if an inline text response is optional"/>
+ <FIELD NAME="responserequired" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Nonzero if an online text response is optional"/>
<FIELD NAME="responsefieldlines" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="15" SEQUENCE="false" COMMENT="Approximate height, in lines, of the input box the students should be given for their response."/>
<FIELD NAME="attachments" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether, and how many, attachments a student is allowed to include with their response. -1 means unlimited."/>
<FIELD NAME="attachmentsrequired" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The number of attachments that should be required"/>
$string['formateditor'] = 'HTML editor';
$string['formateditorfilepicker'] = 'HTML editor with file picker';
$string['formatmonospaced'] = 'Plain text, monospaced font';
-$string['formatnoinline'] = 'No inline text';
+$string['formatnoinline'] = 'No online text';
$string['formatplain'] = 'Plain text';
$string['graderinfo'] = 'Information for graders';
$string['graderinfoheader'] = 'Grader Information';
-$string['mustattach'] = 'When "no inline text" is selected, or responses are optional, you must allow at least one attachment.';
-$string['mustrequire'] = 'When "no inline text" is selected, or responses are optional, you must require at least one attachment.';
+$string['mustattach'] = 'When "No online text" is selected, or responses are optional, you must allow at least one attachment.';
+$string['mustrequire'] = 'When "No online text" is selected, or responses are optional, you must require at least one attachment.';
$string['mustrequirefewer'] = 'You cannot require more attachments than you allow.';
$string['nlines'] = '{$a} lines';
$string['pluginname'] = 'Essay';
-$string['pluginname_help'] = 'In response to a question (that may include an image) the respondent writes an answer of a paragraph or two. The essay question will not be assigned a grade until it has been reviewed by a teacher and manually graded.';
+$string['pluginname_help'] = 'In response to a question, the respondent may upload one or more files and/or enter text online. A response template may be provided. Responses must be graded manually.';
$string['pluginname_link'] = 'question/type/essay';
$string['pluginnameadding'] = 'Adding an Essay question';
$string['pluginnameediting'] = 'Editing an Essay question';
-$string['pluginnamesummary'] = 'Allows a response of a few sentences or paragraphs. This must then be graded manually.';
+$string['pluginnamesummary'] = 'Allows a response of a file upload and/or online text. This must then be graded manually.';
$string['responsefieldlines'] = 'Input box size';
$string['responseformat'] = 'Response format';
$string['responseoptions'] = 'Response Options';
}
public function is_complete_response(array $response) {
- // Determine if the given response has inline text and attachments.
+ // Determine if the given response has online text and attachments.
$hasinlinetext = array_key_exists('answer', $response) && ($response['answer'] !== '');
$hasattachments = array_key_exists('attachments', $response)
&& $response['attachments'] instanceof question_response_files;
Then I should see "You must supply a value here."
When I set the following fields to these values:
| Question name | Edited essay-001 name |
- | Response format | No inline text |
+ | Response format | No online text |
And I press "id_submitbutton"
- Then I should see "When \"no inline text\" is selected, or responses are optional, you must allow at least one attachment."
+ Then I should see "When \"No online text\" is selected, or responses are optional, you must allow at least one attachment."
When I set the following fields to these values:
| Response format | Plain text |
And I press "id_submitbutton"
}
/**
- * Makes an essay question without an inline text editor.
+ * Makes an essay question without an online text editor.
* @return qtype_essay_question
*/
public function make_essay_question_noinline() {
$this->assertTrue($essay->is_complete_response(
array('answer' => '', 'attachments' => $attachments[2])));
- // Test the case in which both the response and inline text are optional.
+ // Test the case in which both the response and online text are optional.
$essay->attachmentsrequired = 0;
// Providing no answer and no attachment should result in an incomplete
$string['docsformat'] = 'Default document import format';
$string['drawingformat'] = 'Default drawing import format';
$string['googledocs:view'] = 'View Google Drive repository';
-$string['importformat'] = 'Configure the default import formats from google';
+$string['importformat'] = 'Configure the default import formats from Google';
$string['pluginname'] = 'Google Drive';
$string['presentationformat'] = 'Default presentation import format';
$string['spreadsheetformat'] = 'Default spreadsheet import format';
$string['issuer'] = 'OAuth 2 service';
-$string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the Google Drive API. If the services does not exist yet, you might need to create it.';
+$string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the Google Drive API. If the service does not exist yet, you will need to create it.';
$string['servicenotenabled'] = 'Access not configured. Make sure the service \'Drive API\' is enabled.';
-$string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth Services configuration">OAuth 2 Services Configuration</a>';
+$string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth 2 services configuration">OAuth 2 services configuration</a>';
$string['searchfor'] = 'Search for {$a}';
$string['internal'] = 'Internal (files stored in Moodle)';
$string['external'] = 'External (only links stored in Moodle)';
-$string['both'] = 'Internal and External';
+$string['both'] = 'Internal and external';
$string['supportedreturntypes'] = 'Supported files';
$string['defaultreturntype'] = 'Default return type';
$string['fileoptions'] = 'The types and defaults for returned files is configurable here. Note that all files linked externally will be updated so that the owner is the Moodle system account.';
$string['owner'] = 'Owned by: {$a}';
-$string['cachedef_folder'] = 'Google File IDs for folders in the system account';
+$string['cachedef_folder'] = 'Google file IDs for folders in the system account';
// Deprecated since Moodle 3.3.
-$string['oauthinfo'] = '<p>To use this plugin, you must register your site with Google, as described in the documentation <a href="{$a->docsurl}">Google OAuth 2.0 setup</a>.</p><p>As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':</p><p>{$a->callbackurl}</p><p>Once registered, you will be provided with a client ID and secret which can be used to configure all Google Drive and Picasa plugins.</p><p>Please also note that you will have to enable the service \'Drive API\'.</p>';
+$string['oauthinfo'] = '<p>To use this plugin, you must register your site with Google, as described in the documentation <a href="{$a->docsurl}">Google OAuth 2.0 setup</a>.</p><p>As part of the registration process, you will need to enter the following URL as \'Authorized Redirect URIs\':</p><p>{$a->callbackurl}</p><p>Once registered, you will be provided with a client ID and secret which can be used to configure certain other Google Drive and Picasa plugins.</p><p>Please also note that you will have to enable the service \'Drive API\'.</p>';
$string['secret'] = 'Secret';
$string['clientid'] = 'Client ID';
throw new repository_exception('cannotdownload', 'repository');
}
- $client = $this->get_user_oauth_client();
- $base = 'https://www.googleapis.com/drive/v3';
-
$source = json_decode($reference);
+ $client = null;
+ if (!empty($source->usesystem)) {
+ $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
+ } else {
+ $client = $this->get_user_oauth_client();
+ }
+
+ $base = 'https://www.googleapis.com/drive/v3';
+
$newfilename = false;
if ($source->exportformat == 'download') {
$params = ['alt' => 'media'];
$storedfile->get_filepath(),
$storedfile->get_filename());
- if (empty($options['offline']) && !empty($info) && $info->is_writable()) {
+ if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
// Add the current user as an OAuth writer.
$systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
// Update the returned reference so that the stored_file in moodle points to the newly copied file.
$source->id = $newsource->id;
$source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : '';
+ $source->usesystem = true;
if (empty($source->link)) {
$source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : '';
}
return get_string('unknownsource', 'repository');
}
$source = json_decode($reference);
+ if (empty($source->usesystem)) {
+ return '';
+ }
$systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
if ($systemauth === false) {
namespace repository_onedrive;
use \core\task\scheduled_task;
+use DateTime;
+use DateInterval;
+use repository_exception;
+use \core\oauth2\rest_exception;
defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot . '/repository/lib.php');
+
/**
* Simple task to delete temporary permission records.
* @package repository_onedrive
$expires->sub(new DateInterval("P7D"));
$timestamp = $expires->getTimestamp();
- $issuerid = get_config('repository_onedrive', 'issuerid');
- $issuer = \core\oauth2\api::get_issuer_by_id($issuerid);
+ $issuerid = get_config('onedrive', 'issuerid');
+ $issuer = \core\oauth2\api::get_issuer($issuerid);
// Add the current user as an OAuth writer.
$systemauth = \core\oauth2\api::get_system_oauth_client($issuer);
$details = 'Cannot connect as system user';
throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
}
- $systemservice = new repository_onedrive\rest($systemauth);
+ $systemservice = new \repository_onedrive\rest($systemauth);
foreach ($accessrecords as $access) {
if ($access->get('timemodified') < $timestamp) {
$params = ['permissionid' => $access->get('permissionid'), 'itemid' => $access->get('itemid')];
- $systemservice->call('delete_permission', $params);
+ try {
+ $systemservice->call('delete_permission', $params);
+ } catch (rest_exception $re) {
+ // We log and give up here or we will always fail for eternity.
+ mtrace('Failed to remove access from file: ' . $access->get('itemid'));
+ }
$access->delete();
}
}
],
'response' => 'json'
],
- 'list_permissions' => [
- 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions',
- 'method' => 'get',
+ 'create_permission' => [
+ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/invite',
+ 'method' => 'post',
'args' => [
- '$select' => PARAM_RAW,
- '$expand' => PARAM_RAW,
- 'fileid' => PARAM_RAW,
- '$skip' => PARAM_INT,
- '$skipToken' => PARAM_RAW,
- '$count' => PARAM_INT
+ 'fileid' => PARAM_RAW
],
'response' => 'json'
],
- 'create_permission' => [
- 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/invite',
+ 'create_upload' => [
+ 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{parentid}:/{filename}:/createUploadSession',
'method' => 'post',
'args' => [
- 'fileid' => PARAM_RAW
+ 'parentid' => PARAM_RAW,
+ 'filename' => PARAM_RAW
],
'response' => 'json'
],
],
'response' => 'json'
],
- 'get_drive' => [
- 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive',
- 'method' => 'get',
- 'args' => [],
- 'response' => 'json'
- ],
'delete_file_by_path' => [
'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/root:/{fullpath}',
'method' => 'delete',
],
'response' => 'json'
],
- 'copy_share' => [
- 'endpoint' => 'https://graph.microsoft.com/v1.0/shares/{sharetoken}/root/copy',
- 'method' => 'post',
- 'args' => [
- 'sharetoken' => PARAM_RAW,
- ],
- 'response' => 'json'
- ],
'delete_permission' => [
'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions/{permissionid}',
'method' => 'delete',
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$string['both'] = 'Internal and External';
-$string['cachedef_folder'] = 'OneDrive File IDs for folders in the system account';
+$string['both'] = 'Internal and external';
+$string['cachedef_folder'] = 'OneDrive file IDs for folders in the system account';
$string['configplugin'] = 'Configure OneDrive plugin';
-$string['confirmimportskydrive'] = 'Are you sure you want to import all files from the "Microsoft SkyDrive" repository to the "Microsoft OneDrive" repository? As long as the Microsoft OneDrive repository is already configured and working - all imported files will continue working as before. There is no way to undo these changes.';
+$string['confirmimportskydrive'] = 'Are you sure you want to import all files from the Microsoft SkyDrive repository to the Microsoft OneDrive repository? The Microsoft OneDrive repository must be configured and working for imported files to continue working as before. Warning: This action cannot be undone!';
$string['defaultreturntype'] = 'Default return type';
$string['external'] = 'External (only links stored in Moodle)';
$string['fileoptions'] = 'The types and defaults for returned files is configurable here. Note that all files linked externally will be updated so that the owner is the Moodle system account.';
$string['importskydrivefiles'] = 'Import files from Microsoft SkyDrive repository';
$string['internal'] = 'Internal (files stored in Moodle)';
-$string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the services does not exist yet, you might need to create it.';
+$string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the service does not exist yet, you will need to create it.';
$string['issuer'] = 'OAuth 2 service';
-$string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth Services configuration">OAuth 2 Services Configuration</a>';
+$string['mysitenotfound'] = 'You have never logged into OneDrive before. You must login to OneDrive at least once it before it can be used with Moodle.';
+$string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth 2 services configuration">OAuth 2 services configuration</a>';
$string['owner'] = 'Owned by: {$a}';
$string['pluginname'] = 'Microsoft OneDrive';
$string['removetempaccesstask'] = 'Remove temporary write access from controlled links.';
$string['searchfor'] = 'Search for {$a}';
$string['servicenotenabled'] = 'Access not configured.';
-$string['skydrivefilesexist'] = 'Files found in the Microsoft SkyDrive repository. This repository is deprecated by Microsoft - the files can be automatically imported to this Microsoft OneDrive repository.';
+$string['skydrivefilesexist'] = 'Files found in the Microsoft SkyDrive repository. This repository has been deprecated by Microsoft, however the files may be imported to the Microsoft OneDrive repository.';
$string['skydrivefilesimported'] = 'All files were imported from the Microsoft SkyDrive repository.';
$string['skydrivefilesnotimported'] = 'Some files could not be imported from the Microsoft SkyDrive repository.';
$string['onedrive:view'] = 'View OneDrive repository';
} catch (Exception $e) {
if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
throw new repository_exception('servicenotenabled', 'repository_onedrive');
- } else {
- throw $e;
+ } else if (strpos($e->getMessage(), 'mysite not found') !== false) {
+ throw new repository_exception('mysitenotfound', 'repository_onedrive');
}
}
if ($this->disabled) {
throw new repository_exception('cannotdownload', 'repository');
}
+ $sourceinfo = json_decode($reference);
+
+ $client = null;
+ if (!empty($sourceinfo->usesystem)) {
+ $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
+ } else {
+ $client = $this->get_user_oauth_client();
+ }
- $client = $this->get_user_oauth_client();
$base = 'https://graph.microsoft.com/v1.0/';
- $sourceinfo = json_decode($reference);
$sourceurl = new moodle_url($base . 'me/drive/items/' . $sourceinfo->id . '/content');
$source = $sourceurl->out(false);
$storedfile->get_filepath(),
$storedfile->get_filename());
- if (empty($options['offline']) && !empty($info) && $info->is_writable()) {
+ if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
// Add the current user as an OAuth writer.
$systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
}
}
- /**
- * List the permissions on a file.
- *
- * @param \repository_onedrive\rest $client Authenticated client.
- * @param string $fileid The id of the file.
- * @return array
- */
- protected function list_file_permissions(\repository_onedrive\rest $client, $fileid) {
- $fields = "id,roles,link,grantedTo";
- return $client->call('list_permissions', ['fileid' => $fileid, '$select' => $fields]);
- }
-
/**
* See if a folder exists within a folder
*
return true;
}
-
- /**
- * Get a file summary by full path.
- *
- * @param \repository_onedrive\rest $client Authenticated client.
- * @param string $fullpath
- * @return stdClass
- */
- protected function get_file_summary_by_path(\repository_onedrive\rest $client, $fullpath) {
- $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser";
- $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]);
- if (empty($response->id)) {
- $details = 'Cannot get file summary:' . $fullpath;
- throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
- }
- return $response;
- }
-
/**
* Create a folder within a folder
*
return $response;
}
- /**
- * Get the id of this users root drive.
- *
- * @param \repository_onedrive\rest $client Authenticated client.
- *
- * @return string id
- */
- protected function get_root_drive_id(\repository_onedrive\rest $client) {
- $response = $client->call('get_drive', []);
-
- if (empty($response->id)) {
- $details = 'Cannot get driveid';
- throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
- }
- return $response->id;
- }
-
/**
* Add a writer to the permissions on the file (temporary).
*
return true;
}
- /**
- * Add a writer to the permissions on the file.
- *
- * @param \repository_onedrive\rest $client Authenticated client.
- * @param string $fileid The file we are updating.
- * @param string $useremail The user email of the writer account to add.
- * @return boolean
- */
- protected function add_writer_to_file(\repository_onedrive\rest $client, $fileid, $useremail) {
- $updateeditor = [
- 'recipients' => [ [ 'email' => $useremail ] ],
- 'roles' => ['write'],
- 'requireSignIn' => true,
- 'sendInvitation' => false
- ];
- $params = [ 'fileid' => $fileid ];
- $response = $client->call('create_permission', $params, json_encode($updateeditor));
- if (empty($response->value)) {
- $details = 'Cannot add user ' . $useremail . ' as a writer for document: ' . $fileid;
- throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
- }
- return true;
- }
-
/**
* Allow anyone with the link to read the file.
*
$details = 'Cannot update link sharing for the document: ' . $fileid;
throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
}
- return true;
+ return $response->link->webUrl;
}
/**
- * Copy a shared file to a new folder.
+ * Given a filename, use the core_filetypes registered types to guess a mimetype.
*
- * @param \repository_onedrive\rest $client Authenticated client.
- * @param string $sharetoken The share we are querying.
- * @param string $newdrive Id of the drive to copy to.
- * @param string $parentid Id of the folder to copy to.
- * @return stdClass
+ * If no mimetype is known, return 'application/unknown';
+ *
+ * @param string $filename
+ * @return string $mimetype
*/
- protected function copy_share(\repository_onedrive\rest $client, $sharetoken, $newdrive, $parentid) {
- $folder = [
- 'parentReference' => ['id' => $parentid, 'driveId' => $newdrive]
- ];
- $params = ['sharetoken' => $sharetoken];
- $response = $client->call('copy_share', $params, json_encode($folder));
- return true;
+ protected function get_mimetype_from_filename($filename) {
+ $mimetype = 'application/unknown';
+ $types = core_filetypes::get_types();
+ $fileextension = '.bin';
+ if (strpos($filename, '.') !== false) {
+ $fileextension = substr($filename, strrpos($filename, '.') + 1);
+ }
+
+ if (isset($types[$fileextension])) {
+ $mimetype = $types[$fileextension]['type'];
+ }
+ return $mimetype;
}
/**
- * From MS docs - to get a share token from a url, do this:
- * Reference: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/shares_get
- * To access a sharing URL using the shares API, the URL needs to be transformed into a sharing token.
- * To transform a URL into a sharing token:
- * Base64 encode the sharing URL.
- * Convert the base64 encoded data to unpadded base64url format by:
- * Trim trailing = characeters from the string.
- * Replace unsafe URL characters with an equivelent character; replace / with _ and + with -.
- * Append u! to the beginning of the string.
+ * Upload a file to onedrive.
*
- * @param string $shareurl
- * @return string The sharing token
+ * @param \repository_onedrive\rest $service Authenticated client.
+ * @param \curl $curl Curl client to perform the put operation.
+ * @param string $filepath The local path to the file to upload
+ * @param string $mimetype The new mimetype
+ * @param string $parentid The folder to put it.
+ * @param string $filename The name of the new file
+ * @return string $fileid
*/
- protected function get_share_token($shareurl) {
- return 'u!' . str_replace(['/', '+'], ['_', '-'], rtrim(base64_encode($shareurl), '='));
+ protected function upload_file(\repository_onedrive\rest $service, \curl $curl, $filepath, $mimetype, $parentid, $filename) {
+ // Start an upload session.
+ // Docs https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/item_createuploadsession link.
+
+ $params = ['parentid' => $parentid, 'filename' => urlencode($filename)];
+ $behaviour = [ 'item' => [ "@microsoft.graph.conflictBehavior" => "rename" ] ];
+ $created = $service->call('create_upload', $params, json_encode($behaviour));
+ if (empty($created->uploadUrl)) {
+ $details = 'Cannot begin upload session:' . $parentid;
+ throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
+ }
+
+ $options = ['file' => $filepath];
+ $curl->setHeader('Content-type: ' . $mimetype);
+ $size = filesize($filepath);
+ $curl->setHeader('Content-Range: bytes 0-' . ($size - 1) . '/' . $size);
+ $response = $curl->put($created->uploadUrl, $options);
+ if ($curl->errno == 0) {