Merge branch 'wip-MDL-60280-master' of git://github.com/marinaglancy/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 16 Oct 2017 11:46:19 +0000 (13:46 +0200)
committerDavid Monllao <davidm@moodle.com>
Mon, 16 Oct 2017 11:46:19 +0000 (13:46 +0200)
556 files changed:
.eslintignore
.stylelintignore
admin/index.php
admin/renderer.php
admin/settings/analytics.php
admin/tool/analytics/classes/output/renderer.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/usertours/amd/build/usertours.min.js
admin/tool/usertours/amd/src/usertours.js
admin/tool/usertours/templates/resettour.mustache [new file with mode: 0644]
admin/webservice/testclient.php
admin/webservice/testclient_forms.php
analytics/classes/admin_setting_predictor.php
analytics/classes/dataset_manager.php
analytics/classes/local/analyser/base.php
analytics/classes/local/analyser/by_course.php
analytics/classes/local/analyser/sitewide.php
analytics/classes/local/target/base.php
analytics/classes/local/target/discrete.php
analytics/classes/local/target/linear.php
analytics/classes/model.php
analytics/tests/dataset_manager_test.php
analytics/tests/fixtures/test_analyser.php [new file with mode: 0644]
analytics/tests/model_test.php
analytics/tests/prediction_test.php
backup/backupfilesedit.php
backup/moodle2/restore_activity_task.class.php
backup/moodle2/restore_block_task.class.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/restorefile.php
backup/util/ui/renderer.php
blocks/calendar_upcoming/block_calendar_upcoming.php
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_filter.min.js
calendar/amd/build/drag_drop_data_store.min.js
calendar/amd/build/event_form.min.js
calendar/amd/build/month_navigation_drag_drop.min.js
calendar/amd/build/month_view_drag_drop.min.js
calendar/amd/src/calendar.js
calendar/amd/src/calendar_filter.js
calendar/amd/src/drag_drop_data_store.js
calendar/amd/src/event_form.js
calendar/amd/src/month_navigation_drag_drop.js
calendar/amd/src/month_view_drag_drop.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/day_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/external/event_subscription_exporter.php
calendar/classes/external/footer_options_exporter.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/mappers/create_update_form_mapper.php
calendar/lib.php
calendar/templates/event_filter_key.mustache
calendar/templates/event_icon.mustache [new file with mode: 0644]
calendar/templates/event_subscription.mustache
calendar/templates/footer_options.mustache
calendar/templates/month_detailed.mustache
calendar/templates/upcoming_mini.mustache [new file with mode: 0644]
calendar/tests/behat/calendar.feature
calendar/tests/calendar_event_exporter_test.php [new file with mode: 0644]
calendar/tests/lib_test.php
completion/classes/external.php
completion/tests/externallib_test.php
composer.json
composer.lock
course/classes/search/mycourse.php
course/externallib.php
course/lib.php
course/renderer.php
course/tests/courselib_test.php
course/tests/search_test.php
enrol/cohort/lib.php
enrol/cohort/tests/cohortlib_test.php
enrol/database/lib.php
enrol/database/tests/lib_test.php [new file with mode: 0644]
enrol/flatfile/lib.php
enrol/flatfile/tests/flatfile_test.php
enrol/locallib.php
enrol/lti/lib.php
enrol/lti/tests/lib_test.php
enrol/manual/lib.php
enrol/manual/tests/lib_test.php
enrol/meta/lib.php
enrol/meta/tests/plugin_test.php
enrol/paypal/lib.php
enrol/paypal/tests/paypal_test.php
enrol/self/lib.php
enrol/self/tests/self_test.php
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/db/upgradelib.php
filter/mathjaxloader/filter.php
filter/mathjaxloader/readme_moodle.txt
filter/mathjaxloader/settings.php
filter/mathjaxloader/tests/filter_test.php [new file with mode: 0644]
filter/mathjaxloader/tests/upgradelib_test.php
filter/mathjaxloader/version.php
group/tests/behat/create_groups.feature
install/lang/lt/admin.php
install/lang/lt/install.php
lang/en/admin.php
lang/en/analytics.php
lang/en/calendar.php
lang/en/completion.php
lang/en/deprecated.txt
lang/en/moodle.php
lang/en/role.php
lib/accesslib.php
lib/adodb/adodb-active-record.inc.php
lib/adodb/adodb-active-recordx.inc.php
lib/adodb/adodb-csvlib.inc.php
lib/adodb/adodb-datadict.inc.php
lib/adodb/adodb-error.inc.php
lib/adodb/adodb-errorhandler.inc.php
lib/adodb/adodb-errorpear.inc.php
lib/adodb/adodb-exceptions.inc.php
lib/adodb/adodb-iterator.inc.php
lib/adodb/adodb-lib.inc.php
lib/adodb/adodb-memcache.lib.inc.php
lib/adodb/adodb-pager.inc.php
lib/adodb/adodb-pear.inc.php
lib/adodb/adodb-perf.inc.php
lib/adodb/adodb-php4.inc.php
lib/adodb/adodb-time.inc.php
lib/adodb/adodb.inc.php
lib/adodb/datadict/datadict-access.inc.php
lib/adodb/datadict/datadict-db2.inc.php
lib/adodb/datadict/datadict-firebird.inc.php
lib/adodb/datadict/datadict-generic.inc.php
lib/adodb/datadict/datadict-ibase.inc.php
lib/adodb/datadict/datadict-informix.inc.php
lib/adodb/datadict/datadict-mssql.inc.php
lib/adodb/datadict/datadict-mssqlnative.inc.php
lib/adodb/datadict/datadict-mysql.inc.php
lib/adodb/datadict/datadict-oci8.inc.php
lib/adodb/datadict/datadict-postgres.inc.php
lib/adodb/datadict/datadict-sapdb.inc.php
lib/adodb/datadict/datadict-sqlite.inc.php
lib/adodb/datadict/datadict-sybase.inc.php
lib/adodb/drivers/adodb-access.inc.php
lib/adodb/drivers/adodb-ado.inc.php
lib/adodb/drivers/adodb-ado5.inc.php
lib/adodb/drivers/adodb-ado_access.inc.php
lib/adodb/drivers/adodb-ado_mssql.inc.php
lib/adodb/drivers/adodb-borland_ibase.inc.php
lib/adodb/drivers/adodb-csv.inc.php
lib/adodb/drivers/adodb-db2.inc.php
lib/adodb/drivers/adodb-db2oci.inc.php
lib/adodb/drivers/adodb-db2ora.inc.php
lib/adodb/drivers/adodb-fbsql.inc.php
lib/adodb/drivers/adodb-firebird.inc.php
lib/adodb/drivers/adodb-ibase.inc.php
lib/adodb/drivers/adodb-informix.inc.php
lib/adodb/drivers/adodb-informix72.inc.php
lib/adodb/drivers/adodb-ldap.inc.php
lib/adodb/drivers/adodb-mssql.inc.php
lib/adodb/drivers/adodb-mssqlnative.inc.php
lib/adodb/drivers/adodb-mssqlpo.inc.php
lib/adodb/drivers/adodb-mysql.inc.php
lib/adodb/drivers/adodb-mysqli.inc.php
lib/adodb/drivers/adodb-mysqlpo.inc.php
lib/adodb/drivers/adodb-mysqlt.inc.php
lib/adodb/drivers/adodb-netezza.inc.php
lib/adodb/drivers/adodb-oci8.inc.php
lib/adodb/drivers/adodb-oci805.inc.php
lib/adodb/drivers/adodb-oci8po.inc.php
lib/adodb/drivers/adodb-oci8quercus.inc.php
lib/adodb/drivers/adodb-odbc.inc.php
lib/adodb/drivers/adodb-odbc_db2.inc.php
lib/adodb/drivers/adodb-odbc_mssql.inc.php
lib/adodb/drivers/adodb-odbc_mssql2012.inc.php [deleted file]
lib/adodb/drivers/adodb-odbc_oracle.inc.php
lib/adodb/drivers/adodb-odbtp.inc.php
lib/adodb/drivers/adodb-odbtp_unicode.inc.php
lib/adodb/drivers/adodb-oracle.inc.php
lib/adodb/drivers/adodb-pdo.inc.php
lib/adodb/drivers/adodb-pdo_mssql.inc.php
lib/adodb/drivers/adodb-pdo_mysql.inc.php
lib/adodb/drivers/adodb-pdo_oci.inc.php
lib/adodb/drivers/adodb-pdo_pgsql.inc.php
lib/adodb/drivers/adodb-pdo_sqlite.inc.php
lib/adodb/drivers/adodb-postgres.inc.php
lib/adodb/drivers/adodb-postgres64.inc.php
lib/adodb/drivers/adodb-postgres7.inc.php
lib/adodb/drivers/adodb-postgres8.inc.php
lib/adodb/drivers/adodb-postgres9.inc.php
lib/adodb/drivers/adodb-proxy.inc.php
lib/adodb/drivers/adodb-sapdb.inc.php
lib/adodb/drivers/adodb-sqlanywhere.inc.php
lib/adodb/drivers/adodb-sqlite.inc.php
lib/adodb/drivers/adodb-sqlite3.inc.php
lib/adodb/drivers/adodb-sqlitepo.inc.php
lib/adodb/drivers/adodb-sybase.inc.php
lib/adodb/drivers/adodb-sybase_ase.inc.php
lib/adodb/drivers/adodb-text.inc.php
lib/adodb/drivers/adodb-vfp.inc.php
lib/adodb/perf/perf-db2.inc.php
lib/adodb/perf/perf-informix.inc.php
lib/adodb/perf/perf-mssql.inc.php
lib/adodb/perf/perf-mssqlnative.inc.php
lib/adodb/perf/perf-mysql.inc.php
lib/adodb/perf/perf-oci8.inc.php
lib/adodb/perf/perf-postgres.inc.php
lib/adodb/pivottable.inc.php
lib/adodb/readme_moodle.txt
lib/adodb/rsfilter.inc.php
lib/adodb/toexport.inc.php
lib/adodb/tohtml.inc.php
lib/amd/build/mustache.min.js
lib/amd/src/mustache.js
lib/bennu/readme_moodle.txt
lib/classes/access/get_user_capability_course_helper.php [new file with mode: 0644]
lib/classes/analytics/target/course_dropout.php
lib/classes/component.php
lib/classes/event/course_module_completion_updated.php
lib/classes/event/user_info_category_created.php [new file with mode: 0644]
lib/classes/event/user_info_category_deleted.php [new file with mode: 0644]
lib/classes/event/user_info_category_updated.php [new file with mode: 0644]
lib/classes/event/user_info_field_created.php [new file with mode: 0644]
lib/classes/event/user_info_field_deleted.php [new file with mode: 0644]
lib/classes/event/user_info_field_updated.php [new file with mode: 0644]
lib/classes/oauth2/client.php
lib/classes/task/search_index_task.php
lib/classes/task/search_optimize_task.php
lib/completionlib.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/enrollib.php
lib/filebrowser/file_info_context_course.php
lib/filebrowser/tests/file_browser_test.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/form.js
lib/html2text/Html2Text.php
lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php
lib/htmlpurifier/readme_moodle.txt
lib/jquery/jquery-3.1.0.min.js [deleted file]
lib/jquery/jquery-3.2.1.js [moved from lib/jquery/jquery-3.1.0.js with 94% similarity]
lib/jquery/jquery-3.2.1.min.js [new file with mode: 0644]
lib/jquery/plugins.php
lib/maxmind/GeoIp2/Database/Reader.php
lib/maxmind/GeoIp2/Exception/HttpException.php
lib/maxmind/GeoIp2/Exception/InvalidRequestException.php
lib/maxmind/GeoIp2/Model/AbstractModel.php
lib/maxmind/GeoIp2/Model/AnonymousIp.php
lib/maxmind/GeoIp2/Model/Asn.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/City.php
lib/maxmind/GeoIp2/Model/ConnectionType.php
lib/maxmind/GeoIp2/Model/Country.php
lib/maxmind/GeoIp2/Model/Domain.php
lib/maxmind/GeoIp2/Model/Enterprise.php
lib/maxmind/GeoIp2/Model/Insights.php
lib/maxmind/GeoIp2/Model/Isp.php
lib/maxmind/GeoIp2/ProviderInterface.php
lib/maxmind/GeoIp2/Record/AbstractPlaceRecord.php
lib/maxmind/GeoIp2/Record/AbstractRecord.php
lib/maxmind/GeoIp2/Record/City.php
lib/maxmind/GeoIp2/Record/Continent.php
lib/maxmind/GeoIp2/Record/Country.php
lib/maxmind/GeoIp2/Record/Location.php
lib/maxmind/GeoIp2/Record/MaxMind.php
lib/maxmind/GeoIp2/Record/Postal.php
lib/maxmind/GeoIp2/Record/RepresentedCountry.php
lib/maxmind/GeoIp2/Record/Subdivision.php
lib/maxmind/GeoIp2/Record/Traits.php
lib/maxmind/GeoIp2/WebService/Client.php
lib/maxmind/MaxMind/Db/Reader.php
lib/maxmind/readme_moodle.txt [moved from lib/maxmind/README_moodle.txt with 77% similarity]
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
lib/moodlelib.php
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Autoloader.php
lib/mustache/src/Mustache/Cache.php
lib/mustache/src/Mustache/Cache/AbstractCache.php
lib/mustache/src/Mustache/Cache/FilesystemCache.php
lib/mustache/src/Mustache/Cache/NoopCache.php
lib/mustache/src/Mustache/Compiler.php
lib/mustache/src/Mustache/Context.php
lib/mustache/src/Mustache/Engine.php
lib/mustache/src/Mustache/Exception.php
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
lib/mustache/src/Mustache/Exception/LogicException.php
lib/mustache/src/Mustache/Exception/RuntimeException.php
lib/mustache/src/Mustache/Exception/SyntaxException.php
lib/mustache/src/Mustache/Exception/UnknownFilterException.php
lib/mustache/src/Mustache/Exception/UnknownHelperException.php
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
lib/mustache/src/Mustache/HelperCollection.php
lib/mustache/src/Mustache/LambdaHelper.php
lib/mustache/src/Mustache/Loader.php
lib/mustache/src/Mustache/Loader/ArrayLoader.php
lib/mustache/src/Mustache/Loader/CascadingLoader.php
lib/mustache/src/Mustache/Loader/FilesystemLoader.php
lib/mustache/src/Mustache/Loader/InlineLoader.php
lib/mustache/src/Mustache/Loader/MutableLoader.php
lib/mustache/src/Mustache/Loader/ProductionFilesystemLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/StringLoader.php
lib/mustache/src/Mustache/Logger.php
lib/mustache/src/Mustache/Logger/AbstractLogger.php
lib/mustache/src/Mustache/Logger/StreamLogger.php
lib/mustache/src/Mustache/Parser.php
lib/mustache/src/Mustache/Source.php [new file with mode: 0644]
lib/mustache/src/Mustache/Source/FilesystemSource.php [new file with mode: 0644]
lib/mustache/src/Mustache/Template.php
lib/mustache/src/Mustache/Tokenizer.php
lib/myprofilelib.php
lib/phpmailer/README.md
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/VERSION
lib/phpmailer/changelog.md
lib/phpmailer/language/phpmailer.lang-ba.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-cs.php [moved from lib/phpmailer/language/phpmailer.lang-cz.php with 95% similarity]
lib/phpmailer/language/phpmailer.lang-da.php [moved from lib/phpmailer/language/phpmailer.lang-dk.php with 100% similarity]
lib/phpmailer/language/phpmailer.lang-fr.php
lib/phpmailer/language/phpmailer.lang-nb.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-nl.php
lib/phpmailer/language/phpmailer.lang-no.php [deleted file]
lib/phpmailer/language/phpmailer.lang-pt_br.php [moved from lib/phpmailer/language/phpmailer.lang-br.php with 65% similarity]
lib/phpmailer/language/phpmailer.lang-rs.php [moved from lib/phpmailer/language/phpmailer.lang-sr.php with 96% similarity]
lib/phpmailer/language/phpmailer.lang-sv.php [moved from lib/phpmailer/language/phpmailer.lang-se.php with 73% similarity]
lib/phpmailer/language/phpmailer.lang-tr.php
lib/phpmailer/language/phpmailer.lang-zh_cn.php
lib/phpmailer/moodle_phpmailer.php
lib/phpmailer/src/Exception.php [new file with mode: 0644]
lib/phpmailer/src/PHPMailer.php [moved from lib/phpmailer/class.phpmailer.php with 66% similarity]
lib/phpmailer/src/SMTP.php [moved from lib/phpmailer/class.smtp.php with 70% similarity]
lib/requirejs/moodle-config.js
lib/spout/readme_moodle.txt
lib/spout/src/Spout/Common/Escaper/ODS.php
lib/spout/src/Spout/Common/Escaper/XLSX.php
lib/spout/src/Spout/Common/Helper/FileSystemHelper.php
lib/spout/src/Spout/Reader/AbstractReader.php
lib/spout/src/Spout/Reader/CSV/Reader.php
lib/spout/src/Spout/Reader/CSV/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/RowIterator.php
lib/spout/src/Spout/Reader/CSV/Sheet.php
lib/spout/src/Spout/Reader/CSV/SheetIterator.php
lib/spout/src/Spout/Reader/Common/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/XMLProcessor.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Reader.php
lib/spout/src/Spout/Reader/ODS/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/RowIterator.php
lib/spout/src/Spout/Reader/ODS/Sheet.php
lib/spout/src/Spout/Reader/ODS/SheetIterator.php
lib/spout/src/Spout/Reader/Wrapper/SimpleXMLElement.php [deleted file]
lib/spout/src/Spout/Reader/Wrapper/XMLReader.php
lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php
lib/spout/src/Spout/Reader/XLSX/Reader.php
lib/spout/src/Spout/Reader/XLSX/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/RowIterator.php
lib/spout/src/Spout/Reader/XLSX/Sheet.php
lib/spout/src/Spout/Reader/XLSX/SheetIterator.php
lib/spout/src/Spout/Writer/AbstractWriter.php
lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php
lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php
lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php
lib/spout/src/Spout/Writer/Common/Sheet.php
lib/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php
lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php
lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php
lib/spout/src/Spout/Writer/ODS/Writer.php
lib/spout/src/Spout/Writer/Style/Style.php
lib/spout/src/Spout/Writer/Style/StyleBuilder.php
lib/spout/src/Spout/Writer/WriterInterface.php
lib/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php
lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php
lib/tcpdf/CHANGELOG.TXT
lib/tcpdf/README.TXT
lib/tcpdf/composer.json
lib/tcpdf/include/tcpdf_static.php
lib/tcpdf/readme_moodle.txt
lib/tcpdf/tcpdf.php
lib/tests/accesslib_test.php
lib/tests/completionlib_test.php
lib/tests/event_profile_field_test.php [new file with mode: 0644]
lib/tests/htmlpurifier_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/webdavlib.php
media/player/videojs/amd/build/Youtube-lazy.min.js
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/build/video-lazy.min.js
media/player/videojs/amd/build/videojs-flash-lazy.min.js [new file with mode: 0644]
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/amd/src/loader.js
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/videojs-flash-lazy.js [new file with mode: 0644]
media/player/videojs/classes/plugin.php
media/player/videojs/readme_moodle.txt
media/player/videojs/styles.css
media/player/videojs/thirdpartylibs.xml
media/player/videojs/videojs/lang/ar.js
media/player/videojs/videojs/lang/de.js
media/player/videojs/videojs/lang/en.js
media/player/videojs/videojs/lang/es.js
media/player/videojs/videojs/lang/fr.js
media/player/videojs/videojs/lang/gl.js [new file with mode: 0644]
media/player/videojs/videojs/lang/nl.js
media/player/videojs/videojs/lang/pt-PT.js [new file with mode: 0644]
media/player/videojs/videojs/lang/sk.js [new file with mode: 0644]
media/player/videojs/videojs/lang/tr.js
media/player/videojs/videojs/lang/vi.js
media/player/videojs/videojs/lang/zh-CN.js
media/player/videojs/videojs/video-js.swf
message/classes/search/base_message.php
message/classes/search/message_received.php
message/classes/search/message_sent.php
message/tests/search_received_test.php [moved from message/tests/search_test_received.php with 77% similarity]
message/tests/search_sent_test.php [moved from message/tests/search_test_sent.php with 75% similarity]
mod/assign/amd/build/grading_actions.min.js
mod/assign/amd/build/grading_navigation.min.js
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/src/grading_actions.js
mod/assign/amd/src/grading_navigation.js
mod/assign/amd/src/grading_panel.js
mod/assign/feedback/editpdf/fpdi/LICENSE
mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85.php
mod/assign/feedback/editpdf/fpdi/filters/FilterASCIIHexDecode.php
mod/assign/feedback/editpdf/fpdi/filters/FilterLZW.php
mod/assign/feedback/editpdf/fpdi/fpdf_tpl.php
mod/assign/feedback/editpdf/fpdi/fpdi.php
mod/assign/feedback/editpdf/fpdi/fpdi_bridge.php
mod/assign/feedback/editpdf/fpdi/fpdi_pdf_parser.php
mod/assign/feedback/editpdf/fpdi/pdf_context.php
mod/assign/feedback/editpdf/fpdi/pdf_parser.php
mod/assign/feedback/editpdf/fpdi/readme_moodle.txt
mod/assign/feedback/editpdf/thirdpartylibs.xml
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/templates/grading_actions.mustache
mod/assign/tests/lib_test.php
mod/book/classes/search/chapter.php
mod/book/lib.php
mod/book/tests/behat/reorganize_chapters.feature [new file with mode: 0644]
mod/book/tests/search_test.php
mod/choice/lib.php
mod/choice/tests/lib_test.php
mod/data/backup/moodle2/backup_data_stepslib.php
mod/data/backup/moodle2/restore_data_stepslib.php
mod/data/classes/search/entry.php
mod/data/db/tag.php [new file with mode: 0644]
mod/data/edit.php
mod/data/export.php
mod/data/export_form.php
mod/data/import.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/locallib.php
mod/data/styles.css
mod/data/templates.php
mod/data/tests/behat/view_entries.feature
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/data/tests/lib_test.php
mod/data/tests/search_test.php
mod/data/version.php
mod/forum/classes/search/post.php
mod/forum/db/install.xml
mod/forum/db/upgrade.php
mod/forum/lib.php
mod/forum/tests/search_test.php
mod/forum/version.php
mod/glossary/classes/search/entry.php
mod/glossary/lib.php
mod/glossary/tests/search_test.php
mod/lti/locallib.php
mod/quiz/attemptlib.php
mod/quiz/report/overview/report.php
mod/quiz/report/overview/tests/report_test.php
mod/quiz/report/responses/report.php
mod/survey/classes/search/activity.php
mod/survey/tests/search_test.php [new file with mode: 0644]
mod/wiki/classes/search/collaborative_page.php
mod/wiki/tests/search_test.php
mod/workshop/classes/external.php
mod/workshop/tests/external_test.php
pix/i/completion-auto-n-override.png [new file with mode: 0644]
pix/i/completion-auto-n-override.svg [new file with mode: 0644]
pix/i/completion-auto-y-override.png [new file with mode: 0644]
pix/i/completion-auto-y-override.svg [new file with mode: 0644]
pix/i/completion-manual-n-override.png [new file with mode: 0644]
pix/i/completion-manual-n-override.svg [new file with mode: 0644]
pix/i/completion-manual-y-override.png [new file with mode: 0644]
pix/i/completion-manual-y-override.svg [new file with mode: 0644]
report/insights/classes/output/insight.php
report/insights/classes/output/insights_list.php
report/insights/lang/en/report_insights.php
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/insights/templates/insights_list.mustache
report/progress/amd/build/completion_override.min.js [new file with mode: 0644]
report/progress/amd/src/completion_override.js [new file with mode: 0644]
report/progress/index.php
report/progress/tests/behat/activity_completion_report.feature [new file with mode: 0644]
search/classes/base.php
search/classes/base_activity.php
search/classes/base_block.php
search/classes/base_mod.php
search/classes/engine.php
search/classes/manager.php
search/classes/skip_future_documents_iterator.php [new file with mode: 0644]
search/cli/indexer.php
search/engine/solr/classes/engine.php
search/tests/base_activity_test.php
search/tests/base_block_test.php
search/tests/manager_test.php
search/upgrade.txt
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/scss/moodle/calendar.scss
theme/boost/settings.php
theme/boost/templates/columns2.mustache
theme/boost/templates/mod_assign/grading_actions.mustache
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/report_insights/insight.mustache [deleted file]
theme/bootstrapbase/templates/report_insights/insight_details.mustache [deleted file]
theme/bootstrapbase/templates/report_insights/insights_list.mustache [deleted file]
theme/upgrade.txt
user/amd/build/unified_filter.min.js
user/amd/src/unified_filter.js
user/classes/search/user.php
user/lib.php
user/profile/definelib.php
user/profile/field/checkbox/field.class.php
user/profile/field/menu/field.class.php
user/profile/field/upgrade.txt [new file with mode: 0644]
user/profile/lib.php
user/templates/unified_filter.mustache
user/tests/behat/filter_participants.feature
user/tests/search_test.php
version.php
webservice/upgrade.txt
webservice/upload.php
webservice/xmlrpc/lib.php

