Merge branch 'MDL-62025_master' of git://github.com/markn86/moodle
authorJun Pataleta <jun@moodle.com>
Wed, 18 Apr 2018 08:26:26 +0000 (16:26 +0800)
committerJun Pataleta <jun@moodle.com>
Wed, 18 Apr 2018 08:26:26 +0000 (16:26 +0800)
570 files changed:
.eslintignore
.stylelintignore
admin/cli/install.php
admin/cli/upgrade.php
admin/tool/lp/templates/manage_competencies_page.mustache
admin/tool/monitor/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/monitor/classes/rule.php
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/monitor/tests/privacy_test.php [new file with mode: 0644]
admin/tool/policy/accept.php [new file with mode: 0644]
admin/tool/policy/acceptances.php [new file with mode: 0644]
admin/tool/policy/amd/build/acceptances_filter.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/acceptances_filter_datasource.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/acceptmodal.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/managedocsactions.min.js [new file with mode: 0644]
admin/tool/policy/amd/build/policyactions.min.js [new file with mode: 0644]
admin/tool/policy/amd/src/acceptances_filter.js [new file with mode: 0644]
admin/tool/policy/amd/src/acceptances_filter_datasource.js [new file with mode: 0644]
admin/tool/policy/amd/src/acceptmodal.js [new file with mode: 0644]
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js [new file with mode: 0644]
admin/tool/policy/amd/src/managedocsactions.js [new file with mode: 0644]
admin/tool/policy/amd/src/policyactions.js [new file with mode: 0644]
admin/tool/policy/classes/acceptances_table.php [new file with mode: 0644]
admin/tool/policy/classes/api.php [new file with mode: 0644]
admin/tool/policy/classes/event/acceptance_base.php [new file with mode: 0644]
admin/tool/policy/classes/event/acceptance_created.php [new file with mode: 0644]
admin/tool/policy/classes/event/acceptance_updated.php [new file with mode: 0644]
admin/tool/policy/classes/external.php [new file with mode: 0644]
admin/tool/policy/classes/form/accept_policy.php [new file with mode: 0644]
admin/tool/policy/classes/form/policydoc.php [new file with mode: 0644]
admin/tool/policy/classes/output/acceptances.php [new file with mode: 0644]
admin/tool/policy/classes/output/acceptances_filter.php [new file with mode: 0644]
admin/tool/policy/classes/output/guestconsent.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_agreedocs.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_managedocs_list.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_nopermission.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_viewalldoc.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_viewdoc.php [new file with mode: 0644]
admin/tool/policy/classes/output/renderer.php [new file with mode: 0644]
admin/tool/policy/classes/output/user_agreement.php [new file with mode: 0644]
admin/tool/policy/classes/policy_exporter.php [new file with mode: 0644]
admin/tool/policy/classes/policy_version.php [new file with mode: 0644]
admin/tool/policy/classes/policy_version_exporter.php [new file with mode: 0644]
admin/tool/policy/classes/privacy/local/sitepolicy/handler.php [new file with mode: 0644]
admin/tool/policy/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/policy/db/access.php [new file with mode: 0644]
admin/tool/policy/db/events.php [new file with mode: 0644]
admin/tool/policy/db/install.xml [new file with mode: 0644]
admin/tool/policy/db/services.php [new file with mode: 0644]
admin/tool/policy/editpolicydoc.php [new file with mode: 0644]
admin/tool/policy/index.php [new file with mode: 0644]
admin/tool/policy/lang/en/tool_policy.php [new file with mode: 0644]
admin/tool/policy/lib.php [new file with mode: 0644]
admin/tool/policy/managedocs.php [new file with mode: 0644]
admin/tool/policy/pix/agreedno.png [new file with mode: 0644]
admin/tool/policy/pix/agreedno.svg [new file with mode: 0644]
admin/tool/policy/pix/agreedyes.png [new file with mode: 0644]
admin/tool/policy/pix/agreedyes.svg [new file with mode: 0644]
admin/tool/policy/pix/agreedyesonbehalf.png [new file with mode: 0644]
admin/tool/policy/pix/agreedyesonbehalf.svg [new file with mode: 0644]
admin/tool/policy/pix/level.png [new file with mode: 0644]
admin/tool/policy/pix/level.svg [new file with mode: 0644]
admin/tool/policy/readme_moodle.txt [new file with mode: 0644]
admin/tool/policy/settings.php [new file with mode: 0644]
admin/tool/policy/styles.css [new file with mode: 0644]
admin/tool/policy/templates/acceptances.mustache [new file with mode: 0644]
admin/tool/policy/templates/acceptances_filter.mustache [new file with mode: 0644]
admin/tool/policy/templates/guestconsent.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_agreedocs.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_managedocs_list.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_nopermission.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_viewalldoc.mustache [new file with mode: 0644]
admin/tool/policy/templates/page_viewdoc.mustache [new file with mode: 0644]
admin/tool/policy/templates/user_agreement.mustache [new file with mode: 0644]
admin/tool/policy/tests/api_test.php [new file with mode: 0644]
admin/tool/policy/tests/behat/acceptances.feature [new file with mode: 0644]
admin/tool/policy/tests/behat/behat_tool_policy.php [new file with mode: 0644]
admin/tool/policy/tests/behat/consent.feature [new file with mode: 0644]
admin/tool/policy/tests/behat/managepolicies.feature [new file with mode: 0644]
admin/tool/policy/tests/externallib_test.php [new file with mode: 0644]
admin/tool/policy/tests/privacy_provider_test.php [new file with mode: 0644]
admin/tool/policy/thirdpartylibs.xml [new file with mode: 0644]
admin/tool/policy/user.php [new file with mode: 0644]
admin/tool/policy/version.php [new file with mode: 0644]
admin/tool/policy/view.php [new file with mode: 0644]
admin/tool/policy/viewall.php [new file with mode: 0644]
admin/tool/profiling/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/profiling/lang/en/tool_profiling.php
admin/tool/recyclebin/classes/category_bin.php
admin/tool/recyclebin/classes/course_bin.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/processor.php
backup/controller/restore_controller.class.php
backup/controller/tests/controller_test.php
backup/converter/convertlib.php
backup/converter/imscc11/backuplib.php
backup/converter/moodle1/lib.php
backup/converter/moodle1/tests/moodle1_converter_test.php
backup/import.php
backup/moodle2/tests/moodle2_test.php
backup/restorefile.php
backup/util/factories/backup_factory.class.php
backup/util/helper/backup_file_manager.class.php
backup/util/helper/backup_general_helper.class.php
backup/util/helper/backup_helper.class.php
backup/util/helper/convert_helper.class.php
backup/util/plan/backup_plan.class.php
backup/util/plan/restore_plan.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/renderer.php
backup/util/ui/restore_ui_stage.class.php
blocks/community/communitycourse.php
blocks/myoverview/templates/course-event-list-item.mustache
blocks/myoverview/templates/course-summary.mustache
blocks/myoverview/templates/courses-view-course-item.mustache
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/event-list-item.mustache
blocks/myoverview/templates/timeline-view.mustache
blocks/tests/behat/behat_blocks.php
calendar/templates/month_detailed.mustache
config-dist.php
course/classes/management_renderer.php
course/format/singleactivity/classes/privacy/provider.php [new file with mode: 0644]
course/format/singleactivity/lang/en/format_singleactivity.php
course/format/social/classes/privacy/provider.php [new file with mode: 0644]
course/format/social/lang/en/format_social.php
course/format/topics/classes/privacy/provider.php [new file with mode: 0644]
course/format/topics/lang/en/format_topics.php
course/format/weeks/classes/privacy/provider.php [new file with mode: 0644]
course/format/weeks/lang/en/format_weeks.php
course/tests/behat/category_change_visibility.feature
course/tests/behat/category_management.feature
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_change_visibility.feature
course/tests/behat/course_search.feature
course/tests/behat/navigate_course_list.feature
course/tests/restore_test.php
enrol/paypal/ipn.php
grade/export/ods/classes/privacy/provider.php [new file with mode: 0644]
grade/export/ods/lang/en/gradeexport_ods.php
grade/export/txt/classes/privacy/provider.php [new file with mode: 0644]
grade/export/txt/lang/en/gradeexport_txt.php
grade/export/xls/classes/privacy/provider.php [new file with mode: 0644]
grade/export/xls/lang/en/gradeexport_xls.php
grade/export/xml/classes/privacy/provider.php [new file with mode: 0644]
grade/export/xml/lang/en/gradeexport_xml.php
grade/import/csv/classes/privacy/provider.php [new file with mode: 0644]
grade/import/csv/lang/en/gradeimport_csv.php
grade/import/direct/classes/privacy/provider.php [new file with mode: 0644]
grade/import/direct/lang/en/gradeimport_direct.php
grade/import/xml/classes/privacy/provider.php [new file with mode: 0644]
grade/import/xml/lang/en/gradeimport_xml.php
grade/report/grader/classes/privacy/provider.php [new file with mode: 0644]
grade/report/grader/lang/en/gradereport_grader.php
grade/report/grader/tests/privacy_test.php [new file with mode: 0644]
grade/report/history/classes/privacy/provider.php [new file with mode: 0644]
grade/report/history/lang/en/gradereport_history.php
grade/report/outcomes/classes/privacy/provider.php [new file with mode: 0644]
grade/report/outcomes/lang/en/gradereport_outcomes.php
grade/report/overview/classes/privacy/provider.php [new file with mode: 0644]
grade/report/overview/lang/en/gradereport_overview.php
grade/report/singleview/classes/privacy/provider.php [new file with mode: 0644]
grade/report/singleview/lang/en/gradereport_singleview.php
grade/report/user/classes/privacy/provider.php [new file with mode: 0644]
grade/report/user/lang/en/gradereport_user.php
grade/report/user/tests/privacy_test.php [new file with mode: 0644]
install.php
install/lang/id/moodle.php
lang/en/admin.php
lang/en/editor.php
lang/en/install.php
lang/en/portfolio.php
lang/en/repository.php
lib/amd/build/adapter.min.js [new file with mode: 0644]
lib/amd/src/adapter.js [new file with mode: 0644]
lib/behat/classes/partial_named_selector.php
lib/classes/hub/publication.php
lib/classes/plugin_manager.php
lib/classes/task/file_temp_cleanup_task.php
lib/editor/atto/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/atto/db/upgrade.php
lib/editor/atto/lang/en/editor_atto.php
lib/editor/atto/plugins/recordrtc/classes/privacy/provider.php [moved from theme/boost/classes/output/core/admin_renderer.php with 60% similarity]
lib/editor/atto/plugins/recordrtc/lang/en/atto_recordrtc.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/lib.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/i/audiortc.png [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/i/videortc.png [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/pix/icon.png [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/settings.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/version.php [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button-min.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-button/moodle-atto_recordrtc-button.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/button/build.json [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/button/js/button.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/button/meta/button.json [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/build.json [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/abstractmodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/audiomodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/commonmodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/compatcheckmodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/videomodule.js [new file with mode: 0644]
lib/editor/atto/plugins/recordrtc/yui/src/recording/meta/recording.json [new file with mode: 0644]
lib/editor/atto/settings.php
lib/editor/atto/tests/privacy_provider.php [new file with mode: 0644]
lib/editor/atto/version.php
lib/editor/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tests/privacy_provider_test.php [new file with mode: 0644]
lib/editor/textarea/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/textarea/lang/en/editor_textarea.php
lib/editor/tinymce/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/formslib.php
lib/outputrenderers.php
lib/setup.php
lib/setuplib.php
lib/tablelib.php
lib/testing/classes/util.php
lib/tests/behat/alpha_chooser.feature
lib/tests/behat/behat_forms.php
lib/tests/behat/readonlyform.feature [new file with mode: 0644]
lib/tests/cronlib_test.php
lib/tests/fixtures/readonlyform.php [new file with mode: 0644]
lib/tests/scheduled_task_test.php
lib/thirdpartylibs.xml
lib/upgradelib.php
media/player/html5audio/classes/privacy/provider.php [new file with mode: 0644]
media/player/html5audio/lang/en/media_html5audio.php
media/player/html5video/classes/privacy/provider.php [new file with mode: 0644]
media/player/html5video/lang/en/media_html5video.php
media/player/swf/classes/privacy/provider.php [new file with mode: 0644]
media/player/swf/lang/en/media_swf.php
media/player/videojs/classes/privacy/provider.php [new file with mode: 0644]
media/player/videojs/lang/en/media_videojs.php
media/player/vimeo/classes/privacy/provider.php [new file with mode: 0644]
media/player/vimeo/lang/en/media_vimeo.php
media/player/youtube/classes/privacy/provider.php [new file with mode: 0644]
media/player/youtube/lang/en/media_youtube.php
mod/assignment/type/offline/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/offline/lang/en/assignment_offline.php
mod/assignment/type/online/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/online/lang/en/assignment_online.php
mod/assignment/type/upload/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/upload/lang/en/assignment_upload.php
mod/assignment/type/uploadsingle/classes/privacy/provider.php [new file with mode: 0644]
mod/assignment/type/uploadsingle/lang/en/assignment_uploadsingle.php
mod/book/tool/exportimscp/classes/privacy/provider.php [new file with mode: 0644]
mod/book/tool/exportimscp/lang/en/booktool_exportimscp.php
mod/book/tool/importhtml/classes/privacy/provider.php [new file with mode: 0644]
mod/book/tool/importhtml/lang/en/booktool_importhtml.php
mod/book/tool/print/classes/privacy/provider.php [new file with mode: 0644]
mod/book/tool/print/lang/en/booktool_print.php
mod/feedback/classes/privacy/provider.php [new file with mode: 0644]
mod/feedback/lang/en/feedback.php
mod/feedback/tests/privacy_test.php [new file with mode: 0644]
mod/lti/locallib.php
mod/quiz/report/responses/last_responses_table.php
mod/quiz/report/statistics/report.php
mod/quiz/tests/tags_test.php
mod/scorm/db/install.xml
mod/scorm/db/upgrade.php
mod/scorm/version.php
mod/upgrade.txt
phpunit.xml.dist
portfolio/boxnet/classes/privacy/provider.php [new file with mode: 0644]
portfolio/boxnet/lang/en/portfolio_boxnet.php
portfolio/boxnet/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/classes/privacy/legacy_polyfill.php [new file with mode: 0644]
portfolio/classes/privacy/portfolio_provider.php [new file with mode: 0644]
portfolio/classes/privacy/provider.php [new file with mode: 0644]
portfolio/download/classes/privacy/provider.php [new file with mode: 0644]
portfolio/download/lang/en/portfolio_download.php
portfolio/flickr/classes/privacy/provider.php [new file with mode: 0644]
portfolio/flickr/lang/en/portfolio_flickr.php
portfolio/flickr/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/googledocs/classes/privacy/provider.php [new file with mode: 0644]
portfolio/googledocs/lang/en/portfolio_googledocs.php
portfolio/googledocs/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/mahara/classes/privacy/provider.php [new file with mode: 0644]
portfolio/mahara/lang/en/portfolio_mahara.php
portfolio/mahara/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/picasa/classes/privacy/provider.php [new file with mode: 0644]
portfolio/picasa/lang/en/portfolio_picasa.php
portfolio/picasa/tests/privacy_provider_test.php [new file with mode: 0644]
portfolio/tests/privacy_legacy_polyfill_test.php [new file with mode: 0644]
portfolio/tests/privacy_provider_test.php [new file with mode: 0644]
privacy/classes/tests/request/content_writer.php
privacy/tests/tests_content_writer_test.php
question/classes/statistics/questions/calculator.php
repository/areafiles/classes/privacy/provider.php [new file with mode: 0644]
repository/areafiles/lang/en/repository_areafiles.php
repository/boxnet/classes/privacy/provider.php [new file with mode: 0644]
repository/boxnet/lang/en/repository_boxnet.php
repository/classes/privacy/provider.php [new file with mode: 0644]
repository/coursefiles/classes/privacy/provider.php [new file with mode: 0644]
repository/coursefiles/lang/en/repository_coursefiles.php
repository/dropbox/classes/privacy/provider.php [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/equella/classes/privacy/provider.php [new file with mode: 0644]
repository/equella/lang/en/repository_equella.php
repository/filesystem/classes/privacy/provider.php [new file with mode: 0644]
repository/filesystem/lang/en/repository_filesystem.php
repository/flickr/classes/privacy/provider.php [new file with mode: 0644]
repository/flickr/lang/en/repository_flickr.php
repository/flickr_public/classes/privacy/provider.php [new file with mode: 0644]
repository/flickr_public/lang/en/repository_flickr_public.php
repository/googledocs/classes/privacy/provider.php [new file with mode: 0644]
repository/googledocs/lang/en/repository_googledocs.php
repository/local/classes/privacy/provider.php [new file with mode: 0644]
repository/local/lang/en/repository_local.php
repository/merlot/classes/privacy/provider.php [new file with mode: 0644]
repository/merlot/lang/en/repository_merlot.php
repository/onedrive/classes/privacy/provider.php [new file with mode: 0644]
repository/onedrive/lang/en/repository_onedrive.php
repository/onedrive/tests/privacy_test.php [new file with mode: 0644]
repository/picasa/classes/privacy/provider.php [new file with mode: 0644]
repository/picasa/lang/en/repository_picasa.php
repository/recent/classes/privacy/provider.php [new file with mode: 0644]
repository/recent/lang/en/repository_recent.php
repository/s3/classes/privacy/provider.php [new file with mode: 0644]
repository/s3/lang/en/repository_s3.php
repository/skydrive/classes/privacy/provider.php [new file with mode: 0644]
repository/skydrive/lang/en/repository_skydrive.php
repository/tests/privacy_test.php [new file with mode: 0644]
repository/upload/classes/privacy/provider.php [new file with mode: 0644]
repository/upload/lang/en/repository_upload.php
repository/url/classes/privacy/provider.php [new file with mode: 0644]
repository/url/lang/en/repository_url.php
repository/user/classes/privacy/provider.php [new file with mode: 0644]
repository/user/lang/en/repository_user.php
repository/webdav/classes/privacy/provider.php [new file with mode: 0644]
repository/webdav/lang/en/repository_webdav.php
repository/wikimedia/classes/privacy/provider.php [new file with mode: 0644]
repository/wikimedia/lang/en/repository_wikimedia.php
repository/youtube/classes/privacy/provider.php [new file with mode: 0644]
repository/youtube/lang/en/repository_youtube.php
search/classes/output/form/search.php
search/index.php
search/tests/behat/search_query.feature
tag/classes/tag.php
tag/tests/behat/edit_tag.feature
theme/boost/amd/build/alert.min.js
theme/boost/amd/build/button.min.js
theme/boost/amd/build/carousel.min.js
theme/boost/amd/build/collapse.min.js
theme/boost/amd/build/dropdown.min.js
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/build/modal.min.js
theme/boost/amd/build/popover.min.js
theme/boost/amd/build/scrollspy.min.js
theme/boost/amd/build/tab.min.js
theme/boost/amd/build/tooltip.min.js
theme/boost/amd/build/util.min.js
theme/boost/amd/src/alert.js
theme/boost/amd/src/button.js
theme/boost/amd/src/carousel.js
theme/boost/amd/src/collapse.js
theme/boost/amd/src/dropdown.js
theme/boost/amd/src/form-display-errors.js
theme/boost/amd/src/modal.js
theme/boost/amd/src/popover.js
theme/boost/amd/src/scrollspy.js
theme/boost/amd/src/tab.js
theme/boost/amd/src/tooltip.js
theme/boost/amd/src/util.js
theme/boost/classes/output/core_course/management/renderer.php [new file with mode: 0644]
theme/boost/classes/output/core_renderer.php
theme/boost/cli/import-bootswatch.php
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/LICENSE
theme/boost/scss/bootstrap/_alert.scss
theme/boost/scss/bootstrap/_animation.scss [deleted file]
theme/boost/scss/bootstrap/_badge.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_button-group.scss
theme/boost/scss/bootstrap/_buttons.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_close.scss
theme/boost/scss/bootstrap/_code.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_custom.scss [deleted file]
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_forms.scss
theme/boost/scss/bootstrap/_functions.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_grid.scss
theme/boost/scss/bootstrap/_images.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_jumbotron.scss
theme/boost/scss/bootstrap/_list-group.scss
theme/boost/scss/bootstrap/_media.scss
theme/boost/scss/bootstrap/_mixins.scss
theme/boost/scss/bootstrap/_modal.scss
theme/boost/scss/bootstrap/_nav.scss
theme/boost/scss/bootstrap/_navbar.scss
theme/boost/scss/bootstrap/_normalize.scss [deleted file]
theme/boost/scss/bootstrap/_pagination.scss
theme/boost/scss/bootstrap/_popover.scss
theme/boost/scss/bootstrap/_print.scss
theme/boost/scss/bootstrap/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_root.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_tables.scss
theme/boost/scss/bootstrap/_tags.scss [deleted file]
theme/boost/scss/bootstrap/_tooltip.scss
theme/boost/scss/bootstrap/_transitions.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_type.scss
theme/boost/scss/bootstrap/_utilities.scss
theme/boost/scss/bootstrap/_variables.scss
theme/boost/scss/bootstrap/bootstrap-flex.scss [deleted file]
theme/boost/scss/bootstrap/bootstrap-grid.scss
theme/boost/scss/bootstrap/bootstrap-reboot.scss
theme/boost/scss/bootstrap/bootstrap.scss
theme/boost/scss/bootstrap/mixins/_alert.scss
theme/boost/scss/bootstrap/mixins/_background-variant.scss
theme/boost/scss/bootstrap/mixins/_badge.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_border-radius.scss
theme/boost/scss/bootstrap/mixins/_box-shadow.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_breakpoints.scss
theme/boost/scss/bootstrap/mixins/_buttons.scss
theme/boost/scss/bootstrap/mixins/_cards.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_caret.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_clearfix.scss
theme/boost/scss/bootstrap/mixins/_float.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_forms.scss
theme/boost/scss/bootstrap/mixins/_gradients.scss
theme/boost/scss/bootstrap/mixins/_grid-framework.scss
theme/boost/scss/bootstrap/mixins/_grid.scss
theme/boost/scss/bootstrap/mixins/_hover.scss
theme/boost/scss/bootstrap/mixins/_image.scss
theme/boost/scss/bootstrap/mixins/_list-group.scss
theme/boost/scss/bootstrap/mixins/_nav-divider.scss
theme/boost/scss/bootstrap/mixins/_navbar-align.scss
theme/boost/scss/bootstrap/mixins/_pagination.scss
theme/boost/scss/bootstrap/mixins/_progress.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_pulls.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_reset-filter.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_reset-text.scss
theme/boost/scss/bootstrap/mixins/_resize.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_tab-focus.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_tag.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_text-emphasis.scss
theme/boost/scss/bootstrap/mixins/_text-hide.scss
theme/boost/scss/bootstrap/mixins/_text-truncate.scss
theme/boost/scss/bootstrap/mixins/_transition.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_visibility.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_align.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_background.scss
theme/boost/scss/bootstrap/utilities/_borders.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_display.scss
theme/boost/scss/bootstrap/utilities/_embed.scss [moved from theme/boost/scss/bootstrap/_responsive-embed.scss with 60% similarity]
theme/boost/scss/bootstrap/utilities/_flex.scss
theme/boost/scss/bootstrap/utilities/_float.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_position.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_pulls.scss [deleted file]
theme/boost/scss/bootstrap/utilities/_sizing.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_spacing.scss
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/bootstrap/utilities/_visibility.scss
theme/boost/scss/fontawesome.scss [new file with mode: 0644]
theme/boost/scss/fontawesome/moodle-path.scss [deleted file]
theme/boost/scss/fontawesome/readme_moodle.txt
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/backup-restore.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bootswatch.scss
theme/boost/scss/moodle/bs2-compat.scss
theme/boost/scss/moodle/bs4alphacompat.scss [new file with mode: 0644]
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/debug.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modal.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/responsive-tabs.scss [deleted file]
theme/boost/scss/moodle/search.scss
theme/boost/scss/moodle/undo.scss
theme/boost/scss/moodle/user.scss
theme/boost/scss/preset/default.scss
theme/boost/scss/preset/plain.scss
theme/boost/templates/admin_setting_tabs.mustache
theme/boost/templates/block_search_forums/search_form.mustache
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/action_menu.mustache
theme/boost/templates/core/action_menu_trigger.mustache
theme/boost/templates/core/block.mustache
theme/boost/templates/core/custom_menu_item.mustache
theme/boost/templates/core/dataformat_selector.mustache
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/core/filemanager_modal_generallayout.mustache
theme/boost/templates/core/filemanager_page_generallayout.mustache
theme/boost/templates/core/form_autocomplete_selection.mustache
theme/boost/templates/core/help_icon.mustache
theme/boost/templates/core/initials_bar.mustache [new file with mode: 0644]
theme/boost/templates/core/loginform.mustache
theme/boost/templates/core/modal.mustache
theme/boost/templates/core/preferences_groups.mustache
theme/boost/templates/core/settings_link_page.mustache
theme/boost/templates/core/settings_link_page_single.mustache
theme/boost/templates/core/signup_form_layout.mustache
theme/boost/templates/core/tabtree.mustache
theme/boost/templates/core_admin/setting.mustache
theme/boost/templates/core_form/element-advcheckbox-inline.mustache
theme/boost/templates/core_form/element-advcheckbox.mustache
theme/boost/templates/core_form/element-autocomplete-inline.mustache
theme/boost/templates/core_form/element-autocomplete.mustache
theme/boost/templates/core_form/element-button.mustache
theme/boost/templates/core_form/element-checkbox-inline.mustache
theme/boost/templates/core_form/element-checkbox.mustache
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/boost/templates/core_form/element-password.mustache
theme/boost/templates/core_form/element-passwordunmask.mustache
theme/boost/templates/core_form/element-radio-inline.mustache
theme/boost/templates/core_form/element-radio.mustache
theme/boost/templates/core_form/element-select-inline.mustache
theme/boost/templates/core_form/element-select.mustache
theme/boost/templates/core_form/element-selectgroups-inline.mustache
theme/boost/templates/core_form/element-selectgroups.mustache
theme/boost/templates/core_form/element-selectwithlink.mustache
theme/boost/templates/core_form/element-tags-inline.mustache
theme/boost/templates/core_form/element-tags.mustache
theme/boost/templates/core_form/element-template-inline.mustache
theme/boost/templates/core_form/element-template.mustache
theme/boost/templates/core_form/element-text-inline.mustache
theme/boost/templates/core_form/element-text.mustache
theme/boost/templates/core_form/element-textarea.mustache
theme/boost/templates/core_form/element-url.mustache
theme/boost/templates/core_grades/edit_tree.mustache
theme/boost/templates/custom_menu_footer.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/footer.mustache [new file with mode: 0644]
theme/boost/templates/head.mustache [new file with mode: 0644]
theme/boost/templates/header.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/mod_forum/quick_search_form.mustache
theme/boost/templates/nav-drawer.mustache
theme/boost/templates/navbar-secure.mustache [moved from theme/boost/templates/header-secure.mustache with 66% similarity]
theme/boost/templates/navbar.mustache [new file with mode: 0644]
theme/boost/templates/secure.mustache
theme/boost/templates/tool_usertours/tourstep.mustache
theme/boost/tests/behat/behat_theme_boost_behat_action_menu.php
theme/boost/tests/behat/behat_theme_boost_behat_navigation.php
theme/boost/thirdpartylibs.xml
theme/boost/upgrade.txt
user/tests/behat/set_default_homepage.feature
version.php
webservice/rest/classes/privacy/provider.php [new file with mode: 0644]
webservice/rest/lang/en/webservice_rest.php
webservice/soap/classes/privacy/provider.php [new file with mode: 0644]
webservice/soap/lang/en/webservice_soap.php
webservice/xmlrpc/classes/privacy/provider.php [new file with mode: 0644]
webservice/xmlrpc/lang/en/webservice_xmlrpc.php

index 4cbb8e3..296343b 100644 (file)
@@ -3,6 +3,7 @@
 */**/build/
 node_modules/
 vendor/
+admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
 enrol/lti/ims-blti/
@@ -56,6 +57,7 @@ lib/maxmind/MaxMind/
 lib/ltiprovider/
 lib/amd/src/truncate.js
 lib/fonts/
+lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 media/player/videojs/amd/src/video-lazy.js
index 140b4aa..bdd33f8 100644 (file)
@@ -4,6 +4,7 @@ theme/clean/style/custom.css
 theme/more/style/custom.css
 node_modules/
 vendor/
+admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
 enrol/lti/ims-blti/
@@ -57,6 +58,7 @@ lib/maxmind/MaxMind/
 lib/ltiprovider/
 lib/amd/src/truncate.js
 lib/fonts/
+lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 media/player/videojs/amd/src/video-lazy.js
index 1ff43fe..1e5837f 100644 (file)
@@ -442,6 +442,7 @@ if ($interactive) {
     }
 }
 $CFG->tempdir       = $CFG->dataroot.'/temp';
+$CFG->backuptempdir = $CFG->tempdir.'/backup';
 $CFG->cachedir      = $CFG->dataroot.'/cache';
 $CFG->localcachedir = $CFG->dataroot.'/localcache';
 
index 6c10b9a..5915e24 100644 (file)
@@ -131,10 +131,11 @@ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
     cli_error(get_string('pluginschecktodo', 'admin'));
 }
 
+$a = new stdClass();
+$a->oldversion = $oldversion;
+$a->newversion = $newversion;
+
 if ($interactive) {
-    $a = new stdClass();
-    $a->oldversion = $oldversion;
-    $a->newversion = $newversion;
     echo cli_heading(get_string('databasechecking', '', $a)) . PHP_EOL;
 }
 
@@ -193,5 +194,5 @@ admin_apply_default_settings(NULL, false);
 // to immediately start browsing the site.
 upgrade_themes();
 
-echo get_string('cliupgradefinished', 'admin')."\n";
+echo get_string('cliupgradefinished', 'admin', $a)."\n";
 exit(0); // 0 means success
index e99f7c5..9f5c317 100644 (file)
@@ -39,7 +39,7 @@
 </h2>
 <div>{{{framework.description}}}</div>
     <h3>{{#str}}competencies, core_competency{{/str}}</h3>
-    <div class="row-fluid">
+    <div class="row-fluid row">
         <div class="span6 col-lg-6">
             <p>
                 <form data-region="filtercompetencies" data-frameworkid="{{framework.id}}" class="form-inline">
@@ -56,7 +56,7 @@
         </div>
 
         <div class="span6 card col-lg-6">
-            <div class="card-block">
+            <div class="card-block card-body">
                 <div class="card-title">
                     <h4 data-region="selected-competency">{{#str}}selectedcompetency, tool_lp{{/str}}</h4>
                         <span data-region="competencyactionsmenu" class="pull-xs-right">
diff --git a/admin/tool/monitor/classes/privacy/provider.php b/admin/tool/monitor/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..9b481d3
--- /dev/null
@@ -0,0 +1,211 @@
+<?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/>.
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    tool_monitor
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_monitor\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\writer;
+use \tool_monitor\subscription_manager;
+use \tool_monitor\rule_manager;
+
+/**
+ * Privacy provider for tool_monitor
+ *
+ * @package    tool_monitor
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Get information about the user data stored by this plugin.
+     *
+     * @param  collection $collection An object for storing metadata.
+     * @return collection The metadata.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $toolmonitorrules = [
+            'description' => 'privacy:metadata:description',
+            'name' => 'privacy:metadata:name',
+            'userid' => 'privacy:metadata:userid',
+            'plugin' => 'privacy:metadata:plugin',
+            'eventname' => 'privacy:metadata:eventname',
+            'template' => 'privacy:metadata:template',
+            'frequency' => 'privacy:metadata:frequency',
+            'timewindow' => 'privacy:metadata:timewindow',
+            'timemodified' => 'privacy:metadata:timemodifiedrule',
+            'timecreated' => 'privacy:metadata:timecreatedrule'
+        ];
+        $toolmonitorsubscriptions = [
+            'userid' => 'privacy:metadata:useridsub',
+            'timecreated' => 'privacy:metadata:timecreatedsub',
+            'lastnotificationsent' => 'privacy:metadata:lastnotificationsent',
+            'inactivedate' => 'privacy:metadata:inactivedate'
+        ];
+        // Tool monitor history doesn't look like it is used at all.
+        $toolmonitorhistory = [
+            'userid' => 'privacy:metadata:useridhistory',
+            'timesent' => 'privacy:metadata:timesent'
+        ];
+        $collection->add_database_table('tool_monitor_rules', $toolmonitorrules, 'privacy:metadata:rulessummary');
+        $collection->add_database_table('tool_monitor_subscriptions', $toolmonitorsubscriptions,
+                'privacy:metadata:subscriptionssummary');
+        $collection->add_database_table('tool_monitor_history', $toolmonitorhistory, 'privacy:metadata:historysummary');
+        $collection->link_subsystem('core_message', 'privacy:metadata:messagesummary');
+        return $collection;
+    }
+
+    /**
+     * Return all contexts for this userid. In this situation the user context.
+     *
+     * @param  int $userid The user ID.
+     * @return contextlist The list of context IDs.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $params = ['useridrules' => $userid, 'useridsubscriptions' => $userid, 'contextuserrule' => CONTEXT_USER,
+                'contextusersub' => CONTEXT_USER];
+        $sql = "SELECT DISTINCT ctx.id
+                  FROM {context} ctx
+             LEFT JOIN {tool_monitor_rules} mr ON ctx.instanceid = mr.userid AND ctx.contextlevel = :contextuserrule
+             LEFT JOIN {tool_monitor_subscriptions} ms ON ctx.instanceid = ms.userid AND ctx.contextlevel = :contextusersub
+                 WHERE (ms.userid = :useridrules OR mr.userid = :useridsubscriptions)";
+
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+        return $contextlist;
+    }
+
+    /**
+     * Export all event monitor information for the list of contexts and this user.
+     *
+     * @param  approved_contextlist $contextlist The list of approved contexts for a user.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+        // Export rules.
+        $context = \context_user::instance($contextlist->get_user()->id);
+        $rules = $DB->get_records('tool_monitor_rules', ['userid' => $contextlist->get_user()->id]);
+        if ($rules) {
+            static::export_monitor_rules($rules, $context);
+        }
+        // Export subscriptions.
+        $subscriptions = subscription_manager::get_user_subscriptions(0, 0, $contextlist->get_user()->id);
+        if ($subscriptions) {
+            static::export_monitor_subscriptions($subscriptions, $context);
+        }
+    }
+
+    /**
+     * Delete all user data for this context.
+     *
+     * @param  \context $context The context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        // Only delete data for user contexts.
+        if ($context->contextlevel == CONTEXT_USER) {
+            static::delete_user_data($context->instanceid);
+        }
+    }
+
+    /**
+     * Delete all user data for this user only.
+     *
+     * @param  approved_contextlist $contextlist The list of approved contexts for a user.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        static::delete_user_data($contextlist->get_user()->id);
+    }
+
+    /**
+     * This does the deletion of user data for the event monitor.
+     *
+     * @param  int $userid The user ID
+     */
+    protected static function delete_user_data(int $userid) {
+        global $DB;
+        // Delete this user's subscriptions first.
+        subscription_manager::delete_user_subscriptions($userid);
+        // Because we only use user contexts the instance ID is the user ID.
+        // Get the rules and check if this user has the capability to delete them.
+        $rules = $DB->get_records('tool_monitor_rules', ['userid' => $userid]);
+        foreach ($rules as $ruledata) {
+            $rule = rule_manager::get_rule($ruledata);
+            // If no-one is suscribed to the rule then it is safe to delete.
+            if ($rule->can_manage_rule($userid) && subscription_manager::count_rule_subscriptions($rule->id) == 0) {
+                $rule->delete_rule();
+            }
+        }
+    }
+
+    /**
+     * This formats and then exports the monitor rules.
+     *
+     * @param  array $rules The monitor rules.
+     * @param  context_user $context The user context
+     */
+    protected static function export_monitor_rules(array $rules, \context_user $context) {
+        foreach ($rules as $rule) {
+            $rule = rule_manager::get_rule($rule);
+            $ruledata = new \stdClass();
+            $ruledata->name = $rule->name;
+            $ruledata->eventname = $rule->get_event_name();
+            $ruledata->description = $rule->get_description($context);
+            $ruledata->plugin = $rule->get_plugin_name();
+            $ruledata->template = $rule->template;
+            $ruledata->frequency = $rule->get_filters_description();
+            $ruledata->course = $rule->get_course_name($context);
+            $ruledata->timecreated = transform::datetime($rule->timecreated);
+            $ruledata->timemodified = transform::datetime($rule->timemodified);
+            writer::with_context($context)->export_data([get_string('privacy:createdrules', 'tool_monitor'),
+                    $rule->name . '_' . $rule->id], $ruledata);
+        }
+    }
+
+    /**
+     * This formats and then exports the event monitor subscriptions.
+     *
+     * @param  array $subscriptions Subscriptions
+     * @param  \context_user $context The user context
+     */
+    protected static function export_monitor_subscriptions(array $subscriptions, \context_user $context) {
+        foreach ($subscriptions as $subscription) {
+            $subscriptiondata = new \stdClass();
+            $subscriptiondata->instancename = $subscription->get_instance_name();
+            $subscriptiondata->eventname = $subscription->get_event_name();
+            $subscriptiondata->frequency = $subscription->get_filters_description();
+            $subscriptiondata->name = $subscription->get_name($context);
+            $subscriptiondata->description = $subscription->get_description($context);
+            $subscriptiondata->pluginname = $subscription->get_plugin_name();
+            $subscriptiondata->course = $subscription->get_course_name($context);
+            $subscriptiondata->timecreated = transform::datetime($subscription->timecreated);
+            $subscriptiondata->lastnotificationsent = transform::datetime($subscription->lastnotificationsent);
+            writer::with_context($context)->export_data([get_string('privacy:subscriptions', 'tool_monitor'),
+                    $subscriptiondata->name . '_' . $subscription->id, $subscriptiondata->course, $subscriptiondata->instancename],
+                    $subscriptiondata);
+        }
+    }
+}
index 3c5e6d2..4cc3920 100644 (file)
@@ -51,14 +51,15 @@ class rule {
     }
 
     /**
-     * Can the current user manage this rule?
+     * Can the user manage this rule? Defaults to $USER.
      *
+     * @param int $userid Check against this userid.
      * @return bool true if the current user can manage this rule, else false.
      */
-    public function can_manage_rule() {
+    public function can_manage_rule($userid = null) {
         $courseid = $this->courseid;
         $context = empty($courseid) ? \context_system::instance() : \context_course::instance($this->courseid);
-        return has_capability('tool/monitor:managerules', $context);
+        return has_capability('tool/monitor:managerules', $context, $userid);
     }
 
     /**
index ed41095..81d20bb 100644 (file)
@@ -79,6 +79,28 @@ $string['monitor:managetool'] = 'Enable/disable event monitoring';
 $string['monitor:subscribe'] = 'Subscribe to event monitor rules';
 $string['norules'] = 'There are no event monitoring rules.';
 $string['pluginname'] = 'Event monitor';
+$string['privacy:createdrules'] = 'Event monitor rules I created';
+$string['privacy:metadata:description'] = 'Description of the rule';
+$string['privacy:metadata:eventname'] = 'Fully qualified name of the event';
+$string['privacy:metadata:frequency'] = 'Frequency of notifications';
+$string['privacy:metadata:historysummary'] = 'Stores the history of the message notifications sent';
+$string['privacy:metadata:inactivedate'] = 'Period of time, in days, after which an inactive subscription will be removed completely';
+$string['privacy:metadata:lastnotificationsent'] = 'When a notification was last sent for this subscription.';
+$string['privacy:metadata:messagesummary'] = 'Notifications are sent to the message system.';
+$string['privacy:metadata:name'] = 'Name of the rule';
+$string['privacy:metadata:plugin'] = 'Frankenstlye name of the plugin';
+$string['privacy:metadata:rulessummary'] = 'This stores monitor rules.';
+$string['privacy:metadata:subscriptionssummary'] = 'Stores user subscriptions to various rules';
+$string['privacy:metadata:template'] = 'Message template';
+$string['privacy:metadata:timecreatedrule'] = 'When this rule was created';
+$string['privacy:metadata:timecreatedsub'] = 'When this subscription was created';
+$string['privacy:metadata:timemodifiedrule'] = 'When this rule was last modified';
+$string['privacy:metadata:timesent'] = 'When the message was sent';
+$string['privacy:metadata:timewindow'] = 'Time window in seconds';
+$string['privacy:metadata:userid'] = 'Id of user who created the rule';
+$string['privacy:metadata:useridhistory'] = 'User to whom this notification was sent';
+$string['privacy:metadata:useridsub'] = 'User id of the subscriber';
+$string['privacy:subscriptions'] = 'My event monitor subscriptions';
 $string['processevents'] = 'Process events';
 $string['rulename'] = 'Rule name';
 $string['ruleareyousure'] = 'Are you sure you want to delete the rule "{$a}"?';
diff --git a/admin/tool/monitor/tests/privacy_test.php b/admin/tool/monitor/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..5c5f11c
--- /dev/null
@@ -0,0 +1,289 @@
+<?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/>.
+
+/**
+ * Privacy test for the event monitor
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2018 Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \tool_monitor\privacy\provider;
+use \core_privacy\local\request\approved_contextlist;
+
+/**
+ * Privacy test for the event monitor
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2018 Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_privacy_testcase extends advanced_testcase {
+
+    /**
+     * Set up method.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+        // Enable monitor.
+        set_config('enablemonitor', 1, 'tool_monitor');
+    }
+
+    /**
+     * Assign a capability to $USER
+     * The function creates a student $USER if $USER->id is empty
+     *
+     * @param string $capability capability name
+     * @param int $contextid
+     * @param int $roleid
+     * @return int the role id - mainly returned for creation, so calling function can reuse it
+     */
+    public static function assign_user_capability($capability, $contextid, $roleid = null) {
+        global $USER;
+
+        // Create a new student $USER if $USER doesn't exist.
+        if (empty($USER->id)) {
+            $user  = self::getDataGenerator()->create_user();
+            self::setUser($user);
+        }
+
+        if (empty($roleid)) {
+            $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description');
+        }
+
+        assign_capability($capability, CAP_ALLOW, $roleid, $contextid);
+
+        role_assign($roleid, $USER->id, $contextid);
+
+        accesslib_clear_all_caches_for_unit_testing();
+
+        return $roleid;
+    }
+
+    /**
+     * Test that a collection with data is returned when calling this function.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('tool_monitor');
+        $collection = provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Check that a user context is returned if there is any user data for this user.
+     */
+    public function test_get_contexts_for_userid() {
+        $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext = \context_user::instance($user->id);
+        $usercontext2 = \context_user::instance($user2->id);
+        $this->assertEmpty(provider::get_contexts_for_userid($user->id));
+        $this->assertEmpty(provider::get_contexts_for_userid($user2->id));
+
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        // Create a rule with this user.
+        $this->setUser($user);
+        $rule = $monitorgenerator->create_rule();
+        $contextlist = provider::get_contexts_for_userid($user->id);
+
+        // Check that we only get back one context.
+        $this->assertCount(1, $contextlist);
+
+        // Check that a context is returned for just creating a rule.
+        $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]);
+
+        $this->setUser($user2);
+
+        $record = new stdClass();
+        $record->courseid = 0;
+        $record->userid = $user2->id;
+        $record->ruleid = $rule->id;
+
+        $subscription = $monitorgenerator->create_subscription($record);
+        $contextlist = provider::get_contexts_for_userid($user2->id);
+
+        // Check that we only get back one context.
+        $this->assertCount(1, $contextlist);
+
+        // Check that a context is returned for just subscribing to a rule.
+        $this->assertEquals($usercontext2->id, $contextlist->get_contextids()[0]);
+    }
+
+    /**
+     * Test that user data is exported correctly.
+     */
+    public function test_export_user_data() {
+        $user = $this->getDataGenerator()->create_user();
+        $usercontext = \context_user::instance($user->id);
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $this->setUser($user);
+        $rulerecord = (object)['name' => 'privacy rule'];
+        $rule = $monitorgenerator->create_rule($rulerecord);
+
+        $secondrulerecord = (object)['name' => 'privacy rule2'];
+        $rule2 = $monitorgenerator->create_rule($secondrulerecord);
+
+        $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id];
+        $subscription = $monitorgenerator->create_subscription($subscription);
+
+        $writer = \core_privacy\local\request\writer::with_context($usercontext);
+        $this->assertFalse($writer->has_any_data());
+
+        $approvedlist = new approved_contextlist($user, 'tool_monitor', [$usercontext->id]);
+        provider::export_user_data($approvedlist);
+
+        // Check that the rules created by this user are exported.
+        $this->assertEquals($rulerecord->name, $writer->get_data([get_string('privacy:createdrules', 'tool_monitor'),
+                $rulerecord->name . '_' . $rule->id])->name);
+        $this->assertEquals($secondrulerecord->name, $writer->get_data([get_string('privacy:createdrules', 'tool_monitor'),
+                $secondrulerecord->name . '_' . $rule2->id])->name);
+
+        // Check that the subscriptions for this user are also exported.
+        $this->assertEquals($rulerecord->name, $writer->get_data([get_string('privacy:subscriptions', 'tool_monitor'),
+                $rulerecord->name . '_' . $subscription->id, 'Site' , 'All events'])->name);
+    }
+
+    /**
+     * Test deleting all user data for a specific context.
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext = \context_user::instance($user->id);
+        $usercontext2 = \context_user::instance($user2->id);
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $this->setUser($user);
+        // Need to give user one the ability to manage rules.
+        $this->assign_user_capability('tool/monitor:managerules', \context_system::instance());
+
+        $rulerecord = (object)['name' => 'privacy rule'];
+        $rule = $monitorgenerator->create_rule($rulerecord);
+
+        $secondrulerecord = (object)['name' => 'privacy rule2'];
+        $rule2 = $monitorgenerator->create_rule($secondrulerecord);
+
+        $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id];
+        $subscription = $monitorgenerator->create_subscription($subscription);
+
+        // Have user 2 subscribe to the second rule created by user 1.
+        $subscription2 = (object)['ruleid' => $rule2->id, 'userid' => $user2->id];
+        $subscription2 = $monitorgenerator->create_subscription($subscription2);
+
+        $this->setUser($user2);
+        $thirdrulerecord = (object)['name' => 'privacy rule for second user'];
+        $rule3 = $monitorgenerator->create_rule($thirdrulerecord);
+
+        $subscription3 = (object)['ruleid' => $rule3->id, 'userid' => $user2->id];
+        $subscription3 = $monitorgenerator->create_subscription($subscription3);
+
+        // Try a different context first.
+        provider::delete_data_for_all_users_in_context(context_system::instance());
+
+        // Get all of the monitor rules.
+        $dbrules = $DB->get_records('tool_monitor_rules');
+
+        // All of the rules should still be present.
+        $this->assertCount(3, $dbrules);
+        $this->assertEquals($user->id, $dbrules[$rule->id]->userid);
+        $this->assertEquals($user->id, $dbrules[$rule2->id]->userid);
+        $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid);
+
+        // Delete everything for the first user context.
+        provider::delete_data_for_all_users_in_context($usercontext);
+
+        // Get all of the monitor rules.
+        $dbrules = $DB->get_records('tool_monitor_rules');
+
+        // Only the rules for user 1 that does not have any more subscriptions should be deleted (the first rule).
+        $this->assertCount(2, $dbrules);
+        $this->assertEquals($user->id, $dbrules[$rule2->id]->userid);
+        $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid);
+
+        // Get all of the monitor subscriptions.
+        $dbsubs = $DB->get_records('tool_monitor_subscriptions');
+        // There should be two subscriptions left, both for user 2.
+        $this->assertCount(2, $dbsubs);
+        $this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid);
+        $this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid);
+    }
+
+    /**
+     * This should work identical to the above test.
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $usercontext = \context_user::instance($user->id);
+        $usercontext2 = \context_user::instance($user2->id);
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+        $this->setUser($user);
+        // Need to give user one the ability to manage rules.
+        $this->assign_user_capability('tool/monitor:managerules', \context_system::instance());
+
+        $rulerecord = (object)['name' => 'privacy rule'];
+        $rule = $monitorgenerator->create_rule($rulerecord);
+
+        $secondrulerecord = (object)['name' => 'privacy rule2'];
+        $rule2 = $monitorgenerator->create_rule($secondrulerecord);
+
+        $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id];
+        $subscription = $monitorgenerator->create_subscription($subscription);
+
+        // Have user 2 subscribe to the second rule created by user 1.
+        $subscription2 = (object)['ruleid' => $rule2->id, 'userid' => $user2->id];
+        $subscription2 = $monitorgenerator->create_subscription($subscription2);
+
+        $this->setUser($user2);
+        $thirdrulerecord = (object)['name' => 'privacy rule for second user'];
+        $rule3 = $monitorgenerator->create_rule($thirdrulerecord);
+
+        $subscription3 = (object)['ruleid' => $rule3->id, 'userid' => $user2->id];
+        $subscription3 = $monitorgenerator->create_subscription($subscription3);
+
+        $approvedlist = new approved_contextlist($user, 'tool_monitor', [$usercontext->id]);
+
+        // Delete everything for the first user.
+        provider::delete_data_for_user($approvedlist);
+
+        // Get all of the monitor rules.
+        $dbrules = $DB->get_records('tool_monitor_rules');
+
+        // Only the rules for user 1 that does not have any more subscriptions should be deleted (the first rule).
+        $this->assertCount(2, $dbrules);
+        $this->assertEquals($user->id, $dbrules[$rule2->id]->userid);
+        $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid);
+
+        // Get all of the monitor subscriptions.
+        $dbsubs = $DB->get_records('tool_monitor_subscriptions');
+        // There should be two subscriptions left, both for user 2.
+        $this->assertCount(2, $dbsubs);
+        $this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid);
+        $this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid);
+    }
+}
diff --git a/admin/tool/policy/accept.php b/admin/tool/policy/accept.php
new file mode 100644 (file)
index 0000000..8a79198
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Accept policies on behalf of users (non-JS version)
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__.'/../../../config.php');
+require_once($CFG->dirroot.'/user/editlib.php');
+
+$userids = optional_param_array('userids', null, PARAM_INT);
+$versionids = optional_param_array('versionids', null, PARAM_INT);
+$returnurl = optional_param('returnurl', null, PARAM_LOCALURL);
+
+require_login();
+if (isguestuser()) {
+    print_error('noguest');
+}
+$context = context_system::instance();
+
+$PAGE->set_context($context);
+$PAGE->set_url(new moodle_url('/admin/tool/policy/accept.php'));
+
+if ($returnurl) {
+    $returnurl = new moodle_url($returnurl);
+} else if (count($userids) == 1) {
+    $userid = reset($userids);
+    $returnurl = new moodle_url('/admin/tool/policy/user.php', ['userid' => $userid]);
+} else {
+    $returnurl = new moodle_url('/admin/tool/policy/acceptances.php');
+}
+// Initialise the form, this will also validate users, versions and check permission to accept policies.
+$form = new \tool_policy\form\accept_policy(null,
+    ['versionids' => $versionids, 'userids' => $userids, 'showbuttons' => true]);
+$form->set_data(['returnurl' => $returnurl]);
+
+if ($form->is_cancelled()) {
+    redirect($returnurl);
+} else if ($form->get_data()) {
+    $form->process();
+    redirect($returnurl);
+}
+
+$output = $PAGE->get_renderer('tool_policy');
+echo $output->header();
+echo $output->heading(get_string('consentdetails', 'tool_policy'));
+$form->display();
+echo $output->footer();
diff --git a/admin/tool/policy/acceptances.php b/admin/tool/policy/acceptances.php
new file mode 100644 (file)
index 0000000..dbf0dc9
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * View user acceptances to the policies
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__.'/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+use core\output\notification;
+
+$policyid = optional_param('policyid', null, PARAM_INT);
+$versionid = optional_param('versionid', null, PARAM_INT);
+$versionid = optional_param('versionid', null, PARAM_INT);
+$filtersapplied = optional_param_array('unified-filters', [], PARAM_NOTAGS);
+
+$acceptancesfilter = new \tool_policy\output\acceptances_filter($policyid, $versionid, $filtersapplied);
+$policyid = $acceptancesfilter->get_policy_id_filter();
+$versionid = $acceptancesfilter->get_version_id_filter();
+
+// Set up the page as an admin page 'tool_policy_managedocs'.
+$urlparams = ($policyid ? ['policyid' => $policyid] : []) + ($versionid ? ['versionid' => $versionid] : []);
+admin_externalpage_setup('tool_policy_acceptances', '', $urlparams,
+    new moodle_url('/admin/tool/policy/acceptances.php'));
+
+$acceptancesfilter->validate_ids();
+$output = $PAGE->get_renderer('tool_policy');
+if ($acceptancesfilter->get_versions()) {
+    $acceptances = new \tool_policy\acceptances_table('tool_policy_user_acceptances', $acceptancesfilter, $output);
+    if ($acceptances->is_downloading()) {
+        $acceptances->download();
+    }
+}
+
+echo $output->header();
+echo $output->heading(get_string('useracceptances', 'tool_policy'));
+echo $output->render($acceptancesfilter);
+if (!empty($acceptances)) {
+    $acceptances->display();
+} else if ($acceptancesfilter->get_avaliable_policies()) {
+    // There are no non-guest policies.
+    echo $output->notification(get_string('selectpolicyandversion', 'tool_policy'), notification::NOTIFY_INFO);
+} else {
+    // There are no non-guest policies.
+    echo $output->notification(get_string('nopolicies', 'tool_policy'), notification::NOTIFY_INFO);
+}
+echo $output->footer();
diff --git a/admin/tool/policy/amd/build/acceptances_filter.min.js b/admin/tool/policy/amd/build/acceptances_filter.min.js
new file mode 100644 (file)
index 0000000..793e5af
Binary files /dev/null and b/admin/tool/policy/amd/build/acceptances_filter.min.js differ
diff --git a/admin/tool/policy/amd/build/acceptances_filter_datasource.min.js b/admin/tool/policy/amd/build/acceptances_filter_datasource.min.js
new file mode 100644 (file)
index 0000000..567ed4a
Binary files /dev/null and b/admin/tool/policy/amd/build/acceptances_filter_datasource.min.js differ
diff --git a/admin/tool/policy/amd/build/acceptmodal.min.js b/admin/tool/policy/amd/build/acceptmodal.min.js
new file mode 100644 (file)
index 0000000..8c9aab2
Binary files /dev/null and b/admin/tool/policy/amd/build/acceptmodal.min.js differ
diff --git a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js
new file mode 100644 (file)
index 0000000..8ea0109
Binary files /dev/null and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js differ
diff --git a/admin/tool/policy/amd/build/managedocsactions.min.js b/admin/tool/policy/amd/build/managedocsactions.min.js
new file mode 100644 (file)
index 0000000..5c351dc
Binary files /dev/null and b/admin/tool/policy/amd/build/managedocsactions.min.js differ
diff --git a/admin/tool/policy/amd/build/policyactions.min.js b/admin/tool/policy/amd/build/policyactions.min.js
new file mode 100644 (file)
index 0000000..9a87001
Binary files /dev/null and b/admin/tool/policy/amd/build/policyactions.min.js differ
diff --git a/admin/tool/policy/amd/src/acceptances_filter.js b/admin/tool/policy/amd/src/acceptances_filter.js
new file mode 100644 (file)
index 0000000..b02c64b
--- /dev/null
@@ -0,0 +1,144 @@
+// 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/>.
+
+/**
+ * Unified filter page JS module for the course participants page.
+ *
+ * @module     tool_policy/acceptances_filter
+ * @package    tool_policy
+ * @copyright  2017 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],
+    function($, Autocomplete, Str, Notification) {
+
+        /**
+         * Selectors.
+         *
+         * @access private
+         * @type {{UNIFIED_FILTERS: string}}
+         */
+        var SELECTORS = {
+            UNIFIED_FILTERS: '#unified-filters'
+        };
+
+        /**
+         * Init function.
+         *
+         * @method init
+         * @private
+         */
+        var init = function() {
+            var stringkeys = [{
+                key: 'filterplaceholder',
+                component: 'tool_policy'
+            }, {
+                key: 'nofiltersapplied',
+                component: 'tool_policy'
+            }];
+
+            M.util.js_pending('acceptances_filter_datasource');
+            Str.get_strings(stringkeys).done(function(langstrings) {
+                var placeholder = langstrings[0];
+                var noSelectionString = langstrings[1];
+                Autocomplete.enhance(SELECTORS.UNIFIED_FILTERS, true, 'tool_policy/acceptances_filter_datasource', placeholder,
+                    false, true, noSelectionString, true)
+                    .then(function() {
+                        M.util.js_complete('acceptances_filter_datasource');
+
+                        return;
+                    })
+                    .fail(Notification.exception);
+            }).fail(Notification.exception);
+
+            var last = $(SELECTORS.UNIFIED_FILTERS).val();
+            $(SELECTORS.UNIFIED_FILTERS).on('change', function() {
+                var current = $(this).val();
+                var listoffilters = [];
+                var textfilters = [];
+                var updatedselectedfilters = false;
+
+                $.each(current, function(index, catoption) {
+                    var catandoption = catoption.split(':', 2);
+                    if (catandoption.length !== 2) {
+                        textfilters.push(catoption);
+                        return true; // Text search filter.
+                    }
+
+                    var category = catandoption[0];
+                    var option = catandoption[1];
+
+                    // The last option (eg. 'Teacher') out of a category (eg. 'Role') in this loop is the one that was last
+                    // selected, so we want to use that if there are multiple options from the same category. Eg. The user
+                    // may have chosen to filter by the 'Student' role, then wanted to filter by the 'Teacher' role - the
+                    // last option in the category to be selected (in this case 'Teacher') will come last, so will overwrite
+                    // 'Student' (after this if). We want to let the JS know that the filters have been updated.
+                    if (typeof listoffilters[category] !== 'undefined') {
+                        updatedselectedfilters = true;
+                    }
+
+                    listoffilters[category] = option;
+                    return true;
+                });
+
+                // Check if we have something to remove from the list of filters.
+                if (updatedselectedfilters) {
+                    // Go through and put the list into something we can use to update the list of filters.
+                    var updatefilters = [];
+                    for (var category in listoffilters) {
+                        updatefilters.push(category + ":" + listoffilters[category]);
+                    }
+                    updatefilters = updatefilters.concat(textfilters);
+                    $(this).val(updatefilters);
+                }
+
+                // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.
+                if (last.join(',') != current.join(',')) {
+                    this.form.submit();
+                }
+            });
+        };
+
+        /**
+         * Return the unified user filter form.
+         *
+         * @method getForm
+         * @return {DOMElement}
+         */
+        var getForm = function() {
+            return $(SELECTORS.UNIFIED_FILTERS).closest('form');
+        };
+
+        return /** @alias module:core/form-autocomplete */ {
+            /**
+             * Initialise the unified user filter.
+             *
+             * @method init
+             */
+            init: function() {
+                init();
+            },
+
+            /**
+             * Return the unified user filter form.
+             *
+             * @method getForm
+             * @return {DOMElement}
+             */
+            getForm: function() {
+                return getForm();
+            }
+        };
+    });
diff --git a/admin/tool/policy/amd/src/acceptances_filter_datasource.js b/admin/tool/policy/amd/src/acceptances_filter_datasource.js
new file mode 100644 (file)
index 0000000..7b1f98e
--- /dev/null
@@ -0,0 +1,93 @@
+// 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/>.
+
+/**
+ * Datasource for the tool_policy/acceptances_filter.
+ *
+ * This module is compatible with core/form-autocomplete.
+ *
+ * @package    tool_policy
+ * @copyright  2017 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
+
+    return /** @alias module:tool_policy/acceptances_filter_datasource */ {
+        /**
+         * List filter options.
+         *
+         * @param {String} selector The select element selector.
+         * @param {String} query The query string.
+         * @return {Promise}
+         */
+        list: function(selector, query) {
+            var filteredOptions = [];
+
+            var el = $(selector);
+            var originalOptions = $(selector).data('originaloptionsjson');
+            var selectedFilters = el.val();
+            $.each(originalOptions, function(index, option) {
+                // Skip option if it does not contain the query string.
+                if ($.trim(query) !== '' && option.label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) === -1) {
+                    return true;
+                }
+                // Skip filters that have already been selected.
+                if ($.inArray(option.value, selectedFilters) > -1) {
+                    return true;
+                }
+
+                filteredOptions.push(option);
+                return true;
+            });
+
+            var deferred = new $.Deferred();
+            deferred.resolve(filteredOptions);
+
+            return deferred.promise();
+        },
+
+        /**
+         * Process the results for auto complete elements.
+         *
+         * @param {String} selector The selector of the auto complete element.
+         * @param {Array} results An array or results.
+         * @return {Array} New array of results.
+         */
+        processResults: function(selector, results) {
+            var options = [];
+            $.each(results, function(index, data) {
+                options.push({
+                    value: data.value,
+                    label: data.label
+                });
+            });
+            return options;
+        },
+
+        /**
+         * Source of data for Ajax element.
+         *
+         * @param {String} selector The selector of the auto complete element.
+         * @param {String} query The query string.
+         * @param {Function} callback A callback function receiving an array of results.
+         */
+        /* eslint-disable promise/no-callback-in-promise */
+        transport: function(selector, query, callback) {
+            this.list(selector, query).then(callback).catch(Notification.exception);
+        }
+    };
+
+});
diff --git a/admin/tool/policy/amd/src/acceptmodal.js b/admin/tool/policy/amd/src/acceptmodal.js
new file mode 100644 (file)
index 0000000..07b9ed7
--- /dev/null
@@ -0,0 +1,243 @@
+// 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/>.
+
+/**
+ * Add policy consent modal to the page
+ *
+ * @module     tool_policy/acceptmodal
+ * @class      AcceptOnBehalf
+ * @package    tool_policy
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/notification', 'core/fragment',
+        'core/ajax', 'core/yui'],
+    function($, Str, ModalFactory, ModalEvents, Notification, Fragment, Ajax, Y) {
+
+        "use strict";
+
+        /**
+         * Constructor
+         *
+         * @param {int} contextid
+         *
+         * Each call to init gets it's own instance of this class.
+         */
+        var AcceptOnBehalf = function(contextid) {
+            this.contextid = contextid;
+            this.init();
+        };
+
+        /**
+         * @var {Modal} modal
+         * @private
+         */
+        AcceptOnBehalf.prototype.modal = null;
+
+        /**
+         * @var {int} contextid
+         * @private
+         */
+        AcceptOnBehalf.prototype.contextid = -1;
+
+        /**
+         * @var {Array} strings
+         * @private
+         */
+        AcceptOnBehalf.prototype.stringKeys = [
+            {
+                key: 'consentdetails',
+                component: 'tool_policy'
+            },
+            {
+                key: 'iagreetothepolicy',
+                component: 'tool_policy'
+            },
+            {
+                key: 'selectusersforconsent',
+                component: 'tool_policy'
+            },
+            {
+                key: 'ok'
+            }
+        ];
+
+        /**
+         * Initialise the class.
+         *
+         * @private
+         */
+        AcceptOnBehalf.prototype.init = function() {
+            // Initialise for links accepting policies for individual users.
+            var triggers = $('a[data-action=acceptmodal]');
+            triggers.on('click', function(e) {
+                e.preventDefault();
+                var href = $(e.currentTarget).attr('href'),
+                    formData = href.slice(href.indexOf('?') + 1);
+                this.showFormModal(formData);
+            }.bind(this));
+
+            // Initialise for multiple users acceptance form.
+            triggers = $('form[data-action=acceptmodal]');
+            triggers.on('submit', function(e) {
+                e.preventDefault();
+                if ($(e.currentTarget).find('input[type=checkbox][name="userids[]"]:checked').length) {
+                    var formData = $(e.currentTarget).serialize();
+                    this.showFormModal(formData, triggers);
+                } else {
+                    Str.get_strings(this.stringKeys).done(function(strings) {
+                        Notification.alert('', strings[2], strings[3]);
+                    });
+                }
+            }.bind(this));
+        };
+
+        /**
+         * Show modal with a form
+         *
+         * @param {String} formData
+         * @param {object} triggerElement The trigger HTML jQuery object
+         */
+        AcceptOnBehalf.prototype.showFormModal = function(formData, triggerElement) {
+            // Fetch the title string.
+            Str.get_strings(this.stringKeys).done(function(strings) {
+                // Create the modal.
+                ModalFactory.create({
+                    type: ModalFactory.types.SAVE_CANCEL,
+                    title: strings[0],
+                    body: ''
+                }, triggerElement).done(function(modal) {
+                    this.modal = modal;
+                    this.setupFormModal(formData, strings[1]);
+                }.bind(this));
+            }.bind(this))
+                .fail(Notification.exception);
+        };
+
+        /**
+         * Setup form inside a modal
+         *
+         * @param {String} formData
+         * @param {String} saveText
+         */
+        AcceptOnBehalf.prototype.setupFormModal = function(formData, saveText) {
+            var modal = this.modal;
+
+            modal.setLarge();
+
+            modal.setSaveButtonText(saveText);
+
+            // We want to reset the form every time it is opened.
+            modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this));
+
+            modal.setBody(this.getBody(formData));
+
+            // We catch the modal save event, and use it to submit the form inside the modal.
+            // Triggering a form submission will give JS validation scripts a chance to check for errors.
+            modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));
+            // We also catch the form submit event and use it to submit the form with ajax.
+            modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));
+
+            modal.show();
+        };
+
+        /**
+         * Load the body of the modal (contains the form)
+         *
+         * @method getBody
+         * @private
+         * @param {String} formData
+         * @return {Promise}
+         */
+        AcceptOnBehalf.prototype.getBody = function(formData) {
+            if (typeof formData === "undefined") {
+                formData = {};
+            }
+            // Get the content of the modal.
+            var params = {jsonformdata: JSON.stringify(formData)};
+            return Fragment.loadFragment('tool_policy', 'accept_on_behalf', this.contextid, params);
+        };
+
+        /**
+         * Submit the form inside the modal via AJAX request
+         *
+         * @method submitFormAjax
+         * @private
+         * @param {Event} e Form submission event.
+         */
+        AcceptOnBehalf.prototype.submitFormAjax = function(e) {
+            // We don't want to do a real form submission.
+            e.preventDefault();
+
+            // Convert all the form elements values to a serialised string.
+            var formData = this.modal.getRoot().find('form').serialize();
+
+            var requests = Ajax.call([{
+                methodname: 'tool_policy_submit_accept_on_behalf',
+                args: {jsonformdata: JSON.stringify(formData)}
+            }]);
+            requests[0].done(function(data) {
+                if (data.validationerrors) {
+                    this.modal.setBody(this.getBody(formData));
+                } else {
+                    this.close();
+                }
+            }.bind(this)).fail(Notification.exception);
+        };
+
+        /**
+         * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.
+         *
+         * @method submitForm
+         * @param {Event} e Form submission event.
+         * @private
+         */
+        AcceptOnBehalf.prototype.submitForm = function(e) {
+            e.preventDefault();
+            this.modal.getRoot().find('form').submit();
+        };
+
+        /**
+         * Close the modal
+         */
+        AcceptOnBehalf.prototype.close = function() {
+            this.destroy();
+            document.location.reload();
+        };
+
+        /**
+         * Destroy the modal
+         */
+        AcceptOnBehalf.prototype.destroy = function() {
+            Y.use('moodle-core-formchangechecker', function() {
+                M.core_formchangechecker.reset_form_dirty_state();
+            });
+            this.modal.destroy();
+        };
+
+        return /** @alias module:tool_policy/acceptmodal */ {
+            // Public variables and functions.
+            /**
+             * Attach event listeners to initialise this module.
+             *
+             * @method init
+             * @param {int} contextid The contextid for the course.
+             * @return {AcceptOnBehalf}
+             */
+            getInstance: function(contextid) {
+                return new AcceptOnBehalf(contextid);
+            }
+        };
+    });
diff --git a/admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js b/admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
new file mode 100644 (file)
index 0000000..22688c5
--- /dev/null
@@ -0,0 +1,254 @@
+/**\r
+ *     \r
+ * JQUERY EU COOKIE LAW POPUPS\r
+ * version 1.0.1\r
+ * \r
+ * Code on Github:\r
+ * https://github.com/wimagguc/jquery-eu-cookie-law-popup\r
+ * \r
+ * To see a live demo, go to:\r
+ * http://www.wimagguc.com/2015/03/jquery-eu-cookie-law-popup/\r
+ * \r
+ * by Richard Dancsi\r
+ * http://www.wimagguc.com/\r
+ * \r
+ */\r
+\r
+define(\r
+['jquery'],\r
+function($) {\r
+\r
+// for ie9 doesn't support debug console >>>\r
+if (!window.console) window.console = {};\r
+if (!window.console.log) window.console.log = function () { };\r
+// ^^^\r
+\r
+$.fn.euCookieLawPopup = (function() {\r
+\r
+       var _self = this;\r
+\r
+       ///////////////////////////////////////////////////////////////////////////////////////////////\r
+       // PARAMETERS (MODIFY THIS PART) //////////////////////////////////////////////////////////////\r
+       _self.params = {\r
+               cookiePolicyUrl : 'http://www.wimagguc.com/?cookie-policy',\r
+               popupPosition : 'top',\r
+               colorStyle : 'default',\r
+               compactStyle : false,\r
+               popupTitle : 'This website is using cookies',\r
+               popupText : 'We use cookies to ensure that we give you the best experience on our website. If you continue without changing your settings, we\'ll assume that you are happy to receive all cookies on this website.',\r
+               buttonContinueTitle : 'Continue',\r
+               buttonLearnmoreTitle : 'Learn&nbsp;more',\r
+               buttonLearnmoreOpenInNewWindow : true,\r
+               agreementExpiresInDays : 30,\r
+               autoAcceptCookiePolicy : false,\r
+               htmlMarkup : null\r
+       };\r
+\r
+       ///////////////////////////////////////////////////////////////////////////////////////////////\r
+       // VARIABLES USED BY THE FUNCTION (DON'T MODIFY THIS PART) ////////////////////////////////////\r
+       _self.vars = {\r
+               INITIALISED : false,\r
+               HTML_MARKUP : null,\r
+               COOKIE_NAME : 'EU_COOKIE_LAW_CONSENT'\r
+       };\r
+\r
+       ///////////////////////////////////////////////////////////////////////////////////////////////\r
+       // PRIVATE FUNCTIONS FOR MANIPULATING DATA ////////////////////////////////////////////////////\r
+\r
+       // Overwrite default parameters if any of those is present\r
+       var parseParameters = function(object, markup, settings) {\r
+\r
+               if (object) {\r
+                       var className = $(object).attr('class') ? $(object).attr('class') : '';\r
+                       if (className.indexOf('eupopup-top') > -1) {\r
+                               _self.params.popupPosition = 'top';\r
+                       }\r
+                       else if (className.indexOf('eupopup-fixedtop') > -1) {\r
+                               _self.params.popupPosition = 'fixedtop';\r
+                       }\r
+                       else if (className.indexOf('eupopup-bottomright') > -1) {\r
+                               _self.params.popupPosition = 'bottomright';\r
+                       }\r
+                       else if (className.indexOf('eupopup-bottomleft') > -1) {\r
+                               _self.params.popupPosition = 'bottomleft';\r
+                       }\r
+                       else if (className.indexOf('eupopup-bottom') > -1) {\r
+                               _self.params.popupPosition = 'bottom';\r
+                       }\r
+                       else if (className.indexOf('eupopup-block') > -1) {\r
+                               _self.params.popupPosition = 'block';\r
+                       }\r
+                       if (className.indexOf('eupopup-color-default') > -1) {\r
+                               _self.params.colorStyle = 'default';\r
+                       }\r
+                       else if (className.indexOf('eupopup-color-inverse') > -1) {\r
+                               _self.params.colorStyle = 'inverse';\r
+                       }\r
+                       if (className.indexOf('eupopup-style-compact') > -1) {\r
+                               _self.params.compactStyle = true;\r
+                       }\r
+               }\r
+\r
+               if (markup) {\r
+                       _self.params.htmlMarkup = markup;\r
+               }\r
+\r
+               if (settings) {\r
+                       if (typeof settings.cookiePolicyUrl !== 'undefined') {\r
+                               _self.params.cookiePolicyUrl = settings.cookiePolicyUrl;\r
+                       }\r
+                       if (typeof settings.popupPosition !== 'undefined') {\r
+                               _self.params.popupPosition = settings.popupPosition;\r
+                       }\r
+                       if (typeof settings.colorStyle !== 'undefined') {\r
+                               _self.params.colorStyle = settings.colorStyle;\r
+                       }\r
+                       if (typeof settings.popupTitle !== 'undefined') {\r
+                               _self.params.popupTitle = settings.popupTitle;\r
+                       }\r
+                       if (typeof settings.popupText !== 'undefined') {\r
+                               _self.params.popupText = settings.popupText;\r
+                       }\r
+                       if (typeof settings.buttonContinueTitle !== 'undefined') {\r
+                               _self.params.buttonContinueTitle = settings.buttonContinueTitle;\r
+                       }\r
+                       if (typeof settings.buttonLearnmoreTitle !== 'undefined') {\r
+                               _self.params.buttonLearnmoreTitle = settings.buttonLearnmoreTitle;\r
+                       }\r
+                       if (typeof settings.buttonLearnmoreOpenInNewWindow !== 'undefined') {\r
+                               _self.params.buttonLearnmoreOpenInNewWindow = settings.buttonLearnmoreOpenInNewWindow;\r
+                       }\r
+                       if (typeof settings.agreementExpiresInDays !== 'undefined') {\r
+                               _self.params.agreementExpiresInDays = settings.agreementExpiresInDays;\r
+                       }\r
+                       if (typeof settings.autoAcceptCookiePolicy !== 'undefined') {\r
+                               _self.params.autoAcceptCookiePolicy = settings.autoAcceptCookiePolicy;\r
+                       }\r
+                       if (typeof settings.htmlMarkup !== 'undefined') {\r
+                               _self.params.htmlMarkup = settings.htmlMarkup;\r
+                       }\r
+               }\r
+\r
+       };\r
+\r
+       var createHtmlMarkup = function() {\r
+\r
+               if (_self.params.htmlMarkup) {\r
+                       return _self.params.htmlMarkup;\r
+               }\r
+\r
+               var html = \r
+                       '<div class="eupopup-container' + \r
+                           ' eupopup-container-' + _self.params.popupPosition + \r
+                           (_self.params.compactStyle ? ' eupopup-style-compact' : '') + \r
+                               ' eupopup-color-' + _self.params.colorStyle + '">' +\r
+                               '<div class="eupopup-head">' + _self.params.popupTitle + '</div>' +\r
+                               '<div class="eupopup-body">' + _self.params.popupText + '</div>' +\r
+                               '<div class="eupopup-buttons">' +\r
+                                 '<a href="#" class="eupopup-button eupopup-button_1">' + _self.params.buttonContinueTitle + '</a>' +\r
+                                 '<a href="' + _self.params.cookiePolicyUrl + '"' +\r
+                                       (_self.params.buttonLearnmoreOpenInNewWindow ? ' target=_blank ' : '') +\r
+                                       ' class="eupopup-button eupopup-button_2">' + _self.params.buttonLearnmoreTitle + '</a>' +\r
+                                 '<div class="clearfix"></div>' +\r
+                               '</div>' +\r
+                               '<a href="#" class="eupopup-closebutton">x</a>' +\r
+                       '</div>';\r
+\r
+               return html;\r
+       };\r
+\r
+       // Storing the consent in a cookie\r
+       var setUserAcceptsCookies = function(consent) {\r
+               var d = new Date();\r
+               var expiresInDays = _self.params.agreementExpiresInDays * 24 * 60 * 60 * 1000;\r
+               d.setTime( d.getTime() + expiresInDays );\r
+               var expires = "expires=" + d.toGMTString();\r
+               document.cookie = _self.vars.COOKIE_NAME + '=' + consent + "; " + expires + ";path=/";\r
+\r
+               $(document).trigger("user_cookie_consent_changed", {'consent' : consent});\r
+       };\r
+\r
+       // Let's see if we have a consent cookie already\r
+       var userAlreadyAcceptedCookies = function() {\r
+               var userAcceptedCookies = false;\r
+               var cookies = document.cookie.split(";");\r
+               for (var i = 0; i < cookies.length; i++) {\r
+                       var c = cookies[i].trim();\r
+                       if (c.indexOf(_self.vars.COOKIE_NAME) == 0) {\r
+                               userAcceptedCookies = c.substring(_self.vars.COOKIE_NAME.length + 1, c.length);\r
+                       }\r
+               }\r
+\r
+               return userAcceptedCookies;\r
+       };\r
+       \r
+       var hideContainer = function() {\r
+               // $('.eupopup-container').slideUp(200);\r
+               $('.eupopup-container').animate({\r
+                       opacity: 0,\r
+                       height: 0\r
+               }, 200, function() {\r
+                       $('.eupopup-container').hide(0);\r
+               });\r
+       };\r
+\r
+       ///////////////////////////////////////////////////////////////////////////////////////////////\r
+       // PUBLIC FUNCTIONS  //////////////////////////////////////////////////////////////////////////\r
+       var publicfunc = {\r
+\r
+               // INITIALIZE EU COOKIE LAW POPUP /////////////////////////////////////////////////////////\r
+               init : function(settings) {\r
+\r
+                       parseParameters(\r
+                               $(".eupopup").first(),\r
+                               $(".eupopup-markup").html(),\r
+                               settings);\r
+\r
+                       // No need to display this if user already accepted the policy\r
+                       if (userAlreadyAcceptedCookies()) {\r
+                               return;\r
+                       }\r
+\r
+                       // We should initialise only once\r
+                       if (_self.vars.INITIALISED) {\r
+                               return;\r
+                       }\r
+                       _self.vars.INITIALISED = true;\r
+\r
+                       // Markup and event listeners >>>\r
+                       _self.vars.HTML_MARKUP = createHtmlMarkup();\r
+\r
+                       if ($('.eupopup-block').length > 0) {\r
+                               $('.eupopup-block').append(_self.vars.HTML_MARKUP);\r
+                       } else {\r
+                               $('BODY').append(_self.vars.HTML_MARKUP);\r
+                       }\r
+\r
+                       $('.eupopup-button_1').click(function() {\r
+                               setUserAcceptsCookies(true);\r
+                               hideContainer();\r
+                               return false;\r
+                       });\r
+                       $('.eupopup-closebutton').click(function() {\r
+                               setUserAcceptsCookies(true);\r
+                               hideContainer();\r
+                               return false;\r
+                       });\r
+                       // ^^^ Markup and event listeners\r
+\r
+                       // Ready to start!\r
+                       $('.eupopup-container').show();\r
+\r
+                       // In case it's alright to just display the message once \r
+                       if (_self.params.autoAcceptCookiePolicy) {\r
+                               setUserAcceptsCookies(true);\r
+                       }\r
+\r
+               }\r
+\r
+       };\r
+\r
+       return publicfunc;\r
+});\r
+\r
+});\r
diff --git a/admin/tool/policy/amd/src/managedocsactions.js b/admin/tool/policy/amd/src/managedocsactions.js
new file mode 100644 (file)
index 0000000..972299d
--- /dev/null
@@ -0,0 +1,154 @@
+// 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/>.
+
+/**
+ * Adds support for confirmation via JS modal for some management actions at the Manage policies page.
+ *
+ * @module      tool_policy/managedocsactions
+ * @package     tool_policy
+ * @copyright   2018 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core/log',
+    'core/config',
+    'core/str',
+    'core/modal_factory',
+    'core/modal_events'
+], function($, Log, Config, Str, ModalFactory, ModalEvents) {
+
+    "use strict";
+
+    /**
+     * List of action selectors.
+     *
+     * @property {string} LINKS - Selector for all action links
+     * @property {string} MAKE_CURRENT
+     */
+    var ACTION = {
+        LINKS: '[data-action]',
+        MAKE_CURRENT: '[data-action="makecurrent"]',
+        INACTIVATE: '[data-action="inactivate"]',
+        DELETE: '[data-action="delete"]'
+    };
+
+    /**
+     * @constructor
+     * @param {Element} base - Management area wrapping element
+     */
+    function ManageDocsActions(base) {
+        this.base = base;
+
+        this.initEvents();
+    }
+
+    /**
+     * Register event listeners.
+     */
+    ManageDocsActions.prototype.initEvents = function() {
+        var self = this;
+
+        self.base.on('click', ACTION.LINKS, function(e) {
+            e.stopPropagation();
+
+            var link = $(e.currentTarget);
+            var promise;
+            var strings;
+
+            if (link.is(ACTION.MAKE_CURRENT)) {
+                promise = Str.get_strings([
+                    {key: 'activating', component: 'tool_policy'},
+                    {key: 'activateconfirm', component: 'tool_policy', param: {
+                        name: link.closest('[data-policy-name]').attr('data-policy-name'),
+                        revision: link.closest('[data-policy-revision]').attr('data-policy-revision')
+                    }},
+                    {key: 'activateconfirmyes', component: 'tool_policy'}
+                ]);
+
+            } else if (link.is(ACTION.INACTIVATE)) {
+                promise = Str.get_strings([
+                    {key: 'inactivating', component: 'tool_policy'},
+                    {key: 'inactivatingconfirm', component: 'tool_policy', param: {
+                        name: link.closest('[data-policy-name]').attr('data-policy-name'),
+                        revision: link.closest('[data-policy-revision]').attr('data-policy-revision')
+                    }},
+                    {key: 'inactivatingconfirmyes', component: 'tool_policy'}
+                ]);
+
+            } else if (link.is(ACTION.DELETE)) {
+                promise = Str.get_strings([
+                    {key: 'deleting', component: 'tool_policy'},
+                    {key: 'deleteconfirm', component: 'tool_policy', param: {
+                        name: link.closest('[data-policy-name]').attr('data-policy-name'),
+                        revision: link.closest('[data-policy-revision]').attr('data-policy-revision')
+                    }},
+                    {key: 'delete', component: 'core'}
+                ]);
+
+            } else {
+                Log.error('unknown action type detected', 'tool_policy/managedocsactions');
+                return;
+            }
+
+            e.preventDefault();
+
+            promise.then(function(strs) {
+                strings = strs;
+                return ModalFactory.create({
+                    title: strings[0],
+                    body: strings[1],
+                    type: ModalFactory.types.SAVE_CANCEL
+                });
+
+            }).then(function(modal) {
+                modal.setSaveButtonText(strings[2]);
+                modal.getRoot().on(ModalEvents.save, function() {
+                    window.location.href = link.attr('href') + '&sesskey=' + Config.sesskey + '&confirm=1';
+                });
+
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    modal.destroy();
+                });
+
+                modal.show();
+                return true;
+
+            }).catch(function(e) {
+                Log.error(e);
+                return false;
+            });
+        });
+    };
+
+    return {
+        /**
+         * Factory method returning instance of the ManageDocsActions
+         *
+         * @param {String} baseid - ID of the management area wrapping element
+         * @return {ManageDocsActions}
+         */
+        init: function(baseid) {
+            var base = $(document.getElementById(baseid));
+
+            if (base.length) {
+                return new ManageDocsActions(base);
+
+            } else {
+                throw new Error("managedocsactions: Invalid base element identifier");
+            }
+        }
+    };
+});
diff --git a/admin/tool/policy/amd/src/policyactions.js b/admin/tool/policy/amd/src/policyactions.js
new file mode 100644 (file)
index 0000000..ae70f48
--- /dev/null
@@ -0,0 +1,122 @@
+// 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/>.
+
+/**
+ * Policy actions.
+ *
+ * @module     tool_policy/policyactions
+ * @package    tool_policy
+ * @copyright  2018 Sara Arjona (sara@moodle.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core/ajax',
+    'core/notification',
+    'core/modal_factory',
+    'core/modal_events'],
+function($, Ajax, Notification, ModalFactory, ModalEvents) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{VIEW_POLICY: string}}
+     */
+    var ACTIONS = {
+        VIEW_POLICY: '[data-action="view"]'
+    };
+
+    /**
+     * PolicyActions class.
+     */
+    var PolicyActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    PolicyActions.prototype.registerEvents = function() {
+        $(ACTIONS.VIEW_POLICY).click(function(e) {
+            e.preventDefault();
+
+            var versionid = $(this).data('versionid');
+            var behalfid = $(this).data('behalfid');
+
+            var params = {
+                'versionid': versionid,
+                'behalfid': behalfid
+            };
+
+            var request = {
+                methodname: 'tool_policy_get_policy_version',
+                args: params
+            };
+
+            var promises = Ajax.call([request]);
+            var modalTitle = '';
+            var modalType = ModalFactory.types.DEFAULT;
+            $.when(promises[0]).then(function(data) {
+                if (data.result.policy) {
+                    modalTitle = data.result.policy.name;
+                    return data.result.policy.content;
+                }
+                // Fail.
+                Notification.addNotification({
+                    message: data.warnings[0].message,
+                    type: 'error'
+                });
+                return false;
+
+            }).then(function(html) {
+                if (html != false) {
+                    return ModalFactory.create({
+                        title: modalTitle,
+                        body: html,
+                        type: modalType,
+                        large: true
+                    }).then(function(modal) {
+                        // Handle hidden event.
+                        modal.getRoot().on(ModalEvents.hidden, function() {
+                            // Destroy when hidden.
+                            modal.destroy();
+                        });
+
+                        return modal;
+                    });
+                }
+                return false;
+            }).done(function(modal) {
+                // Show the modal.
+                modal.show();
+            }).fail(Notification.exception);
+        });
+
+    };
+
+    return /** @alias module:tool_policy/policyactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the actions helper.
+         *
+         * @method init
+         * @return {PolicyActions}
+         */
+        'init': function() {
+            return new PolicyActions();
+        }
+    };
+});
diff --git a/admin/tool/policy/classes/acceptances_table.php b/admin/tool/policy/classes/acceptances_table.php
new file mode 100644 (file)
index 0000000..a723519
--- /dev/null
@@ -0,0 +1,648 @@
+<?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/>.
+
+/**
+ * View user acceptances to the policies
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy;
+
+use tool_policy\output\acceptances_filter;
+use tool_policy\output\renderer;
+use tool_policy\output\user_agreement;
+use core_user;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/lib/tablelib.php');
+
+/**
+ * Class acceptances_table
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class acceptances_table extends \table_sql {
+
+    /** @var array */
+    protected $versionids;
+
+    /** @var acceptances_filter */
+    protected $acceptancesfilter;
+
+    /** @var renderer */
+    protected $output;
+
+    /**
+     * @var string[] The list of countries.
+     */
+    protected $countries;
+
+    /** @var bool are there any users that this user can agree on behalf of */
+    protected $canagreeany = false;
+
+    /**
+     * Constructor.
+     *
+     * @param string $uniqueid Table identifier.
+     * @param acceptances_filter $acceptancesfilter
+     * @param renderer $output
+     */
+    public function __construct($uniqueid, acceptances_filter $acceptancesfilter, renderer $output) {
+        global $CFG;
+        parent::__construct($uniqueid);
+        $this->set_attribute('id', 'acceptancetable');
+        $this->acceptancesfilter = $acceptancesfilter;
+        $this->is_downloading(optional_param('download', 0, PARAM_ALPHA), 'user_acceptances');
+        $this->baseurl = $acceptancesfilter->get_url();
+        $this->output = $output;
+
+        $this->versionids = [];
+        $versions = $acceptancesfilter->get_versions();
+        if (count($versions) > 1) {
+            foreach ($versions as $version) {
+                $this->versionids[$version->id] = $version->name;
+            }
+        } else {
+            $version = reset($versions);
+            $this->versionids[$version->id] = $version->name;
+            if ($version->status != policy_version::STATUS_ACTIVE) {
+                $this->versionids[$version->id] .= '<br>' . $version->revision;
+            }
+        }
+
+        $extrafields = get_extra_user_fields(\context_system::instance());
+        $userfields = \user_picture::fields('u', $extrafields);
+
+        $this->set_sql("$userfields",
+            "{user} u",
+            'u.id <> :siteguestid AND u.deleted = 0',
+            ['siteguestid' => $CFG->siteguest]);
+        if (!$this->is_downloading()) {
+            $this->add_column_header('select', get_string('select'), false, 'colselect');
+        }
+        $this->add_column_header('fullname', get_string('fullnameuser', 'core'));
+        foreach ($extrafields as $field) {
+            $this->add_column_header($field, get_user_field_name($field));
+        }
+
+        if (!$this->is_downloading() && !has_capability('tool/policy:acceptbehalf', \context_system::instance())) {
+            // We will need to check capability to accept on behalf in each user's context, preload users contexts.
+            $this->sql->fields .= ',' . \context_helper::get_preload_record_columns_sql('ctx');
+            $this->sql->from .= ' JOIN {context} ctx ON ctx.contextlevel = :usercontextlevel AND ctx.instanceid = u.id';
+            $this->sql->params['usercontextlevel'] = CONTEXT_USER;
+        }
+
+        if ($this->acceptancesfilter->get_single_version()) {
+            $this->configure_for_single_version();
+        } else {
+            $this->configure_for_multiple_versions();
+        }
+
+        $this->build_sql_for_search_string($extrafields);
+        $this->build_sql_for_capability_filter();
+        $this->build_sql_for_roles_filter();
+
+        $this->sortable(true, 'firstname');
+    }
+
+    /**
+     * Remove randomness from the list by always sorting by user id in the end
+     *
+     * @return array
+     */
+    public function get_sort_columns() {
+        $c = parent::get_sort_columns();
+        $c['u.id'] = SORT_ASC;
+        return $c;
+    }
+
+    /**
+     * Allows to add only one column name and header to the table (parent class methods only allow to set all).
+     *
+     * @param string $key
+     * @param string $label
+     * @param bool $sortable
+     * @param string $columnclass
+     */
+    protected function add_column_header($key, $label, $sortable = true, $columnclass = '') {
+        if (empty($this->columns)) {
+            $this->define_columns([$key]);
+            $this->define_headers([$label]);
+        } else {
+            $this->columns[$key] = count($this->columns);
+            $this->column_style[$key] = array();
+            $this->column_class[$key] = $columnclass;
+            $this->column_suppress[$key] = false;
+            $this->headers[] = $label;
+        }
+        if ($columnclass !== null) {
+            $this->column_class($key, $columnclass);
+        }
+        if (!$sortable) {
+            $this->no_sorting($key);
+        }
+    }
+
+    /**
+     * Helper configuration method.
+     */
+    protected function configure_for_single_version() {
+        $userfieldsmod = get_all_user_name_fields(true, 'm', null, 'mod');
+        $v = key($this->versionids);
+        $this->sql->fields .= ", $userfieldsmod, a{$v}.status AS status{$v}, a{$v}.note, ".
+           "a{$v}.timemodified, a{$v}.usermodified AS usermodified{$v}";
+
+        $join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}";
+        $filterstatus = $this->acceptancesfilter->get_status_filter();
+        if ($filterstatus == 1) {
+            $this->sql->from .= " $join AND a{$v}.status=1";
+        } else {
+            $this->sql->from .= " LEFT $join";
+        }
+
+        $this->sql->from .= " LEFT JOIN {user} m ON m.id = a{$v}.usermodified AND m.id <> u.id AND a{$v}.status = 1";
+
+        $this->sql->params['versionid' . $v] = $v;
+
+        if ($filterstatus === 0) {
+            $this->sql->where .= " AND (a{$v}.status IS NULL OR a{$v}.status = 0)";
+        }
+
+        $this->add_column_header('status' . $v, get_string('agreed', 'tool_policy'), true, 'mdl-align');
+        $this->add_column_header('timemodified', get_string('agreedon', 'tool_policy'));
+        $this->add_column_header('usermodified' . $v, get_string('agreedby', 'tool_policy'));
+        $this->add_column_header('note', get_string('acceptancenote', 'tool_policy'), false);
+    }
+
+    /**
+     * Helper configuration method.
+     */
+    protected function configure_for_multiple_versions() {
+        $this->add_column_header('statusall', get_string('acceptancestatusoverall', 'tool_policy'));
+        $filterstatus = $this->acceptancesfilter->get_status_filter();
+        $statusall = [];
+        foreach ($this->versionids as $v => $versionname) {
+            $this->sql->fields .= ", a{$v}.status AS status{$v}, a{$v}.usermodified AS usermodified{$v}";
+            $join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}";
+            if ($filterstatus == 1) {
+                $this->sql->from .= " {$join} AND a{$v}.status=1";
+            } else {
+                $this->sql->from .= " LEFT {$join}";
+            }
+            $this->sql->params['versionid' . $v] = $v;
+            $this->add_column_header('status' . $v, $versionname, true, 'mdl-align');
+            $statusall[] = "COALESCE(a{$v}.status, 0)";
+        }
+        $this->sql->fields .= ",".join('+', $statusall)." AS statusall";
+
+        if ($filterstatus === 0) {
+            $statussql = [];
+            foreach ($this->versionids as $v => $versionname) {
+                $statussql[] = "a{$v}.status IS NULL OR a{$v}.status = 0";
+            }
+            $this->sql->where .= " AND (u.policyagreed = 0 OR ".join(" OR ", $statussql).")";
+        }
+    }
+
+    /**
+     * Download the data.
+     */
+    public function download() {
+        \core\session\manager::write_close();
+        $this->out(0, false);
+        exit;
+    }
+
+    /**
+     * Get sql to add to where statement.
+     *
+     * @return string
+     */
+    public function get_sql_where() {
+        list($where, $params) = parent::get_sql_where();
+        $where = preg_replace('/firstname/', 'u.firstname', $where);
+        $where = preg_replace('/lastname/', 'u.lastname', $where);
+        return [$where, $params];
+    }
+
+    /**
+     * Helper SQL query builder.
+     *
+     * @param array $userfields
+     */
+    protected function build_sql_for_search_string($userfields) {
+        global $DB, $USER;
+
+        $search = $this->acceptancesfilter->get_search_strings();
+        if (empty($search)) {
+            return;
+        }
+
+        $wheres = [];
+        $params = [];
+        foreach ($search as $index => $keyword) {
+            $searchkey1 = 'search' . $index . '1';
+            $searchkey2 = 'search' . $index . '2';
+            $searchkey3 = 'search' . $index . '3';
+            $searchkey4 = 'search' . $index . '4';
+            $searchkey5 = 'search' . $index . '5';
+            $searchkey6 = 'search' . $index . '6';
+            $searchkey7 = 'search' . $index . '7';
+
+            $conditions = array();
+            // Search by fullname.
+            $fullname = $DB->sql_fullname('u.firstname', 'u.lastname');
+            $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
+
+            // Search by email.
+            $email = $DB->sql_like('u.email', ':' . $searchkey2, false, false);
+            if (!in_array('email', $userfields)) {
+                $maildisplay = 'maildisplay' . $index;
+                $userid1 = 'userid' . $index . '1';
+                // Prevent users who hide their email address from being found by others
+                // who aren't allowed to see hidden email addresses.
+                $email = "(". $email ." AND (" .
+                    "u.maildisplay <> :$maildisplay " .
+                    "OR u.id = :$userid1". // User can always find himself.
+                    "))";
+                $params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
+                $params[$userid1] = $USER->id;
+            }
+            $conditions[] = $email;
+
+            // Search by idnumber.
+            $idnumber = $DB->sql_like('u.idnumber', ':' . $searchkey3, false, false);
+            if (!in_array('idnumber', $userfields)) {
+                $userid2 = 'userid' . $index . '2';
+                // Users who aren't allowed to see idnumbers should at most find themselves
+                // when searching for an idnumber.
+                $idnumber = "(". $idnumber . " AND u.id = :$userid2)";
+                $params[$userid2] = $USER->id;
+            }
+            $conditions[] = $idnumber;
+
+            // Search by middlename.
+            $middlename = $DB->sql_like('u.middlename', ':' . $searchkey4, false, false);
+            $conditions[] = $middlename;
+
+            // Search by alternatename.
+            $alternatename = $DB->sql_like('u.alternatename', ':' . $searchkey5, false, false);
+            $conditions[] = $alternatename;
+
+            // Search by firstnamephonetic.
+            $firstnamephonetic = $DB->sql_like('u.firstnamephonetic', ':' . $searchkey6, false, false);
+            $conditions[] = $firstnamephonetic;
+
+            // Search by lastnamephonetic.
+            $lastnamephonetic = $DB->sql_like('u.lastnamephonetic', ':' . $searchkey7, false, false);
+            $conditions[] = $lastnamephonetic;
+
+            $wheres[] = "(". implode(" OR ", $conditions) .") ";
+            $params[$searchkey1] = "%$keyword%";
+            $params[$searchkey2] = "%$keyword%";
+            $params[$searchkey3] = "%$keyword%";
+            $params[$searchkey4] = "%$keyword%";
+            $params[$searchkey5] = "%$keyword%";
+            $params[$searchkey6] = "%$keyword%";
+            $params[$searchkey7] = "%$keyword%";
+        }
+
+        $this->sql->where .= ' AND '.join(' AND ', $wheres);
+        $this->sql->params += $params;
+    }
+
+    /**
+     * If there is a filter to find users who can/cannot accept on their own behalf add it to the SQL query
+     */
+    protected function build_sql_for_capability_filter() {
+        global $CFG;
+        $hascapability = $this->acceptancesfilter->get_capability_accept_filter();
+        if ($hascapability === null) {
+            return;
+        }
+
+        list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context(\context_system::instance(), 'tool/policy:accept');
+
+        if (empty($neededroles)) {
+            // There are no roles that allow to accept agreement on one own's behalf.
+            $this->sql->where .= $hascapability ? ' AND 1=0' : '';
+            return;
+        }
+
+        if (empty($forbiddenroles)) {
+            // There are no roles that prohibit to accept agreement on one own's behalf.
+            $this->sql->where .= ' AND ' . $this->sql_has_role($neededroles, $hascapability);
+            return;
+        }
+
+        $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
+        if (!empty($neededroles[$defaultuserroleid])) {
+            // Default role allows to accept agreement. Make sure user has/does not have one of the roles prohibiting it.
+            $this->sql->where .= ' AND ' . $this->sql_has_role($forbiddenroles, !$hascapability);
+            return;
+        }
+
+        if ($hascapability) {
+            // User has at least one role allowing to accept and no roles prohibiting.
+            $this->sql->where .= ' AND ' . $this->sql_has_role($neededroles);
+            $this->sql->where .= ' AND ' . $this->sql_has_role($forbiddenroles, false);
+        } else {
+            // Option 1: User has one of the roles prohibiting to accept.
+            $this->sql->where .= ' AND (' . $this->sql_has_role($forbiddenroles);
+            // Option 2: User has none of the roles allowing to accept.
+            $this->sql->where .= ' OR ' . $this->sql_has_role($neededroles, false) . ")";
+        }
+    }
+
+    /**
+     * Returns SQL snippet for users that have (do not have) one of the given roles in the system context
+     *
+     * @param array $roles list of roles indexed by role id
+     * @param bool $positive true: return users who HAVE roles; false: return users who DO NOT HAVE roles
+     * @return string
+     */
+    protected function sql_has_role($roles, $positive = true) {
+        global $CFG;
+        if (empty($roles)) {
+            return $positive ? '1=0' : '1=1';
+        }
+        $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
+        if (!empty($roles[$defaultuserroleid])) {
+            // No need to query, everybody has the default role.
+            return $positive ? '1=1' : '1=0';
+        }
+        return "u.id " . ($positive ? "" : "NOT") . " IN (
+                SELECT userid
+                FROM {role_assignments}
+                WHERE contextid = " . SYSCONTEXTID . " AND roleid IN (" . implode(',', array_keys($roles)) . ")
+            )";
+    }
+
+    /**
+     * If there is a filter by user roles add it to the SQL query.
+     */
+    protected function build_sql_for_roles_filter() {
+        foreach ($this->acceptancesfilter->get_role_filters() as $roleid) {
+            $this->sql->where .= ' AND ' . $this->sql_has_role([$roleid => $roleid]);
+        }
+    }
+
+    /**
+     * Hook that can be overridden in child classes to wrap a table in a form
+     * for example. Called only when there is data to display and not
+     * downloading.
+     */
+    public function wrap_html_start() {
+        echo \html_writer::start_tag('form',
+            ['action' => new \moodle_url('/admin/tool/policy/accept.php'), 'data-action' => 'acceptmodal']);
+        echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
+        echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl',
+            'value' => $this->get_return_url()]);
+        foreach (array_keys($this->versionids) as $versionid) {
+            echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => "versionids[{$versionid}]",
+                'value' => $versionid]);
+        }
+    }
+
+    /**
+     * Hook that can be overridden in child classes to wrap a table in a form
+     * for example. Called only when there is data to display and not
+     * downloading.
+     */
+    public function wrap_html_finish() {
+        global $PAGE;
+        if ($this->canagreeany) {
+            echo \html_writer::empty_tag('input', ['type' => 'submit',
+                'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary']);
+            $PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]);
+        }
+        echo "</form>\n";
+    }
+
+    /**
+     * Render the table.
+     */
+    public function display() {
+        $this->out(100, true);
+    }
+
+    /**
+     * Call appropriate methods on this table class to perform any processing on values before displaying in table.
+     * Takes raw data from the database and process it into human readable format, perhaps also adding html linking when
+     * displaying table as html, adding a div wrap, etc.
+     *
+     * See for example col_fullname below which will be called for a column whose name is 'fullname'.
+     *
+     * @param array|object $row row of data from db used to make one row of the table.
+     * @return array one row for the table, added using add_data_keyed method.
+     */
+    public function format_row($row) {
+        \context_helper::preload_from_record($row);
+        $row->canaccept = false;
+        $row->user = \user_picture::unalias($row, [], $this->useridfield);
+        $row->select = null;
+        if (!$this->is_downloading()) {
+            if (has_capability('tool/policy:acceptbehalf', \context_system::instance()) ||
+                has_capability('tool/policy:acceptbehalf', \context_user::instance($row->id))) {
+                $row->canaccept = true;
+                $row->select = \html_writer::empty_tag('input',
+                    ['type' => 'checkbox', 'name' => 'userids[]', 'value' => $row->id, 'class' => 'usercheckbox',
+                    'id' => 'selectuser' . $row->id]) .
+                \html_writer::tag('label', get_string('selectuser', 'tool_policy', $this->username($row->user, false)),
+                    ['for' => 'selectuser' . $row->id, 'class' => 'accesshide']);
+                $this->canagreeany = true;
+            }
+        }
+        return parent::format_row($row);
+    }
+
+    /**
+     * Get the column fullname value.
+     *
+     * @param stdClass $row
+     * @return string
+     */
+    public function col_fullname($row) {
+        global $OUTPUT;
+        $userpic = $this->is_downloading() ? '' : $OUTPUT->user_picture($row->user);
+        return $userpic . $this->username($row->user, true);
+    }
+
+    /**
+     * User name with a link to profile
+     *
+     * @param stdClass $user
+     * @param bool $profilelink show link to profile (when we are downloading never show links)
+     * @return string
+     */
+    protected function username($user, $profilelink = true) {
+        $canviewfullnames = has_capability('moodle/site:viewfullnames', \context_system::instance()) ||
+            has_capability('moodle/site:viewfullnames', \context_user::instance($user->id));
+        $name = fullname($user, $canviewfullnames);
+        if (!$this->is_downloading() && $profilelink) {
+            $profileurl = new \moodle_url('/user/profile.php', array('id' => $user->id));
+            return \html_writer::link($profileurl, $name);
+        }
+        return $name;
+    }
+
+    /**
+     * Helper.
+     */
+    protected function get_return_url() {
+        $pageurl = $this->baseurl;
+        if ($this->currpage) {
+            $pageurl = new \moodle_url($pageurl, [$this->request[TABLE_VAR_PAGE] => $this->currpage]);
+        }
+        return $pageurl;
+    }
+
+    /**
+     * Return agreement status
+     *
+     * @param int $versionid either id of an individual version or empty for overall status
+     * @param stdClass $row
+     * @return string
+     */
+    protected function status($versionid, $row) {
+        $onbehalf = false;
+        $versions = $versionid ? [$versionid => $this->versionids[$versionid]] : $this->versionids; // List of versions.
+        $accepted = []; // List of versionids that user has accepted.
+
+        foreach ($versions as $v => $name) {
+            if (!empty($row->{'status' . $v})) {
+                $accepted[] = $v;
+                $agreedby = $row->{'usermodified' . $v};
+                if ($agreedby && $agreedby != $row->id) {
+                    $onbehalf = true;
+                }
+            }
+        }
+
+        if ($versionid) {
+            $str = new \lang_string($accepted ? 'yes' : 'no');
+        } else {
+            $str = new \lang_string('acceptancecount', 'tool_policy', (object)[
+                'agreedcount' => count($accepted),
+                'policiescount' => count($versions)
+            ]);
+        }
+
+        if ($this->is_downloading()) {
+            return $str->out();
+        } else {
+            $s = $this->output->render(new user_agreement($row->id, $accepted, $this->get_return_url(),
+                $versions, $onbehalf, $row->canaccept));
+            if (!$versionid) {
+                $s .= '<br>' . \html_writer::link(new \moodle_url('/admin/tool/policy/user.php',
+                        ['userid' => $row->id, 'returnurl' => $this->get_return_url()]), $str);
+            }
+            return $s;
+        }
+    }
+
+    /**
+     * Get the column timemodified value.
+     *
+     * @param stdClass $row
+     * @return string
+     */
+    public function col_timemodified($row) {
+        if ($row->timemodified) {
+            if ($this->is_downloading()) {
+                // Use timestamp format readable for both machines and humans.
+                return date_format_string($row->timemodified, '%Y-%m-%d %H:%M:%S %Z');
+            } else {
+                // Use localised calendar format.
+                return userdate($row->timemodified, get_string('strftimedatetime'));
+            }
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get the column note value.
+     *
+     * @param stdClass $row
+     * @return string
+     */
+    public function col_note($row) {
+        if ($this->is_downloading()) {
+            return $row->note;
+        } else {
+            return format_text($row->note, FORMAT_MOODLE);
+        }
+    }
+
+    /**
+     * Get the column statusall value.
+     *
+     * @param stdClass $row
+     * @return string
+     */
+    public function col_statusall($row) {
+        return $this->status(0, $row);
+    }
+
+    /**
+     * Generate the country column.
+     *
+     * @param \stdClass $data
+     * @return string
+     */
+    public function col_country($data) {
+        if ($data->country && $this->countries === null) {
+            $this->countries = get_string_manager()->get_list_of_countries();
+        }
+        if (!empty($this->countries[$data->country])) {
+            return $this->countries[$data->country];
+        }
+        return '';
+    }
+
+    /**
+     * You can override this method in a child class. See the description of
+     * build_table which calls this method.
+     *
+     * @param string $column
+     * @param stdClass $row
+     * @return string
+     */
+    public function other_cols($column, $row) {
+        if (preg_match('/^status([\d]+)$/', $column, $matches)) {
+            $versionid = $matches[1];
+            return $this->status($versionid, $row);
+        }
+        if (preg_match('/^usermodified([\d]+)$/', $column, $matches)) {
+            if ($row->$column && $row->$column != $row->id) {
+                $user = (object)['id' => $row->$column];
+                username_load_fields_from_object($user, $row, 'mod');
+                return $this->username($user, true);
+            }
+            return ''; // User agreed by themselves.
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/policy/classes/api.php b/admin/tool/policy/classes/api.php
new file mode 100644 (file)
index 0000000..fab6d03
--- /dev/null
@@ -0,0 +1,942 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\output\renderer} class.
+ *
+ * @package     tool_policy
+ * @category    output
+ * @copyright   2018 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy;
+
+use coding_exception;
+use context_helper;
+use context_system;
+use context_user;
+use stdClass;
+use tool_policy\event\acceptance_created;
+use tool_policy\event\acceptance_updated;
+use user_picture;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Provides the API of the policies plugin.
+ *
+ * @copyright 2018 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class api {
+
+    /**
+     * Return current (active) policies versions.
+     *
+     * @param array $audience If defined, filter against the given audience (AUDIENCE_ALL always included)
+     * @return array of stdClass - exported {@link tool_policy\policy_version_exporter} instances
+     */
+    public static function list_current_versions($audience = null) {
+
+        $current = [];
+
+        foreach (static::list_policies() as $policy) {
+            if (empty($policy->currentversion)) {
+                continue;
+            }
+            if ($audience && !in_array($policy->currentversion->audience, [policy_version::AUDIENCE_ALL, $audience])) {
+                continue;
+            }
+            $current[] = $policy->currentversion;
+        }
+
+        return $current;
+    }
+
+    /**
+     * Checks if there are any current policies defined and returns their ids only
+     *
+     * @param array $audience If defined, filter against the given audience (AUDIENCE_ALL always included)
+     * @return array of version ids indexed by policies ids
+     */
+    public static function get_current_versions_ids($audience = null) {
+        global $DB;
+        $sql = "SELECT v.policyid, v.id
+             FROM {tool_policy} d
+             LEFT JOIN {tool_policy_versions} v ON v.policyid = d.id
+             WHERE d.currentversionid = v.id";
+        $params = [];
+        if ($audience) {
+            $sql .= " AND v.audience IN (?, ?)";
+            $params = [$audience, policy_version::AUDIENCE_ALL];
+        }
+        return $DB->get_records_sql_menu($sql . " ORDER BY d.sortorder", $params);
+    }
+
+    /**
+     * Returns a list of all policy documents and their versions.
+     *
+     * @param array|int|null $ids Load only the given policies, defaults to all.
+     * @param int $countacceptances return number of user acceptances for each version
+     * @return array of stdClass - exported {@link tool_policy\policy_exporter} instances
+     */
+    public static function list_policies($ids = null, $countacceptances = false) {
+        global $DB, $PAGE;
+
+        $versionfields = policy_version::get_sql_fields('v', 'v_');
+
+        $sql = "SELECT d.id, d.currentversionid, d.sortorder, $versionfields ";
+
+        if ($countacceptances) {
+            $sql .= ", COALESCE(ua.acceptancescount, 0) AS acceptancescount ";
+        }
+
+        $sql .= " FROM {tool_policy} d
+             LEFT JOIN {tool_policy_versions} v ON v.policyid = d.id ";
+
+        if ($countacceptances) {
+            $sql .= " LEFT JOIN (
+                            SELECT policyversionid, COUNT(*) AS acceptancescount
+                            FROM {tool_policy_acceptances}
+                            GROUP BY policyversionid
+                        ) ua ON ua.policyversionid = v.id ";
+        }
+
+        $sql .= " WHERE v.id IS NOT NULL ";
+
+        $params = [];
+
+        if ($ids) {
+            list($idsql, $idparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
+            $sql .= " AND d.id $idsql";
+            $params = array_merge($params, $idparams);
+        }
+
+        $sql .= " ORDER BY d.sortorder ASC, v.timecreated DESC";
+
+        $policies = [];
+        $versions = [];
+        $rs = $DB->get_recordset_sql($sql, $params);
+
+        foreach ($rs as $r) {
+            if (!isset($policies[$r->id])) {
+                $policies[$r->id] = (object) [
+                    'id' => $r->id,
+                    'currentversionid' => $r->currentversionid,
+                    'sortorder' => $r->sortorder,
+                ];
+            }
+
+            $versiondata = policy_version::extract_record($r, 'v_');
+
+            if ($countacceptances && $versiondata->audience != policy_version::AUDIENCE_GUESTS) {
+                $versiondata->acceptancescount = $r->acceptancescount;
+            }
+
+            $versions[$r->id][$versiondata->id] = $versiondata;
+        }
+
+        $rs->close();
+
+        foreach (array_keys($policies) as $policyid) {
+            static::fix_revision_values($versions[$policyid]);
+        }
+
+        $return = [];
+        $context = context_system::instance();
+        $output = $PAGE->get_renderer('tool_policy');
+
+        foreach ($policies as $policyid => $policydata) {
+            $versionexporters = [];
+            foreach ($versions[$policyid] as $versiondata) {
+                if ($policydata->currentversionid == $versiondata->id) {
+                    $versiondata->status = policy_version::STATUS_ACTIVE;
+                } else if ($versiondata->archived) {
+                    $versiondata->status = policy_version::STATUS_ARCHIVED;
+                } else {
+                    $versiondata->status = policy_version::STATUS_DRAFT;
+                }
+                $versionexporters[] = new policy_version_exporter($versiondata, [
+                    'context' => $context,
+                ]);
+            }
+            $policyexporter = new policy_exporter($policydata, [
+                'versions' => $versionexporters,
+            ]);
+            $return[] = $policyexporter->export($output);
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns total number of users who are expected to accept site policy
+     *
+     * @return int|null
+     */
+    public static function count_total_users() {
+        global $DB, $CFG;
+        static $cached = null;
+        if ($cached === null) {
+            $cached = $DB->count_records_select('user', 'deleted = 0 AND id <> ?', [$CFG->siteguest]);
+        }
+        return $cached;
+    }
+
+    /**
+     * Load a particular policy document version.
+     *
+     * @param int $versionid ID of the policy document version.
+     * @param array $policies cached result of self::list_policies() in case this function needs to be called in a loop
+     * @return stdClass - exported {@link tool_policy\policy_exporter} instance
+     */
+    public static function get_policy_version($versionid, $policies = null) {
+        if ($policies === null) {
+            $policies = self::list_policies();
+        }
+        foreach ($policies as $policy) {
+            if ($policy->currentversionid == $versionid) {
+                return $policy->currentversion;
+
+            } else {
+                foreach ($policy->draftversions as $draft) {
+                    if ($draft->id == $versionid) {
+                        return $draft;
+                    }
+                }
+
+                foreach ($policy->archivedversions as $archived) {
+                    if ($archived->id == $versionid) {
+                        return $archived;
+                    }
+                }
+            }
+        }
+
+        throw new \moodle_exception('errorpolicyversionnotfound', 'tool_policy');
+    }
+
+    /**
+     * Make sure that each version has a unique revision value.
+     *
+     * Empty value are replaced with a timecreated date. Duplicates are suffixed with v1, v2, v3, ... etc.
+     *
+     * @param array $versions List of objects with id, timecreated and revision properties
+     */
+    public static function fix_revision_values(array $versions) {
+
+        $byrev = [];
+
+        foreach ($versions as $version) {
+            if ($version->revision === '') {
+                $version->revision = userdate($version->timecreated, get_string('strftimedate', 'core_langconfig'));
+            }
+            $byrev[$version->revision][$version->id] = true;
+        }
+
+        foreach ($byrev as $origrevision => $versionids) {
+            $cnt = count($byrev[$origrevision]);
+            if ($cnt > 1) {
+                foreach ($versionids as $versionid => $unused) {
+                    foreach ($versions as $version) {
+                        if ($version->id == $versionid) {
+                            $version->revision = $version->revision.' - v'.$cnt;
+                            $cnt--;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Can the user view the given policy version document?
+     *
+     * @param stdClass $policy - exported {@link tool_policy\policy_exporter} instance
+     * @param int $behalfid The id of user on whose behalf the user is viewing the policy
+     * @param int $userid The user whom access is evaluated, defaults to the current one
+     * @return bool
+     */
+    public static function can_user_view_policy_version($policy, $behalfid = null, $userid = null) {
+        global $USER;
+
+        if ($policy->status == policy_version::STATUS_ACTIVE) {
+            return true;
+        }
+
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+
+        // Check if the user is viewing the policy on someone else's behalf.
+        // Typical scenario is a parent viewing the policy on behalf of her child.
+        if ($behalfid > 0) {
+            $behalfcontext = context_user::instance($behalfid);
+
+            if ($behalfid != $userid && !has_capability('tool/policy:acceptbehalf', $behalfcontext, $userid)) {
+                return false;
+            }
+
+            // Check that the other user (e.g. the child) has access to the policy.
+            // Pass a negative third parameter to avoid eventual endless loop.
+            // We do not support grand-parent relations.
+            return static::can_user_view_policy_version($policy, -1, $behalfid);
+        }
+
+        // Users who can manage policies, can see all versions.
+        if (has_capability('tool/policy:managedocs', context_system::instance(), $userid)) {
+            return true;
+        }
+
+        // User who can see all acceptances, must be also allowed to see what was accepted.
+        if (has_capability('tool/policy:viewacceptances', context_system::instance(), $userid)) {
+            return true;
+        }
+
+        // Users have access to all the policies they have ever accepted.
+        if (static::is_user_version_accepted($userid, $policy->id)) {
+            return true;
+        }
+
+        // Check if the user could get access through some of her minors.
+        if ($behalfid === null) {
+            foreach (static::get_user_minors($userid) as $minor) {
+                if (static::can_user_view_policy_version($policy, $minor->id, $userid)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Return the user's minors - other users on which behalf we can accept policies.
+     *
+     * Returned objects contain all the standard user name and picture fields as well as the context instanceid.
+     *
+     * @param int $userid The id if the user with parental responsibility
+     * @param array $extrafields Extra fields to be included in result
+     * @return array of objects
+     */
+    public static function get_user_minors($userid, array $extrafields = null) {
+        global $DB;
+
+        $ctxfields = context_helper::get_preload_record_columns_sql('c');
+        $namefields = get_all_user_name_fields(true, 'u');
+        $pixfields = user_picture::fields('u', $extrafields);
+
+        $sql = "SELECT $ctxfields, $namefields, $pixfields
+                  FROM {role_assignments} ra
+                  JOIN {context} c ON c.contextlevel = ".CONTEXT_USER." AND ra.contextid = c.id
+                  JOIN {user} u ON c.instanceid = u.id
+                 WHERE ra.userid = ?
+              ORDER BY u.lastname ASC, u.firstname ASC";
+
+        $rs = $DB->get_recordset_sql($sql, [$userid]);
+
+        $minors = [];
+
+        foreach ($rs as $record) {
+            context_helper::preload_from_record($record);
+            $childcontext = context_user::instance($record->id);
+            if (has_capability('tool/policy:acceptbehalf', $childcontext, $userid)) {
+                $minors[$record->id] = $record;
+            }
+        }
+
+        $rs->close();
+
+        return $minors;
+    }
+
+    /**
+     * Prepare data for the {@link \tool_policy\form\policydoc} form.
+     *
+     * @param \tool_policy\policy_version $version persistent representing the version.
+     * @return stdClass form data
+     */
+    public static function form_policydoc_data(policy_version $version) {
+
+        $data = $version->to_record();
+        $summaryfieldoptions = static::policy_summary_field_options();
+        $contentfieldoptions = static::policy_content_field_options();
+
+        if (empty($data->id)) {
+            // Adding a new version of a policy document.
+            $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context']);
+            $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context']);
+
+        } else {
+            // Editing an existing policy document version.
+            $data = file_prepare_standard_editor($data, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'],
+                'tool_policy', 'policydocumentsummary', $data->id);
+            $data = file_prepare_standard_editor($data, 'content', $contentfieldoptions, $contentfieldoptions['context'],
+                'tool_policy', 'policydocumentcontent', $data->id);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Save the data from the policydoc form as a new policy document.
+     *
+     * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
+     * @return \tool_policy\policy_version persistent
+     */
+    public static function form_policydoc_add(stdClass $form) {
+        global $DB;
+
+        $form = clone($form);
+
+        $form->policyid = $DB->insert_record('tool_policy', (object) [
+            'sortorder' => 999,
+        ]);
+
+        static::distribute_policy_document_sortorder();
+
+        return static::form_policydoc_update_new($form);
+    }
+
+    /**
+     * Save the data from the policydoc form as a new policy document version.
+     *
+     * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
+     * @return \tool_policy\policy_version persistent
+     */
+    public static function form_policydoc_update_new(stdClass $form) {
+        global $DB;
+
+        if (empty($form->policyid)) {
+            throw new coding_exception('Invalid policy document ID');
+        }
+
+        $form = clone($form);
+
+        $form->id = $DB->insert_record('tool_policy_versions', (new policy_version(0, (object) [
+            'timecreated' => time(),
+            'policyid' => $form->policyid,
+        ]))->to_record());
+
+        return static::form_policydoc_update_overwrite($form);
+    }
+
+
+    /**
+     * Save the data from the policydoc form, overwriting the existing policy document version.
+     *
+     * @param stdClass $form data submitted from the {@link \tool_policy\form\policydoc} form.
+     * @return \tool_policy\policy_version persistent
+     */
+    public static function form_policydoc_update_overwrite(stdClass $form) {
+
+        $form = clone($form);
+        unset($form->timecreated);
+
+        $summaryfieldoptions = static::policy_summary_field_options();
+        $form = file_postupdate_standard_editor($form, 'summary', $summaryfieldoptions, $summaryfieldoptions['context'],
+            'tool_policy', 'policydocumentsummary', $form->id);
+        unset($form->summary_editor);
+        unset($form->summarytrust);
+
+        $contentfieldoptions = static::policy_content_field_options();
+        $form = file_postupdate_standard_editor($form, 'content', $contentfieldoptions, $contentfieldoptions['context'],
+            'tool_policy', 'policydocumentcontent', $form->id);
+        unset($form->content_editor);
+        unset($form->contenttrust);
+
+        unset($form->status);
+        unset($form->save);
+        unset($form->saveasdraft);
+        unset($form->minorchange);
+
+        $policyversion = new policy_version($form->id, $form);
+        $policyversion->update();
+
+        return $policyversion;
+    }
+
+    /**
+     * Make the given version the current active one.
+     *
+     * @param int $versionid
+     */
+    public static function make_current($versionid) {
+        global $DB, $USER;
+
+        $policyversion = new policy_version($versionid);
+        if (! $policyversion->get('id') || $policyversion->get('archived')) {
+            throw new coding_exception('Version not found or is archived');
+        }
+
+        // Archive current version of this policy.
+        if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyversion->get('policyid')])) {
+            if ($currentversionid == $versionid) {
+                // Already current, do not change anything.
+                return;
+            }
+            $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]);
+        }
+
+        // Set given version as current.
+        $DB->set_field('tool_policy', 'currentversionid', $policyversion->get('id'), ['id' => $policyversion->get('policyid')]);
+
+        // Reset the policyagreed flag to force everybody re-accept the policies.
+        $DB->set_field('user', 'policyagreed', 0);
+
+        // Make sure that the current user is not immediately redirected to the policy acceptance page.
+        if (isloggedin() && !isguestuser()) {
+            $USER->policyagreed = 1;
+        }
+    }
+
+    /**
+     * Inactivate the policy document - no version marked as current and the document does not apply.
+     *
+     * @param int $policyid
+     */
+    public static function inactivate($policyid) {
+        global $DB;
+
+        if ($currentversionid = $DB->get_field('tool_policy', 'currentversionid', ['id' => $policyid])) {
+            // Archive the current version.
+            $DB->set_field('tool_policy_versions', 'archived', 1, ['id' => $currentversionid]);
+            // Unset current version for the policy.
+            $DB->set_field('tool_policy', 'currentversionid', null, ['id' => $policyid]);
+        }
+    }
+
+    /**
+     * Create a new draft policy document from an archived version.
+     *
+     * @param int $versionid
+     * @return \tool_policy\policy_version persistent
+     */
+    public static function revert_to_draft($versionid) {
+        $policyversion = new policy_version($versionid);
+        if (!$policyversion->get('id') || !$policyversion->get('archived')) {
+            throw new coding_exception('Version not found or is not archived');
+        }
+
+        $formdata = static::form_policydoc_data($policyversion);
+        // Unarchived the new version.
+        $formdata->archived = 0;
+        return static::form_policydoc_update_new($formdata);
+    }
+
+    /**
+     * Can the current version be deleted
+     *
+     * @param stdClass $version object describing version, contains fields policyid, id, status, archived, audience, ...
+     */
+    public static function can_delete_version($version) {
+        // TODO MDL-61900 allow to delete not only draft versions.
+        return has_capability('tool/policy:managedocs', context_system::instance()) &&
+                $version->status == policy_version::STATUS_DRAFT;
+    }
+
+    /**
+     * Delete the given version (if it is a draft). Also delete policy if this is the only version.
+     *
+     * @param int $versionid
+     */
+    public static function delete($versionid) {
+        global $DB;
+
+        $version = static::get_policy_version($versionid);
+        if (!self::can_delete_version($version)) {
+            // Current version can not be deleted.
+            return;
+        }
+
+        $DB->delete_records('tool_policy_versions', ['id' => $versionid]);
+
+        if (!$DB->record_exists('tool_policy_versions', ['policyid' => $version->policyid])) {
+            // This is a single version in a policy. Delete the policy.
+            $DB->delete_records('tool_policy', ['id' => $version->policyid]);
+        }
+    }
+
+    /**
+     * Editor field options for the policy summary text.
+     *
+     * @return array
+     */
+    public static function policy_summary_field_options() {
+        global $CFG;
+        require_once($CFG->libdir.'/formslib.php');
+
+        return [
+            'subdirs' => false,
+            'maxfiles' => -1,
+            'context' => context_system::instance(),
+        ];
+    }
+
+    /**
+     * Editor field options for the policy content text.
+     *
+     * @return array
+     */
+    public static function policy_content_field_options() {
+        global $CFG;
+        require_once($CFG->libdir.'/formslib.php');
+
+        return [
+            'subdirs' => false,
+            'maxfiles' => -1,
+            'context' => context_system::instance(),
+        ];
+    }
+
+    /**
+     * Re-sets the sortorder field of the policy documents to even values.
+     */
+    protected static function distribute_policy_document_sortorder() {
+        global $DB;
+
+        $sql = "SELECT p.id, p.sortorder, MAX(v.timecreated) AS timerecentcreated
+                  FROM {tool_policy} p
+             LEFT JOIN {tool_policy_versions} v ON v.policyid = p.id
+              GROUP BY p.id, p.sortorder
+              ORDER BY p.sortorder ASC, timerecentcreated ASC";
+
+        $rs = $DB->get_recordset_sql($sql);
+        $sortorder = 10;
+
+        foreach ($rs as $record) {
+            if ($record->sortorder != $sortorder) {
+                $DB->set_field('tool_policy', 'sortorder', $sortorder, ['id' => $record->id]);
+            }
+            $sortorder = $sortorder + 2;
+        }
+
+        $rs->close();
+    }
+
+    /**
+     * Change the policy document's sortorder.
+     *
+     * @param int $policyid
+     * @param int $step
+     */
+    protected static function move_policy_document($policyid, $step) {
+        global $DB;
+
+        $sortorder = $DB->get_field('tool_policy', 'sortorder', ['id' => $policyid], MUST_EXIST);
+        $DB->set_field('tool_policy', 'sortorder', $sortorder + $step, ['id' => $policyid]);
+        static::distribute_policy_document_sortorder();
+    }
+
+    /**
+     * Move the given policy document up in the list.
+     *
+     * @param id $policyid
+     */
+    public static function move_up($policyid) {
+        static::move_policy_document($policyid, -3);
+    }
+
+    /**
+     * Move the given policy document down in the list.
+     *
+     * @param id $policyid
+     */
+    public static function move_down($policyid) {
+        static::move_policy_document($policyid, 3);
+    }
+
+    /**
+     * Returns list of acceptances for this user.
+     *
+     * @param int $userid id of a user.
+     * @param int|array $versions list of policy versions.
+     * @return array list of acceptances indexed by versionid.
+     */
+    public static function get_user_acceptances($userid, $versions = null) {
+        global $DB;
+
+        list($vsql, $vparams) = ['', []];
+        if (!empty($versions)) {
+            list($vsql, $vparams) = $DB->get_in_or_equal($versions, SQL_PARAMS_NAMED, 'ver');
+            $vsql = ' AND a.policyversionid ' . $vsql;
+        }
+
+        $userfieldsmod = get_all_user_name_fields(true, 'm', null, 'mod');
+        $sql = "SELECT u.id AS mainuserid, a.policyversionid, a.status, a.lang, a.timemodified, a.usermodified, a.note,
+                  u.policyagreed, $userfieldsmod
+                  FROM {user} u
+                  INNER JOIN {tool_policy_acceptances} a ON a.userid = u.id AND a.userid = :userid $vsql
+                  LEFT JOIN {user} m ON m.id = a.usermodified";
+        $params = ['userid' => $userid];
+        $result = $DB->get_recordset_sql($sql, $params + $vparams);
+
+        $acceptances = [];
+        foreach ($result as $row) {
+            if (!empty($row->policyversionid)) {
+                $acceptances[$row->policyversionid] = $row;
+            }
+        }
+        $result->close();
+
+        return $acceptances;
+    }
+
+    /**
+     * Returns version acceptance for this user.
+     *
+     * @param int $userid User identifier.
+     * @param int $versionid Policy version identifier.
+     * @param array|null $acceptances List of policy version acceptances indexed by versionid.
+     * @return stdClass|null Acceptance object if the user has ever accepted this version or null if not.
+     */
+    public static function get_user_version_acceptance($userid, $versionid, $acceptances = null) {
+        if (empty($acceptances)) {
+            $acceptances = static::get_user_acceptances($userid, $versionid);
+        }
+        if (array_key_exists($versionid, $acceptances)) {
+            // The policy version has ever been accepted.
+            return $acceptances[$versionid];
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns version acceptance for this user.
+     *
+     * @param int $userid User identifier.
+     * @param int $versionid Policy version identifier.
+     * @param array|null $acceptances Iist of policy version acceptances indexed by versionid.
+     * @return bool True if this user has accepted this policy version; false otherwise.
+     */
+    public static function is_user_version_accepted($userid, $versionid, $acceptances = null) {
+        $acceptance = static::get_user_version_acceptance($userid, $versionid, $acceptances);
+        if (!empty($acceptance)) {
+            return $acceptance->status;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the list of policies and versions that current user is able to see and the respective acceptance records for
+     * the selected user.
+     *
+     * @param int $userid
+     * @return array array with the same structure that list_policies() returns with additional attribute acceptance for versions
+     */
+    public static function get_policies_with_acceptances($userid) {
+        // Get the list of policies and versions that current user is able to see
+        // and the respective acceptance records for the selected user.
+        $policies = static::list_policies();
+        $acceptances = static::get_user_acceptances($userid);
+        $ret = [];
+        foreach ($policies as $policy) {
+            $versions = [];
+            if ($policy->currentversion && $policy->currentversion->audience != policy_version::AUDIENCE_GUESTS) {
+                if (isset($acceptances[$policy->currentversion->id])) {
+                    $policy->currentversion->acceptance = $acceptances[$policy->currentversion->id];
+                } else {
+                    $policy->currentversion->acceptance = 0;
+                }
+                $versions[] = $policy->currentversion;
+            }
+            foreach ($policy->archivedversions as $version) {
+                if ($version->audience != policy_version::AUDIENCE_GUESTS
+                        && static::can_user_view_policy_version($version, $userid)) {
+                    $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : 0;
+                    $versions[] = $version;
+                }
+            }
+            if ($versions) {
+                $ret[] = (object)['id' => $policy->id, 'versions' => $versions];
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Accepts the current revisions of all policies that the user has not yet accepted
+     *
+     * @param array|int $policyversionid
+     * @param int|null $userid
+     * @param string|null $note
+     * @param string|null $lang
+     */
+    public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
+        global $DB, $USER;
+        if (!isloggedin() || isguestuser()) {
+            throw new \moodle_exception('noguest');
+        }
+        if (!$userid) {
+            $userid = $USER->id;
+        }
+        $usercontext = \context_user::instance($userid);
+        if ($userid == $USER->id) {
+            require_capability('tool/policy:accept', context_system::instance());
+        } else {
+            require_capability('tool/policy:acceptbehalf', $usercontext);
+        }
+
+        if (empty($policyversionid)) {
+            return;
+        } else if (!is_array($policyversionid)) {
+            $policyversionid = [$policyversionid];
+        }
+        list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED);
+        $sql = "SELECT v.id AS versionid, a.*
+                  FROM {tool_policy_versions} v
+                  LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
+                  WHERE (a.id IS NULL or a.status <> 1) AND v.id " . $sql;
+        $needacceptance = $DB->get_records_sql($sql, ['userid' => $userid] + $params);
+
+        $updatedata = ['status' => 1, 'lang' => $lang ?: current_language(),
+            'timemodified' => time(), 'usermodified' => $USER->id, 'note' => $note];
+        foreach ($needacceptance as $versionid => $currentacceptance) {
+            unset($currentacceptance->versionid);
+            if ($currentacceptance->id) {
+                $updatedata['id'] = $currentacceptance->id;
+                $DB->update_record('tool_policy_acceptances', $updatedata);
+                acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
+            } else {
+                $updatedata['timecreated'] = $updatedata['timemodified'];
+                $updatedata['policyversionid'] = $versionid;
+                $updatedata['userid'] = $userid;
+                $updatedata['id'] = $DB->insert_record('tool_policy_acceptances', $updatedata);
+                acceptance_created::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
+            }
+        }
+
+        static::update_policyagreed($userid);
+    }
+
+    /**
+     * Make sure that $user->policyagreed matches the agreement to the policies
+     *
+     * @param int|stdClass|null $user user to check (null for current user)
+     */
+    public static function update_policyagreed($user = null) {
+        global $DB, $USER, $CFG;
+        require_once($CFG->dirroot.'/user/lib.php');
+
+        if (!$user || (is_numeric($user) && $user == $USER->id)) {
+            $user = $USER;
+        } else if (!is_object($user)) {
+            $user = $DB->get_record('user', ['id' => $user], 'id, policyagreed');
+        }
+
+        $sql = "SELECT d.id, a.status
+                  FROM {tool_policy} d
+                  INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid
+                  LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
+                  WHERE (v.audience = :audience OR v.audience = :audienceall)";
+        $params = [
+            'audience' => policy_version::AUDIENCE_LOGGEDIN,
+            'audienceall' => policy_version::AUDIENCE_ALL,
+            'userid' => $user->id
+        ];
+        $policies = $DB->get_records_sql_menu($sql, $params);
+        $acceptedpolicies = array_filter($policies);
+        $policyagreed = (count($policies) == count($acceptedpolicies)) ? 1 : 0;
+
+        if ($user->policyagreed != $policyagreed) {
+            $user->policyagreed = $policyagreed;
+            $DB->set_field('user', 'policyagreed', $policyagreed, ['id' => $user->id]);
+        }
+    }
+
+    /**
+     * May be used to revert accidentally granted acceptance for another user
+     *
+     * @param int $policyversionid
+     * @param int $userid
+     * @param null $note
+     */
+    public static function revoke_acceptance($policyversionid, $userid, $note = null) {
+        global $DB, $USER;
+        if (!isloggedin() || isguestuser()) {
+            throw new \moodle_exception('noguest');
+        }
+        if (!$userid) {
+            $userid = $USER->id;
+        }
+        $usercontext = \context_user::instance($userid);
+        if ($userid == $USER->id) {
+            require_capability('tool/policy:accept', context_system::instance());
+        } else {
+            require_capability('tool/policy:acceptbehalf', $usercontext);
+        }
+
+        if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
+                ['policyversionid' => $policyversionid, 'userid' => $userid])) {
+            $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(),
+                'usermodified' => $USER->id, 'note' => $note];
+            $DB->update_record('tool_policy_acceptances', $updatedata);
+            acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
+        }
+
+        static::update_policyagreed($userid);
+    }
+
+    /**
+     * Create user policy acceptances when the user is created.
+     *
+     * @param \core\event\user_created $event
+     */
+    public static function create_acceptances_user_created(\core\event\user_created $event) {
+        global $CFG, $DB;
+
+        // Do nothing if not set as the site policies handler.
+        if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
+            return;
+        }
+
+        $userid = $event->objectid;
+        $lang = current_language();
+        $user = $event->get_record_snapshot('user', $userid);
+        // Do nothing if the user has not accepted the current policies.
+        if (!$user->policyagreed) {
+            return;
+        }
+        // Remove the presignup cache after the user account is created.
+        $cache = \cache::make('core', 'presignup');
+        $cache->delete('tool_policy_userpolicyagreed');
+        $cache->delete('tool_policy_viewedpolicies');
+
+        // Get all active policies.
+        $currentpolicyversions = static::get_current_versions_ids(policy_version::AUDIENCE_LOGGEDIN);
+        // Save active policies as accepted by the user.
+        if (!empty($currentpolicyversions)) {
+            $acceptances = array();
+            foreach ($currentpolicyversions as $policy) {
+                $acceptances[] = array(
+                    'policyversionid' => $policy,
+                    'userid' => $userid,
+                    'status' => 1,
+                    'lang' => $lang,
+                    'usermodified' => 0,
+                    'timecreated' => time(),
+                    'timemodified' => time()
+                );
+            }
+            $DB->insert_records('tool_policy_acceptances', $acceptances);
+        }
+    }
+}
diff --git a/admin/tool/policy/classes/event/acceptance_base.php b/admin/tool/policy/classes/event/acceptance_base.php
new file mode 100644 (file)
index 0000000..1ecf6c9
--- /dev/null
@@ -0,0 +1,117 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\event\acceptance_base} class.
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\event;
+
+use core\event\base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for acceptance_created and acceptance_updated events.
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class acceptance_base extends base {
+
+    /**
+     * Initialise the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_policy_acceptances';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Create event from record.
+     *
+     * @param stdClass $record
+     * @return acceptance_created
+     */
+    public static function create_from_record($record) {
+        $event = static::create([
+            'objectid' => $record->id,
+            'relateduserid' => $record->userid,
+            'context' => \context_user::instance($record->userid),
+            'other' => [
+                'policyversionid' => $record->policyversionid,
+                'note' => $record->note,
+                'status' => $record->status,
+            ],
+        ]);
+        $event->add_record_snapshot($event->objecttable, $record);
+        return $event;
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/admin/tool/policy/acceptance.php', array('userid' => $this->relateduserid,
+            'versionid' => $this->other['policyversionid']));
+    }
+
+    /**
+     * Get the object ID mapping.
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'tool_policy', 'restore' => \core\event\base::NOT_MAPPED);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (empty($this->other['policyversionid'])) {
+            throw new \coding_exception('The \'policyversionid\' value must be set');
+        }
+
+        if (!isset($this->other['status'])) {
+            throw new \coding_exception('The \'status\' value must be set');
+        }
+
+        if (empty($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
+
+    /**
+     * No mapping required for this event because this event is not backed up.
+     *
+     * @return bool
+     */
+    public static function get_other_mapping() {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/policy/classes/event/acceptance_created.php b/admin/tool/policy/classes/event/acceptance_created.php
new file mode 100644 (file)
index 0000000..b81b9d1
--- /dev/null
@@ -0,0 +1,73 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\event\acceptance_created} class.
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\event;
+
+use core\event\base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event acceptance_created
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class acceptance_created extends acceptance_base {
+
+    /**
+     * Initialise the event.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'c';
+    }
+
+    /**
+     * Returns event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_acceptance_created', 'tool_policy');
+    }
+
+    /**
+     * Get the event description.
+     *
+     * @return string
+     */
+    public function get_description() {
+        if ($this->other['status'] == 1) {
+            $action = 'added consent to';
+        } else if ($this->other['status'] == -1) {
+            $action = 'revoked consent to';
+        } else {
+            $action = 'created an empty consent record for';
+        }
+        return "The user with id '{$this->userid}' $action the policy with revision {$this->other['policyversionid']} ".
+            "for the user with id '{$this->relateduserid}'";
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/policy/classes/event/acceptance_updated.php b/admin/tool/policy/classes/event/acceptance_updated.php
new file mode 100644 (file)
index 0000000..4bec14f
--- /dev/null
@@ -0,0 +1,73 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\event\acceptance_updated} class.
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\event;
+
+use core\event\base;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event acceptance_updated
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class acceptance_updated extends acceptance_base {
+
+    /**
+     * Initialise the event.
+     */
+    protected function init() {
+        parent::init();
+        $this->data['crud'] = 'u';
+    }
+
+    /**
+     * Returns event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_acceptance_updated', 'tool_policy');
+    }
+
+    /**
+     * Get the event description.
+     *
+     * @return string
+     */
+    public function get_description() {
+        if ($this->other['status'] == 1) {
+            $action = 'added consent to';
+        } else if ($this->other['status'] == -1) {
+            $action = 'revoked consent to';
+        } else {
+            $action = 'updated consent to';
+        }
+        return "The user with id '{$this->userid}' $action the policy with revision {$this->other['policyversionid']} ".
+            "for the user with id '{$this->relateduserid}'";
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/policy/classes/external.php b/admin/tool/policy/classes/external.php
new file mode 100644 (file)
index 0000000..34caab9
--- /dev/null
@@ -0,0 +1,199 @@
+<?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/>.
+
+/**
+ * Class containing the external API functions functions for the Policy tool.
+ *
+ * @package    tool_policy
+ * @copyright  2018 Sara Arjona (sara@moodle.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use coding_exception;
+use context_system;
+use context_user;
+use core\invalid_persistent_exception;
+use dml_exception;
+use external_api;
+use external_description;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use invalid_parameter_exception;
+use moodle_exception;
+use restricted_context_exception;
+use tool_policy\api;
+use tool_policy\form\accept_policy;
+
+/**
+ * Class external.
+ *
+ * The external API for the Policy tool.
+ *
+ * @copyright   2018 Sara Arjona (sara@moodle.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+    /**
+     * Parameter description for get_policy_version_parameters().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_policy_version_parameters() {
+        return new external_function_parameters([
+            'versionid' => new external_value(PARAM_INT, 'The policy version ID', VALUE_REQUIRED),
+            'behalfid' => new external_value(PARAM_INT, 'The id of user on whose behalf the user is viewing the policy',
+                VALUE_DEFAULT, 0)
+        ]);
+    }
+
+    /**
+     * Fetch the details of a policy version.
+     *
+     * @param int $versionid The policy version ID.
+     * @param int $behalfid The id of user on whose behalf the user is viewing the policy.
+     * @return array
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws restricted_context_exception
+     * @throws moodle_exception
+     */
+    public static function get_policy_version($versionid, $behalfid = null) {
+        global $PAGE;
+
+        $result = [];
+        $warnings = [];
+        $params = external_api::validate_parameters(self::get_policy_version_parameters(), [
+            'versionid' => $versionid,
+            'behalfid' => $behalfid
+        ]);
+        $versionid = $params['versionid'];
+        $behalfid = $params['behalfid'];
+
+        $context = context_system::instance();
+        $PAGE->set_context($context);
+
+        try {
+            // Validate if the user has access to the policy version.
+            $version = api::get_policy_version($versionid);
+            if (!api::can_user_view_policy_version($version, $behalfid)) {
+                $warnings[] = [
+                    'item' => $versionid,
+                    'warningcode' => 'errorusercantviewpolicyversion',
+                    'message' => get_string('errorusercantviewpolicyversion', 'tool_policy')
+                ];
+            } else if (!empty($version)) {
+                $version = api::get_policy_version($versionid);
+                $policy['name'] = $version->name;
+                $policy['versionid'] = $versionid;
+                list($policy['content'], $notusedformat) = external_format_text(
+                    $version->content,
+                    $version->contentformat,
+                    SYSCONTEXTID,
+                    'tool_policy',
+                    'policydocumentcontent',
+                    $version->id
+                );
+                $result['policy'] = $policy;
+            }
+        } catch (moodle_exception $e) {
+            $warnings[] = [
+                'item' => $versionid,
+                'warningcode' => 'errorpolicyversionnotfound',
+                'message' => get_string('errorpolicyversionnotfound', 'tool_policy')
+            ];
+        }
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Parameter description for get_policy_version().
+     *
+     * @return external_description
+     */
+    public static function get_policy_version_returns() {
+        return new external_single_structure([
+            'result' => new external_single_structure([
+                            'policy' => new external_single_structure([
+                                    'name' => new external_value(PARAM_RAW, 'The policy version name', VALUE_OPTIONAL),
+                                    'versionid' => new external_value(PARAM_INT, 'The policy version id', VALUE_OPTIONAL),
+                                    'content' => new external_value(PARAM_RAW, 'The policy version content', VALUE_OPTIONAL)
+                                    ], 'Policy information', VALUE_OPTIONAL)
+                            ]),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Describes the parameters for submit_create_group_form webservice.
+     * @return external_function_parameters
+     */
+    public static function submit_accept_on_behalf_parameters() {
+        return new external_function_parameters(
+            array(
+                'jsonformdata' => new external_value(PARAM_RAW, 'The data from the create group form, encoded as a json array')
+            )
+        );
+    }
+
+    /**
+     * Submit the create group form.
+     *
+     * @param string $jsonformdata The data from the form, encoded as a json array.
+     * @return int new group id.
+     */
+    public static function submit_accept_on_behalf($jsonformdata) {
+        // We always must pass webservice params through validate_parameters.
+        $params = self::validate_parameters(self::submit_accept_on_behalf_parameters(),
+            ['jsonformdata' => $jsonformdata]);
+
+        self::validate_context(context_system::instance());
+
+        $serialiseddata = json_decode($params['jsonformdata']);
+
+        $data = array();
+        parse_str($serialiseddata, $data);
+
+        // The last param is the ajax submitted data.
+        $mform = new accept_policy(null, $data, 'post', '', null, true, $data);
+
+        // Do the action.
+        $mform->process();
+
+        return true;
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function submit_accept_on_behalf_returns() {
+        return new external_value(PARAM_BOOL, 'success');
+    }
+}
diff --git a/admin/tool/policy/classes/form/accept_policy.php b/admin/tool/policy/classes/form/accept_policy.php
new file mode 100644 (file)
index 0000000..5b54e2c
--- /dev/null
@@ -0,0 +1,161 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\form\accept_policy} class.
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\form;
+
+use tool_policy\api;
+use tool_policy\policy_version;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * Represents the form for accepting a policy.
+ *
+ * @package     tool_policy
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class accept_policy extends \moodleform {
+
+    /**
+     * Defines the form fields.
+     */
+    public function definition() {
+        global $PAGE;
+        $mform = $this->_form;
+
+        if (empty($this->_customdata['userids']) || !is_array($this->_customdata['userids'])) {
+            throw new \moodle_exception('missingparam', 'error', '', 'userids');
+        }
+        if (empty($this->_customdata['versionids']) || !is_array($this->_customdata['versionids'])) {
+            throw new \moodle_exception('missingparam', '', '', 'versionids');
+        }
+        $userids = clean_param_array($this->_customdata['userids'], PARAM_INT);
+        $versionids = clean_param_array($this->_customdata['versionids'], PARAM_INT);
+        $usernames = $this->validate_and_get_users($userids);
+        $versionnames = $this->validate_and_get_versions($versionids);
+
+        foreach ($usernames as $userid => $name) {
+            $mform->addElement('hidden', 'userids['.$userid.']', $userid);
+            $mform->setType('userids['.$userid.']', PARAM_INT);
+        }
+
+        foreach ($versionnames as $versionid => $name) {
+            $mform->addElement('hidden', 'versionids['.$versionid.']', $versionid);
+            $mform->setType('versionids['.$versionid.']', PARAM_INT);
+        }
+
+        $mform->addElement('hidden', 'returnurl');
+        $mform->setType('returnurl', PARAM_LOCALURL);
+
+        $mform->addElement('static', 'user', get_string('acceptanceusers', 'tool_policy'), join(', ', $usernames));
+        $mform->addElement('static', 'policy', get_string('acceptancepolicies', 'tool_policy'),
+            join(', ', $versionnames));
+
+        $mform->addElement('static', 'ack', '', get_string('acceptanceacknowledgement', 'tool_policy'));
+
+        $mform->addElement('textarea', 'note', get_string('acceptancenote', 'tool_policy'));
+        $mform->setType('note', PARAM_NOTAGS);
+
+        if (!empty($this->_customdata['showbuttons'])) {
+            $this->add_action_buttons(true, get_string('iagreetothepolicy', 'tool_policy'));
+        }
+
+        $PAGE->requires->js_call_amd('tool_policy/policyactions', 'init');
+    }
+
+    /**
+     * Validate userids and return usernames
+     *
+     * @param array $userids
+     * @return array (userid=>username)
+     */
+    protected function validate_and_get_users($userids) {
+        global $DB, $USER;
+        $usernames = [];
+        list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+        $params['usercontextlevel'] = CONTEXT_USER;
+        $users = $DB->get_records_sql("SELECT u.id, " . get_all_user_name_fields(true, 'u') . ", " .
+                \context_helper::get_preload_record_columns_sql('ctx') .
+            " FROM {user} u JOIN {context} ctx ON ctx.contextlevel=:usercontextlevel AND ctx.instanceid = u.id
+            WHERE u.id " . $sql, $params);
+
+        $acceptany = has_capability('tool/policy:acceptbehalf', \context_system::instance());
+        foreach ($userids as $userid) {
+            if (!isset($users[$userid])) {
+                throw new \dml_missing_record_exception('user', 'id=?', [$userid]);
+            }
+            $user = $users[$userid];
+            if (isguestuser($user)) {
+                throw new \moodle_exception('noguest');
+            }
+            if ($userid == $USER->id) {
+                require_capability('tool/policy:accept', \context_system::instance());
+            } else if (!$acceptany) {
+                \context_helper::preload_from_record($user);
+                require_capability('tool/policy:acceptbehalf', \context_user::instance($userid));
+            }
+            $usernames[$userid] = fullname($user);
+        }
+        return $usernames;
+    }
+
+    /**
+     * Validate versionids and return their names
+     *
+     * @param array $versionids
+     * @return array (versionid=>name)
+     */
+    protected function validate_and_get_versions($versionids) {
+        $versionnames = [];
+        $policies = api::list_policies();
+        foreach ($versionids as $versionid) {
+            $version = api::get_policy_version($versionid, $policies);
+            if ($version->audience == policy_version::AUDIENCE_GUESTS) {
+                throw new \moodle_exception('errorpolicyversionnotfound', 'tool_policy');
+            }
+            $url = new \moodle_url('/admin/tool/policy/view.php', ['versionid' => $version->id]);
+            $policyname = $version->name;
+            if ($version->status != policy_version::STATUS_ACTIVE) {
+                $policyname .= ' ' . $version->revision;
+            }
+            $versionnames[$version->id] = \html_writer::link($url, $policyname,
+                ['data-action' => 'view', 'data-versionid' => $version->id]);
+        }
+        return $versionnames;
+    }
+
+    /**
+     * Process form submission
+     */
+    public function process() {
+        if ($data = $this->get_data()) {
+            foreach ($data->userids as $userid) {
+                \tool_policy\api::accept_policies($data->versionids, $userid, $data->note);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/policy/classes/form/policydoc.php b/admin/tool/policy/classes/form/policydoc.php
new file mode 100644 (file)
index 0000000..91e4ce0
--- /dev/null
@@ -0,0 +1,164 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\form\policydoc} class.
+ *
+ * @package     tool_policy
+ * @category    output
+ * @copyright   2018 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\form;
+
+use context_system;
+use html_writer;
+use moodleform;
+use tool_policy\api;
+use tool_policy\policy_version;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Defines the form for editing a policy document version.
+ *
+ * @copyright 2018 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class policydoc extends moodleform {
+
+    /**
+     * Defines the form fields.
+     */
+    public function definition() {
+
+        $mform = $this->_form;
+        $formdata = $this->_customdata['formdata'];
+
+        $mform->addElement('text', 'name', get_string('policydocname', 'tool_policy'), ['maxlength' => 1333]);
+        $mform->settype('name', PARAM_TEXT);
+        $mform->addRule('name', null, 'required', null, 'client');
+        $mform->addRule('name', get_string('maximumchars', '', 1333), 'maxlength', 1333, 'client');
+
+        $options = [];
+        foreach ([policy_version::TYPE_SITE,
+                  policy_version::TYPE_PRIVACY,
+                  policy_version::TYPE_THIRD_PARTY,
+                  policy_version::TYPE_OTHER] as $type) {
+            $options[$type] = get_string('policydoctype'.$type, 'tool_policy');
+        }
+        $mform->addElement('select', 'type', get_string('policydoctype', 'tool_policy'), $options);
+
+        $options = [];
+        foreach ([policy_version::AUDIENCE_ALL,
+                  policy_version::AUDIENCE_LOGGEDIN,
+                  policy_version::AUDIENCE_GUESTS] as $audience) {
+            $options[$audience] = get_string('policydocaudience'.$audience, 'tool_policy');
+        }
+        $mform->addElement('select', 'audience', get_string('policydocaudience', 'tool_policy'), $options);
+
+        if (empty($formdata->id)) {
+            $default = userdate(time(), get_string('strftimedate', 'core_langconfig'));
+        } else {
+            $default = userdate($formdata->timecreated, get_string('strftimedate', 'core_langconfig'));
+        }
+        $mform->addElement('text', 'revision', get_string('policydocrevision', 'tool_policy'),
+            ['maxlength' => 1333, 'placeholder' => $default]);
+        $mform->settype('revision', PARAM_TEXT);
+        $mform->addRule('revision', get_string('maximumchars', '', 1333), 'maxlength', 1333, 'client');
+
+        $mform->addElement('editor', 'summary_editor', get_string('policydocsummary', 'tool_policy'), ['rows' => 7],
+            api::policy_summary_field_options());
+        $mform->addRule('summary_editor', null, 'required', null, 'client');
+
+        $mform->addElement('editor', 'content_editor', get_string('policydoccontent', 'tool_policy'), null,
+            api::policy_content_field_options());
+        $mform->addRule('content_editor', null, 'required', null, 'client');
+
+        if (!$formdata->id || $formdata->status == policy_version::STATUS_DRAFT) {
+            // Creating a new version or editing a draft/archived version.
+            $mform->addElement('hidden', 'minorchange');
+            $mform->setType('minorchange', PARAM_INT);
+
+            $statusgrp = [
+                $mform->createElement('radio', 'status', '', get_string('status'.policy_version::STATUS_ACTIVE, 'tool_policy'),
+                    policy_version::STATUS_ACTIVE),
+                $mform->createElement('radio', 'status', '', get_string('status'.policy_version::STATUS_DRAFT, 'tool_policy'),
+                    policy_version::STATUS_DRAFT),
+                $mform->createElement('static', 'statusinfo', '', html_writer::div(get_string('statusinfo', 'tool_policy'),
+                    'muted text-muted')),
+            ];
+            $mform->addGroup($statusgrp, null, get_string('status', 'tool_policy'), ['<br>'], false);
+
+        } else {
+            // Editing an active version.
+            $mform->addElement('hidden', 'status', policy_version::STATUS_ACTIVE);
+            $mform->setType('status', PARAM_INT);
+
+            $statusgrp = [
+                $mform->createElement('checkbox', 'minorchange', '', get_string('minorchange', 'tool_policy')),
+                $mform->createElement('static', 'minorchangeinfo', '',
+                    html_writer::div(get_string('minorchangeinfo', 'tool_policy'), 'muted text-muted')),
+            ];
+            $mform->addGroup($statusgrp, null, get_string('status', 'tool_policy'), ['<br>'], false);
+        }
+
+        // Add "Save" button and, optionally, "Save as draft".
+        $buttonarray = [];
+        $buttonarray[] = $mform->createElement('submit', 'save', get_string('save', 'tool_policy'));
+        if ($formdata->id && $formdata->status == policy_version::STATUS_ACTIVE) {
+            $buttonarray[] = $mform->createElement('submit', 'saveasdraft', get_string('saveasdraft', 'tool_policy'));
+        }
+        $buttonarray[] = $mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+
+        $this->set_data($formdata);
+    }
+
+    /**
+     * Form validation
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if (!empty($data['minorchange']) && !empty($data['saveasdraft'])) {
+            // If minorchange is checked and "save as draft" is pressed - return error.
+            $errors['minorchange'] = get_string('errorsaveasdraft', 'tool_policy');
+        }
+        return $errors;
+    }
+
+    /**
+     * Return submitted data if properly submitted or returns NULL if validation fails or
+     * if there is no submitted data.
+     *
+     * @return object submitted data; NULL if not valid or not submitted or cancelled
+     */
+    public function get_data() {
+        if ($data = parent::get_data()) {
+            if (!empty($data->saveasdraft)) {
+                $data->status = policy_version::STATUS_DRAFT;
+            }
+        }
+        return $data;
+    }
+}
diff --git a/admin/tool/policy/classes/output/acceptances.php b/admin/tool/policy/classes/output/acceptances.php
new file mode 100644 (file)
index 0000000..dada079
--- /dev/null
@@ -0,0 +1,133 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\output\acceptances} class.
+ *
+ * @package     tool_policy
+ * @category    output
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\output;
+
+use tool_policy\api;
+
+defined('MOODLE_INTERNAL') || die();
+
+use moodle_url;
+use renderable;
+use renderer_base;
+use single_button;
+use templatable;
+use tool_policy\policy_version;
+
+/**
+ * List of users and their acceptances
+ *
+ * @copyright 2018 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class acceptances implements renderable, templatable {
+
+    /** @var id */
+    protected $userid;
+
+    /** @var moodle_url */
+    protected $returnurl;
+
+    /**
+     * Contructor.
+     *
+     * @param int $userid
+     * @param string|moodle_url $returnurl
+     */
+    public function __construct($userid, $returnurl = null) {
+        $this->userid = $userid;
+        $this->returnurl = $returnurl ? (new moodle_url($returnurl))->out(false) : null;
+    }
+
+    /**
+     * Export the page data for the mustache template.
+     *
+     * @param renderer_base $output renderer to be used to render the page elements.
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = (object)[];
+        $data->hasonbehalfagreements = false;
+        $data->pluginbaseurl = (new moodle_url('/admin/tool/policy'))->out(false);
+        $data->returnurl = $this->returnurl;
+
+        // Get the list of policies and versions that current user is able to see
+        // and the respective acceptance records for the selected user.
+        $policies = api::get_policies_with_acceptances($this->userid);
+
+        $canviewfullnames = has_capability('moodle/site:viewfullnames', \context_system::instance());
+        foreach ($policies as $policy) {
+
+            foreach ($policy->versions as $version) {
+                unset($version->summary);
+                unset($version->content);
+                $version->iscurrent = ($version->status == policy_version::STATUS_ACTIVE);
+                $version->name = $version->name;
+                $version->revision = $version->revision;
+                $returnurl = new moodle_url('/admin/tool/policy/user.php', ['userid' => $this->userid]);
+                $version->viewurl = (new moodle_url('/admin/tool/policy/view.php', [
+                    'policyid' => $policy->id,
+                    'versionid' => $version->id,
+                    'returnurl' => $returnurl->out(false),
+                ]))->out(false);
+
+                if (!empty($version->acceptance->status)) {
+                    $acceptance = $version->acceptance;
+                    $version->timeaccepted = userdate($acceptance->timemodified, get_string('strftimedatetime'));
+                    $onbehalf = $acceptance->usermodified && $acceptance->usermodified != $this->userid;
+                    $version->agreement = new user_agreement($this->userid, [$version->id], $returnurl,
+                        [$version->id => $version->name], $onbehalf);
+                    if ($onbehalf) {
+                        $usermodified = (object)['id' => $acceptance->usermodified];
+                        username_load_fields_from_object($usermodified, $acceptance, 'mod');
+                        $profileurl = new \moodle_url('/user/profile.php', array('id' => $usermodified->id));
+                        $version->acceptedby = \html_writer::link($profileurl, fullname($usermodified, $canviewfullnames ||
+                            has_capability('moodle/site:viewfullnames', \context_user::instance($acceptance->usermodified))));
+                        $data->hasonbehalfagreements = true;
+                    }
+                    $version->note = format_text($acceptance->note);
+                } else if ($version->iscurrent) {
+                    $version->agreement = new user_agreement($this->userid, [], $returnurl, [$version->id => $version->name]);
+                }
+                if (isset($version->agreement)) {
+                    $version->agreement = $version->agreement->export_for_template($output);
+                }
+            }
+
+            if ($policy->versions[0]->status != policy_version::STATUS_ACTIVE) {
+                // Add an empty "currentversion" on top.
+                $policy->versions = [0 => (object)[]] + $policy->versions;
+            }
+
+            $policy->versioncount = count($policy->versions);
+            $policy->versions = array_values($policy->versions);
+            $policy->versions[0]->isfirst = 1;
+            $policy->versions[0]->hasarchived = (count($policy->versions) > 1);
+        }
+
+        $data->policies = array_values($policies);
+        return $data;
+    }
+}
diff --git a/admin/tool/policy/classes/output/acceptances_filter.php b/admin/tool/policy/classes/output/acceptances_filter.php
new file mode 100644 (file)
index 0000000..347a509
--- /dev/null
@@ -0,0 +1,465 @@
+<?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/>.
+
+/**
+ * Provides {@link tool_policy\output\acceptances_filter} class.
+ *
+ * @package     tool_policy
+ * @category    output
+ * @copyright   2018 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_policy\output;
+
+use tool_policy\api;
+use tool_policy\policy_version;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Implements the widget allowing to filter the acceptance records.
+ *
+ * @copyright 2018 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class acceptances_filter implements \templatable, \renderable {
+
+    /** @var array $filtersapplied The list of selected filter options. */
+    protected $filtersapplied;
+
+    /** @var string $searchstring */
+    protected $searchstrings;
+
+    /** @var array list of available versions */
+    protected $versions = null;
+
+    /** @var array list of available roles for the filter */
+    protected $roles;
+
+    /** @var array cached list of all available policies, to retrieve use {@link self::get_avaliable_policies()} */
+    protected $policies;
+
+    /** @var int */
+    const FILTER_SEARCH_STRING = 0;
+
+    /** @var int */
+    const FILTER_POLICYID = 1;
+
+    /** @var int */
+    const FILTER_VERSIONID = 2;
+
+    /** @var int */
+    const FILTER_CAPABILITY_ACCEPT = 3;
+
+    /** @var int */
+    const FILTER_STATUS = 4;
+
+    /** @var int */
+    const FILTER_ROLE = 5;
+
+    /**
+     * Constructor.
+     *
+     * @param array $policyid Specified policy id