index 0622612..7d19e9c 100644 (file)
@@ -62,6 +62,8 @@ lib/validateurlsyntax.php
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
+media/player/videojs/amd/src/videojs-flash-lazy.js
+media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
index cfcf702..c35c560 100644 (file)
@@ -63,6 +63,8 @@ lib/validateurlsyntax.php
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
+media/player/videojs/amd/src/videojs-flash-lazy.js
+media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
index b077d44..e8f0816 100644 (file)
@@ -863,6 +863,7 @@ $cachewarnings = cache_helper::warnings();
 // Check if there are events 1 API handlers.
 $eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
 $themedesignermode = !empty($CFG->themedesignermode);
+$mobileconfigured = !empty($CFG->enablemobilewebservice);
 
 // Check if a directory with development libraries exists.
 if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules'))) {
@@ -877,4 +878,5 @@ $output = $PAGE->get_renderer('core', 'admin');
 
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
-                                       $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir);
+                                       $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
+                                       $mobileconfigured);
index 7dd4808..1fd8cb5 100644 (file)
@@ -278,13 +278,14 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param array $eventshandlers Events 1 API handlers.
      * @param bool $themedesignermode Warn about the theme designer mode.
      * @param bool $devlibdir Warn about development libs directory presence.
+     * @param bool $mobileconfigured Whether the mobile web services have been enabled
      *
      * @return string HTML to output.
      */
     public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
-            $themedesignermode = false, $devlibdir = false) {
+            $themedesignermode = false, $devlibdir = false, $mobileconfigured = false) {
         global $CFG;
         $output = '';
 
@@ -303,6 +304,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->cache_warnings($cachewarnings);
         $output .= $this->events_handlers($eventshandlers);
         $output .= $this->registration_warning($registered);
+        $output .= $this->mobile_configuration_warning($mobileconfigured);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
         ////  IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
@@ -830,6 +832,23 @@ class core_admin_renderer extends plugin_renderer_base {
         return $this->registration_warning(\core\hub\registration::is_registered());
     }
 
+    /**
+     * Display a warning about the Mobile Web Services being disabled.
+     *
+     * @param boolean $mobileconfigured true if mobile web services are enabled
+     * @return string HTML to output.
+     */
+    protected function mobile_configuration_warning($mobileconfigured) {
+        $output = '';
+        if (!$mobileconfigured) {
+            $settingslink = new moodle_url('/admin/settings.php', ['section' => 'mobilesettings']);
+            $configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin'));
+            $output .= $this->warning(get_string('mobilenotconfiguredwarning', 'admin') . '&nbsp;' . $configurebutton);
+        }
+
+        return $output;
+    }
+
     /**
      * Helper method to render the information about the available Moodle update
      *
index e8247c8..b42a252 100644 (file)
@@ -101,5 +101,10 @@ if ($hassiteconfig) {
         // Disable web interface evaluation and get predictions.
         $settings->add(new admin_setting_configcheckbox('analytics/onlycli', new lang_string('onlycli', 'analytics'),
             new lang_string('onlycliinfo', 'analytics'), 1));
+
+        // Training and prediction time limit per model.
+        $settings->add(new admin_setting_configduration('analytics/modeltimelimit', new lang_string('modeltimelimit', 'analytics'),
+            new lang_string('modeltimelimitinfo', 'analytics'), 20 * MINSECS));
+
     }
 }
index 32bdcfc..628b097 100644 (file)
@@ -159,11 +159,12 @@ class renderer extends plugin_renderer_base {
             if ($trainresults->status == 0) {
                 $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
-            } else if ($trainresults->status === \core_analytics\model::NO_DATASET) {
+            } else if ($trainresults->status === \core_analytics\model::NO_DATASET ||
+                    $trainresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
                 $output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $trainresults->status),
+                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $trainresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
@@ -183,11 +184,12 @@ class renderer extends plugin_renderer_base {
             if ($predictresults->status == 0) {
                 $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
-            } else if ($predictresults->status === \core_analytics\model::NO_DATASET) {
+            } else if ($predictresults->status === \core_analytics\model::NO_DATASET ||
+                    $predictresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
                 $output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $predictresults->status),
+                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $predictresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
index df38a14..3cdb723 100644 (file)
@@ -30,7 +30,7 @@ $string['bettercli'] = 'Evaluating models and generating predictions may involve
 $string['cantguessstartdate'] = 'Can\'t guess the start date';
 $string['cantguessenddate'] = 'Can\'t guess the end date';
 $string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
-$string['clievaluationandpredictions'] = 'A cron task iterates through enabled models and gets predictions. Models evaluation via command line is disabled. You can allow these processes to be executed manually via web interface by enabling <a href="{$a}">\'onlycli\' analytics setting</a>';
+$string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
 $string['enabled'] = 'Enabled';
index 417008a..efc5448 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js and b/admin/tool/usertours/amd/build/usertours.min.js differ
index 4541f23..044180b 100644 (file)
@@ -78,18 +78,22 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
          * @method  addResetLink
          */
         addResetLink: function() {
-            str.get_string('resettouronpage', 'tool_usertours')
-                .done(function(s) {
-                    // Grab the last item in the page of these.
-                    $('footer, .logininfo')
-                    .last()
-                    .append(
-                        '<div class="usertour">' +
-                            '<a href="#" data-action="tool_usertours/resetpagetour">' +
-                                s +
-                            '</a>' +
-                        '</div>'
-                    );
+            var ele;
+            // Append the link to the most suitable place on the page
+            // with fallback to legacy selectors and finally the body
+            // if there is no better place.
+            if ($('.tool_usertours-resettourcontainer').length) {
+                ele = $('.tool_usertours-resettourcontainer');
+            } else if ($('.logininfo').length) {
+                ele = $('.logininfo');
+            } else if ($('footer').length) {
+                ele = $('footer');
+            } else {
+                ele = $('body');
+            }
+            templates.render('tool_usertours/resettour', {})
+                .done(function(html, js) {
+                    templates.appendNodeContents(ele, html, js);
                 });
         },
 
diff --git a/admin/tool/usertours/templates/resettour.mustache b/admin/tool/usertours/templates/resettour.mustache
new file mode 100644 (file)
index 0000000..b1fe728
--- /dev/null
@@ -0,0 +1,27 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_usertours/resettour
+
+    Adds the Reset user tour on this page link.
+
+    Example context (json):
+    {}
+}}
+<div class="usertour">
+    <a href="#" data-action="tool_usertours/resetpagetour">{{#str}}resettouronpage, tool_usertours{{/str}}</a>
+</div>
index 444e384..e56c325 100644 (file)
@@ -54,7 +54,7 @@ foreach ($allfunctions as $f) {
         //some plugins may want to have own test client forms
         include_once($CFG->dirroot.'/'.$finfo->testclientpath);
     }
-    $class = $f->name.'_form';
+    $class = $f->name.'_testclient_form';
     if (class_exists($class)) {
         $functions[$f->name] = $f->name;
         continue;
@@ -103,7 +103,7 @@ if (!$function or !$protocol) {
     die;
 }
 
-$class = $function.'_form';
+$class = $function.'_testclient_form';
 
 $mform = new $class(null, array('authmethod' => $authmethod));
 $mform->set_data(array('function'=>$function, 'protocol'=>$protocol));
index b01b78b..9f89f30 100644 (file)
@@ -29,19 +29,23 @@ class webservice_test_client_form extends moodleform {
 // === Test client forms ===
 
 /**
- * Form class for create_categories() web service function test.
+ * Base class for implementations of WS test client forms.
  *
  * @package   core_webservice
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @copyright 2012 Fabio Souto
+ * @copyright 2017 Marina Glancy
  */
-class core_course_create_categories_form extends moodleform {
+abstract class webservice_test_client_base_form extends moodleform {
+
+    /**
+     * Definition of the parameters used by this WS function
+     */
+    protected abstract function test_client_definition();
+
     /**
      * The form definition.
      */
     public function definition() {
-        global $CFG;
-
         $mform = $this->_form;
 
         $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
@@ -59,19 +63,7 @@ class core_course_create_categories_form extends moodleform {
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'name[0]', 'name[0]');
-        $mform->addElement('text', 'parent[0]', 'parent[0]');
-        $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
-        $mform->addElement('text', 'description[0]', 'description[0]');
-        $mform->addElement('text', 'name[1]', 'name[1]');
-        $mform->addElement('text', 'parent[1]', 'parent[1]');
-        $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
-        $mform->addElement('text', 'description[1]', 'description[1]');
-        $mform->setType('name', core_user::get_property_type('firstname'));
-        $mform->setType('parent', core_user::get_property_type('id'));
-        $mform->setType('idnumber', core_user::get_property_type('idnumber'));
-        $mform->setType('description', core_user::get_property_type('description'));
+        $mform->setType('authmethod', PARAM_ALPHA);
 
         $mform->addElement('hidden', 'function');
         $mform->setType('function', PARAM_PLUGIN);
@@ -79,6 +71,8 @@ class core_course_create_categories_form extends moodleform {
         $mform->addElement('hidden', 'protocol');
         $mform->setType('protocol', PARAM_ALPHA);
 
+        $this->test_client_definition();
+
         $this->add_action_buttons(true, get_string('execute', 'webservice'));
     }
 
@@ -90,19 +84,51 @@ class core_course_create_categories_form extends moodleform {
         if (!$data = $this->get_data()) {
             return null;
         }
-        // Remove unused from form data.
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
+        return array_diff_key((array)$data, ['submitbutton' => 1, 'protocol' => 1, 'function' => 1,
+            'wsusername' => 1, 'wspassword' => 1, 'token' => 1, 'authmethod' => 1]);
+    }
+}
+
+/**
+ * Form class for create_categories() web service function test.
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2012 Fabio Souto
+ */
+class core_course_create_categories_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+        $mform = $this->_form;
+        $mform->addElement('text', 'name[0]', 'name[0]');
+        $mform->addElement('text', 'parent[0]', 'parent[0]');
+        $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
+        $mform->addElement('text', 'description[0]', 'description[0]');
+        $mform->addElement('text', 'name[1]', 'name[1]');
+        $mform->addElement('text', 'parent[1]', 'parent[1]');
+        $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
+        $mform->addElement('text', 'description[1]', 'description[1]');
+        $mform->setType('name', PARAM_TEXT);
+        $mform->setType('parent', PARAM_INT);
+        $mform->setType('idnumber', PARAM_RAW);
+        $mform->setType('description', PARAM_RAW);
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        if (!$data = $this->get_data()) {
+            return null;
+        }
 
         $params = array();
         $params['categories'] = array();
         for ($i=0; $i<10; $i++) {
-            if (empty($data->name[$i]) or empty($data->parent[$i])) {
+            if (empty($data->name[$i])) {
                 continue;
             }
             $params['categories'][] = array('name'=>$data->name[$i], 'parent'=>$data->parent[$i],
@@ -119,48 +145,21 @@ class core_course_create_categories_form extends moodleform {
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @copyright 2012 Fabio Souto
  */
-class core_course_delete_categories_form extends moodleform {
+class core_course_delete_categories_testclient_form extends webservice_test_client_base_form {
     /**
      * The form definition.
      */
-    public function definition() {
-        global $CFG;
-
+    protected function test_client_definition() {
         $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'id[0]', 'id[0]');
         $mform->addElement('text', 'newparent[0]', 'newparent[0]');
         $mform->addElement('text', 'recursive[0]', 'recursive[0]');
         $mform->addElement('text', 'id[1]', 'id[1]');
         $mform->addElement('text', 'newparent[1]', 'newparent[1]');
         $mform->addElement('text', 'recursive[1]', 'recursive[1]');
-        $mform->setType('id', core_user::get_property_type('id'));
+        $mform->setType('id', PARAM_INT);
         $mform->setType('newparent', PARAM_INT);
         $mform->setType('recursive', PARAM_BOOL);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
     }
 
     /**
@@ -171,15 +170,6 @@ class core_course_delete_categories_form extends moodleform {
         if (!$data = $this->get_data()) {
             return null;
         }
-        // Remove unused from form data.
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
         $params = array();
         $params['categories'] = array();
         for ($i=0; $i<10; $i++) {
@@ -207,31 +197,12 @@ class core_course_delete_categories_form extends moodleform {
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @copyright 2012 Fabio Souto
  */
-class core_course_update_categories_form extends moodleform {
+class core_course_update_categories_testclient_form extends webservice_test_client_base_form {
     /**
      * The form definition.
      */
-    public function definition() {
-        global $CFG;
-
+    protected function test_client_definition() {
         $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'id[0]', 'id[0]');
         $mform->addElement('text', 'name[0]', 'name[0]');
         $mform->addElement('text', 'parent[0]', 'parent[0]');
@@ -242,19 +213,11 @@ class core_course_update_categories_form extends moodleform {
         $mform->addElement('text', 'parent[1]', 'parent[1]');
         $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
         $mform->addElement('text', 'description[1]', 'description[1]');
-        $mform->setType('id', core_user::get_property_type('id'));
-        $mform->setType('name', core_user::get_property_type('firstname'));
+        $mform->setType('id', PARAM_INT);
+        $mform->setType('name', PARAM_TEXT);
         $mform->setType('parent', PARAM_INT);
-        $mform->setType('idnumber', core_user::get_property_type('idnumber'));
-        $mform->setType('description', core_user::get_property_type('description'));
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
+        $mform->setType('idnumber', PARAM_RAW);
+        $mform->setType('description', PARAM_RAW);
     }
 
     /**
@@ -265,15 +228,6 @@ class core_course_update_categories_form extends moodleform {
         if (!$data = $this->get_data()) {
             return null;
         }
-        // Remove unused from form data.
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
         $params = array();
         $params['categories'] = array();
         for ($i=0; $i<10; $i++) {
@@ -299,4 +253,100 @@ class core_course_update_categories_form extends moodleform {
         }
         return $params;
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Test class for WS function core_fetch_notifications
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_fetch_notifications_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+        $mform = $this->_form;
+        $mform->addElement('text', 'contextid', 'contextid');
+        $mform->setType('contextid', PARAM_INT);
+        $mform->setDefault('contextid', context_system::instance()->id);
+    }
+}
+
+/**
+ * Test class for WS function get_site_info
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_webservice_get_site_info_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+    }
+}
+
+/**
+ * Test class for WS function core_get_string
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_get_string_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+        $mform = $this->_form;
+        $mform->addElement('text', 'stringid', 'stringid');
+        $mform->setType('stringid', PARAM_STRINGID);
+        $mform->addElement('text', 'component', 'component');
+        $mform->setType('component', PARAM_COMPONENT);
+        $mform->addElement('text', 'lang', 'lang');
+        $mform->setType('lang', PARAM_LANG);
+        $mform->addElement('text', 'stringparams_name[1]', 'Parameter 1 name');
+        $mform->setType('stringparams_name[1]', PARAM_ALPHANUMEXT);
+        $mform->addElement('text', 'stringparams_value[1]', 'Parameter 1 value');
+        $mform->setType('stringparams_value[1]', PARAM_RAW);
+        $mform->addElement('text', 'stringparams_name[2]', 'Parameter 2 name');
+        $mform->setType('stringparams_name[2]', PARAM_ALPHANUMEXT);
+        $mform->addElement('text', 'stringparams_value[2]', 'Parameter 2 value');
+        $mform->setType('stringparams_value[2]', PARAM_RAW);
+        $mform->addElement('text', 'stringparams_name[3]', 'Parameter 3 name');
+        $mform->setType('stringparams_name[3]', PARAM_ALPHANUMEXT);
+        $mform->addElement('text', 'stringparams_value[3]', 'Parameter 3 value');
+        $mform->setType('stringparams_value[3]', PARAM_RAW);
+        $mform->addElement('static', 'paramnote', '', 'If a parameter is not an object, only specify "Parameter 1 value"');
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        $params = parent::get_params();
+        if ($params === null) {
+            return null;
+        }
+
+        $params['stringparams'] = [];
+        for ($idx = 1; $idx <= 3; $idx++) {
+            $name = isset($params['stringparams_name'][$idx]) ? strval($params['stringparams_name'][$idx]) : '';
+            $value = isset($params['stringparams_value'][$idx]) ? strval($params['stringparams_value'][$idx]) : '';
+            if ($name !== '' || $value !== '') {
+                if ($name === '') {
+                    $params['stringparams'][] = ['value' => $value];
+                } else {
+                    $params['stringparams'][] = ['name' => $name, 'value' => $value];
+                }
+            }
+        }
+        unset($params['stringparams_name']);
+        unset($params['stringparams_value']);
+        return $params;
+    }
+}
index 203ad55..c523697 100644 (file)
@@ -58,6 +58,15 @@ class admin_setting_predictor extends \admin_setting_configselect {
             return get_string('errorprocessornotready', 'analytics', $isready);
         }
 
+        $currentvalue = get_config('analytics', 'predictionsprocessor');
+        if (!empty($currentvalue) && $currentvalue != str_replace('\\\\', '\\', $data)) {
+            // Clear all models data.
+            $models = \core_analytics\manager::get_all_models();
+            foreach ($models as $model) {
+                $model->clear();
+            }
+        }
+
         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
     }
 }
index 4b457e8..b467168 100644 (file)
@@ -202,6 +202,61 @@ class dataset_manager {
             '/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/', self::EVALUATION_FILENAME);
     }
 
+    /**
+     * Gets the list of files that couldn't be previously used for training and prediction.
+     *
+     * @param int $modelid
+     * @param bool $includetarget
+     * @param string[] $timesplittingids
+     * @return null
+     */
+    public static function get_pending_files($modelid, $includetarget, $timesplittingids) {
+        global $DB;
+
+        $fs = get_file_storage();
+
+        if ($includetarget) {
+            $filearea = self::LABELLED_FILEAREA;
+            $usedfileaction = 'trained';
+        } else {
+            $filearea = self::UNLABELLED_FILEAREA;
+            $usedfileaction = 'predicted';
+        }
+
+        $select = 'modelid = :modelid AND action = :action';
+        $params = array('modelid' => $modelid, 'action' => $usedfileaction);
+        $usedfileids = $DB->get_fieldset_select('analytics_used_files', 'fileid', $select, $params);
+
+        // Very likely that we will only have 1 time splitting method here.
+        $filesbytimesplitting = array();
+        foreach ($timesplittingids as $timesplittingid) {
+
+            $filepath = '/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/';
+            $files = $fs->get_directory_files(\context_system::instance()->id, 'analytics', $filearea, $modelid, $filepath);
+            foreach ($files as $file) {
+
+                // Discard evaluation files.
+                if ($file->get_filename() === self::EVALUATION_FILENAME) {
+                    continue;
+                }
+
+                // No dirs.
+                if ($file->is_directory()) {
+                    continue;
+                }
+
+                // Already used for training.
+                if (in_array($file->get_id(), $usedfileids)) {
+                    continue;
+                }
+
+                $filesbytimesplitting[$timesplittingid][] = $file;
+            }
+        }
+
+        return $filesbytimesplitting;
+    }
+
     /**
      * Deletes previous evaluation files of this model.
      *
index 63c75b5..8d792b4 100644 (file)
@@ -118,7 +118,7 @@ abstract class base {
      * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
      * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
      *
-     * @return \core_analytics\analysable[]
+     * @return \core_analytics\analysable[] Array of analysable elements using the analysable id as array key.
      */
     abstract public function get_analysables();
 
@@ -180,23 +180,63 @@ abstract class base {
      * @return \stored_file[]
      */
     public function get_analysable_data($includetarget) {
+        global $DB;
+
+        // Time limit control.
+        $modeltimelimit = intval(get_config('analytics', 'modeltimelimit'));
 
         $filesbytimesplitting = array();
 
-        $analysables = $this->get_analysables();
-        foreach ($analysables as $analysable) {
+        list($analysables, $processedanalysables) = $this->get_sorted_analysables($includetarget);
+
+        $inittime = time();
+        foreach ($analysables as $key => $analysable) {
 
             $files = $this->process_analysable($analysable, $includetarget);
 
             // Later we will need to aggregate data by time splitting method.
             foreach ($files as $timesplittingid => $file) {
-                $filesbytimesplitting[$timesplittingid][$analysable->get_id()] = $file;
+                $filesbytimesplitting[$timesplittingid][] = $file;
+            }
+
+            $this->update_analysable_analysed_time($processedanalysables, $analysable->get_id(), $includetarget);
+
+            // Apply time limit.
+            if (!$this->options['evaluation']) {
+                $timespent = time() - $inittime;
+                if ($modeltimelimit <= $timespent) {
+                    break;
+                }
+            }
+
+            unset($analysables[$key]);
+        }
+
+        if ($this->options['evaluation'] === false) {
+            // Look for previous training and prediction files we generated and couldn't be used
+            // by machine learning backends because they weren't big enough.
+
+            $pendingfiles = \core_analytics\dataset_manager::get_pending_files($this->modelid, $includetarget,
+                array_keys($filesbytimesplitting));
+            foreach ($pendingfiles as $timesplittingid => $files) {
+                foreach ($files as $file) {
+                    $filesbytimesplitting[$timesplittingid][] = $file;
+                }
             }
         }
 
         // We join the datasets by time splitting method.
         $timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
 
+        if (!empty($pendingfiles)) {
+            // We must remove them now as they are already part of another dataset.
+            foreach ($pendingfiles as $timesplittingid => $files) {
+                foreach ($files as $file) {
+                    $file->delete();
+                }
+            }
+        }
+
         return $timesplittingfiles;
     }
 
@@ -721,4 +761,86 @@ abstract class base {
             $data[1][] = $value;
         }
     }
+
+    /**
+     * Returns the list of analysables sorted in processing priority order.
+     *
+     * It will first return analysables that have never been analysed before
+     * and it will continue with the ones we have already seen by timeanalysed DESC
+     * order.
+     *
+     * @param bool $includetarget
+     * @return array(0 => \core_analytics\analysable[], 1 => \stdClass[])
+     */
+    protected function get_sorted_analysables($includetarget) {
+
+        $analysables = $this->get_analysables();
+
+        // Get the list of analysables that have been already processed.
+        $processedanalysables = $this->get_processed_analysables($includetarget);
+
+        // We want to start processing analysables we have not yet processed and later continue
+        // with analysables that we already processed.
+        $unseen = array_diff_key($analysables, $processedanalysables);
+
+        // Var $processed first as we want to respect its timeanalysed DESC order so analysables that
+        // have recently been processed are on the bottom of the stack.
+        $seen = array_intersect_key($processedanalysables, $analysables);
+        array_walk($seen, function(&$value, $analysableid) use ($analysables) {
+            // We replace the analytics_used_analysables record by the analysable object.
+            $value = $analysables[$analysableid];
+        });
+
+        return array($unseen + $seen, $processedanalysables);
+    }
+
+    /**
+     * Get analysables that have been already processed.
+     *
+     * @param bool $includetarget
+     * @return \stdClass[]
+     */
+    protected function get_processed_analysables($includetarget) {
+        global $DB;
+
+        $params = array('modelid' => $this->modelid);
+        $params['action'] = ($includetarget) ? 'training' : 'prediction';
+        $select = 'modelid = :modelid and action = :action';
+
+        // Weird select fields ordering for performance (analysableid key matching, analysableid is also unique by modelid).
+        return $DB->get_records_select('analytics_used_analysables', $select,
+            $params, 'timeanalysed DESC', 'analysableid, modelid, action, timeanalysed, id AS primarykey');
+    }
+
+    /**
+     * Updates the analysable analysis time.
+     *
+     * @param array $processedanalysables
+     * @param int $analysableid
+     * @param bool $includetarget
+     * @return null
+     */
+    protected function update_analysable_analysed_time($processedanalysables, $analysableid, $includetarget) {
+        global $DB;
+
+        if (!empty($processedanalysables[$analysableid])) {
+            $obj = $processedanalysables[$analysableid];
+
+            $obj->id = $obj->primarykey;
+            unset($obj->primarykey);
+
+            $obj->timeanalysed = time();
+            $DB->update_record('analytics_used_analysables', $obj);
+
+        } else {
+
+            $obj = new \stdClass();
+            $obj->modelid = $this->modelid;
+            $obj->action = ($includetarget) ? 'training' : 'prediction';
+            $obj->analysableid = $analysableid;
+            $obj->timeanalysed = time();
+
+            $DB->insert_record('analytics_used_analysables', $obj);
+        }
+    }
 }
index 8e5b266..6661806 100644 (file)
@@ -55,7 +55,7 @@ abstract class by_course extends base {
         foreach ($courses as $course) {
             // Skip the frontpage course.
             $analysable = \core_analytics\course::instance($course);
-            $analysables[] = $analysable;
+            $analysables[$analysable->get_id()] = $analysable;
         }
 
         if (empty($analysables)) {
index 2a21359..075b930 100644 (file)
@@ -42,6 +42,6 @@ abstract class sitewide extends base {
      */
     public function get_analysables() {
         $analysable = new \core_analytics\site();
-        return array($analysable);
+        return array(SYSCONTEXTID => $analysable);
     }
 }
index 5b741d2..3e84120 100644 (file)
@@ -261,7 +261,7 @@ abstract class base extends \core_analytics\calculable {
     }
 
     /**
-     * Should the model callback be triggered?
+     * This method determines if a prediction is interesing for the model or not.
      *
      * @param mixed $predictedvalue
      * @param float $predictionscore
index cbd8fe0..26044c7 100644 (file)
@@ -152,7 +152,10 @@ abstract class discrete extends base {
     }
 
     /**
-     * Should the model callback be triggered?
+     * This method determines if a prediction is interesing for the model or not.
+     *
+     * This method internally calls ignored_predicted_classes to skip classes
+     * flagged by the target as not important for users.
      *
      * @param mixed $predictedvalue
      * @param float $predictionscore
index d16ad96..f10d843 100644 (file)
@@ -84,7 +84,7 @@ abstract class linear extends base {
     }
 
     /**
-     * Should the model callback be triggered?
+     * This method determines if a prediction is interesing for the model or not.
      *
      * @param mixed $predictedvalue
      * @param float $predictionscore
index 4bf9ea1..f04eb96 100644 (file)
@@ -53,12 +53,12 @@ class model {
     /**
      * Model with low prediction accuracy.
      */
-    const EVALUATE_LOW_SCORE = 4;
+    const LOW_SCORE = 4;
 
     /**
      * Not enough data to evaluate the model properly.
      */
-    const EVALUATE_NOT_ENOUGH_DATA = 8;
+    const NOT_ENOUGH_DATA = 8;
 
     /**
      * Invalid analysable for the time splitting method.
@@ -437,7 +437,7 @@ class model {
                 $this->model->indicators !== $indicatorsstr) {
 
             // Delete generated predictions before changing the model version.
-            $this->clear_model();
+            $this->clear();
 
             // It needs to be reset as the version changes.
             $this->uniqueid = null;
@@ -474,9 +474,9 @@ class model {
 
         \core_analytics\manager::check_can_manage_models();
 
-        $this->clear_model();
+        $this->clear();
 
-        // Method self::clear_model is already clearing the current model version.
+        // Method self::clear is already clearing the current model version.
         $predictor = \core_analytics\manager::get_predictions_processor();
         $predictor->delete_output_dir($this->get_output_dir(array(), true));
 
@@ -633,6 +633,10 @@ class model {
         $result->status = $predictorresult->status;
         $result->info = $predictorresult->info;
 
+        if ($result->status !== self::OK) {
+            return $result;
+        }
+
         $this->flag_file_as_used($samplesfile, 'trained');
 
         // Mark the model as trained if it wasn't.
@@ -689,7 +693,7 @@ class model {
         $samplesfile = $samplesdata[$this->model->timesplitting];
 
         // We need to throw an exception if we are trying to predict stuff that was already predicted.
-        $params = array('modelid' => $this->model->id, 'fileid' => $samplesfile->get_id(), 'action' => 'predicted');
+        $params = array('modelid' => $this->model->id, 'action' => 'predicted', 'fileid' => $samplesfile->get_id());
         if ($predicted = $DB->get_record('analytics_used_files', $params)) {
             throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
         }
@@ -717,6 +721,10 @@ class model {
             $result->predictions = $this->format_predictor_predictions($predictorresult);
         }
 
+        if ($result->status !== self::OK) {
+            return $result;
+        }
+
         if ($result->predictions) {
             $samplecontexts = $this->execute_prediction_callbacks($result->predictions, $indicatorcalculations);
         }
@@ -780,15 +788,16 @@ class model {
 
         // Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
         $samplecontexts = array();
+        $records = array();
 
         foreach ($predictions as $uniquesampleid => $prediction) {
 
-            if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
+            // The unique sample id contains both the sampleid and the rangeindex.
+            list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
 
-                // The unique sample id contains both the sampleid and the rangeindex.
-                list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
+            if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
 
-                // Store the predicted values.
+                // Prepare the record to store the predicted values.
                 list($record, $samplecontext) = $this->prepare_prediction_record($sampleid, $rangeindex, $prediction->prediction,
                     $prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
 
@@ -932,8 +941,6 @@ class model {
      * @return \context
      */
     protected function prepare_prediction_record($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
-        global $DB;
-
         $context = $this->get_analyser()->sample_access_context($sampleid);
 
         $record = new \stdClass();
@@ -946,6 +953,15 @@ class model {
         $record->calculations = $calculations;
         $record->timecreated = time();
 
+        $analysable = $this->get_analyser()->get_sample_analysable($sampleid);
+        $timesplitting = $this->get_time_splitting();
+        $timesplitting->set_analysable($analysable);
+        $range = $timesplitting->get_range_by_index($rangeindex);
+        if ($range) {
+            $record->timestart = $range['start'];
+            $record->timeend = $range['end'];
+        }
+
         return array($record, $context);
     }
 
@@ -983,7 +999,7 @@ class model {
             }
 
             // Delete generated predictions before changing the model version.
-            $this->clear_model();
+            $this->clear();
 
             // It needs to be reset as the version changes.
             $this->uniqueid = null;
@@ -1261,7 +1277,7 @@ class model {
             $outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
         }
 
-        // Append model id
+        // Append model id.
         $outputdir .= DIRECTORY_SEPARATOR . $this->model->id;
         if (!$onlymodelid) {
             // Append version + subdirs.
@@ -1428,9 +1444,11 @@ class model {
      *
      * @return void
      */
-    private function clear_model() {
+    public function clear() {
         global $DB;
 
+        \core_analytics\manager::check_can_manage_models();
+
         // Delete current model version stored stuff.
         $predictor = \core_analytics\manager::get_predictions_processor();
         $predictor->clear_model($this->get_unique_id(), $this->get_output_dir());
@@ -1446,6 +1464,7 @@ class model {
         $DB->delete_records('analytics_predict_samples', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
+        $DB->delete_records('analytics_used_analysables', array('modelid' => $this->model->id));
 
         // Purge all generated files.
         \core_analytics\dataset_manager::clear_model_files($this->model->id);
index d36baf5..3856c5f 100644 (file)
@@ -34,22 +34,30 @@ defined('MOODLE_INTERNAL') || die();
 class dataset_manager_testcase extends advanced_testcase {
 
     /**
-     * test_create_dataset
+     * setUp
      *
-     * @return
+     * @return null
      */
-    public function test_create_dataset() {
+    public function setUp() {
         $this->resetAfterTest(true);
 
-        $sharedtoprows = array(
+        $this->sharedtoprows = array(
             array('var1', 'var2'),
             array('value1', 'value2'),
             array('header1', 'header2')
         );
+    }
+
+    /**
+     * test_create_dataset
+     *
+     * @return null
+     */
+    public function test_create_dataset() {
 
         $dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
         $dataset1->init_process();
-        $dataset1data = array_merge($sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+        $dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
         $f1 = $dataset1->store($dataset1data);
         $dataset1->close_process();
 
@@ -63,26 +71,19 @@ class dataset_manager_testcase extends advanced_testcase {
     /**
      * test_merge_datasets
      *
-     * @return
+     * @return null
      */
     public function test_merge_datasets() {
-        $this->resetAfterTest(true);
-
-        $sharedtoprows = array(
-            array('var1', 'var2'),
-            array('value1', 'value2'),
-            array('header1', 'header2')
-        );
 
         $dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
         $dataset1->init_process();
-        $dataset1data = array_merge($sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+        $dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
         $f1 = $dataset1->store($dataset1data);
         $dataset1->close_process();
 
         $dataset2 = new \core_analytics\dataset_manager(1, 2, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
         $dataset2->init_process();
-        $dataset2data = array_merge($sharedtoprows, array(array('no', 'no', 'no')));
+        $dataset2data = array_merge($this->sharedtoprows, array(array('no', 'no', 'no')));
         $f2 = $dataset2->store($dataset2data);
         $dataset2->close_process();
 
@@ -97,4 +98,70 @@ class dataset_manager_testcase extends advanced_testcase {
         $this->assertContains('value1', $mergedfilecontents);
         $this->assertContains('header1', $mergedfilecontents);
     }
+
+    /**
+     * test_get_pending_files
+     *
+     * @return null
+     */
+    public function test_get_pending_files() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $fakemodelid = 123;
+        $timesplittingids = array(
+            '\core\analytics\time_splitting\quarters',
+            '\core\analytics\time_splitting\quarters_accum',
+        );
+
+        // No files.
+        $this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids));
+        $this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
+
+        // We will reuse this analysable file to create training and prediction datasets (analysable level files are
+        // merged into training and prediction files).
+        $analysabledataset = new \core_analytics\dataset_manager($fakemodelid, 1, 'whatever',
+            \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
+        $analysabledataset->init_process();
+        $analysabledatasetdata = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+        $file = $analysabledataset->store($analysabledatasetdata);
+        $analysabledataset->close_process();
+
+        // Evaluation files ignored.
+        $evaluationdataset = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
+            '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, true);
+
+        $this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids));
+        $this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
+
+        // Training and prediction files are not mixed up.
+        $trainingfile1 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
+            '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
+        $trainingfile2 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
+            '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
+
+        $bytimesplitting = \core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids);
+        $this->assertFalse(isset($bytimesplitting['\core\analytics\time_splitting\quarters_accum']));
+        $this->assertCount(2, $bytimesplitting['\core\analytics\time_splitting\quarters']);
+        $this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
+
+        $predictionfile = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
+            '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::UNLABELLED_FILEAREA, false);
+        $bytimesplitting = \core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids);
+        $this->assertFalse(isset($bytimesplitting['\core\analytics\time_splitting\quarters_accum']));
+        $this->assertCount(1, $bytimesplitting['\core\analytics\time_splitting\quarters']);
+
+        // Already used for training and prediction are discarded.
+        $usedfile = (object)['modelid' => $fakemodelid, 'fileid' => $trainingfile1->get_id(), 'action' => 'trained',
+            'time' => time()];
+        $DB->insert_record('analytics_used_files', $usedfile);
+        $bytimesplitting = \core_analytics\dataset_manager::get_pending_files($fakemodelid, true, $timesplittingids);
+        $this->assertCount(1, $bytimesplitting['\core\analytics\time_splitting\quarters']);
+
+        $usedfile->fileid = $predictionfile->get_id();
+        $usedfile->action = 'predicted';
+        $DB->insert_record('analytics_used_files', $usedfile);
+        $this->assertEmpty(\core_analytics\dataset_manager::get_pending_files($fakemodelid, false, $timesplittingids));
+    }
 }
diff --git a/analytics/tests/fixtures/test_analyser.php b/analytics/tests/fixtures/test_analyser.php
new file mode 100644 (file)
index 0000000..736388c
--- /dev/null
@@ -0,0 +1,48 @@
+<?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/>.
+
+/**
+ * Test analyser.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test analyser.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David MonllaĆ³ {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_analyser extends \core\analytics\analyser\courses {
+
+    /**
+     * Overwritten to add a delay.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @param mixed $includetarget
+     * @return null
+     */
+    public function process_analysable($analysable, $includetarget) {
+        // A bit more than 1 second.
+        usleep(1100000);
+        return parent::process_analysable($analysable, $includetarget);
+    }
+}
index fcb7eac..2685b64 100644 (file)
@@ -28,6 +28,8 @@ require_once(__DIR__ . '/fixtures/test_indicator_max.php');
 require_once(__DIR__ . '/fixtures/test_indicator_min.php');
 require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
 require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
+require_once(__DIR__ . '/fixtures/test_analyser.php');
 
 /**
  * Unit tests for the model.
@@ -153,13 +155,10 @@ class analytics_model_testcase extends advanced_testcase {
         $modelversionoutputdir = $this->model->get_output_dir();
         $this->assertTrue(is_dir($modelversionoutputdir));
 
-        // Update to an empty time splitting method to force clear_model execution.
-        $this->model->update(1, false, '');
+        // Update to an empty time splitting method to force model::clear execution.
+        $this->model->clear();
         $this->assertFalse(is_dir($modelversionoutputdir));
 
-        // Restore previous time splitting method.
-        $this->model->enable('\core\analytics\time_splitting\no_splitting');
-
         // Check that most of the stuff got deleted.
         $this->assertEquals(1, $DB->count_records('analytics_models', array('id' => $this->modelobj->id)));
         $this->assertEquals(1, $DB->count_records('analytics_models_log', array('modelid' => $this->modelobj->id)));
@@ -256,6 +255,64 @@ class analytics_model_testcase extends advanced_testcase {
         $this->assertTrue(\core_analytics\model::exists($target));
     }
 
+    /**
+     * test_model_timelimit
+     *
+     * @return null
+     */
+    public function test_model_timelimit() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        set_config('modeltimelimit', 2, 'analytics');
+
+        $courses = array();
+        for ($i = 0; $i < 5; $i++) {
+            $course = $this->getDataGenerator()->create_course();
+            $analysable = new \core_analytics\course($course);
+            $courses[$analysable->get_id()] = $course;
+        }
+
+        $target = new test_target_course_level_shortname();
+        $analyser = new test_analyser(1, $target, [], [], []);
+
+        // Each analysable element takes 1.1 secs, so the max (and likely) number of analysable
+        // elements that will be processed is 2.
+        $analyser->get_analysable_data(false);
+        $params = array('modelid' => 1, 'action' => 'prediction');
+        $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
+
+        $analyser->get_analysable_data(false);
+        $this->assertLessThanOrEqual(4, $DB->count_records('analytics_used_analysables', $params));
+
+        // Check that analysable elements have been processed following the analyser order
+        // (course->sortorder here). We can not check this nicely after next get_analysable_data round
+        // because the first analysed element will be analysed again.
+        $analysedelems = $DB->get_records('analytics_used_analysables', $params, 'timeanalysed ASC');
+        // Just a default for the first checked element.
+        $last = (object)['sortorder' => PHP_INT_MAX];
+        foreach ($analysedelems as $analysed) {
+            if ($courses[$analysed->analysableid]->sortorder > $last->sortorder) {
+                $this->fail('Analysable elements have not been analysed sorted by course sortorder.');
+            }
+            $last = $courses[$analysed->analysableid];
+        }
+
+        $analyser->get_analysable_data(false);
+        $this->assertGreaterThanOrEqual(5, $DB->count_records('analytics_used_analysables', $params));
+
+        // New analysable elements are immediately pulled.
+        $this->getDataGenerator()->create_course();
+        $analyser->get_analysable_data(false);
+        $this->assertGreaterThanOrEqual(6, $DB->count_records('analytics_used_analysables', $params));
+
+        // Training and prediction data do not get mixed.
+        $analyser->get_analysable_data(true);
+        $params = array('modelid' => 1, 'action' => 'training');
+        $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
+    }
+
     /**
      * Generates a model log record.
      */
index 7f30037..f97db57 100644 (file)
@@ -273,6 +273,96 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         return $this->add_prediction_processors($cases);
     }
 
+    /**
+     * Test the system classifiers returns.
+     *
+     * This test checks that all mlbackend plugins in the system are able to return proper status codes
+     * even under weird situations.
+     *
+     * @dataProvider provider_ml_classifiers_return
+     * @param int $success
+     * @param int $nsamples
+     * @param int $classes
+     * @param string $predictionsprocessorclass
+     * @return void
+     */
+    public function test_ml_classifiers_return($success, $nsamples, $classes, $predictionsprocessorclass) {
+        $this->resetAfterTest();
+
+        $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
+        if ($predictionsprocessor->is_ready() !== true) {
+            $this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
+        }
+
+        if ($nsamples % count($classes) != 0) {
+            throw new \coding_exception('The number of samples should be divisible by the number of classes');
+        }
+        $samplesperclass = $nsamples / count($classes);
+
+        // Metadata (we pass 2 classes even if $classes only provides 1 class samples as we want to test
+        // what the backend does in this case.
+        $dataset = "nfeatures,targetclasses,targettype" . PHP_EOL;
+        $dataset .= "3,\"[0,1]\",\"discrete\"" . PHP_EOL;
+
+        // Headers.
+        $dataset .= "feature1,feature2,feature3,target" . PHP_EOL;
+        foreach ($classes as $class) {
+            for ($i = 0; $i < $samplesperclass; $i++) {
+                $dataset .= "1,0,1,$class" . PHP_EOL;
+            }
+        }
+
+        $trainingfile = array(
+            'contextid' => \context_system::instance()->id,
+            'component' => 'analytics',
+            'filearea' => 'labelled',
+            'itemid' => 123,
+            'filepath' => '/',
+            'filename' => 'whocares.csv'
+        );
+        $fs = get_file_storage();
+        $dataset = $fs->create_file_from_string($trainingfile, $dataset);
+
+        // Training should work correctly if at least 1 sample of each class is included.
+        $dir = make_request_directory();
+        $result = $predictionsprocessor->train_classification('whatever', $dataset, $dir);
+
+        switch ($success) {
+            case 'yes':
+                $this->assertEquals(\core_analytics\model::OK, $result->status);
+                break;
+            case 'no':
+                $this->assertNotEquals(\core_analytics\model::OK, $result->status);
+                break;
+            case 'maybe':
+            default:
+                // We just check that an object is returned so we don't have an empty check,
+                // what we really want to check is that an exception was not thrown.
+                $this->assertInstanceOf(\stdClass::class, $result);
+        }
+    }
+
+    /**
+     * test_ml_classifiers_return provider
+     *
+     * We can not be very specific here as test_ml_classifiers_return only checks that
+     * mlbackend plugins behave and expected and control properly backend errors even
+     * under weird situations.
+     *
+     * @return array
+     */
+    public function provider_ml_classifiers_return() {
+        // Using verbose options as the first argument for readability.
+        $cases = array(
+            '1-samples' => array('maybe', 1, [0]),
+            '2-samples-same-class' => array('maybe', 2, [0]),
+            '2-samples-different-classes' => array('yes', 2, [0, 1]),
+            '4-samples-different-classes' => array('yes', 4, [0, 1])
+        );
+
+        // We need to test all system prediction processors.
+        return $this->add_prediction_processors($cases);
+    }
 
     /**
      * Basic test to check that prediction processors work as expected.
@@ -426,8 +516,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
                 'expectedresults' => array(
                     // The course duration is too much to be processed by in weekly basis.
                     '\core\analytics\time_splitting\weekly' => \core_analytics\model::NO_DATASET,
-                    '\core\analytics\time_splitting\single_range' => \core_analytics\model::EVALUATE_LOW_SCORE,
-                    '\core\analytics\time_splitting\quarters' => \core_analytics\model::EVALUATE_LOW_SCORE,
+                    '\core\analytics\time_splitting\single_range' => \core_analytics\model::LOW_SCORE,
+                    '\core\analytics\time_splitting\quarters' => \core_analytics\model::LOW_SCORE,
                 )
             ),
             'good' => array(
index 86ce136..9c171ae 100644 (file)
@@ -42,6 +42,9 @@ $url = new moodle_url('/backup/backupfilesedit.php', array('currentcontext'=>$cu
 
 require_login($course, false, $cm);
 require_capability('moodle/restore:uploadfile', $context);
+if ($filearea == 'automated' && !can_download_from_backup_filearea($filearea, $context)) {
+    throw new required_capability_exception($context, 'moodle/backup:downloadfile', 'nopermissions', '');
+}
 
 $PAGE->set_url($url);
 $PAGE->set_context($context);
index a1b9b8b..6d5e6a5 100644 (file)
@@ -182,6 +182,15 @@ abstract class restore_activity_task extends restore_task {
         // Activity competencies.
         $this->add_step(new restore_activity_competencies_structure_step('activity_competencies', 'competencies.xml'));
 
+        // Search reindexing, if enabled and if not restoring entire course.
+        if (\core_search\manager::is_indexing_enabled()) {
+            $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE;
+            $wholecourse = $wholecourse || ($this->setting_exists('overwrite_conf') && $this->get_setting_value('overwrite_conf'));
+            if (!$wholecourse) {
+                $this->add_step(new restore_activity_search_index('activity_search_index'));
+            }
+        }
+
         // At the end, mark it as built
         $this->built = true;
     }
index 64e5b99..eb5cd47 100644 (file)
@@ -98,6 +98,15 @@ abstract class restore_block_task extends restore_task {
             $this->add_step(new restore_comments_structure_step('block_comments', 'comments.xml'));
         }
 
+        // Search reindexing (if enabled).
+        if (\core_search\manager::is_indexing_enabled()) {
+            $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE;
+            $wholecourse = $wholecourse || $this->setting_exists('overwrite_conf') && $this->get_setting_value('overwrite_conf');
+            if (!$wholecourse) {
+                $this->add_step(new restore_block_search_index('block_search_index'));
+            }
+        }
+
         // At the end, mark it as built
         $this->built = true;
     }
index 74bfa6c..2121c02 100644 (file)
@@ -69,6 +69,11 @@ class restore_course_task extends restore_task {
         // Executed conditionally if restoring to new course or if overwrite_conf setting is enabled
         if ($this->get_target() == backup::TARGET_NEW_COURSE || $this->get_setting_value('overwrite_conf') == true) {
             $this->add_step(new restore_course_structure_step('course_info', 'course.xml'));
+
+            // Search reindexing (if enabled).
+            if (\core_search\manager::is_indexing_enabled()) {
+                $this->add_step(new restore_course_search_index('course_search_index'));
+            }
         }
 
         $this->add_step(new restore_course_legacy_files_step('legacy_files'));
index 1bb3e51..b0b6b76 100644 (file)
@@ -114,6 +114,15 @@ class restore_final_task extends restore_task {
         // Clean the temp dir (conditionally) and drop temp table
         $this->add_step(new restore_drop_and_clean_temp_stuff('drop_and_clean_temp_stuff'));
 
+        // If restoring to a new course or overwriting config, reindex the whole course.
+        if (\core_search\manager::is_indexing_enabled()) {
+            $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE;
+            $wholecourse = $wholecourse || $this->setting_exists('overwrite_conf') && $this->get_setting_value('overwrite_conf');
+            if ($wholecourse) {
+                $this->add_step(new restore_course_search_index('course_search_index'));
+            }
+        }
+
         $this->built = true;
     }
 
index 10872f8..b53ffa6 100644 (file)
@@ -5544,6 +5544,58 @@ class restore_completion_defaults_structure_step extends restore_structure_step
         $this->set_mapping('course_completion_defaults', $oldid, $newid);
     }
 }
+
+/**
+ * Index course after restore.
+ *
+ * @package core_backup
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_course_search_index extends restore_execution_step {
+    /**
+     * When this step is executed, we add the course context to the queue for reindexing.
+     */
+    protected function define_execution() {
+        $context = \context_course::instance($this->task->get_courseid());
+        \core_search\manager::request_index($context);
+    }
+}
+
+/**
+ * Index activity after restore (when not restoring whole course).
+ *
+ * @package core_backup
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_activity_search_index extends restore_execution_step {
+    /**
+     * When this step is executed, we add the activity context to the queue for reindexing.
+     */
+    protected function define_execution() {
+        $context = \context::instance_by_id($this->task->get_contextid());
+        \core_search\manager::request_index($context);
+    }
+}
+
+/**
+ * Index block after restore (when not restoring whole course).
+ *
+ * @package core_backup
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_block_search_index extends restore_execution_step {
+    /**
+     * When this step is executed, we add the block context to the queue for reindexing.
+     */
+    protected function define_execution() {
+        $context = \context_block::instance($this->task->get_blockid());
+        \core_search\manager::request_index($context);
+    }
+}
+
 /**
  * Restore action events.
  *
index 21773b9..3b45eea 100644 (file)
@@ -867,4 +867,92 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated);
         $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified);
     }
+
+    /**
+     * When you restore a site with global search (or search indexing) turned on, then it should
+     * add entries to the search index requests table so that the data gets indexed.
+     */
+    public function test_restore_search_index_requests() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $CFG->enableglobalsearch = true;
+
+        // Create a course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        // Add a forum.
+        $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+        // Add a block.
+        $context = context_course::instance($course->id);
+        $page = new moodle_page();
+        $page->set_context($context);
+        $page->set_course($course);
+        $page->set_pagelayout('standard');
+        $page->set_pagetype('course-view');
+        $page->blocks->load_blocks();
+        $page->blocks->add_block_at_end_of_default_region('html');
+
+        // Initially there should be no search index requests.
+        $this->assertEquals(0, $DB->count_records('search_index_requests'));
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+
+        // Now the course should be requested for index (all search areas).
+        $newcontext = context_course::instance($newcourseid);
+        $requests = array_values($DB->get_records('search_index_requests'));
+        $this->assertCount(1, $requests);
+        $this->assertEquals($newcontext->id, $requests[0]->contextid);
+        $this->assertEquals('', $requests[0]->searcharea);
+
+        get_fast_modinfo($newcourseid);
+
+        // Backup the new course...
+        $CFG->backup_file_logger_level = backup::LOG_NONE;
+        $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid,
+                backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
+                $USER->id);
+        $backupid = $bc->get_backupid();
+        $bc->execute_plan();
+        $bc->destroy();
+
+        // Restore it on top of old course (should duplicate the forum).
+        $rc = new restore_controller($backupid, $course->id,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+                backup::TARGET_EXISTING_ADDING);
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // Get the forums now on the old course.
+        $modinfo = get_fast_modinfo($course->id);
+        $forums = $modinfo->get_instances_of('forum');
+        $this->assertCount(2, $forums);
+
+        // The newer one will be the one with larger ID. (Safe to assume for unit test.)
+        $biggest = null;
+        foreach ($forums as $forum) {
+            if ($biggest === null || $biggest->id < $forum->id) {
+                $biggest = $forum;
+            }
+        }
+        $restoredforumcontext = \context_module::instance($biggest->id);
+
+        // Get the HTML blocks now on the old course.
+        $blockdata = array_values($DB->get_records('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC'));
+        $restoredblockcontext = \context_block::instance($blockdata[0]->id);
+
+        // Check that we have requested index update on both the module and the block.
+        $requests = array_values($DB->get_records('search_index_requests', null, 'id'));
+        $this->assertCount(3, $requests);
+        $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid);
+        $this->assertEquals('', $requests[1]->searcharea);
+        $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid);
+        $this->assertEquals('', $requests[2]->searcharea);
+    }
 }
index 7b0fcd2..be9ae4a 100644 (file)
@@ -79,6 +79,9 @@ if (!check_dir_exists($tmpdir, true, true)) {
 
 // choose the backup file from backup files tree
 if ($action == 'choosebackupfile') {
+    if ($filearea == 'automated') {
+        require_capability('moodle/restore:viewautomatedfilearea', $context);
+    }
     if ($fileinfo = $browser->get_file_info($filecontext, $component, $filearea, $itemid, $filepath, $filename)) {
         if (is_a($fileinfo, 'file_info_stored')) {
             // Use the contenthash rather than copying the file where possible,
index b4855bb..7499d6f 100644 (file)
@@ -573,27 +573,51 @@ class core_backup_renderer extends plugin_renderer_base {
             $params['contextid'] = $viewer->currentcontext->id;
             $params['itemid'] = $file->get_itemid();
             $restoreurl = new moodle_url('/backup/restorefile.php', $params);
+            $restorelink = html_writer::link($restoreurl, get_string('restore'));
+            $downloadlink = html_writer::link($fileurl, get_string('download'));
+
+            // Conditional display of the restore and download links, initially only for the 'automated' filearea.
+            if ($params['filearea'] == 'automated') {
+                if (!has_capability('moodle/restore:viewautomatedfilearea', $viewer->currentcontext)) {
+                    $restorelink = '';
+                }
+                if (!can_download_from_backup_filearea($params['filearea'], $viewer->currentcontext)) {
+                    $downloadlink = '';
+                }
+            }
             $table->data[] = array(
                 $file->get_filename(),
                 userdate($file->get_timemodified()),
                 display_size($file->get_filesize()),
-                html_writer::link($fileurl, get_string('download')),
-                html_writer::link($restoreurl, get_string('restore')),
+                $downloadlink,
+                $restorelink,
                 );
         }
 
         $html = html_writer::table($table);
-        $html .= $this->output->single_button(
-            new moodle_url('/backup/backupfilesedit.php', array(
-                'currentcontext' => $viewer->currentcontext->id,
-                'contextid' => $viewer->filecontext->id,
-                'filearea' => $viewer->filearea,
-                'component' => $viewer->component,
-                'returnurl' => $this->page->url->out())
-            ),
-            get_string('managefiles', 'backup'),
-            'post'
-        );
+
+        // For automated backups, the ability to manage backup files is controlled by the ability to download them.
+        // All files must be from the same file area in a backup_files_viewer.
+        $canmanagebackups = true;
+        if ($viewer->filearea == 'automated') {
+            if (!can_download_from_backup_filearea($viewer->filearea, $viewer->currentcontext)) {
+                $canmanagebackups = false;
+            }
+        }
+
+        if ($canmanagebackups) {
+            $html .= $this->output->single_button(
+                new moodle_url('/backup/backupfilesedit.php', array(
+                        'currentcontext' => $viewer->currentcontext->id,
+                        'contextid' => $viewer->filecontext->id,
+                        'filearea' => $viewer->filearea,
+                        'component' => $viewer->component,
+                        'returnurl' => $this->page->url->out())
+                ),
+                get_string('managefiles', 'backup'),
+                'post'
+            );
+        }
 
         return $html;
     }
index 858a0e4..0bc46c9 100644 (file)
@@ -46,106 +46,36 @@ class block_calendar_upcoming extends block_base {
         $this->content = new stdClass;
         $this->content->text = '';
 
-        $filtercourse    = array();
-        if (empty($this->instance)) { // Overrides: use no course at all.
-            $courseshown = false;
-            $this->content->footer = '';
-
+        $renderer = $this->page->get_renderer('core_calendar');
+        $courseid = $this->page->course->id;
+        $issite = ($courseid == SITEID);
+
+        if ($issite) {
+            // Being displayed at site level. This will cause the filter to fall back to auto-detecting
+            // the list of courses it will be grabbing events from.
+            $course = get_site();
+            $courses = calendar_get_default_courses();
         } else {
-            $courseshown = $this->page->course->id;
-            $this->content->footer = '<div class="gotocal"><a href="'.$CFG->wwwroot.
-                                     '/calendar/view.php?view=upcoming&amp;course='.$courseshown.'">'.
-                                      get_string('gotocalendar', 'calendar').'</a>...</div>';
-            $context = context_course::instance($courseshown);
-            if (has_any_capability(array('moodle/calendar:manageentries', 'moodle/calendar:manageownentries'), $context)) {
-                $this->content->footer .= '<div class="newevent"><a href="'.$CFG->wwwroot.
-                                          '/calendar/event.php?action=new&amp;course='.$courseshown.'">'.
-                                           get_string('newevent', 'calendar').'</a>...</div>';
-            }
-            if ($courseshown == SITEID) {
-                // Being displayed at site level. This will cause the filter to fall back to auto-detecting
-                // the list of courses it will be grabbing events from.
-                $filtercourse = calendar_get_default_courses();
-            } else {
-                // Forcibly filter events to include only those from the particular course we are in.
-                $filtercourse = array($courseshown => $this->page->course);
-            }
-        }
-
-        list($courses, $group, $user) = calendar_set_filters($filtercourse);
-
-        $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
-        if (isset($CFG->calendar_lookahead)) {
-            $defaultlookahead = intval($CFG->calendar_lookahead);
-        }
-        $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead);
-
-        $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS;
-        if (isset($CFG->calendar_maxevents)) {
-            $defaultmaxevents = intval($CFG->calendar_maxevents);
+            // Forcibly filter events to include only those from the particular course we are in.
+            $course = $this->page->course;
+            $courses = [$course->id => $course];
         }
-        $maxevents = get_user_preferences('calendar_maxevents', $defaultmaxevents);
-        $events = calendar_get_upcoming($courses, $group, $user, $lookahead, $maxevents);
+        $calendar = new calendar_information(0, 0, 0, time());
+        $calendar->set_sources($course, $courses);
 
-        if (!empty($this->instance)) {
-            $link = 'view.php?view=day&amp;course='.$courseshown.'&amp;';
-            $showcourselink = ($this->page->course->id == SITEID);
-            $this->content->text = self::get_upcoming_content($events, $link, $showcourselink);
-        }
+        list($data, $template) = calendar_get_view($calendar, 'upcoming_mini');
 
-        if (empty($this->content->text)) {
+        if (empty($data->events)) {
             $this->content->text = '<div class="post">'. get_string('noupcomingevents', 'calendar').'</div>';
+        } else {
+            $this->content->text .= $renderer->render_from_template($template, $data);
         }
 
-        return $this->content;
-    }
-
-    /**
-     * Get the upcoming event block content.
-     *
-     * @param array $events list of events
-     * @param \moodle_url|string $linkhref link to event referer
-     * @param boolean $showcourselink whether links to courses should be shown
-     * @return string|null $content html block content
-     */
-    public static function get_upcoming_content($events, $linkhref = null, $showcourselink = false) {
-        $content = '';
-        $lines = count($events);
-
-        if (!$lines) {
-            return $content;
-        }
-
-        for ($i = 0; $i < $lines; ++$i) {
-            if (!isset($events[$i]->time)) {
-                continue;
-            }
-            $events[$i] = calendar_add_event_metadata($events[$i]);
-            $content .= '<div class="event"><span class="icon c0">' . $events[$i]->icon . '</span>';
-            if (!empty($events[$i]->referer)) {
-                // That's an activity event, so let's provide the hyperlink.
-                $content .= $events[$i]->referer;
-            } else {
-                if (!empty($linkhref)) {
-                    $href = calendar_get_link_href(new \moodle_url(CALENDAR_URL . $linkhref), 0, 0, 0,
-                        $events[$i]->timestart);
-                    $href->set_anchor('event_' . $events[$i]->id);
-                    $content .= \html_writer::link($href, $events[$i]->name);
-                } else {
-                    $content .= $events[$i]->name;
-                }
-            }
-            $events[$i]->time = str_replace('&raquo;', '<br />&raquo;', $events[$i]->time);
-            if ($showcourselink && !empty($events[$i]->courselink)) {
-                $content .= \html_writer::div($events[$i]->courselink, 'course');
-            }
-            $content .= '<div class="date">' . $events[$i]->time . '</div></div>';
-            if ($i < $lines - 1) {
-                $content .= '<hr />';
-            }
-        }
+        $this->content->footer = '<div class="gotocal">
+                <a href="'.$CFG->wwwroot.'/calendar/view.php?view=upcoming&amp;course='.$courseid.'">'.
+                get_string('gotocalendar', 'calendar').'</a>...</div>';
 
-        return $content;
+        return $this->content;
     }
 }
 
index da91072..fbec430 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
index ef94d15..ddf2010 100644 (file)
Binary files a/calendar/amd/build/calendar_filter.min.js and b/calendar/amd/build/calendar_filter.min.js differ
index e2cc7de..5185c8b 100644 (file)
Binary files a/calendar/amd/build/drag_drop_data_store.min.js and b/calendar/amd/build/drag_drop_data_store.min.js differ
index 1f6e3ff..0abfe87 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index a8888bf..6e6cbc3 100644 (file)
Binary files a/calendar/amd/build/month_navigation_drag_drop.min.js and b/calendar/amd/build/month_navigation_drag_drop.min.js differ
index b2ee13d..5381270 100644 (file)
Binary files a/calendar/amd/build/month_view_drag_drop.min.js and b/calendar/amd/build/month_view_drag_drop.min.js differ
index 4f10cb0..62ce49d 100644 (file)
@@ -257,10 +257,12 @@ define([
             var target = $(e.target);
             var eventId = null;
 
-            if (target.is(SELECTORS.EVENT_LINK)) {
-                eventId = target.attr('data-event-id');
+            var eventLink = target.closest(SELECTORS.EVENT_LINK);
+
+            if (eventLink.length) {
+                eventId = eventLink.data('eventId');
             } else {
-                eventId = target.find(SELECTORS.EVENT_LINK).attr('data-event-id');
+                eventId = target.find(SELECTORS.EVENT_LINK).data('eventId');
             }
 
             renderEventSummaryModal(eventId);
index 5352a7c..622dafd 100644 (file)
@@ -64,7 +64,7 @@ function(
         // Toggle the hidden. We need to render the template before we change the value.
         data.hidden = !data.hidden;
 
-        return Str.get_string('eventtype' + data.type, 'calendar')
+        return Str.get_string('eventtype' + data.eventtype, 'calendar')
         .then(function(nameStr) {
             data.name = nameStr;
 
@@ -90,7 +90,7 @@ function(
     var fireFilterChangedEvent = function(data) {
         M.util.js_pending("month-mini-filterChanged");
         $('body').trigger(CalendarEvents.filterChanged, {
-            type: data.type,
+            type: data.eventtype,
             hidden: data.hidden,
         });
         M.util.js_complete("month-mini-filterChanged");
@@ -104,7 +104,7 @@ function(
      */
     var getFilterData = function(target) {
         return {
-            type: target.data('eventtype'),
+            eventtype: target.data('eventtype'),
             hidden: target.data('eventtype-hidden'),
         };
     };
index 6414f8e..893d11a 100644 (file)
@@ -30,6 +30,14 @@ define([], function() {
     var eventId = null;
     /* @var {int|null} durationDays How many days the event spans */
     var durationDays = null;
+    /* @var {int|null} minTimestart The earliest valid timestart */
+    var minTimestart = null;
+    /* @var {int|null} maxTimestart The latest valid tiemstart */
+    var maxTimestart = null;
+    /* @var {string|null} minError Error message for min timestamp violation */
+    var minError = null;
+    /* @var {string|null} maxError Error message for max timestamp violation */
+    var maxError = null;
 
     /**
      * Store the id of the event being dragged.
@@ -76,12 +84,108 @@ define([], function() {
         return durationDays;
     };
 
+    /**
+     * Store the minimum timestart valid for an event being dragged.
+     *
+     * @param {int} timestamp The unix timstamp
+     */
+    var setMinTimestart = function(timestamp) {
+        minTimestart = timestamp;
+    };
+
+    /**
+     * Get the minimum valid timestart.
+     *
+     * @return {int|null}
+     */
+    var getMinTimestart = function() {
+        return minTimestart;
+    };
+
+    /**
+     * Check if a minimum timestamp is set.
+     *
+     * @return {bool}
+     */
+    var hasMinTimestart = function() {
+        return minTimestart !== null;
+    };
+
+    /**
+     * Store the maximum timestart valid for an event being dragged.
+     *
+     * @param {int} timestamp The unix timstamp
+     */
+    var setMaxTimestart = function(timestamp) {
+        maxTimestart = timestamp;
+    };
+
+    /**
+     * Get the maximum valid timestart.
+     *
+     * @return {int|null}
+     */
+    var getMaxTimestart = function() {
+        return maxTimestart;
+    };
+
+    /**
+     * Check if a maximum timestamp is set.
+     *
+     * @return {bool}
+     */
+    var hasMaxTimestart = function() {
+        return maxTimestart !== null;
+    };
+
+    /**
+     * Store the error string to display if trying to drag an event
+     * earlier than the minimum allowed date.
+     *
+     * @param {string} message The error message
+     */
+    var setMinError = function(message) {
+        minError = message;
+    };
+
+    /**
+     * Get the error message for a minimum time start violation.
+     *
+     * @return {string|null}
+     */
+    var getMinError = function() {
+        return minError;
+    };
+
+    /**
+     * Store the error string to display if trying to drag an event
+     * later than the maximum allowed date.
+     *
+     * @param {string} message The error message
+     */
+    var setMaxError = function(message) {
+        maxError = message;
+    };
+
+    /**
+     * Get the error message for a maximum time start violation.
+     *
+     * @return {string|null}
+     */
+    var getMaxError = function() {
+        return maxError;
+    };
+
     /**
      * Reset all of the stored values.
      */
     var clearAll = function() {
         setEventId(null);
         setDurationDays(null);
+        setMinTimestart(null);
+        setMaxTimestart(null);
+        setMinError(null);
+        setMaxError(null);
     };
 
     return {
@@ -90,6 +194,16 @@ define([], function() {
         hasEventId: hasEventId,
         setDurationDays: setDurationDays,
         getDurationDays: getDurationDays,
+        setMinTimestart: setMinTimestart,
+        getMinTimestart: getMinTimestart,
+        hasMinTimestart: hasMinTimestart,
+        setMaxTimestart: setMaxTimestart,
+        getMaxTimestart: getMaxTimestart,
+        hasMaxTimestart: hasMaxTimestart,
+        setMinError: setMinError,
+        getMinError: getMinError,
+        setMaxError: setMaxError,
+        getMaxError: getMaxError,
         clearAll: clearAll
     };
 });
index f537f6d..2b6707d 100644 (file)
@@ -74,14 +74,14 @@ define(['jquery'], function($) {
         var filterGroupSelectOptions = function() {
             var selectedCourseId = courseGroupSelect.val();
             var selectedIndex = null;
-
+            var hasGroups = false;
             groupSelectOptions.each(function(index, element) {
                 element = $(element);
 
                 if (element.attr('data-course-id') == selectedCourseId) {
                     element.removeClass('hidden');
                     element.prop('disabled', false);
-
+                    hasGroups = true;
                     if (selectedIndex === null || element.attr('selected')) {
                         selectedIndex = index;
                     }
@@ -91,6 +91,12 @@ define(['jquery'], function($) {
                 }
             });
 
+            if (hasGroups) {
+                groupSelect.prop('disabled', false);
+            } else {
+                groupSelect.prop('disabled', true);
+            }
+
             groupSelect.prop('selectedIndex', selectedIndex);
         };
 
index 0e192df..dca8f61 100644 (file)
@@ -119,6 +119,11 @@ define([
      * @param {event} e The dragover event
      */
     var dragoverHandler = function(e) {
+        // Ignore dragging of non calendar events.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
         e.preventDefault();
         var target = getTargetFromEvent(e);
 
@@ -153,6 +158,11 @@ define([
      * @param {event} e The dragstart event
      */
     var dragleaveHandler = function(e) {
+        // Ignore dragging of non calendar events.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
         var target = getTargetFromEvent(e);
 
         if (!target) {
@@ -176,6 +186,11 @@ define([
      * @param {event} e The drop event
      */
     var dropHandler = function(e) {
+        // Ignore dragging of non calendar events.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
         removeDropZoneIndicator();
         var target = getTargetFromEvent(e);
 
index 05b3733..b94af90 100644 (file)
  */
 define([
             'jquery',
+            'core/notification',
+            'core/str',
             'core_calendar/events',
             'core_calendar/drag_drop_data_store'
         ],
         function(
             $,
+            Notification,
+            Str,
             CalendarEvents,
             DataStore
         ) {
@@ -40,7 +44,10 @@ define([
         DROP_ZONE: '[data-drop-zone="month-view-day"]',
         WEEK: '[data-region="month-view-week"]',
     };
-    var HOVER_CLASS = 'bg-primary text-white';
+    var INVALID_DROP_ZONE_CLASS = 'bg-faded';
+    var INVALID_HOVER_CLASS = 'bg-danger text-white';
+    var VALID_HOVER_CLASS = 'bg-primary text-white';
+    var ALL_CLASSES = INVALID_DROP_ZONE_CLASS + ' ' + INVALID_HOVER_CLASS + ' ' + VALID_HOVER_CLASS;
     /* @var {bool} registered If the event listeners have been added */
     var registered = false;
 
@@ -56,10 +63,73 @@ define([
         return (dropZone.length) ? dropZone : null;
     };
 
+    /**
+     * Determine if the given dropzone element is within the acceptable
+     * time range.
+     *
+     * The drop zone timestamp is midnight on that day so we should check
+     * that the event's acceptable timestart value
+     *
+     * @param {object} dropZone The drop zone day from the calendar
+     * @return {bool}
+     */
+    var isValidDropZone = function(dropZone) {
+        var dropTimestamp = dropZone.attr('data-day-timestamp');
+        var minTimestart = DataStore.getMinTimestart();
+        var maxTimestart = DataStore.getMaxTimestart();
+
+        if (minTimestart && minTimestart > dropTimestamp) {
+            return false;
+        }
+
+        if (maxTimestart && maxTimestart < dropTimestamp) {
+            return false;
+        }
+
+        return true;
+    };
+
+    /**
+     * Get the error string to display for a given drop zone element
+     * if it is invalid.
+     *
+     * @param {object} dropZone The drop zone day from the calendar
+     * @return {string}
+     */
+    var getDropZoneError = function(dropZone) {
+        var dropTimestamp = dropZone.attr('data-day-timestamp');
+        var minTimestart = DataStore.getMinTimestart();
+        var maxTimestart = DataStore.getMaxTimestart();
+
+        if (minTimestart && minTimestart > dropTimestamp) {
+            return DataStore.getMinError();
+        }
+
+        if (maxTimestart && maxTimestart < dropTimestamp) {
+            return DataStore.getMaxError();
+        }
+
+        return null;
+    };
+
+    /**
+     * Remove all of the styling from each of the drop zones in the calendar.
+     */
+    var clearAllDropZonesState = function() {
+        $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) {
+            dropZone = $(dropZone);
+            dropZone.removeClass(ALL_CLASSES);
+        });
+    };
+
     /**
      * Update the hover state for the event in the calendar to reflect
      * which days the event will be moved to.
      *
+     * If the drop zone is not being hovered then it will apply some
+     * styling to reflect whether the drop zone is a valid or invalid
+     * drop place for the current dragging event.
+     *
      * This funciton supports events spanning multiple days and will
      * recurse to highlight (or remove highlight) each of the days
      * that the event will be moved to.
@@ -79,10 +149,22 @@ define([
             count = DataStore.getDurationDays();
         }
 
+        var valid = isValidDropZone(dropZone);
+        dropZone.removeClass(ALL_CLASSES);
+
         if (hovered) {
-            dropZone.addClass(HOVER_CLASS);
+
+            if (valid) {
+                dropZone.addClass(VALID_HOVER_CLASS);
+            } else {
+                dropZone.addClass(INVALID_HOVER_CLASS);
+            }
         } else {
-            dropZone.removeClass(HOVER_CLASS);
+            dropZone.removeClass(VALID_HOVER_CLASS + ' ' + INVALID_HOVER_CLASS);
+
+            if (!valid) {
+                dropZone.addClass(INVALID_DROP_ZONE_CLASS);
+            }
         }
 
         count--;
@@ -110,6 +192,21 @@ define([
         }
     };
 
+    /**
+     * Find all of the calendar event drop zones in the calendar and update the display
+     * for the user to indicate which zones are valid and invalid.
+     */
+    var updateAllDropZonesState = function() {
+        $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) {
+            dropZone = $(dropZone);
+
+            if (!isValidDropZone(dropZone)) {
+                updateHoverState(dropZone, false);
+            }
+        });
+    };
+
+
     /**
      * Set up the module level variables to track which event is being
      * dragged and how many days it spans.
@@ -117,27 +214,49 @@ define([
      * @param {event} e The dragstart event
      */
     var dragstartHandler = function(e) {
-        var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE);
+        var target = $(e.target);
+        var draggableElement = target.closest(SELECTORS.DRAGGABLE);
 
-        if (!eventElement.length) {
+        if (!draggableElement.length) {
             return;
         }
 
-        eventElement = eventElement.find('[data-event-id]');
-
+        var eventElement = draggableElement.find('[data-event-id]');
         var eventId = eventElement.attr('data-event-id');
+        var minTimestart = draggableElement.attr('data-min-day-timestamp');
+        var maxTimestart = draggableElement.attr('data-max-day-timestamp');
+        var minError = draggableElement.attr('data-min-day-error');
+        var maxError = draggableElement.attr('data-max-day-error');
         var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
         var duration = $(eventsSelector).length;
 
         DataStore.setEventId(eventId);
         DataStore.setDurationDays(duration);
 
+        if (minTimestart) {
+            DataStore.setMinTimestart(minTimestart);
+        }
+
+        if (maxTimestart) {
+            DataStore.setMaxTimestart(maxTimestart);
+        }
+
+        if (minError) {
+            DataStore.setMinError(minError);
+        }
+
+        if (maxError) {
+            DataStore.setMaxError(maxError);
+        }
+
         e.dataTransfer.effectAllowed = "move";
         e.dataTransfer.dropEffect = "move";
         // Firefox requires a value to be set here or the drag won't
         // work and the dragover handler won't fire.
         e.dataTransfer.setData('text/plain', eventId);
         e.dropEffect = "move";
+
+        updateAllDropZonesState();
     };
 
     /**
@@ -150,6 +269,11 @@ define([
      * @param {event} e The dragstart event
      */
     var dragoverHandler = function(e) {
+        // Ignore dragging of non calendar events.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
         e.preventDefault();
 
         var dropZone = getDropZoneFromEvent(e);
@@ -171,6 +295,11 @@ define([
      * @param {event} e The dragstart event
      */
     var dragleaveHandler = function(e) {
+        // Ignore dragging of non calendar events.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
         var dropZone = getDropZoneFromEvent(e);
 
         if (!dropZone) {
@@ -193,30 +322,66 @@ define([
      * @param {event} e The dragstart event
      */
     var dropHandler = function(e) {
+        // Ignore dragging of non calendar events.
+        if (!DataStore.hasEventId()) {
+            return;
+        }
+
         var dropZone = getDropZoneFromEvent(e);
 
         if (!dropZone) {
             DataStore.clearAll();
+            clearAllDropZonesState();
             return;
         }
 
-        var eventId = DataStore.getEventId();
-        var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
-        var eventElement = $(eventElementSelector);
-        var origin = null;
-        var destination = $(e.target).closest(SELECTORS.DROP_ZONE);
+        if (isValidDropZone(dropZone)) {
+            var eventId = DataStore.getEventId();
+            var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]';
+            var eventElement = $(eventElementSelector);
+            var origin = null;
+
+            if (eventElement.length) {
+                origin = eventElement.closest(SELECTORS.DROP_ZONE);
+            }
 
-        if (eventElement.length) {
-            origin = eventElement.closest(SELECTORS.DROP_ZONE);
+            $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, dropZone]);
+        } else {
+            // If the drop zone is not valid then there is not need for us to
+            // try to process it. Instead we can just show an error to the user.
+            var message = getDropZoneError(dropZone);
+            Str.get_string('errorinvaliddate', 'calendar').then(function(string) {
+                Notification.exception({
+                    name: string,
+                    message: message || string
+                });
+            });
         }
 
-        updateHoverState(dropZone, false);
-        $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, destination]);
         DataStore.clearAll();
+        clearAllDropZonesState();
 
         e.preventDefault();
     };
 
+    /**
+     * Clear the data store and remove the drag indicators from the UI
+     * when the drag event has finished.
+     */
+    var dragendHandler = function() {
+        DataStore.clearAll();
+        clearAllDropZonesState();
+    };
+
+    /**
+     * Re-render the drop zones in the new month to highlight
+     * which areas are or aren't acceptable to drop the calendar
+     * event.
+     */
+    var calendarMonthChangedHandler = function() {
+        updateAllDropZonesState();
+    };
+
     return {
         /**
          * Initialise the event handlers for the drag events.
@@ -231,6 +396,8 @@ define([
                 document.addEventListener('dragover', dragoverHandler, false);
                 document.addEventListener('dragleave', dragleaveHandler, false);
                 document.addEventListener('drop', dropHandler, false);
+                document.addEventListener('dragend', dragendHandler, false);
+                $('body').on(CalendarEvents.monthChanged, calendarMonthChangedHandler);
                 registered = true;
             }
         },
index 423cd92..051d049 100644 (file)
@@ -26,6 +26,7 @@ namespace core_calendar\external;
 
 defined('MOODLE_INTERNAL') || die();
 
+use \core_calendar\local\event\container;
 use \core_course\external\course_summary_exporter;
 use \renderer_base;
 require_once($CFG->dirroot . '/course/lib.php');
@@ -57,6 +58,22 @@ class calendar_event_exporter extends event_exporter_base {
         $values['popupname'] = [
             'type' => PARAM_RAW,
         ];
+        $values['mindaytimestamp'] = [
+            'type' => PARAM_INT,
+            'optional' => true
+        ];
+        $values['mindayerror'] = [
+            'type' => PARAM_TEXT,
+            'optional' => true
+        ];
+        $values['maxdaytimestamp'] = [
+            'type' => PARAM_INT,
+            'optional' => true
+        ];
+        $values['maxdayerror'] = [
+            'type' => PARAM_TEXT,
+            'optional' => true
+        ];
 
         return $values;
     }
@@ -89,9 +106,9 @@ class calendar_event_exporter extends event_exporter_base {
         } else {
             // TODO MDL-58866 We do not have any way to find urls for events outside of course modules.
             $course = $event->get_course()->get('id') ?: SITEID;
-
             $url = course_get_url($course);
         }
+
         $values['url'] = $url->out(false);
         $values['islastday'] = false;
         $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
@@ -153,6 +170,10 @@ class calendar_event_exporter extends event_exporter_base {
 
         $values['calendareventtype'] = $this->get_calendar_event_type();
 
+        if ($event->get_course_module()) {
+            $values = array_merge($values, $this->get_module_timestamp_limits($event));
+        }
+
         return $values;
     }
 
@@ -184,4 +205,111 @@ class calendar_event_exporter extends event_exporter_base {
 
         return $type;
     }
+
+    /**
+     * Return the set of minimum and maximum date timestamp values
+     * for the given event.
+     *
+     * @param event_interface $event
+     * @return array
+     */
+    protected function get_module_timestamp_limits($event) {
+        $values = [];
+        $mapper = container::get_event_mapper();
+        $starttime = $event->get_times()->get_start_time();
+
+        list($min, $max) = component_callback(
+            'mod_' . $event->get_course_module()->get('modname'),
+            'core_calendar_get_valid_event_timestart_range',
+            [$mapper->from_event_to_legacy_event($event)],
+            [null, null]
+        );
+
+        if ($min) {
+            $values = array_merge($values, $this->get_module_timestamp_min_limit($starttime, $min));
+        }
+
+        if ($max) {
+            $values = array_merge($values, $this->get_module_timestamp_max_limit($starttime, $max));
+        }
+
+        return $values;
+    }
+
+    /**
+     * Get the correct minimum midnight day limit based on the event start time
+     * and the module's minimum timestamp limit.
+     *
+     * @param DateTimeInterface $starttime The event start time
+     * @param array $min The module's minimum limit for the event
+     */
+    protected function get_module_timestamp_min_limit(\DateTimeInterface $starttime, $min) {
+        // We need to check that the minimum valid time is earlier in the
+        // day than the current event time so that if the user drags and drops
+        // the event to this day (which changes the date but not the time) it
+        // will result in a valid time start for the event.
+        //
+        // For example:
+        // An event that starts on 2017-01-10 08:00 with a minimum cutoff
+        // of 2017-01-05 09:00 means that 2017-01-05 is not a valid start day
+        // for the drag and drop because it would result in the event start time
+        // being set to 2017-01-05 08:00, which is invalid. Instead the minimum
+        // valid start day would be 2017-01-06.
+        $values = [];
+        $timestamp = $min[0];
+        $errorstring = $min[1];
+        $mindate = (new \DateTimeImmutable())->setTimestamp($timestamp);
+        $minstart = $mindate->setTime(
+            $starttime->format('H'),
+            $starttime->format('i'),
+            $starttime->format('s')
+        );
+        $midnight = usergetmidnight($timestamp);
+
+        if ($mindate <= $minstart) {
+            $values['mindaytimestamp'] = $midnight;
+        } else {
+            $tomorrow = (new \DateTime())->setTimestamp($midnight)->modify('+1 day');
+            $values['mindaytimestamp'] = $tomorrow->getTimestamp();
+        }
+
+        // Get the human readable error message to display if the min day
+        // timestamp is violated.
+        $values['mindayerror'] = $errorstring;
+        return $values;
+    }
+
+    /**
+     * Get the correct maximum midnight day limit based on the event start time
+     * and the module's maximum timestamp limit.
+     *
+     * @param DateTimeInterface $starttime The event start time
+     * @param array $max The module's maximum limit for the event
+     */
+    protected function get_module_timestamp_max_limit(\DateTimeInterface $starttime, $max) {
+        // We're doing a similar calculation here as we are for the minimum
+        // day timestamp. See the explanation above.
+        $values;
+        $timestamp = $max[0];
+        $errorstring = $max[1];
+        $maxdate = (new \DateTimeImmutable())->setTimestamp($timestamp);
+        $maxstart = $maxdate->setTime(
+            $starttime->format('H'),
+            $starttime->format('i'),
+            $starttime->format('s')
+        );
+        $midnight = usergetmidnight($timestamp);
+
+        if ($maxdate >= $maxstart) {
+            $values['maxdaytimestamp'] = $midnight;
+        } else {
+            $yesterday = (new \DateTime())->setTimestamp($midnight)->modify('-1 day');
+            $values['maxdaytimestamp'] = $yesterday->getTimestamp();
+        }
+
+        // Get the human readable error message to display if the max day
+        // timestamp is violated.
+        $values['maxdayerror'] = $errorstring;
+        return $values;
+    }
 }
index ebe8d86..6600b23 100644 (file)
@@ -130,6 +130,10 @@ class day_exporter extends exporter {
                 'type' => calendar_event_exporter::read_properties_definition(),
                 'multiple' => true,
             ],
+            'hasevents' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
             'calendareventtypes' => [
                 'type' => PARAM_RAW,
                 'multiple' => true,
@@ -211,6 +215,8 @@ class day_exporter extends exporter {
             return $exporter->export($output);
         }, $eventexporters);
 
+        $return['hasevents'] = !empty($return['events']);
+
         $return['calendareventtypes'] = array_map(function($exporter) {
             return $exporter->get_calendar_event_type();
         }, $eventexporters);
index 85457e2..0f17777 100644 (file)
@@ -208,6 +208,9 @@ class event_exporter_base extends exporter {
             'editurl' => [
                 'type' => PARAM_URL
             ],
+            'viewurl' => [
+                'type' => PARAM_URL
+            ],
             'formattedtime' => [
                 'type' => PARAM_RAW,
             ],
@@ -280,6 +283,10 @@ class event_exporter_base extends exporter {
         $editurl = new moodle_url('/calendar/event.php', ['action' => 'edit', 'id' => $event->get_id(),
                 'course' => $courseid]);
         $values['editurl'] = $editurl->out(false);
+        $viewurl = new moodle_url('/calendar/view.php', ['view' => 'day', 'course' => $courseid,
+                'time' => $timesort]);
+        $viewurl->set_anchor('event_' . $event->get_id());
+        $values['viewurl'] = $viewurl->out(false);
         $values['formattedtime'] = calendar_format_event_time($legacyevent, time(), null, false,
                 $timesort);
 
index 5a8075b..65bba6f 100644 (file)
@@ -53,9 +53,9 @@ class event_subscription_exporter extends exporter {
             if (!empty($subscription) && $CFG->calendar_showicalsource) {
                 $data->displayeventsource = true;
                 if (!empty($subscription->url)) {
-                    $data->url = $subscription->url;
+                    $data->subscriptionurl = $subscription->url;
                 }
-                $data->name = $subscription->name;
+                $data->subscriptionname = $subscription->name;
             }
         }
 
@@ -72,11 +72,11 @@ class event_subscription_exporter extends exporter {
             'displayeventsource' => [
                 'type' => PARAM_BOOL
             ],
-            'name' => [
+            'subscriptionname' => [
                 'type' => PARAM_RAW,
                 'optional' => true
             ],
-            'url' => [
+            'subscriptionurl' => [
                 'type' => PARAM_URL,
                 'optional' => true
             ],
index e4478bb..9cce687 100644 (file)
@@ -76,18 +76,6 @@ class footer_options_exporter extends exporter {
         return new \single_button($exportcalendarurl, get_string('exportcalendar', 'calendar'));
     }
 
-    /**
-     * Get the iCal url.
-     *
-     * @return string The iCal url.
-     */
-    protected function get_ical_url() {
-        if ($this->token) {
-            return new moodle_url('/calendar/export_execute.php', ['preset_what' => 'all',
-                    'preset_time' => 'recentupcoming', 'userid' => $this->userid, 'authtoken' => $this->token]);
-        }
-    }
-
     /**
      * Get manage subscription button.
      *
@@ -120,7 +108,6 @@ class footer_options_exporter extends exporter {
             if ($managesubscriptionbutton = $this->get_manage_subscriptions_button()) {
                 $values->managesubscriptionbutton = $managesubscriptionbutton->export_for_template($output);
             }
-            $values->icalurl = $this->get_ical_url()->out(false);
         }
 
         return (array) $values;
@@ -141,10 +128,6 @@ class footer_options_exporter extends exporter {
                 'type' => PARAM_RAW,
                 'default' => null,
             ],
-            'icalurl' => [
-                'type' => PARAM_URL,
-                'default' => null,
-            ],
         );
     }
 }
index 8892bfb..0beb6e0 100644 (file)
@@ -132,6 +132,14 @@ class create extends \moodleform {
             }
         }
 
+        if ($eventtype == 'course' && empty($data['courseid'])) {
+            $errors['courseid'] = get_string('selectacourse');
+        }
+
+        if ($eventtype == 'group' && empty($data['groupcourseid'])) {
+            $errors['groupcourseid'] = get_string('selectacourse');
+        }
+
         if ($data['duration'] == 1 && $data['timestart'] > $data['timedurationuntil']) {
             $errors['durationgroup'] = get_string('invalidtimedurationuntil', 'calendar');
         } else if ($data['duration'] == 2 && (trim($data['timedurationminutes']) == '' || $data['timedurationminutes'] < 1)) {
@@ -237,24 +245,19 @@ class create extends \moodleform {
         }
 
         if (isset($eventtypes['course'])) {
-            $courseoptions = [];
-            foreach ($eventtypes['course'] as $course) {
-                $courseoptions[$course->id] = format_string($course->fullname, true,
-                    ['context' => \context_course::instance($course->id)]);
-            }
-
-            $mform->addElement('select', 'courseid', get_string('course'), $courseoptions);
+            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => true]);
             $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
         }
 
         if (isset($eventtypes['group'])) {
-            $courseoptions = [];
-            foreach ($eventtypes['groupcourses'] as $course) {
-                $courseoptions[$course->id] = format_string($course->fullname, true,
-                    ['context' => \context_course::instance($course->id)]);
+            $options = ['limittoenrolled' => true];
+            // Exclude courses without group.
+            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
+                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
+                    array_keys($eventtypes['groupcourses']));
             }
 
-            $mform->addElement('select', 'groupcourseid', get_string('course'), $courseoptions);
+            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
             $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
 
             $groupoptions = [];
@@ -268,6 +271,7 @@ class create extends \moodleform {
 
             $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
             $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
+            // We handle the group select hide/show actions on the event_form module.
         }
     }
 
index 75a688f..91c1435 100644 (file)
@@ -61,7 +61,10 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
             $data->groupid = "{$legacyevent->courseid}-{$legacyevent->groupid}";
             $data->groupcourseid = $legacyevent->courseid;
         }
-
+        if ($legacyevent->eventtype == 'course') {
+            // Set up the correct value for the to display on the form.
+            $data->courseid = $legacyevent->courseid;
+        }
         $data->description = [
             'text' => $data->description,
             'format' => $data->format
@@ -85,26 +88,28 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
     public function from_data_to_event_properties(\stdClass $data) {
         $properties = clone($data);
 
-        // Undo the form definition work around to allow us to have two different
-        // course selectors present depending on which event type the user selects.
-        if (isset($data->groupcourseid)) {
-            $properties->courseid = $data->groupcourseid;
-            unset($properties->groupcourseid);
-        }
-
-        // Pull the group id back out of the value. The form saves the value
-        // as "<courseid>-<groupid>" to allow the javascript to work correctly.
-        if (isset($data->groupid)) {
-            list($courseid, $groupid) = explode('-', $data->groupid);
-            $properties->groupid = $groupid;
-        }
+        if ($data->eventtype == 'group') {
+            if (isset($data->groupcourseid)) {
+                $properties->courseid = $data->groupcourseid;
+                unset($properties->groupcourseid);
+            }
 
-        // Default course id if none is set.
-        if (!isset($properties->courseid)) {
-            if ($properties->eventtype === 'site') {
-                $properties->courseid = SITEID;
+            // Pull the group id back out of the value. The form saves the value
+            // as "<courseid>-<groupid>" to allow the javascript to work correctly.
+            if (isset($data->groupid)) {
+                list($courseid, $groupid) = explode('-', $data->groupid);
+                $properties->groupid = $groupid;
+            }
+        } else {
+            // Default course id if none is set.
+            if (empty($properties->courseid)) {
+                if ($properties->eventtype == 'site') {
+                    $properties->courseid = SITEID;
+                } else {
+                    $properties->courseid = 0;
+                }
             } else {
-                $properties->courseid = 0;
+                $properties->courseid = $data->courseid;
             }
         }
 
index 48754d1..6fc0e13 100644 (file)
@@ -1318,77 +1318,6 @@ function calendar_get_starting_weekday() {
     return $calendartype->get_starting_weekday();
 }
 
-/**
- * Gets the calendar upcoming event.
- *
- * @param array $courses array of courses
- * @param array|int|bool $groups array of groups, group id or boolean for all/no group events
- * @param array|int|bool $users array of users, user id or boolean for all/no user events
- * @param int $daysinfuture number of days in the future we 'll look
- * @param int $maxevents maximum number of events
- * @param int $fromtime start time
- * @return array $output array of upcoming events
- */
-function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxevents, $fromtime=0) {
-    global $COURSE;
-
-    $display = new \stdClass;
-    $display->range = $daysinfuture; // How many days in the future we 'll look.
-    $display->maxevents = $maxevents;
-
-    $output = array();
-
-    $processed = 0;
-    $now = time(); // We 'll need this later.
-    $usermidnighttoday = usergetmidnight($now);
-
-    if ($fromtime) {
-        $display->tstart = $fromtime;
-    } else {
-        $display->tstart = $usermidnighttoday;
-    }
-
-    // This works correctly with respect to the user's DST, but it is accurate
-    // only because $fromtime is always the exact midnight of some day!
-    $display->tend = usergetmidnight($display->tstart + DAYSECS * $display->range + 3 * HOURSECS) - 1;
-
-    // Get the events matching our criteria.
-    $events = calendar_get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses);
-
-    // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after
-    // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month
-    // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra
-    // arguments to this function.
-    $hrefparams = array();
-    if (!empty($courses)) {
-        $courses = array_diff($courses, array(SITEID));
-        if (count($courses) == 1) {
-            $hrefparams['course'] = reset($courses);
-        }
-    }
-
-    if ($events !== false) {
-        foreach ($events as $event) {
-            if (!empty($event->modulename)) {
-                $instances = get_fast_modinfo($event->courseid)->get_instances_of($event->modulename);
-                if (empty($instances[$event->instance]->uservisible)) {
-                    continue;
-                }
-            }
-
-            if ($processed >= $display->maxevents) {
-                break;
-            }
-
-            $event->time = calendar_format_event_time($event, $now, $hrefparams);
-            $output[] = $event;
-            $processed++;
-        }
-    }
-
-    return $output;
-}
-
 /**
  * Get a HTML link to a course.
  *
@@ -2478,19 +2407,11 @@ function calendar_get_all_allowed_types() {
         calendar_get_allowed_types($allowed, $course, $coursegroups);
 
         if (!empty($allowed->courses)) {
-            if (!isset($types['course'])) {
-                $types['course'] = [$course];
-            } else {
-                $types['course'][] = $course;
-            }
+            $types['course'][$course->id] = $course;
         }
 
         if (!empty($allowed->groups)) {
-            if (!isset($types['groupcourses'])) {
-                $types['groupcourses'] = [$course];
-            } else {
-                $types['groupcourses'][] = $course;
-            }
+            $types['groupcourses'][$course->id] = $course;
 
             if (!isset($types['group'])) {
                 $types['group'] = array_values($allowed->groups);
@@ -3147,15 +3068,20 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
     if ($view === 'day') {
         $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], $date['mday']);
         $tend = $tstart + DAYSECS - 1;
-    } else if ($view === 'upcoming') {
+    } else if ($view === 'upcoming' || $view === 'upcoming_mini') {
         if (isset($CFG->calendar_lookahead)) {
             $defaultlookahead = intval($CFG->calendar_lookahead);
         } else {
             $defaultlookahead = CALENDAR_DEFAULT_UPCOMING_LOOKAHEAD;
         }
-
+        $lookahead = get_user_preferences('calendar_lookahead', $defaultlookahead);
+        $defaultmaxevents = CALENDAR_DEFAULT_UPCOMING_MAXEVENTS;
+        if (isset($CFG->calendar_maxevents)) {
+            $defaultmaxevents = intval($CFG->calendar_maxevents);
+        }
+        $maxevents = get_user_preferences('calendar_maxevents', $defaultmaxevents);
         $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], $date['mday'], $date['hours']);
-        $tend = usergetmidnight($tstart + DAYSECS * $defaultlookahead + 3 * HOURSECS) - 1;
+        $tend = usergetmidnight($tstart + DAYSECS * $lookahead + 3 * HOURSECS) - 1;
     } else {
         $tstart = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
         $monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
@@ -3188,6 +3114,14 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
         return $param;
     }, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
 
+    // We need to make sure user calendar preferences are respected.
+    // If max upcoming events is not set then use default value of 40 events.
+    if (isset($maxevents)) {
+        $limit = $maxevents;
+    } else {
+        $limit = 40;
+    }
+
     $events = \core_calendar\local\api::get_events(
         $tstart,
         $tend,
@@ -3195,7 +3129,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
         null,
         null,
         null,
-        40,
+        $limit,
         null,
         $userparam,
         $groupparam,
@@ -3234,10 +3168,15 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
         $day = new \core_calendar\external\calendar_day_exporter($calendar, $related);
         $data = $day->export($renderer);
         $template = 'core_calendar/calendar_day';
-    } else if ($view == "upcoming") {
+    } else if ($view == "upcoming" || $view == "upcoming_mini") {
         $upcoming = new \core_calendar\external\calendar_upcoming_exporter($calendar, $related);
         $data = $upcoming->export($renderer);
-        $template = 'core_calendar/calendar_upcoming';
+
+        if ($view == "upcoming") {
+            $template = 'core_calendar/calendar_upcoming';
+        } else if ($view == "upcoming_mini") {
+            $template = 'core_calendar/upcoming_mini';
+        }
     }
 
     return [$data, $template];
@@ -3408,7 +3347,7 @@ function calendar_get_filter_types() {
 
     return array_map(function($type) {
         return [
-            'type' => $type,
+            'eventtype' => $type,
             'name' => get_string("eventtype{$type}", "calendar"),
         ];
     }, $types);
index be1154b..14123d7 100644 (file)
     {
     }
 }}
-<a href="#" data-action="filter-event-type" data-eventtype="{{type}}" data-eventtype-hidden="{{hidden}}">
+<a href="#" data-action="filter-event-type" data-eventtype="{{eventtype}}" data-eventtype-hidden="{{hidden}}">
     {{^hidden}}
-        <span class="calendar_event_{{type}}">
+        <span class="calendar_event_{{eventtype}}">
             {{#pix}}i/hide, core, {{#str}}hideeventtype, calendar, {{name}}{{/str}}{{/pix}}
         </span>
+        {{> core_calendar/event_icon}}
         <span class="eventname">{{#str}}hideeventtype, calendar, {{name}}{{/str}}</span>
     {{/hidden}}
     {{#hidden}}
-        <span class="calendar_event_{{type}}">
+        <span class="calendar_event_{{eventtype}}">
             {{#pix}}i/show, core, {{#str}}showeventtype, calendar, {{name}}{{/str}}{{/pix}}
         </span>
+        {{> core_calendar/event_icon}}
         <span class="eventname">{{#str}}showeventtype, calendar, {{name}}{{/str}}</span>
     {{/hidden}}
+
 </a>
diff --git a/calendar/templates/event_icon.mustache b/calendar/templates/event_icon.mustache
new file mode 100644 (file)
index 0000000..fb88bd6
--- /dev/null
@@ -0,0 +1,41 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/event_icon
+
+    Event icon display.
+
+    The purpose of this template is to provide a common location for the
+    rendering of event icons.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+        "modulename": "assign"
+    }
+}}
+{{#modulename}}
+    {{#pix}} icon, {{modulename}} {{/pix}}
+{{/modulename}}
+{{^modulename}}
+    {{#pix}} i/{{eventtype}}event, core {{/pix}}
+{{/modulename}}
index 0a57928..ccfd7be 100644 (file)
 {{#subscription}}
     {{#displayeventsource}}
         <div>
-            {{#url}}
-                <p><a href="{{url}}">{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</a></p>
-            {{/url}}
-            {{^url}}
-                <p>{{#str}}subscriptionsource, core_calendar, {{name}}{{/str}}</p>
-            {{/url}}
+            {{#subscriptionurl}}
+                <p><a href="{{subscriptionurl}}">{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</a></p>
+            {{/subscriptionurl}}
+            {{^subscriptionurl}}
+                <p>{{#str}}subscriptionsource, core_calendar, {{{subscriptionname}}}{{/str}}</p>
+            {{/subscriptionurl}}
         </div>
     {{/displayeventsource}}
 {{/subscription}}
index d9769b2..427919e 100644 (file)
@@ -22,8 +22,7 @@
     Example context (json):
     {
         "exportcalendarbutton": "<button class='btn btn-secondary'>Export calendar</button>",
-        "managesubscriptionbutton": "<button class='btn btn-secondary'>Manage subscriptions</button>",
-        "icalurl": "http://example.com/"
+        "managesubscriptionbutton": "<button class='btn btn-secondary'>Manage subscriptions</button>"
     }
 }}
 <div class="bottom">
@@ -33,7 +32,4 @@
     {{#managesubscriptionbutton}}
         {{> core/single_button }}
     {{/managesubscriptionbutton}}
-    {{#icalurl}}
-        <a href="{{icalurl}}" title="{{#str}} quickdownloadcalendar, calendar {{/str}}" class="ical-link m-l-1">iCal</a>
-    {{/icalurl}}
 </div>
index 942e459..34627ea 100644 (file)
                         data-region="day"
                         data-new-event-timestamp="{{neweventtimestamp}}">
                         <div class="hidden-sm-down text-xs-center">
-                            {{#events.0}}
+                            {{#hasevents}}
                                 <a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
-                            {{/events.0}}
-                            {{^events.0}}
+                            {{/hasevents}}
+                            {{^hasevents}}
                                 {{mday}}
-                            {{/events.0}}
-                            {{#events.0}}
+                            {{/hasevents}}
+                            {{#hasevents}}
                                 <div data-region="day-content">
                                     <ul>
                                         {{#events}}
                                             <li class="events-underway">[{{name}}]</li>
                                         {{/underway}}
                                         {{^underway}}
-                                            <li class="calendar_event_{{calendareventtype}}"
-                                                data-region="event-item"
+                                            <li data-region="event-item"
                                                 data-eventtype-{{calendareventtype}}="1"
                                                 {{#canedit}}
                                                     draggable="true"
                                                     data-drag-type="move"
+                                                    {{#mindaytimestamp}}
+                                                        data-min-day-timestamp="{{.}}"
+                                                    {{/mindaytimestamp}}
+                                                    {{#mindayerror}}
+                                                        data-min-day-error="{{.}}"
+                                                    {{/mindayerror}}
+                                                    {{#maxdaytimestamp}}
+                                                        data-max-day-timestamp="{{.}}"
+                                                    {{/maxdaytimestamp}}
+                                                    {{#maxdayerror}}
+                                                        data-max-day-error="{{.}}"
+                                                    {{/maxdayerror}}
                                                 {{/canedit}}>
 
-                                                <a data-action="view-event" data-event-id="{{id}}" href="{{url}}">{{name}}</a>
+                                                <a data-action="view-event" data-event-id="{{id}}" href="{{url}}" title="{{name}}">
+                                                    <span class="badge badge-circle calendar_event_{{calendareventtype}}">
+                                                        &nbsp;
+                                                    </span>
+                                                    {{> core_calendar/event_icon}}
+                                                    {{name}}
+                                                </a>
                                             </li>
                                         {{/underway}}
                                         {{/events}}
                                     </ul>
                                 </div>
-                            {{/events.0}}
+                            {{/hasevents}}
                         </div>
                         <div class="hidden-md-up hidden-desktop">
-                            {{#events.0}}
+                            {{#hasevents}}
                                 <a href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
-                            {{/events.0}}
-                            {{^events.0}}
+                            {{/hasevents}}
+                            {{^hasevents}}
                                 <div data-region="day-content">
                                     {{mday}}
                                 </div>
-                            {{/events.0}}
+                            {{/hasevents}}
                         </div>
                     </td>
                 {{/days}}
diff --git a/calendar/templates/upcoming_mini.mustache b/calendar/templates/upcoming_mini.mustache
new file mode 100644 (file)
index 0000000..8d02953
--- /dev/null
@@ -0,0 +1,43 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/upcoming_mini
+
+    Calendar upcoming view for blocks.
+
+    The purpose of this template is to render the upcoming view for blocks.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="card-text content">
+    {{#events}}
+        <div class="event">
+            <span>{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}</span>
+            <a href="{{viewurl}}">{{{name}}}</a>
+            <div class="date">{{{formattedtime}}}</div>
+        </div>
+        <hr>
+    {{/events}}
+</div>
index 300588c..f35f252 100644 (file)
@@ -53,6 +53,7 @@ Feature: Perform basic calendar functionality
     Given I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | course |
+      | Course        | Course 1 |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event, sucka! |
     And I log out
@@ -70,8 +71,10 @@ Feature: Perform basic calendar functionality
   @javascript
   Scenario: Create a group event
     Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
     And I create a calendar event with form data:
       | Type of event | group |
+      | Group         | Group 1 |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event |
     And I log out
diff --git a/calendar/tests/calendar_event_exporter_test.php b/calendar/tests/calendar_event_exporter_test.php
new file mode 100644 (file)
index 0000000..5ae3edb
--- /dev/null
@@ -0,0 +1,150 @@
+<?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/>.
+
+/**
+ * Calendar event exporter tests tests.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_calendar\external\calendar_event_exporter;
+use core_calendar\local\event\container;
+
+/**
+ * Calendar event exporter testcase.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_event_exporter_testcase extends advanced_testcase {
+    /**
+     * Data provider for the module timestamp min limit test case to confirm
+     * that the minimum time limit is set correctly on the boundary cases.
+     */
+    public function get_module_timestamp_min_limit_test_cases() {
+        $now = time();
+        $todaymidnight = usergetmidnight($now);
+        $tomorrowmidnight = $todaymidnight + DAYSECS;
+        $eightam = $todaymidnight + (60 * 60 * 8);
+        $starttime = (new DateTime())->setTimestamp($eightam);
+
+        return [
+            'before min' => [
+                $starttime,
+                [
+                    ($starttime->getTimestamp() + 1),
+                    'some error'
+                ],
+                $tomorrowmidnight
+            ],
+            'equal min' => [
+                $starttime,
+                [
+                    $starttime->getTimestamp(),
+                    'some error'
+                ],
+                $todaymidnight
+            ],
+            'after min' => [
+                $starttime,
+                [
+                    ($starttime->getTimestamp() - 1),
+                    'some error'
+                ],
+                $todaymidnight
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider get_module_timestamp_min_limit_test_cases()
+     */
+    public function test_get_module_timestamp_min_limit($starttime, $min, $expected) {
+        $class = \core_calendar\external\calendar_event_exporter::class;
+        $mock = $this->getMockBuilder($class)
+            ->disableOriginalConstructor()
+            ->setMethods(null)
+            ->getMock();
+        $reflector = new ReflectionClass($class);
+        $method = $reflector->getMethod('get_module_timestamp_min_limit');
+        $method->setAccessible(true);
+
+        $result = $method->invoke($mock, $starttime, $min);
+        $this->assertEquals($expected, $result['mindaytimestamp']);
+        $this->assertEquals($min[1], $result['mindayerror']);
+    }
+
+    /**
+     * Data provider for the module timestamp min limit test case to confirm
+     * that the minimum time limit is set correctly on the boundary cases.
+     */
+    public function get_module_timestamp_max_limit_test_cases() {
+        $now = time();
+        $todaymidnight = usergetmidnight($now);
+        $yesterdaymidnight = $todaymidnight - DAYSECS;
+        $eightam = $todaymidnight + (60 * 60 * 8);
+        $starttime = (new DateTime())->setTimestamp($eightam);
+
+        return [
+            'before max' => [
+                $starttime,
+                [
+                    ($starttime->getTimestamp() + 1),
+                    'some error'
+                ],
+                $todaymidnight
+            ],
+            'equal max' => [
+                $starttime,
+                [
+                    $starttime->getTimestamp(),
+                    'some error'
+                ],
+                $todaymidnight
+            ],
+            'after max' => [
+                $starttime,
+                [
+                    ($starttime->getTimestamp() - 1),
+                    'some error'
+                ],
+                $yesterdaymidnight
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider get_module_timestamp_max_limit_test_cases()
+     */
+    public function test_get_module_timestamp_max_limit($starttime, $max, $expected) {
+        $class = \core_calendar\external\calendar_event_exporter::class;
+        $mock = $this->getMockBuilder($class)
+            ->disableOriginalConstructor()
+            ->setMethods(null)
+            ->getMock();
+        $reflector = new ReflectionClass($class);
+        $method = $reflector->getMethod('get_module_timestamp_max_limit');
+        $method->setAccessible(true);
+
+        $result = $method->invoke($mock, $starttime, $max);
+        $this->assertEquals($expected, $result['maxdaytimestamp']);
+        $this->assertEquals($max[1], $result['maxdayerror']);
+    }
+}
index 23efe02..1bba607 100644 (file)
@@ -499,7 +499,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $types = calendar_get_all_allowed_types();
         $typecourses = $types['course'];
         $this->assertCount(1, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[0]->id);
+        $this->assertEquals($course1->id, $typecourses[$course1->id]->id);
 
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
 
@@ -543,7 +543,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $types = calendar_get_all_allowed_types();
         $typecourses = $types['course'];
         $this->assertCount(1, $typecourses);
-        $this->assertEquals($course->id, $typecourses[0]->id);
+        $this->assertEquals($course->id, $typecourses[$course->id]->id);
         $this->assertArrayNotHasKey('group', $types);
         $this->assertArrayNotHasKey('groupcourses', $types);
     }
@@ -570,7 +570,7 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $types = calendar_get_all_allowed_types();
         $typecourses = $types['course'];
         $this->assertCount(1, $typecourses);
-        $this->assertEquals($course->id, $typecourses[0]->id);
+        $this->assertEquals($course->id, $typecourses[$course->id]->id);
         $this->assertArrayNotHasKey('group', $types);