From d846efe4e83db80f9fb922bd5e7afea579ed267f Mon Sep 17 00:00:00 2001 From: "Vergata, Sergio" <sergio.vergata@h-da.de> Date: Tue, 9 Feb 2021 16:08:44 +0100 Subject: [PATCH] Initial Commit added new Plugins for moodle --- auth/ldap_syncplus/.travis.yml | 48 + auth/ldap_syncplus/CHANGES.md | 113 + auth/ldap_syncplus/COPYING.txt | 674 ++ auth/ldap_syncplus/README.md | 205 + auth/ldap_syncplus/auth.php | 615 ++ .../classes/privacy/provider.php | 47 + .../ldap_syncplus/classes/task/sync_roles.php | 63 + auth/ldap_syncplus/classes/task/sync_task.php | 60 + auth/ldap_syncplus/cli/sync_users.php | 52 + auth/ldap_syncplus/db/events.php | 33 + auth/ldap_syncplus/db/tasks.php | 48 + auth/ldap_syncplus/db/upgrade.php | 58 + auth/ldap_syncplus/eventhandler.php | 55 + .../lang/en/auth_ldap_syncplus.php | 41 + auth/ldap_syncplus/locallib.php | 27 + auth/ldap_syncplus/settings.php | 356 + .../tests/behat/auth_ldap_syncplus.feature | 15 + auth/ldap_syncplus/version.php | 33 + .../.github/ISSUE_TEMPLATE/bug_report.md | 38 + block/admin_presets/.travis.yml | 45 + block/admin_presets/README.md | 31 + block/admin_presets/block_admin_presets.php | 95 + .../classes/event/preset_deleted.php | 51 + .../classes/event/preset_downloaded.php | 41 + .../classes/event/preset_exported.php | 56 + .../classes/event/preset_imported.php | 56 + .../classes/event/preset_loaded.php | 56 + .../classes/event/preset_previewed.php | 51 + .../classes/event/preset_reverted.php | 51 + .../classes/event/presets_listed.php | 55 + .../classes/privacy/provider.php | 36 + block/admin_presets/db/access.php | 39 + block/admin_presets/db/install.xml | 127 + block/admin_presets/db/upgrade.php | 100 + .../forms/admin_presets_export_form.php | 68 + .../forms/admin_presets_import_form.php | 52 + .../forms/admin_presets_load_form.php | 67 + block/admin_presets/index.php | 69 + .../lang/en/block_admin_presets.php | 120 + .../lib/admin_presets_base.class.php | 650 ++ .../lib/admin_presets_delete.class.php | 116 + .../lib/admin_presets_export.class.php | 235 + .../lib/admin_presets_import.class.php | 180 + .../lib/admin_presets_load.class.php | 292 + .../lib/admin_presets_rollback.class.php | 230 + .../lib/admin_presets_settings_types.php | 1201 +++ block/admin_presets/module.js | 115 + block/admin_presets/pix/check0.gif | Bin 0 -> 608 bytes block/admin_presets/pix/check1.gif | Bin 0 -> 622 bytes block/admin_presets/pix/check2.gif | Bin 0 -> 609 bytes block/admin_presets/settings.php | 40 + block/admin_presets/styles.css | 44 + block/admin_presets/tabs.php | 45 + .../tests/behat/import_settings.feature | 56 + .../tests/behat/revert_changes.feature | 46 + block/admin_presets/version.php | 34 + block/attendance/.travis.yml | 52 + block/attendance/README.md | 12 + block/attendance/block_attendance.php | 160 + block/attendance/classes/privacy/provider.php | 46 + block/attendance/composer.json | 10 + block/attendance/db/access.php | 38 + block/attendance/lang/en/block_attendance.php | 29 + .../tests/behat/attendance_block.feature | 32 + block/attendance/version.php | 32 + block/course_overview_campus/.travis.yml | 54 + block/course_overview_campus/CHANGES.md | 149 + block/course_overview_campus/COPYING.txt | 674 ++ block/course_overview_campus/README.md | 217 + .../amd/build/filter.min.js | 1 + .../amd/build/hidecourse.min.js | 1 + .../course_overview_campus/amd/src/filter.js | 191 + .../amd/src/hidecourse.js | 132 + .../block_course_overview_campus.php | 1408 ++++ .../classes/privacy/provider.php | 141 + block/course_overview_campus/db/access.php | 46 + block/course_overview_campus/db/uninstall.php | 48 + block/course_overview_campus/db/upgrade.php | 49 + .../lang/en/block_course_overview_campus.php | 181 + block/course_overview_campus/lib.php | 37 + block/course_overview_campus/locallib.php | 448 ++ .../course_overview_campus/pix/collapsed.png | Bin 0 -> 134 bytes .../course_overview_campus/pix/collapsed.svg | 3 + block/course_overview_campus/pix/expanded.png | Bin 0 -> 136 bytes block/course_overview_campus/pix/expanded.svg | 3 + block/course_overview_campus/pix/hide.png | Bin 0 -> 185 bytes block/course_overview_campus/pix/hide.svg | 3 + block/course_overview_campus/pix/show.png | Bin 0 -> 201 bytes block/course_overview_campus/pix/show.svg | 3 + block/course_overview_campus/settings.php | 516 ++ block/course_overview_campus/styles.css | 50 + block/course_overview_campus/styles_boost.css | 25 + .../styles_bootstrapbase.css | 18 + block/course_overview_campus/version.php | 31 + local/navbarplus/.travis.yml | 48 + local/navbarplus/CHANGES.md | 121 + local/navbarplus/COPYING.txt | 674 ++ local/navbarplus/README.md | 202 + local/navbarplus/classes/privacy/provider.php | 47 + local/navbarplus/lang/en/local_navbarplus.php | 55 + local/navbarplus/lib.php | 184 + local/navbarplus/settings.php | 52 + local/navbarplus/styles.css | 23 + .../tests/behat/behat_local_navbarplus.php | 209 + .../tests/behat/local_navbarplus.feature | 104 + local/navbarplus/version.php | 32 + mod/attendance/.github/workflows/ci.yml | 101 + mod/attendance/CHANGELOG.md | 39 + mod/attendance/README.md | 28 + mod/attendance/absentee.php | 145 + mod/attendance/attendance.php | 212 + .../backup_attendance_activity_task.class.php | 71 + .../moodle2/backup_attendance_stepslib.php | 110 + ...restore_attendance_activity_task.class.php | 113 + .../moodle2/restore_attendance_stepslib.php | 187 + mod/attendance/calendar.js | 110 + .../analytics/indicator/activity_base.php | 48 + .../analytics/indicator/cognitive_depth.php | 69 + .../analytics/indicator/social_breadth.php | 69 + .../attendance_webservices_handler.php | 161 + mod/attendance/classes/calendar_helpers.php | 185 + .../classes/event/attendance_taken.php | 113 + .../event/attendance_taken_by_student.php | 113 + .../course_module_instance_list_viewed.php | 50 + .../classes/event/report_viewed.php | 99 + .../classes/event/session_added.php | 111 + .../classes/event/session_deleted.php | 111 + .../event/session_duration_updated.php | 111 + .../classes/event/session_ip_shared.php | 92 + .../classes/event/session_report_updated.php | 60 + .../classes/event/session_report_viewed.php | 140 + .../classes/event/session_updated.php | 119 + .../classes/event/sessions_imported.php | 92 + mod/attendance/classes/event/status_added.php | 99 + .../classes/event/status_removed.php | 101 + .../classes/event/status_updated.php | 99 + mod/attendance/classes/form/addsession.php | 398 + mod/attendance/classes/form/addwarning.php | 118 + mod/attendance/classes/form/duration.php | 77 + mod/attendance/classes/form/export.php | 179 + .../classes/form/import/marksessions.php | 97 + .../form/import/marksessions_confirm.php | 132 + .../classes/form/import/sessions.php | 85 + .../classes/form/import/sessions_confirm.php | 73 + .../classes/form/studentattendance.php | 125 + mod/attendance/classes/form/tempmerge.php | 72 + mod/attendance/classes/form/tempuser.php | 82 + mod/attendance/classes/form/tempuseredit.php | 89 + mod/attendance/classes/form/updatesession.php | 235 + mod/attendance/classes/header.php | 80 + .../classes/import/marksessions.php | 302 + mod/attendance/classes/import/sessions.php | 525 ++ mod/attendance/classes/manage_page_params.php | 47 + mod/attendance/classes/notifyqueue.php | 93 + mod/attendance/classes/observer.php | 57 + mod/attendance/classes/output/mobile.php | 455 ++ .../classes/page_with_filter_controls.php | 283 + .../classes/preferences_page_params.php | 73 + mod/attendance/classes/privacy/provider.php | 572 ++ mod/attendance/classes/report_page_params.php | 92 + mod/attendance/classes/search/activity.php | 37 + .../classes/sessions_page_params.php | 66 + mod/attendance/classes/structure.php | 1339 ++++ mod/attendance/classes/summary.php | 367 + mod/attendance/classes/take_page_params.php | 116 + mod/attendance/classes/task/auto_mark.php | 230 + .../task/clear_temporary_passwords.php | 54 + mod/attendance/classes/task/notify.php | 173 + mod/attendance/classes/view_page_params.php | 85 + mod/attendance/composer.json | 10 + mod/attendance/coursesummary.php | 129 + mod/attendance/db/access.php | 156 + mod/attendance/db/events.php | 36 + mod/attendance/db/install.php | 53 + mod/attendance/db/install.xml | 170 + mod/attendance/db/mobile.php | 70 + mod/attendance/db/services.php | 97 + mod/attendance/db/tasks.php | 52 + mod/attendance/db/upgrade.php | 644 ++ mod/attendance/db/upgradelib.php | 55 + mod/attendance/defaultstatus.php | 131 + mod/attendance/export.php | 225 + mod/attendance/externallib.php | 545 ++ mod/attendance/import/marksessions.php | 122 + mod/attendance/import/sessions.php | 94 + mod/attendance/index.php | 93 + .../js/password/attendance_QRCodeRotate.js | 90 + mod/attendance/js/qrcode/README.md | 46 + mod/attendance/js/qrcode/qrcode.js | 614 ++ mod/attendance/js/qrcode/qrcode.min.js | 1 + mod/attendance/lang/en/attendance.php | 632 ++ mod/attendance/lib.php | 538 ++ mod/attendance/locallib.php | 1328 ++++ mod/attendance/manage.php | 100 + mod/attendance/message.html | 66 + mod/attendance/messageselect.php | 183 + mod/attendance/mobilestyles.css | 30 + mod/attendance/mod_form.php | 76 + mod/attendance/module.js | 30 + mod/attendance/password.php | 76 + mod/attendance/password_ajax.php | 57 + mod/attendance/pix/ghost.png | Bin 0 -> 48062 bytes mod/attendance/pix/icon.gif | Bin 0 -> 125 bytes mod/attendance/pix/icon.png | Bin 0 -> 1475 bytes mod/attendance/pix/icon.svg | 114 + mod/attendance/pix/key.svg | 1 + mod/attendance/pix/qrcode.svg | 1 + mod/attendance/pix/redo.png | Bin 0 -> 524 bytes mod/attendance/preferences.php | 170 + mod/attendance/renderables.php | 1029 +++ mod/attendance/renderer.php | 2828 +++++++ mod/attendance/renderhelpers.php | 386 + mod/attendance/report.php | 88 + mod/attendance/resetcalendar.php | 92 + mod/attendance/sessions.php | 227 + mod/attendance/settings.php | 210 + mod/attendance/styles.css | 302 + mod/attendance/take.php | 108 + mod/attendance/tempedit.php | 113 + .../attendance_password_icon.mustache | 21 + .../attendance_password_icon_boost.mustache | 28 + .../templates/mobile_teacher_form.mustache | 105 + .../templates/mobile_user_form.mustache | 82 + .../templates/mobile_view_page.mustache | 144 + mod/attendance/tempmerge.php | 101 + mod/attendance/tempusers.php | 134 + .../tests/behat/attendance_mod.feature | 161 + .../tests/behat/calendar_features.feature | 41 + .../tests/behat/defaultstatus.feature | 30 + .../tests/behat/extra_features.feature | 233 + .../tests/behat/preferences.feature | 56 + mod/attendance/tests/behat/report.feature | 216 + mod/attendance/tests/externallib_test.php | 474 ++ mod/attendance/tests/generator/lib.php | 67 + mod/attendance/thirdpartylibs.xml | 10 + mod/attendance/version.php | 31 + mod/attendance/view.php | 158 + mod/attendance/warnings.php | 203 + ...moodle-mod_attendance-groupfilter-debug.js | 46 + .../moodle-mod_attendance-groupfilter-min.js | 1 + .../moodle-mod_attendance-groupfilter.js | 46 + mod/attendance/yui/src/groupfilter/build.json | 10 + .../yui/src/groupfilter/js/groupfilter.js | 41 + .../yui/src/groupfilter/meta/groupfilter.json | 8 + mod/bigbluebuttonbn/.gitignore | 14 + mod/bigbluebuttonbn/.travis.yml | 196 + mod/bigbluebuttonbn/CHANGES | 35 + mod/bigbluebuttonbn/LICENSE | 674 ++ mod/bigbluebuttonbn/README.md | 68 + mod/bigbluebuttonbn/RELEASENOTES | 375 + ...up_bigbluebuttonbn_activity_task.class.php | 81 + .../backup_bigbluebuttonbn_stepslib.php | 85 + ...re_bigbluebuttonbn_activity_task.class.php | 107 + .../restore_bigbluebuttonbn_stepslib.php | 97 + mod/bigbluebuttonbn/bbb_ajax.php | 141 + mod/bigbluebuttonbn/bbb_broker.php | 77 + mod/bigbluebuttonbn/bbb_view.php | 454 ++ mod/bigbluebuttonbn/brokerlib.php | 904 +++ .../analytics/indicator/activity_base.php | 48 + .../analytics/indicator/cognitive_depth.php | 69 + .../analytics/indicator/social_breadth.php | 69 + .../classes/event/activity_viewed.php | 67 + mod/bigbluebuttonbn/classes/event/base.php | 144 + ...luebuttonbn_activity_management_viewed.php | 66 + mod/bigbluebuttonbn/classes/event/events.php | 63 + .../classes/event/live_session_event.php | 68 + .../classes/event/meeting_created.php | 67 + .../classes/event/meeting_ended.php | 68 + .../classes/event/meeting_joined.php | 68 + .../classes/event/meeting_left.php | 68 + .../classes/event/recording_deleted.php | 67 + .../classes/event/recording_edited.php | 67 + .../classes/event/recording_imported.php | 67 + .../classes/event/recording_protected.php | 67 + .../classes/event/recording_published.php | 67 + .../classes/event/recording_unprotected.php | 67 + .../classes/event/recording_unpublished.php | 67 + .../classes/event/recording_viewed.php | 67 + mod/bigbluebuttonbn/classes/external.php | 255 + .../classes/locallib/bigbluebutton.php | 293 + .../classes/locallib/config.php | 252 + .../classes/locallib/mobileview.php | 160 + .../classes/locallib/notifier.php | 199 + .../classes/output/import_view.php | 125 + mod/bigbluebuttonbn/classes/output/index.php | 233 + mod/bigbluebuttonbn/classes/output/mobile.php | 290 + .../classes/output/renderer.php | 62 + mod/bigbluebuttonbn/classes/plugin.php | 60 + .../classes/privacy/provider.php | 361 + .../classes/search/activity.php | 47 + mod/bigbluebuttonbn/classes/search/tags.php | 128 + .../classes/settings/renderer.php | 223 + .../classes/settings/validator.php | 310 + .../classes/task/completion_update_state.php | 61 + .../classes/task/send_notification.php | 63 + mod/bigbluebuttonbn/composer.json | 21 + mod/bigbluebuttonbn/composer.lock | 6747 +++++++++++++++++ mod/bigbluebuttonbn/config-dist.php | 525 ++ mod/bigbluebuttonbn/db/access.php | 99 + mod/bigbluebuttonbn/db/install.xml | 79 + mod/bigbluebuttonbn/db/log.php | 43 + mod/bigbluebuttonbn/db/mobile.php | 48 + mod/bigbluebuttonbn/db/services.php | 55 + mod/bigbluebuttonbn/db/uninstall.php | 34 + mod/bigbluebuttonbn/db/upgrade.php | 381 + .../fonts/bigbluebutton-font.eot | Bin 0 -> 1746 bytes .../fonts/bigbluebutton-font.svg | 11 + .../fonts/bigbluebutton-font.ttf | Bin 0 -> 1544 bytes .../fonts/bigbluebutton-font.woff | Bin 0 -> 1064 bytes mod/bigbluebuttonbn/import_view.php | 70 + mod/bigbluebuttonbn/index.php | 85 + .../lang/en/bigbluebuttonbn.php | 568 ++ mod/bigbluebuttonbn/lib.php | 1257 +++ mod/bigbluebuttonbn/locallib.php | 3664 +++++++++ mod/bigbluebuttonbn/mobile.notification.js | 39 + mod/bigbluebuttonbn/mod_form.php | 705 ++ mod/bigbluebuttonbn/phpunit.xml | 8 + mod/bigbluebuttonbn/pix/flex_icons.php | 34 + .../pix/i/bigbluebutton-128.png | Bin 0 -> 7752 bytes .../pix/i/bigbluebutton-24.png | Bin 0 -> 1386 bytes .../pix/i/bigbluebutton-256.png | Bin 0 -> 18500 bytes .../pix/i/bigbluebutton-32.png | Bin 0 -> 1939 bytes .../pix/i/bigbluebutton-48.png | Bin 0 -> 3250 bytes .../pix/i/bigbluebutton-64.png | Bin 0 -> 4741 bytes .../pix/i/bigbluebutton-72.png | Bin 0 -> 5183 bytes .../pix/i/bigbluebutton-80.png | Bin 0 -> 5578 bytes .../pix/i/bigbluebutton-96.png | Bin 0 -> 6229 bytes mod/bigbluebuttonbn/pix/i/bigbluebutton.svg | 28 + mod/bigbluebuttonbn/pix/i/processing16.gif | Bin 0 -> 1877 bytes mod/bigbluebuttonbn/pix/i/processing64.gif | Bin 0 -> 7708 bytes mod/bigbluebuttonbn/pix/icon.gif | Bin 0 -> 1246 bytes mod/bigbluebuttonbn/pix/icon.png | Bin 0 -> 1393 bytes mod/bigbluebuttonbn/pix/icon.svg | 55 + mod/bigbluebuttonbn/settings.php | 64 + mod/bigbluebuttonbn/styles.css | 67 + .../templates/import_view.mustache | 64 + .../templates/mobile_view_error.mustache | 34 + .../mobile_view_notification.mustache | 49 + .../templates/mobile_view_page.mustache | 62 + .../tests/behat/add_instance.feature | 63 + .../tests/behat/group_mode.feature | 90 + .../tests/behat/installed.feature | 12 + .../behat/module_test_operational.feature | 34 + .../tests/behat/recordings_import.feature | 95 + mod/bigbluebuttonbn/tests/coverage.php | 52 + .../behat_mod_bigbluebuttonbn_generator.php | 71 + mod/bigbluebuttonbn/tests/generator/lib.php | 232 + mod/bigbluebuttonbn/tests/lib_test.php | 783 ++ mod/bigbluebuttonbn/tests/locallib_test.php | 167 + .../tests/privacy_provider_test.php | 311 + mod/bigbluebuttonbn/tests/recordings_test.php | 118 + mod/bigbluebuttonbn/thirdpartylibs.xml | 10 + .../vendor/firebase/php-jwt/LICENSE | 30 + .../vendor/firebase/php-jwt/README.md | 119 + .../vendor/firebase/php-jwt/composer.json | 27 + .../vendor/firebase/php-jwt/composer.lock | 19 + .../vendor/firebase/php-jwt/package.xml | 77 + .../php-jwt/src/BeforeValidException.php | 7 + .../firebase/php-jwt/src/ExpiredException.php | 7 + .../vendor/firebase/php-jwt/src/JWT.php | 370 + .../php-jwt/src/SignatureInvalidException.php | 7 + mod/bigbluebuttonbn/version.php | 34 + mod/bigbluebuttonbn/view.php | 122 + mod/bigbluebuttonbn/viewlib.php | 364 + ...moodle-mod_bigbluebuttonbn-broker-debug.js | 199 + .../moodle-mod_bigbluebuttonbn-broker-min.js | 1 + .../moodle-mod_bigbluebuttonbn-broker.js | 199 + ...oodle-mod_bigbluebuttonbn-imports-debug.js | 44 + .../moodle-mod_bigbluebuttonbn-imports-min.js | 1 + .../moodle-mod_bigbluebuttonbn-imports.js | 44 + ...oodle-mod_bigbluebuttonbn-modform-debug.js | 345 + .../moodle-mod_bigbluebuttonbn-modform-min.js | 2 + .../moodle-mod_bigbluebuttonbn-modform.js | 345 + ...le-mod_bigbluebuttonbn-recordings-debug.js | 699 ++ ...odle-mod_bigbluebuttonbn-recordings-min.js | 3 + .../moodle-mod_bigbluebuttonbn-recordings.js | 699 ++ .../moodle-mod_bigbluebuttonbn-rooms-debug.js | 297 + .../moodle-mod_bigbluebuttonbn-rooms-min.js | 1 + .../moodle-mod_bigbluebuttonbn-rooms.js | 297 + mod/bigbluebuttonbn/yui/src/broker/build.json | 10 + .../yui/src/broker/js/broker.js | 185 + .../yui/src/broker/meta/broker.json | 12 + .../yui/src/imports/build.json | 10 + .../yui/src/imports/js/imports.js | 39 + .../yui/src/imports/meta/imports.json | 8 + .../yui/src/modform/build.json | 10 + .../yui/src/modform/js/modform.js | 340 + .../yui/src/modform/meta/modform.json | 8 + .../yui/src/recordings/build.json | 11 + .../yui/src/recordings/js/helpers.js | 232 + .../yui/src/recordings/js/recordings.js | 453 ++ .../yui/src/recordings/meta/recordings.json | 12 + mod/bigbluebuttonbn/yui/src/rooms/build.json | 10 + mod/bigbluebuttonbn/yui/src/rooms/js/rooms.js | 283 + .../yui/src/rooms/meta/rooms.json | 12 + mod/choicegroup/README.md | 39 + .../amd/build/choicegroupdatadisplay.min.js | 1 + .../amd/build/select_all_choices.min.js | 1 + .../amd/src/choicegroupdatadisplay.js | 42 + mod/choicegroup/amd/src/select_all_choices.js | 33 + ...backup_choicegroup_activity_task.class.php | 70 + .../backup_choicegroup_settingslib.php | 27 + .../moodle2/backup_choicegroup_stepslib.php | 70 + ...estore_choicegroup_activity_task.class.php | 115 + .../moodle2/restore_choicegroup_stepslib.php | 83 + .../classes/event/choice_removed.php | 101 + .../classes/event/choice_updated.php | 101 + .../course_module_instance_list_viewed.php | 39 + .../classes/event/course_module_viewed.php | 63 + .../classes/event/report_viewed.php | 84 + mod/choicegroup/classes/external.php | 451 ++ mod/choicegroup/classes/output/mobile.php | 154 + mod/choicegroup/classes/privacy/provider.php | 40 + mod/choicegroup/db/access.php | 91 + mod/choicegroup/db/install.xml | 48 + mod/choicegroup/db/log.php | 36 + mod/choicegroup/db/mobile.php | 61 + mod/choicegroup/db/services.php | 65 + mod/choicegroup/db/upgrade.php | 73 + mod/choicegroup/index.php | 118 + mod/choicegroup/javascript.js | 75 + mod/choicegroup/lang/en/choicegroup.php | 165 + mod/choicegroup/lib.php | 1106 +++ mod/choicegroup/mobile/js/courseview.js | 240 + mod/choicegroup/mobile/js/init.js | 600 ++ mod/choicegroup/mod_form.php | 304 + mod/choicegroup/pix/column.png | Bin 0 -> 94 bytes mod/choicegroup/pix/icon.gif | Bin 0 -> 115 bytes mod/choicegroup/pix/icon.svg | 430 ++ mod/choicegroup/pix/row.png | Bin 0 -> 154 bytes mod/choicegroup/renderer.php | 421 + mod/choicegroup/report.php | 304 + mod/choicegroup/settings.php | 36 + mod/choicegroup/styles.css | 274 + mod/choicegroup/styles_app.css | 3 + .../templates/mobile_view_page.mustache | 215 + mod/choicegroup/version.php | 36 + mod/choicegroup/view.php | 277 + mod/choicegroup/yui/form/form.js | 429 ++ mod/publication/.gitlab-ci.yml | 67 + mod/publication/CHANGELOG.txt | 213 + mod/publication/README.md | 102 + mod/publication/amd/build/alignrows.min.js | 2 + .../amd/build/alignrows.min.js.map | 1 + mod/publication/amd/build/filesform.min.js | 2 + .../amd/build/filesform.min.js.map | 1 + .../amd/build/groupapprovalstatus.min.js | 2 + .../amd/build/groupapprovalstatus.min.js.map | 1 + .../amd/build/onlinetextpreview.min.js | 2 + .../amd/build/onlinetextpreview.min.js.map | 1 + mod/publication/amd/src/alignrows.js | 50 + mod/publication/amd/src/filesform.js | 56 + .../amd/src/groupapprovalstatus.js | 143 + mod/publication/amd/src/onlinetextpreview.js | 109 + ...backup_publication_activity_task.class.php | 80 + .../moodle2/backup_publication_stepslib.php | 121 + ...estore_publication_activity_task.class.php | 129 + .../moodle2/restore_publication_stepslib.php | 219 + .../course_module_instance_list_viewed.php | 40 + .../classes/event/course_module_viewed.php | 48 + .../event/publication_approval_changed.php | 124 + .../event/publication_duedate_extended.php | 122 + .../event/publication_file_deleted.php | 125 + .../event/publication_file_imported.php | 122 + .../event/publication_file_uploaded.php | 105 + .../classes/local/allfilestable/base.php | 711 ++ .../classes/local/allfilestable/group.php | 279 + .../classes/local/allfilestable/import.php | 86 + .../classes/local/allfilestable/upload.php | 71 + .../classes/local/filestable/base.php | 169 + .../classes/local/filestable/group.php | 158 + .../classes/local/filestable/import.php | 80 + .../classes/local/filestable/upload.php | 73 + mod/publication/classes/local/tests/base.php | 217 + .../classes/local/tests/publication.php | 178 + mod/publication/classes/observer.php | 203 + mod/publication/classes/privacy/provider.php | 727 ++ .../classes/report_editdates_integration.php | 104 + mod/publication/classes/search/activity.php | 66 + mod/publication/db/access.php | 91 + mod/publication/db/events.php | 37 + mod/publication/db/install.xml | 85 + mod/publication/db/messages.php | 36 + mod/publication/db/services.php | 49 + mod/publication/db/upgrade.php | 206 + mod/publication/externallib.php | 97 + mod/publication/grantextension.php | 99 + mod/publication/index.php | 119 + mod/publication/lang/en/deprecated.txt | 3 + mod/publication/lang/en/publication.php | 269 + mod/publication/lib.php | 281 + mod/publication/locallib.php | 1779 +++++ mod/publication/mod_form.php | 261 + .../mod_publication_allfiles_form.php | 51 + .../mod_publication_files_form.php | 143 + .../mod_publication_grantextension_form.php | 117 + mod/publication/onlinepreview.php | 52 + mod/publication/phpunit.xml | 44 + mod/publication/pix/icon.png | Bin 0 -> 1010 bytes mod/publication/pix/icon.svg | 675 ++ mod/publication/pix/questionmark.png | Bin 0 -> 333 bytes mod/publication/pix/questionmark.svg | 16 + mod/publication/settings.php | 48 + mod/publication/styles.css | 243 + .../templates/approvaltooltip.mustache | 106 + mod/publication/tests/allfilestable_test.php | 140 + mod/publication/tests/generator/lib.php | 82 + mod/publication/tests/privacy_test.php | 374 + mod/publication/upload.php | 184 + mod/publication/upload_form.php | 104 + mod/publication/version.php | 34 + mod/publication/view.php | 289 + mod/scheduler/.travis.yml | 48 + mod/scheduler/README.txt | 244 + mod/scheduler/ajax.php | 63 + mod/scheduler/appointmentforms.php | 205 + .../backup_scheduler_activity_task.class.php | 75 + .../moodle2/backup_scheduler_stepslib.php | 105 + .../restore_scheduler_activity_task.class.php | 113 + .../moodle2/restore_scheduler_stepslib.php | 150 + mod/scheduler/bookingform.php | 180 + .../classes/event/appointment_base.php | 105 + .../classes/event/appointment_list_viewed.php | 77 + mod/scheduler/classes/event/booking_added.php | 77 + .../classes/event/booking_form_viewed.php | 78 + .../classes/event/booking_removed.php | 78 + .../course_module_instance_list_viewed.php | 38 + .../classes/event/scheduler_base.php | 124 + mod/scheduler/classes/event/slot_added.php | 78 + mod/scheduler/classes/event/slot_base.php | 104 + mod/scheduler/classes/event/slot_deleted.php | 86 + mod/scheduler/classes/model/appointment.php | 153 + .../classes/model/appointment_factory.php | 46 + .../classes/model/mvc_child_list.php | 218 + .../classes/model/mvc_child_model_factory.php | 79 + .../classes/model/mvc_child_record_model.php | 79 + mod/scheduler/classes/model/mvc_model.php | 37 + .../classes/model/mvc_model_factory.php | 56 + .../classes/model/mvc_record_model.php | 184 + mod/scheduler/classes/model/scheduler.php | 1248 +++ mod/scheduler/classes/model/slot.php | 496 ++ mod/scheduler/classes/model/slot_factory.php | 46 + .../permission/permissions_manager.php | 114 + .../permission/scheduler_permissions.php | 166 + mod/scheduler/classes/privacy/provider.php | 465 ++ mod/scheduler/classes/search/activity.php | 37 + .../classes/task/purge_unused_slots.php | 52 + mod/scheduler/classes/task/send_reminders.php | 86 + mod/scheduler/customlib.php | 79 + mod/scheduler/datelist.php | 246 + mod/scheduler/db/access.php | 193 + mod/scheduler/db/install.xml | 87 + mod/scheduler/db/messages.php | 41 + mod/scheduler/db/tasks.php | 44 + mod/scheduler/db/upgrade.php | 341 + mod/scheduler/export.php | 138 + mod/scheduler/exportform.php | 191 + mod/scheduler/exportlib.php | 2226 ++++++ mod/scheduler/index.php | 104 + mod/scheduler/lang/en/scheduler.php | 637 ++ mod/scheduler/lib.php | 757 ++ mod/scheduler/locallib.php | 344 + mod/scheduler/mailtemplatelib.php | 221 + mod/scheduler/message_form.php | 122 + mod/scheduler/mod_form.php | 244 + mod/scheduler/pix/attachment.png | Bin 0 -> 710 bytes mod/scheduler/pix/attachment.svg | 55 + mod/scheduler/pix/icon.gif | Bin 0 -> 217 bytes mod/scheduler/pix/icon.png | Bin 0 -> 1271 bytes mod/scheduler/pix/icon.svg | 151 + mod/scheduler/pix/ticked.gif | Bin 0 -> 944 bytes mod/scheduler/pix/unticked.gif | Bin 0 -> 943 bytes mod/scheduler/renderable.php | 700 ++ mod/scheduler/renderer.php | 1087 +++ mod/scheduler/settings.php | 61 + mod/scheduler/slotforms.php | 662 ++ mod/scheduler/studentview.controller.php | 308 + mod/scheduler/studentview.php | 256 + mod/scheduler/styles.css | 217 + mod/scheduler/teacherview.controller.php | 376 + mod/scheduler/teacherview.php | 680 ++ mod/scheduler/tests/behat/add_slots.feature | 110 + .../tests/behat/behat_mod_scheduler.php | 179 + mod/scheduler/tests/behat/conflicts.feature | 198 + .../tests/behat/group_availability.feature | 77 + mod/scheduler/tests/behat/groupmode.feature | 351 + .../tests/behat/groupscheduling.feature | 153 + mod/scheduler/tests/behat/notes.feature | 175 + mod/scheduler/tests/behat/officehours.feature | 96 + mod/scheduler/tests/behat/studentdata.feature | 88 + .../tests/behat/teacherpermissions.feature | 232 + .../tests/behat/tutorappointments.feature | 222 + mod/scheduler/tests/behat/viewslots.feature | 176 + mod/scheduler/tests/fixtures/studentfile.txt | 1 + mod/scheduler/tests/generator/lib.php | 123 + mod/scheduler/tests/model_test.php | 149 + mod/scheduler/tests/permissions_test.php | 296 + mod/scheduler/tests/privacy_test.php | 240 + mod/scheduler/tests/scheduler_test.php | 511 ++ mod/scheduler/tests/slot_test.php | 313 + mod/scheduler/version.php | 35 + mod/scheduler/view.php | 100 + mod/scheduler/viewstatistics.php | 295 + mod/scheduler/viewstudent.php | 153 + .../moodle-mod_scheduler-delselected-debug.js | 44 + .../moodle-mod_scheduler-delselected-min.js | 1 + .../moodle-mod_scheduler-delselected.js | 44 + .../moodle-mod_scheduler-saveseen-debug.js | 69 + .../moodle-mod_scheduler-saveseen-min.js | 1 + .../moodle-mod_scheduler-saveseen.js | 69 + .../moodle-mod_scheduler-studentlist-debug.js | 42 + .../moodle-mod_scheduler-studentlist-min.js | 1 + .../moodle-mod_scheduler-studentlist.js | 42 + mod/scheduler/yui/src/delselected/build.json | 10 + .../yui/src/delselected/js/delselected.js | 40 + .../yui/src/delselected/meta/delselected.json | 7 + mod/scheduler/yui/src/saveseen/build.json | 10 + mod/scheduler/yui/src/saveseen/js/saveseen.js | 64 + .../yui/src/saveseen/meta/saveseen.json | 7 + mod/scheduler/yui/src/studentlist/build.json | 10 + .../yui/src/studentlist/js/studentlist.js | 37 + .../yui/src/studentlist/meta/studentlist.json | 7 + theme/adaptable/.eslintignore | 3 + theme/adaptable/.eslintrc | 240 + theme/adaptable/.travis.yml | 62 + theme/adaptable/CONTRIBUTING.txt | 28 + theme/adaptable/COPYING.txt | 674 ++ theme/adaptable/Changes.md | 313 + theme/adaptable/Gruntfile.js | 151 + theme/adaptable/README.md | 130 + theme/adaptable/amd/build/adaptable.min.js | 1 + theme/adaptable/amd/build/bsoptions.min.js | 1 + theme/adaptable/amd/build/drawer.min.js | 1 + theme/adaptable/amd/build/savebutton.min.js | 1 + theme/adaptable/amd/build/search-input.min.js | 1 + theme/adaptable/amd/build/showsidebar.min.js | 1 + .../amd/build/templatepreview.min.js | 1 + theme/adaptable/amd/build/utils.min.js | 1 + theme/adaptable/amd/build/zoomin.min.js | 1 + theme/adaptable/amd/src/adaptable.js | 127 + theme/adaptable/amd/src/bsoptions.js | 181 + theme/adaptable/amd/src/drawer.js | 36 + theme/adaptable/amd/src/savebutton.js | 37 + theme/adaptable/amd/src/search-input.js | 161 + theme/adaptable/amd/src/showsidebar.js | 37 + theme/adaptable/amd/src/templatepreview.js | 16 + theme/adaptable/amd/src/utils.js | 73 + theme/adaptable/amd/src/zoomin.js | 63 + theme/adaptable/classes/activity.php | 903 +++ theme/adaptable/classes/activity_meta.php | 153 + .../classes/admin_settingspage_tabs.php | 110 + .../classes/output/core/core_renderer.php | 41 + .../classes/output/core/course_renderer.php | 1017 +++ .../core_user/myprofile/editprofile.php | 370 + .../core_user/myprofile/editprofile_form.php | 232 + .../output/core_user/myprofile/renderer.php | 709 ++ .../mod_forum/email/renderer_htmlemail.php | 60 + .../mod_forum/email/renderer_textemail.php | 59 + .../emaildigestbasic/renderer_htmlemail.php | 60 + .../emaildigestbasic/renderer_textemail.php | 60 + .../emaildigestfull/renderer_htmlemail.php | 61 + .../emaildigestfull/renderer_textemail.php | 60 + .../mustache_filesystemstring_loader.php | 67 + .../classes/output/mustache_renderer.php | 65 + .../output/mustachesource_renderer.php | 49 + .../output/topcoll_course_renderer.php | 86 + theme/adaptable/classes/privacy/provider.php | 48 + theme/adaptable/classes/toolbox.php | 567 ++ .../adaptable/classes/traits/null_object.php | 84 + .../classes/traits/single_section_page.php | 353 + theme/adaptable/config.php | 286 + theme/adaptable/db/caches.php | 33 + theme/adaptable/db/upgrade.php | 55 + theme/adaptable/jquery/adaptable_v2_1_1_2.js | 88 + theme/adaptable/jquery/jquery-easing-min.js | 8 + .../adaptable/jquery/jquery-flexslider-min.js | 5 + theme/adaptable/jquery/pace-min.js | 2 + theme/adaptable/jquery/plugins.php | 54 + theme/adaptable/jquery/tickerme.js | 124 + theme/adaptable/lang/en/theme_adaptable.php | 1944 +++++ theme/adaptable/layout/columns1.php | 52 + theme/adaptable/layout/columns2.php | 86 + theme/adaptable/layout/course.php | 233 + theme/adaptable/layout/dashboard.php | 193 + theme/adaptable/layout/embedded.php | 53 + theme/adaptable/layout/frontpage.php | 147 + theme/adaptable/layout/includes/footer.php | 157 + theme/adaptable/layout/includes/head.php | 131 + theme/adaptable/layout/includes/header.php | 344 + .../layout/includes/loginnofooter.php | 37 + .../layout/includes/loginnoheader.php | 70 + theme/adaptable/layout/login.php | 82 + theme/adaptable/layout/maintenance.php | 49 + theme/adaptable/layout/secure.php | 71 + theme/adaptable/lib.php | 833 ++ .../adaptable/libs/admin_confightmleditor.php | 266 + theme/adaptable/package.json | 31 + theme/adaptable/pix/2xlogo.png | Bin 0 -> 9122 bytes theme/adaptable/pix/bkg.png | Bin 0 -> 129467 bytes theme/adaptable/pix/favicon.ico | Bin 0 -> 7406 bytes theme/adaptable/pix/icon.png | Bin 0 -> 1369 bytes .../adaptable/pix/layout-builder/12-0-0-0.png | Bin 0 -> 1357 bytes .../adaptable/pix/layout-builder/3-3-3-3.png | Bin 0 -> 1954 bytes .../adaptable/pix/layout-builder/3-3-6-0.png | Bin 0 -> 1909 bytes .../adaptable/pix/layout-builder/3-6-3-0.png | Bin 0 -> 1896 bytes .../adaptable/pix/layout-builder/3-9-0-0.png | Bin 0 -> 1771 bytes .../adaptable/pix/layout-builder/4-4-4-0.png | Bin 0 -> 1853 bytes .../adaptable/pix/layout-builder/4-8-0-0.png | Bin 0 -> 1894 bytes .../adaptable/pix/layout-builder/5-7-0-0.png | Bin 0 -> 6920 bytes .../adaptable/pix/layout-builder/6-3-3-0.png | Bin 0 -> 1907 bytes .../adaptable/pix/layout-builder/6-6-0-0.png | Bin 0 -> 1644 bytes .../adaptable/pix/layout-builder/7-5-0-0.png | Bin 0 -> 7060 bytes .../adaptable/pix/layout-builder/8-4-0-0.png | Bin 0 -> 1813 bytes .../adaptable/pix/layout-builder/9-3-0-0.png | Bin 0 -> 1708 bytes theme/adaptable/pix/layout.png | Bin 0 -> 54724 bytes theme/adaptable/pix/next.png | Bin 0 -> 1102 bytes theme/adaptable/pix/previous.png | Bin 0 -> 1109 bytes theme/adaptable/pix/quiz/arrow.png | Bin 0 -> 3059 bytes theme/adaptable/pix/quiz/qanswered.png | Bin 0 -> 269 bytes theme/adaptable/pix/quiz/qcheck.png | Bin 0 -> 1120 bytes theme/adaptable/pix/quiz/qdash.png | Bin 0 -> 126 bytes theme/adaptable/pix/quiz/qwhite.png | Bin 0 -> 127 bytes theme/adaptable/pix/quiz/qwrong.png | Bin 0 -> 1132 bytes theme/adaptable/pix/screenshot.png | Bin 0 -> 153364 bytes theme/adaptable/pix/search2.png | Bin 0 -> 1280 bytes theme/adaptable/pix/tile-background.png | Bin 0 -> 3330 bytes theme/adaptable/pix_core/f/archive.svg | 7 + theme/adaptable/pix_core/f/audio.svg | 7 + theme/adaptable/pix_core/f/avi.svg | 7 + theme/adaptable/pix_core/f/bmp.svg | 7 + theme/adaptable/pix_core/f/calc.svg | 7 + theme/adaptable/pix_core/f/chart.svg | 7 + theme/adaptable/pix_core/f/database.svg | 7 + theme/adaptable/pix_core/f/document.svg | 1 + theme/adaptable/pix_core/f/eps.svg | 7 + theme/adaptable/pix_core/f/flash.svg | 7 + theme/adaptable/pix_core/f/folder.svg | 7 + theme/adaptable/pix_core/f/gif.svg | 7 + theme/adaptable/pix_core/f/html.svg | 7 + theme/adaptable/pix_core/f/image.svg | 7 + theme/adaptable/pix_core/f/impress.svg | 7 + theme/adaptable/pix_core/f/jpeg.svg | 1 + theme/adaptable/pix_core/f/markup.svg | 7 + theme/adaptable/pix_core/f/mov.svg | 7 + theme/adaptable/pix_core/f/mp3.svg | 1 + theme/adaptable/pix_core/f/mpeg.svg | 7 + theme/adaptable/pix_core/f/oth.svg | 7 + theme/adaptable/pix_core/f/pdf.svg | 1 + theme/adaptable/pix_core/f/png.svg | 1 + theme/adaptable/pix_core/f/powerpoint.svg | 1 + theme/adaptable/pix_core/f/quicktime.svg | 7 + theme/adaptable/pix_core/f/sourcecode.svg | 7 + theme/adaptable/pix_core/f/spreadsheet.svg | 7 + theme/adaptable/pix_core/f/svg.svg | 7 + theme/adaptable/pix_core/f/text.svg | 7 + theme/adaptable/pix_core/f/unknown.svg | 7 + theme/adaptable/pix_core/f/url.svg | 7 + theme/adaptable/pix_core/f/video.svg | 7 + theme/adaptable/pix_core/f/wav.svg | 7 + theme/adaptable/pix_core/f/wmv.svg | 7 + theme/adaptable/pix_core/f/writer.svg | 7 + theme/adaptable/pix_core/i/loading.gif | Bin 0 -> 4176 bytes theme/adaptable/pix_core/i/loading_small.gif | Bin 0 -> 1456 bytes .../pix_core/i/notifications-black.png | Bin 0 -> 268 bytes .../pix_core/i/notifications-black.svg | 75 + theme/adaptable/pix_core/i/notifications.png | Bin 0 -> 279 bytes theme/adaptable/pix_core/i/notifications.svg | 75 + theme/adaptable/pix_core/s/angry.png | Bin 0 -> 813 bytes theme/adaptable/pix_core/s/approve.png | Bin 0 -> 774 bytes theme/adaptable/pix_core/s/biggrin.png | Bin 0 -> 1178 bytes theme/adaptable/pix_core/s/blackeye.png | Bin 0 -> 929 bytes theme/adaptable/pix_core/s/blush.png | Bin 0 -> 866 bytes theme/adaptable/pix_core/s/clown.png | Bin 0 -> 1472 bytes theme/adaptable/pix_core/s/cool.png | Bin 0 -> 892 bytes theme/adaptable/pix_core/s/dead.png | Bin 0 -> 725 bytes theme/adaptable/pix_core/s/evil.png | Bin 0 -> 969 bytes theme/adaptable/pix_core/s/heart.png | Bin 0 -> 621 bytes theme/adaptable/pix_core/s/kiss.png | Bin 0 -> 873 bytes theme/adaptable/pix_core/s/mixed.png | Bin 0 -> 730 bytes theme/adaptable/pix_core/s/no.png | Bin 0 -> 547 bytes theme/adaptable/pix_core/s/sad.png | Bin 0 -> 739 bytes theme/adaptable/pix_core/s/shy.png | Bin 0 -> 866 bytes theme/adaptable/pix_core/s/sleepy.png | Bin 0 -> 1190 bytes theme/adaptable/pix_core/s/smiley.png | Bin 0 -> 710 bytes theme/adaptable/pix_core/s/surprise.png | Bin 0 -> 1221 bytes theme/adaptable/pix_core/s/thoughtful.png | Bin 0 -> 730 bytes theme/adaptable/pix_core/s/tongueout.png | Bin 0 -> 944 bytes theme/adaptable/pix_core/s/wideeyes.png | Bin 0 -> 792 bytes theme/adaptable/pix_core/s/wink.png | Bin 0 -> 817 bytes theme/adaptable/pix_core/s/yes.png | Bin 0 -> 532 bytes theme/adaptable/pix_core/t/message-black.png | Bin 0 -> 267 bytes theme/adaptable/pix_core/t/message-black.svg | 81 + theme/adaptable/pix_core/t/message.png | Bin 0 -> 350 bytes theme/adaptable/pix_core/t/message.svg | 81 + .../pix_plugins/mod/activequiz/icon.png | Bin 0 -> 1164 bytes .../pix_plugins/mod/activequiz/icon.svg | 1 + .../pix_plugins/mod/adaptivequiz/icon.png | Bin 0 -> 1101 bytes .../pix_plugins/mod/adaptivequiz/icon.svg | 1 + .../adaptable/pix_plugins/mod/assign/icon.png | Bin 0 -> 5758 bytes .../adaptable/pix_plugins/mod/assign/icon.svg | 1 + .../pix_plugins/mod/assignment/icon.png | Bin 0 -> 5758 bytes .../pix_plugins/mod/assignment/icon.svg | 1 + .../pix_plugins/mod/attendance/icon.png | Bin 0 -> 1154 bytes .../pix_plugins/mod/attendance/icon.svg | 1 + .../pix_plugins/mod/basiclti/icon.png | Bin 0 -> 2008 bytes .../pix_plugins/mod/basiclti/icon.svg | 13 + .../pix_plugins/mod/bigbluebuttonbn/icon.png | Bin 0 -> 978 bytes .../pix_plugins/mod/bigbluebuttonbn/icon.svg | 1 + theme/adaptable/pix_plugins/mod/book/icon.png | Bin 0 -> 899 bytes theme/adaptable/pix_plugins/mod/book/icon.svg | 1 + .../pix_plugins/mod/booking/icon.png | Bin 0 -> 1749 bytes .../pix_plugins/mod/booking/icon.svg | 8 + .../pix_plugins/mod/certificate/icon.png | Bin 0 -> 1891 bytes .../pix_plugins/mod/certificate/icon.svg | 1 + theme/adaptable/pix_plugins/mod/chat/icon.png | Bin 0 -> 1295 bytes theme/adaptable/pix_plugins/mod/chat/icon.svg | 1 + .../pix_plugins/mod/checklist/icon.png | Bin 0 -> 1003 bytes .../pix_plugins/mod/checklist/icon.svg | 1 + .../adaptable/pix_plugins/mod/choice/icon.png | Bin 0 -> 1030 bytes .../adaptable/pix_plugins/mod/choice/icon.svg | 1 + .../pix_plugins/mod/choicegroup/icon.png | Bin 0 -> 1437 bytes .../pix_plugins/mod/choicegroup/icon.svg | 1 + .../pix_plugins/mod/courseguide/icon.png | Bin 0 -> 1292 bytes .../pix_plugins/mod/courseguide/icon.svg | 1 + .../pix_plugins/mod/customcert/icon.png | Bin 0 -> 1891 bytes .../pix_plugins/mod/customcert/icon.svg | 1 + theme/adaptable/pix_plugins/mod/data/icon.png | Bin 0 -> 888 bytes theme/adaptable/pix_plugins/mod/data/icon.svg | 1 + .../pix_plugins/mod/equella/icon.png | Bin 0 -> 1263 bytes .../pix_plugins/mod/equella/icon.svg | 1 + .../pix_plugins/mod/feedback/icon.png | Bin 0 -> 1651 bytes .../pix_plugins/mod/feedback/icon.svg | 1 + .../pix_plugins/mod/flashcard/icon.png | Bin 0 -> 1178 bytes .../pix_plugins/mod/flashcard/icon.svg | 1 + .../adaptable/pix_plugins/mod/folder/icon.png | Bin 0 -> 819 bytes .../adaptable/pix_plugins/mod/folder/icon.svg | 1 + .../adaptable/pix_plugins/mod/forum/icon.png | Bin 0 -> 1342 bytes .../adaptable/pix_plugins/mod/forum/icon.svg | 1 + .../pix_plugins/mod/glossary/icon.png | Bin 0 -> 1123 bytes .../pix_plugins/mod/glossary/icon.svg | 1 + .../pix_plugins/mod/helixmedia/icon.png | Bin 0 -> 1093 bytes .../pix_plugins/mod/helixmedia/icon.svg | 1 + .../adaptable/pix_plugins/mod/hotpot/icon.png | Bin 0 -> 1435 bytes .../adaptable/pix_plugins/mod/hotpot/icon.svg | 1 + .../pix_plugins/mod/hotquestion/icon.png | Bin 0 -> 1413 bytes .../pix_plugins/mod/hotquestion/icon.svg | 1 + .../pix_plugins/mod/hsuforum/icon.png | Bin 0 -> 1342 bytes .../pix_plugins/mod/hsuforum/icon.svg | 1 + theme/adaptable/pix_plugins/mod/hvp/icon.png | Bin 0 -> 2113 bytes theme/adaptable/pix_plugins/mod/hvp/icon.svg | 1 + .../adaptable/pix_plugins/mod/imscp/icon.png | Bin 0 -> 1240 bytes .../adaptable/pix_plugins/mod/imscp/icon.svg | 1 + .../adaptable/pix_plugins/mod/jclic/icon.png | Bin 0 -> 1026 bytes .../adaptable/pix_plugins/mod/jclic/icon.svg | 1 + .../pix_plugins/mod/journal/icon.png | Bin 0 -> 1133 bytes .../pix_plugins/mod/journal/icon.svg | 1 + .../adaptable/pix_plugins/mod/label/icon.png | Bin 0 -> 1119 bytes .../adaptable/pix_plugins/mod/label/icon.svg | 1 + .../adaptable/pix_plugins/mod/lesson/icon.png | Bin 0 -> 929 bytes .../adaptable/pix_plugins/mod/lesson/icon.svg | 1 + .../pix_plugins/mod/lightboxgallery/icon.png | Bin 0 -> 1164 bytes .../pix_plugins/mod/lightboxgallery/icon.svg | 1 + theme/adaptable/pix_plugins/mod/lti/icon.png | Bin 0 -> 895 bytes theme/adaptable/pix_plugins/mod/lti/icon.svg | 1 + .../pix_plugins/mod/mapleta/icon.png | Bin 0 -> 1311 bytes .../pix_plugins/mod/mapleta/icon.svg | 1 + .../pix_plugins/mod/mediagallery/icon.png | Bin 0 -> 1117 bytes .../pix_plugins/mod/mediagallery/icon.svg | 1 + theme/adaptable/pix_plugins/mod/page/icon.png | Bin 0 -> 801 bytes theme/adaptable/pix_plugins/mod/page/icon.svg | 1 + .../pix_plugins/mod/pearson/icon.png | Bin 0 -> 1142 bytes .../pix_plugins/mod/pearson/icon.svg | 1 + .../pix_plugins/mod/peerwork/icon.png | Bin 0 -> 1837 bytes .../pix_plugins/mod/peerwork/icon.svg | 7 + .../pix_plugins/mod/questionnaire/icon.png | Bin 0 -> 847 bytes .../pix_plugins/mod/questionnaire/icon.svg | 1 + theme/adaptable/pix_plugins/mod/quiz/icon.png | Bin 0 -> 973 bytes theme/adaptable/pix_plugins/mod/quiz/icon.svg | 1 + .../pix_plugins/mod/recordingsbn/icon.png | Bin 0 -> 6491 bytes .../pix_plugins/mod/recordingsbn/icon.svg | 1 + .../pix_plugins/mod/resource/icon.png | Bin 0 -> 5435 bytes .../pix_plugins/mod/resource/icon.svg | 1 + .../pix_plugins/mod/scheduler/icon.png | Bin 0 -> 1080 bytes .../pix_plugins/mod/scheduler/icon.svg | 1 + .../adaptable/pix_plugins/mod/scorm/icon.png | Bin 0 -> 1288 bytes .../adaptable/pix_plugins/mod/scorm/icon.svg | 1 + .../pix_plugins/mod/structlabel/icon.png | Bin 0 -> 1881 bytes .../pix_plugins/mod/structlabel/icon.svg | 8 + .../adaptable/pix_plugins/mod/survey/icon.png | Bin 0 -> 800 bytes .../adaptable/pix_plugins/mod/survey/icon.svg | 1 + .../pix_plugins/mod/turnitintool/icon.png | Bin 0 -> 1070 bytes .../pix_plugins/mod/turnitintool/icon.svg | 1 + .../pix_plugins/mod/turnitintooltwo/icon.png | Bin 0 -> 1070 bytes .../pix_plugins/mod/turnitintooltwo/icon.svg | 1 + theme/adaptable/pix_plugins/mod/url/icon.png | Bin 0 -> 1087 bytes theme/adaptable/pix_plugins/mod/url/icon.svg | 1 + theme/adaptable/pix_plugins/mod/wiki/icon.png | Bin 0 -> 1179 bytes theme/adaptable/pix_plugins/mod/wiki/icon.svg | 1 + .../pix_plugins/mod/workshop/icon.png | Bin 0 -> 1354 bytes .../pix_plugins/mod/workshop/icon.svg | 1 + theme/adaptable/renderers.php | 3833 ++++++++++ theme/adaptable/scss/card-blocks.scss | 103 + theme/adaptable/settings.php | 75 + ...adaptable_admin_setting_configtemplate.php | 132 + .../adaptable_admin_setting_getprops.php | 145 + .../adaptable_admin_setting_putprops.php | 132 + theme/adaptable/settings/alert_box.php | 249 + theme/adaptable/settings/analytics.php | 132 + .../adaptable/settings/array_definitions.php | 1280 ++++ theme/adaptable/settings/block_regions.php | 64 + theme/adaptable/settings/block_settings.php | 260 + theme/adaptable/settings/buttons.php | 238 + theme/adaptable/settings/category_headers.php | 135 + theme/adaptable/settings/colors.php | 387 + theme/adaptable/settings/course_formats.php | 582 ++ theme/adaptable/settings/custom_css.php | 75 + .../adaptable/settings/dash_block_regions.php | 73 + theme/adaptable/settings/fonts.php | 200 + theme/adaptable/settings/footer.php | 152 + .../adaptable/settings/frontpage_courses.php | 111 + theme/adaptable/settings/frontpage_slider.php | 206 + theme/adaptable/settings/frontpage_ticker.php | 92 + theme/adaptable/settings/header.php | 230 + theme/adaptable/settings/header_menus.php | 137 + .../adaptable/settings/header_navbar_menu.php | 91 + theme/adaptable/settings/header_social.php | 71 + theme/adaptable/settings/header_user.php | 158 + .../settings/importexport_settings.php | 69 + theme/adaptable/settings/layout.php | 198 + .../adaptable/settings/layout_responsive.php | 168 + theme/adaptable/settings/login.php | 121 + theme/adaptable/settings/marketing_blocks.php | 100 + theme/adaptable/settings/navbar_links.php | 84 + theme/adaptable/settings/navbar_settings.php | 284 + theme/adaptable/settings/navbar_styles.php | 133 + theme/adaptable/settings/print.php | 69 + theme/adaptable/settings/templates.php | 84 + theme/adaptable/settings/user.php | 74 + theme/adaptable/style/adaptable.css | 2535 +++++++ theme/adaptable/style/backup-restore.css | 28 + theme/adaptable/style/blocks.css | 781 ++ theme/adaptable/style/bootstrap.css | 28 + theme/adaptable/style/browser.css | 30 + theme/adaptable/style/button.css | 459 ++ theme/adaptable/style/cardblocks.css | 63 + theme/adaptable/style/categorycustom.css | 29 + theme/adaptable/style/core.css | 44 + theme/adaptable/style/course.css | 1006 +++ theme/adaptable/style/custom.css | 32 + theme/adaptable/style/extras.css | 1263 +++ theme/adaptable/style/form.css | 104 + theme/adaptable/style/header.css | 31 + theme/adaptable/style/menu.css | 684 ++ theme/adaptable/style/messages.css | 89 + theme/adaptable/style/navigation.css | 49 + theme/adaptable/style/notifications.css | 80 + theme/adaptable/style/print.css | 133 + theme/adaptable/style/responsive.css | 685 ++ theme/adaptable/style/tabs.css | 71 + theme/adaptable/style/user.css | 115 + ...able_admin_setting_configtemplate.mustache | 41 + ..._setting_configtemplate_nopreview.mustache | 34 + ...min_setting_configtemplate_source.mustache | 39 + .../adaptable_admin_setting_tabs.mustache | 62 + theme/adaptable/templates/core/modal.mustache | 67 + .../core/preferences_groups.mustache | 45 + .../templates/core/progress_bar.mustache | 64 + .../core_course/activity_navigation.mustache | 84 + .../core_message/message_drawer.mustache | 77 + .../core_message/message_popover.mustache | 60 + theme/adaptable/templates/header.mustache | 43 + .../templates/headerloginform.mustache | 36 + .../adaptable/templates/headernavbar.mustache | 161 + .../adaptable/templates/headersearch.mustache | 38 + .../adaptable/templates/headersocial.mustache | 30 + .../templates/headerstyleone.mustache | 152 + .../templates/headerstyletwo.mustache | 103 + .../adaptable/templates/overlaymenu.mustache | 64 + .../templates/overlaymenuitem.mustache | 30 + .../adaptable/templates/savediscard.mustache | 40 + theme/adaptable/templates/tabs.mustache | 63 + .../tool_usertours/tourstep.mustache | 65 + theme/adaptable/templates/usermenu.mustache | 56 + theme/adaptable/tests/PHPUNIT_COMMANDS.txt | 13 + .../adaptable/tests/adaptabletoolbox_test.php | 108 + theme/adaptable/version.php | 48 + 986 files changed, 142476 insertions(+) create mode 100644 auth/ldap_syncplus/.travis.yml create mode 100644 auth/ldap_syncplus/CHANGES.md create mode 100644 auth/ldap_syncplus/COPYING.txt create mode 100644 auth/ldap_syncplus/README.md create mode 100644 auth/ldap_syncplus/auth.php create mode 100644 auth/ldap_syncplus/classes/privacy/provider.php create mode 100644 auth/ldap_syncplus/classes/task/sync_roles.php create mode 100644 auth/ldap_syncplus/classes/task/sync_task.php create mode 100644 auth/ldap_syncplus/cli/sync_users.php create mode 100644 auth/ldap_syncplus/db/events.php create mode 100644 auth/ldap_syncplus/db/tasks.php create mode 100644 auth/ldap_syncplus/db/upgrade.php create mode 100644 auth/ldap_syncplus/eventhandler.php create mode 100644 auth/ldap_syncplus/lang/en/auth_ldap_syncplus.php create mode 100644 auth/ldap_syncplus/locallib.php create mode 100644 auth/ldap_syncplus/settings.php create mode 100644 auth/ldap_syncplus/tests/behat/auth_ldap_syncplus.feature create mode 100644 auth/ldap_syncplus/version.php create mode 100644 block/admin_presets/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 block/admin_presets/.travis.yml create mode 100644 block/admin_presets/README.md create mode 100644 block/admin_presets/block_admin_presets.php create mode 100644 block/admin_presets/classes/event/preset_deleted.php create mode 100644 block/admin_presets/classes/event/preset_downloaded.php create mode 100644 block/admin_presets/classes/event/preset_exported.php create mode 100644 block/admin_presets/classes/event/preset_imported.php create mode 100644 block/admin_presets/classes/event/preset_loaded.php create mode 100644 block/admin_presets/classes/event/preset_previewed.php create mode 100644 block/admin_presets/classes/event/preset_reverted.php create mode 100644 block/admin_presets/classes/event/presets_listed.php create mode 100644 block/admin_presets/classes/privacy/provider.php create mode 100644 block/admin_presets/db/access.php create mode 100644 block/admin_presets/db/install.xml create mode 100644 block/admin_presets/db/upgrade.php create mode 100644 block/admin_presets/forms/admin_presets_export_form.php create mode 100644 block/admin_presets/forms/admin_presets_import_form.php create mode 100644 block/admin_presets/forms/admin_presets_load_form.php create mode 100644 block/admin_presets/index.php create mode 100644 block/admin_presets/lang/en/block_admin_presets.php create mode 100644 block/admin_presets/lib/admin_presets_base.class.php create mode 100644 block/admin_presets/lib/admin_presets_delete.class.php create mode 100644 block/admin_presets/lib/admin_presets_export.class.php create mode 100644 block/admin_presets/lib/admin_presets_import.class.php create mode 100644 block/admin_presets/lib/admin_presets_load.class.php create mode 100644 block/admin_presets/lib/admin_presets_rollback.class.php create mode 100644 block/admin_presets/lib/admin_presets_settings_types.php create mode 100644 block/admin_presets/module.js create mode 100644 block/admin_presets/pix/check0.gif create mode 100644 block/admin_presets/pix/check1.gif create mode 100644 block/admin_presets/pix/check2.gif create mode 100644 block/admin_presets/settings.php create mode 100644 block/admin_presets/styles.css create mode 100644 block/admin_presets/tabs.php create mode 100644 block/admin_presets/tests/behat/import_settings.feature create mode 100644 block/admin_presets/tests/behat/revert_changes.feature create mode 100644 block/admin_presets/version.php create mode 100644 block/attendance/.travis.yml create mode 100644 block/attendance/README.md create mode 100644 block/attendance/block_attendance.php create mode 100644 block/attendance/classes/privacy/provider.php create mode 100644 block/attendance/composer.json create mode 100644 block/attendance/db/access.php create mode 100644 block/attendance/lang/en/block_attendance.php create mode 100644 block/attendance/tests/behat/attendance_block.feature create mode 100644 block/attendance/version.php create mode 100644 block/course_overview_campus/.travis.yml create mode 100644 block/course_overview_campus/CHANGES.md create mode 100644 block/course_overview_campus/COPYING.txt create mode 100644 block/course_overview_campus/README.md create mode 100644 block/course_overview_campus/amd/build/filter.min.js create mode 100644 block/course_overview_campus/amd/build/hidecourse.min.js create mode 100644 block/course_overview_campus/amd/src/filter.js create mode 100644 block/course_overview_campus/amd/src/hidecourse.js create mode 100644 block/course_overview_campus/block_course_overview_campus.php create mode 100644 block/course_overview_campus/classes/privacy/provider.php create mode 100644 block/course_overview_campus/db/access.php create mode 100644 block/course_overview_campus/db/uninstall.php create mode 100644 block/course_overview_campus/db/upgrade.php create mode 100644 block/course_overview_campus/lang/en/block_course_overview_campus.php create mode 100644 block/course_overview_campus/lib.php create mode 100644 block/course_overview_campus/locallib.php create mode 100644 block/course_overview_campus/pix/collapsed.png create mode 100644 block/course_overview_campus/pix/collapsed.svg create mode 100644 block/course_overview_campus/pix/expanded.png create mode 100644 block/course_overview_campus/pix/expanded.svg create mode 100644 block/course_overview_campus/pix/hide.png create mode 100644 block/course_overview_campus/pix/hide.svg create mode 100644 block/course_overview_campus/pix/show.png create mode 100644 block/course_overview_campus/pix/show.svg create mode 100644 block/course_overview_campus/settings.php create mode 100644 block/course_overview_campus/styles.css create mode 100644 block/course_overview_campus/styles_boost.css create mode 100644 block/course_overview_campus/styles_bootstrapbase.css create mode 100644 block/course_overview_campus/version.php create mode 100644 local/navbarplus/.travis.yml create mode 100644 local/navbarplus/CHANGES.md create mode 100644 local/navbarplus/COPYING.txt create mode 100644 local/navbarplus/README.md create mode 100644 local/navbarplus/classes/privacy/provider.php create mode 100644 local/navbarplus/lang/en/local_navbarplus.php create mode 100644 local/navbarplus/lib.php create mode 100644 local/navbarplus/settings.php create mode 100644 local/navbarplus/styles.css create mode 100644 local/navbarplus/tests/behat/behat_local_navbarplus.php create mode 100644 local/navbarplus/tests/behat/local_navbarplus.feature create mode 100644 local/navbarplus/version.php create mode 100644 mod/attendance/.github/workflows/ci.yml create mode 100644 mod/attendance/CHANGELOG.md create mode 100644 mod/attendance/README.md create mode 100644 mod/attendance/absentee.php create mode 100644 mod/attendance/attendance.php create mode 100644 mod/attendance/backup/moodle2/backup_attendance_activity_task.class.php create mode 100644 mod/attendance/backup/moodle2/backup_attendance_stepslib.php create mode 100644 mod/attendance/backup/moodle2/restore_attendance_activity_task.class.php create mode 100644 mod/attendance/backup/moodle2/restore_attendance_stepslib.php create mode 100644 mod/attendance/calendar.js create mode 100644 mod/attendance/classes/analytics/indicator/activity_base.php create mode 100644 mod/attendance/classes/analytics/indicator/cognitive_depth.php create mode 100644 mod/attendance/classes/analytics/indicator/social_breadth.php create mode 100644 mod/attendance/classes/attendance_webservices_handler.php create mode 100644 mod/attendance/classes/calendar_helpers.php create mode 100644 mod/attendance/classes/event/attendance_taken.php create mode 100644 mod/attendance/classes/event/attendance_taken_by_student.php create mode 100644 mod/attendance/classes/event/course_module_instance_list_viewed.php create mode 100644 mod/attendance/classes/event/report_viewed.php create mode 100644 mod/attendance/classes/event/session_added.php create mode 100644 mod/attendance/classes/event/session_deleted.php create mode 100644 mod/attendance/classes/event/session_duration_updated.php create mode 100644 mod/attendance/classes/event/session_ip_shared.php create mode 100644 mod/attendance/classes/event/session_report_updated.php create mode 100644 mod/attendance/classes/event/session_report_viewed.php create mode 100644 mod/attendance/classes/event/session_updated.php create mode 100644 mod/attendance/classes/event/sessions_imported.php create mode 100644 mod/attendance/classes/event/status_added.php create mode 100644 mod/attendance/classes/event/status_removed.php create mode 100644 mod/attendance/classes/event/status_updated.php create mode 100644 mod/attendance/classes/form/addsession.php create mode 100644 mod/attendance/classes/form/addwarning.php create mode 100644 mod/attendance/classes/form/duration.php create mode 100644 mod/attendance/classes/form/export.php create mode 100644 mod/attendance/classes/form/import/marksessions.php create mode 100644 mod/attendance/classes/form/import/marksessions_confirm.php create mode 100644 mod/attendance/classes/form/import/sessions.php create mode 100644 mod/attendance/classes/form/import/sessions_confirm.php create mode 100644 mod/attendance/classes/form/studentattendance.php create mode 100644 mod/attendance/classes/form/tempmerge.php create mode 100644 mod/attendance/classes/form/tempuser.php create mode 100644 mod/attendance/classes/form/tempuseredit.php create mode 100644 mod/attendance/classes/form/updatesession.php create mode 100644 mod/attendance/classes/header.php create mode 100644 mod/attendance/classes/import/marksessions.php create mode 100644 mod/attendance/classes/import/sessions.php create mode 100644 mod/attendance/classes/manage_page_params.php create mode 100644 mod/attendance/classes/notifyqueue.php create mode 100644 mod/attendance/classes/observer.php create mode 100644 mod/attendance/classes/output/mobile.php create mode 100644 mod/attendance/classes/page_with_filter_controls.php create mode 100644 mod/attendance/classes/preferences_page_params.php create mode 100644 mod/attendance/classes/privacy/provider.php create mode 100644 mod/attendance/classes/report_page_params.php create mode 100644 mod/attendance/classes/search/activity.php create mode 100644 mod/attendance/classes/sessions_page_params.php create mode 100644 mod/attendance/classes/structure.php create mode 100644 mod/attendance/classes/summary.php create mode 100644 mod/attendance/classes/take_page_params.php create mode 100644 mod/attendance/classes/task/auto_mark.php create mode 100644 mod/attendance/classes/task/clear_temporary_passwords.php create mode 100644 mod/attendance/classes/task/notify.php create mode 100644 mod/attendance/classes/view_page_params.php create mode 100644 mod/attendance/composer.json create mode 100644 mod/attendance/coursesummary.php create mode 100644 mod/attendance/db/access.php create mode 100644 mod/attendance/db/events.php create mode 100644 mod/attendance/db/install.php create mode 100644 mod/attendance/db/install.xml create mode 100644 mod/attendance/db/mobile.php create mode 100644 mod/attendance/db/services.php create mode 100644 mod/attendance/db/tasks.php create mode 100644 mod/attendance/db/upgrade.php create mode 100644 mod/attendance/db/upgradelib.php create mode 100644 mod/attendance/defaultstatus.php create mode 100644 mod/attendance/export.php create mode 100644 mod/attendance/externallib.php create mode 100644 mod/attendance/import/marksessions.php create mode 100644 mod/attendance/import/sessions.php create mode 100644 mod/attendance/index.php create mode 100644 mod/attendance/js/password/attendance_QRCodeRotate.js create mode 100644 mod/attendance/js/qrcode/README.md create mode 100644 mod/attendance/js/qrcode/qrcode.js create mode 100644 mod/attendance/js/qrcode/qrcode.min.js create mode 100644 mod/attendance/lang/en/attendance.php create mode 100644 mod/attendance/lib.php create mode 100644 mod/attendance/locallib.php create mode 100644 mod/attendance/manage.php create mode 100644 mod/attendance/message.html create mode 100644 mod/attendance/messageselect.php create mode 100644 mod/attendance/mobilestyles.css create mode 100644 mod/attendance/mod_form.php create mode 100644 mod/attendance/module.js create mode 100644 mod/attendance/password.php create mode 100644 mod/attendance/password_ajax.php create mode 100644 mod/attendance/pix/ghost.png create mode 100644 mod/attendance/pix/icon.gif create mode 100644 mod/attendance/pix/icon.png create mode 100644 mod/attendance/pix/icon.svg create mode 100644 mod/attendance/pix/key.svg create mode 100644 mod/attendance/pix/qrcode.svg create mode 100644 mod/attendance/pix/redo.png create mode 100644 mod/attendance/preferences.php create mode 100644 mod/attendance/renderables.php create mode 100644 mod/attendance/renderer.php create mode 100644 mod/attendance/renderhelpers.php create mode 100644 mod/attendance/report.php create mode 100644 mod/attendance/resetcalendar.php create mode 100644 mod/attendance/sessions.php create mode 100644 mod/attendance/settings.php create mode 100644 mod/attendance/styles.css create mode 100644 mod/attendance/take.php create mode 100644 mod/attendance/tempedit.php create mode 100644 mod/attendance/templates/attendance_password_icon.mustache create mode 100644 mod/attendance/templates/attendance_password_icon_boost.mustache create mode 100644 mod/attendance/templates/mobile_teacher_form.mustache create mode 100644 mod/attendance/templates/mobile_user_form.mustache create mode 100644 mod/attendance/templates/mobile_view_page.mustache create mode 100644 mod/attendance/tempmerge.php create mode 100644 mod/attendance/tempusers.php create mode 100644 mod/attendance/tests/behat/attendance_mod.feature create mode 100644 mod/attendance/tests/behat/calendar_features.feature create mode 100644 mod/attendance/tests/behat/defaultstatus.feature create mode 100644 mod/attendance/tests/behat/extra_features.feature create mode 100644 mod/attendance/tests/behat/preferences.feature create mode 100644 mod/attendance/tests/behat/report.feature create mode 100644 mod/attendance/tests/externallib_test.php create mode 100644 mod/attendance/tests/generator/lib.php create mode 100644 mod/attendance/thirdpartylibs.xml create mode 100644 mod/attendance/version.php create mode 100644 mod/attendance/view.php create mode 100644 mod/attendance/warnings.php create mode 100644 mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-debug.js create mode 100644 mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-min.js create mode 100644 mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter.js create mode 100644 mod/attendance/yui/src/groupfilter/build.json create mode 100644 mod/attendance/yui/src/groupfilter/js/groupfilter.js create mode 100644 mod/attendance/yui/src/groupfilter/meta/groupfilter.json create mode 100644 mod/bigbluebuttonbn/.gitignore create mode 100644 mod/bigbluebuttonbn/.travis.yml create mode 100644 mod/bigbluebuttonbn/CHANGES create mode 100644 mod/bigbluebuttonbn/LICENSE create mode 100644 mod/bigbluebuttonbn/README.md create mode 100644 mod/bigbluebuttonbn/RELEASENOTES create mode 100644 mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php create mode 100644 mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php create mode 100644 mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php create mode 100644 mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php create mode 100644 mod/bigbluebuttonbn/bbb_ajax.php create mode 100644 mod/bigbluebuttonbn/bbb_broker.php create mode 100644 mod/bigbluebuttonbn/bbb_view.php create mode 100644 mod/bigbluebuttonbn/brokerlib.php create mode 100644 mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php create mode 100644 mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php create mode 100644 mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php create mode 100644 mod/bigbluebuttonbn/classes/event/activity_viewed.php create mode 100644 mod/bigbluebuttonbn/classes/event/base.php create mode 100644 mod/bigbluebuttonbn/classes/event/bigbluebuttonbn_activity_management_viewed.php create mode 100644 mod/bigbluebuttonbn/classes/event/events.php create mode 100644 mod/bigbluebuttonbn/classes/event/live_session_event.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_created.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_ended.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_joined.php create mode 100644 mod/bigbluebuttonbn/classes/event/meeting_left.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_deleted.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_edited.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_imported.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_protected.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_published.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_unprotected.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_unpublished.php create mode 100644 mod/bigbluebuttonbn/classes/event/recording_viewed.php create mode 100644 mod/bigbluebuttonbn/classes/external.php create mode 100644 mod/bigbluebuttonbn/classes/locallib/bigbluebutton.php create mode 100644 mod/bigbluebuttonbn/classes/locallib/config.php create mode 100644 mod/bigbluebuttonbn/classes/locallib/mobileview.php create mode 100644 mod/bigbluebuttonbn/classes/locallib/notifier.php create mode 100644 mod/bigbluebuttonbn/classes/output/import_view.php create mode 100644 mod/bigbluebuttonbn/classes/output/index.php create mode 100644 mod/bigbluebuttonbn/classes/output/mobile.php create mode 100644 mod/bigbluebuttonbn/classes/output/renderer.php create mode 100644 mod/bigbluebuttonbn/classes/plugin.php create mode 100644 mod/bigbluebuttonbn/classes/privacy/provider.php create mode 100644 mod/bigbluebuttonbn/classes/search/activity.php create mode 100644 mod/bigbluebuttonbn/classes/search/tags.php create mode 100644 mod/bigbluebuttonbn/classes/settings/renderer.php create mode 100644 mod/bigbluebuttonbn/classes/settings/validator.php create mode 100644 mod/bigbluebuttonbn/classes/task/completion_update_state.php create mode 100644 mod/bigbluebuttonbn/classes/task/send_notification.php create mode 100644 mod/bigbluebuttonbn/composer.json create mode 100644 mod/bigbluebuttonbn/composer.lock create mode 100644 mod/bigbluebuttonbn/config-dist.php create mode 100644 mod/bigbluebuttonbn/db/access.php create mode 100644 mod/bigbluebuttonbn/db/install.xml create mode 100644 mod/bigbluebuttonbn/db/log.php create mode 100644 mod/bigbluebuttonbn/db/mobile.php create mode 100644 mod/bigbluebuttonbn/db/services.php create mode 100644 mod/bigbluebuttonbn/db/uninstall.php create mode 100644 mod/bigbluebuttonbn/db/upgrade.php create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.eot create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf create mode 100644 mod/bigbluebuttonbn/fonts/bigbluebutton-font.woff create mode 100644 mod/bigbluebuttonbn/import_view.php create mode 100644 mod/bigbluebuttonbn/index.php create mode 100644 mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php create mode 100644 mod/bigbluebuttonbn/lib.php create mode 100644 mod/bigbluebuttonbn/locallib.php create mode 100644 mod/bigbluebuttonbn/mobile.notification.js create mode 100644 mod/bigbluebuttonbn/mod_form.php create mode 100644 mod/bigbluebuttonbn/phpunit.xml create mode 100644 mod/bigbluebuttonbn/pix/flex_icons.php create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-128.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-24.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-256.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-32.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-48.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-64.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-72.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-80.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton-96.png create mode 100644 mod/bigbluebuttonbn/pix/i/bigbluebutton.svg create mode 100644 mod/bigbluebuttonbn/pix/i/processing16.gif create mode 100644 mod/bigbluebuttonbn/pix/i/processing64.gif create mode 100644 mod/bigbluebuttonbn/pix/icon.gif create mode 100644 mod/bigbluebuttonbn/pix/icon.png create mode 100644 mod/bigbluebuttonbn/pix/icon.svg create mode 100644 mod/bigbluebuttonbn/settings.php create mode 100644 mod/bigbluebuttonbn/styles.css create mode 100644 mod/bigbluebuttonbn/templates/import_view.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_error.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_notification.mustache create mode 100644 mod/bigbluebuttonbn/templates/mobile_view_page.mustache create mode 100644 mod/bigbluebuttonbn/tests/behat/add_instance.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/group_mode.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/installed.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/module_test_operational.feature create mode 100644 mod/bigbluebuttonbn/tests/behat/recordings_import.feature create mode 100644 mod/bigbluebuttonbn/tests/coverage.php create mode 100644 mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php create mode 100644 mod/bigbluebuttonbn/tests/generator/lib.php create mode 100644 mod/bigbluebuttonbn/tests/lib_test.php create mode 100644 mod/bigbluebuttonbn/tests/locallib_test.php create mode 100644 mod/bigbluebuttonbn/tests/privacy_provider_test.php create mode 100644 mod/bigbluebuttonbn/tests/recordings_test.php create mode 100644 mod/bigbluebuttonbn/thirdpartylibs.xml create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/LICENSE create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/README.md create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.json create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.lock create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/package.xml create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/BeforeValidException.php create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/ExpiredException.php create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/JWT.php create mode 100644 mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/SignatureInvalidException.php create mode 100644 mod/bigbluebuttonbn/version.php create mode 100644 mod/bigbluebuttonbn/view.php create mode 100644 mod/bigbluebuttonbn/viewlib.php create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-debug.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-min.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-debug.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-min.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-debug.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-min.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-debug.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-min.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-debug.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-min.js create mode 100644 mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms.js create mode 100644 mod/bigbluebuttonbn/yui/src/broker/build.json create mode 100644 mod/bigbluebuttonbn/yui/src/broker/js/broker.js create mode 100644 mod/bigbluebuttonbn/yui/src/broker/meta/broker.json create mode 100644 mod/bigbluebuttonbn/yui/src/imports/build.json create mode 100644 mod/bigbluebuttonbn/yui/src/imports/js/imports.js create mode 100644 mod/bigbluebuttonbn/yui/src/imports/meta/imports.json create mode 100644 mod/bigbluebuttonbn/yui/src/modform/build.json create mode 100644 mod/bigbluebuttonbn/yui/src/modform/js/modform.js create mode 100644 mod/bigbluebuttonbn/yui/src/modform/meta/modform.json create mode 100644 mod/bigbluebuttonbn/yui/src/recordings/build.json create mode 100644 mod/bigbluebuttonbn/yui/src/recordings/js/helpers.js create mode 100644 mod/bigbluebuttonbn/yui/src/recordings/js/recordings.js create mode 100644 mod/bigbluebuttonbn/yui/src/recordings/meta/recordings.json create mode 100644 mod/bigbluebuttonbn/yui/src/rooms/build.json create mode 100644 mod/bigbluebuttonbn/yui/src/rooms/js/rooms.js create mode 100644 mod/bigbluebuttonbn/yui/src/rooms/meta/rooms.json create mode 100644 mod/choicegroup/README.md create mode 100644 mod/choicegroup/amd/build/choicegroupdatadisplay.min.js create mode 100644 mod/choicegroup/amd/build/select_all_choices.min.js create mode 100644 mod/choicegroup/amd/src/choicegroupdatadisplay.js create mode 100644 mod/choicegroup/amd/src/select_all_choices.js create mode 100644 mod/choicegroup/backup/moodle2/backup_choicegroup_activity_task.class.php create mode 100644 mod/choicegroup/backup/moodle2/backup_choicegroup_settingslib.php create mode 100644 mod/choicegroup/backup/moodle2/backup_choicegroup_stepslib.php create mode 100644 mod/choicegroup/backup/moodle2/restore_choicegroup_activity_task.class.php create mode 100644 mod/choicegroup/backup/moodle2/restore_choicegroup_stepslib.php create mode 100644 mod/choicegroup/classes/event/choice_removed.php create mode 100644 mod/choicegroup/classes/event/choice_updated.php create mode 100644 mod/choicegroup/classes/event/course_module_instance_list_viewed.php create mode 100644 mod/choicegroup/classes/event/course_module_viewed.php create mode 100644 mod/choicegroup/classes/event/report_viewed.php create mode 100644 mod/choicegroup/classes/external.php create mode 100644 mod/choicegroup/classes/output/mobile.php create mode 100644 mod/choicegroup/classes/privacy/provider.php create mode 100644 mod/choicegroup/db/access.php create mode 100644 mod/choicegroup/db/install.xml create mode 100644 mod/choicegroup/db/log.php create mode 100644 mod/choicegroup/db/mobile.php create mode 100644 mod/choicegroup/db/services.php create mode 100644 mod/choicegroup/db/upgrade.php create mode 100644 mod/choicegroup/index.php create mode 100644 mod/choicegroup/javascript.js create mode 100644 mod/choicegroup/lang/en/choicegroup.php create mode 100644 mod/choicegroup/lib.php create mode 100644 mod/choicegroup/mobile/js/courseview.js create mode 100644 mod/choicegroup/mobile/js/init.js create mode 100644 mod/choicegroup/mod_form.php create mode 100644 mod/choicegroup/pix/column.png create mode 100644 mod/choicegroup/pix/icon.gif create mode 100644 mod/choicegroup/pix/icon.svg create mode 100644 mod/choicegroup/pix/row.png create mode 100644 mod/choicegroup/renderer.php create mode 100644 mod/choicegroup/report.php create mode 100644 mod/choicegroup/settings.php create mode 100644 mod/choicegroup/styles.css create mode 100644 mod/choicegroup/styles_app.css create mode 100644 mod/choicegroup/templates/mobile_view_page.mustache create mode 100644 mod/choicegroup/version.php create mode 100644 mod/choicegroup/view.php create mode 100644 mod/choicegroup/yui/form/form.js create mode 100644 mod/publication/.gitlab-ci.yml create mode 100644 mod/publication/CHANGELOG.txt create mode 100644 mod/publication/README.md create mode 100644 mod/publication/amd/build/alignrows.min.js create mode 100644 mod/publication/amd/build/alignrows.min.js.map create mode 100644 mod/publication/amd/build/filesform.min.js create mode 100644 mod/publication/amd/build/filesform.min.js.map create mode 100644 mod/publication/amd/build/groupapprovalstatus.min.js create mode 100644 mod/publication/amd/build/groupapprovalstatus.min.js.map create mode 100644 mod/publication/amd/build/onlinetextpreview.min.js create mode 100644 mod/publication/amd/build/onlinetextpreview.min.js.map create mode 100644 mod/publication/amd/src/alignrows.js create mode 100644 mod/publication/amd/src/filesform.js create mode 100644 mod/publication/amd/src/groupapprovalstatus.js create mode 100644 mod/publication/amd/src/onlinetextpreview.js create mode 100644 mod/publication/backup/moodle2/backup_publication_activity_task.class.php create mode 100644 mod/publication/backup/moodle2/backup_publication_stepslib.php create mode 100644 mod/publication/backup/moodle2/restore_publication_activity_task.class.php create mode 100644 mod/publication/backup/moodle2/restore_publication_stepslib.php create mode 100644 mod/publication/classes/event/course_module_instance_list_viewed.php create mode 100644 mod/publication/classes/event/course_module_viewed.php create mode 100644 mod/publication/classes/event/publication_approval_changed.php create mode 100644 mod/publication/classes/event/publication_duedate_extended.php create mode 100644 mod/publication/classes/event/publication_file_deleted.php create mode 100644 mod/publication/classes/event/publication_file_imported.php create mode 100644 mod/publication/classes/event/publication_file_uploaded.php create mode 100644 mod/publication/classes/local/allfilestable/base.php create mode 100644 mod/publication/classes/local/allfilestable/group.php create mode 100644 mod/publication/classes/local/allfilestable/import.php create mode 100644 mod/publication/classes/local/allfilestable/upload.php create mode 100644 mod/publication/classes/local/filestable/base.php create mode 100644 mod/publication/classes/local/filestable/group.php create mode 100644 mod/publication/classes/local/filestable/import.php create mode 100644 mod/publication/classes/local/filestable/upload.php create mode 100644 mod/publication/classes/local/tests/base.php create mode 100644 mod/publication/classes/local/tests/publication.php create mode 100644 mod/publication/classes/observer.php create mode 100644 mod/publication/classes/privacy/provider.php create mode 100644 mod/publication/classes/report_editdates_integration.php create mode 100644 mod/publication/classes/search/activity.php create mode 100644 mod/publication/db/access.php create mode 100644 mod/publication/db/events.php create mode 100644 mod/publication/db/install.xml create mode 100644 mod/publication/db/messages.php create mode 100644 mod/publication/db/services.php create mode 100644 mod/publication/db/upgrade.php create mode 100644 mod/publication/externallib.php create mode 100644 mod/publication/grantextension.php create mode 100644 mod/publication/index.php create mode 100644 mod/publication/lang/en/deprecated.txt create mode 100644 mod/publication/lang/en/publication.php create mode 100644 mod/publication/lib.php create mode 100644 mod/publication/locallib.php create mode 100644 mod/publication/mod_form.php create mode 100644 mod/publication/mod_publication_allfiles_form.php create mode 100644 mod/publication/mod_publication_files_form.php create mode 100644 mod/publication/mod_publication_grantextension_form.php create mode 100644 mod/publication/onlinepreview.php create mode 100644 mod/publication/phpunit.xml create mode 100644 mod/publication/pix/icon.png create mode 100644 mod/publication/pix/icon.svg create mode 100644 mod/publication/pix/questionmark.png create mode 100644 mod/publication/pix/questionmark.svg create mode 100644 mod/publication/settings.php create mode 100644 mod/publication/styles.css create mode 100644 mod/publication/templates/approvaltooltip.mustache create mode 100644 mod/publication/tests/allfilestable_test.php create mode 100644 mod/publication/tests/generator/lib.php create mode 100644 mod/publication/tests/privacy_test.php create mode 100644 mod/publication/upload.php create mode 100644 mod/publication/upload_form.php create mode 100644 mod/publication/version.php create mode 100644 mod/publication/view.php create mode 100644 mod/scheduler/.travis.yml create mode 100644 mod/scheduler/README.txt create mode 100644 mod/scheduler/ajax.php create mode 100644 mod/scheduler/appointmentforms.php create mode 100644 mod/scheduler/backup/moodle2/backup_scheduler_activity_task.class.php create mode 100644 mod/scheduler/backup/moodle2/backup_scheduler_stepslib.php create mode 100644 mod/scheduler/backup/moodle2/restore_scheduler_activity_task.class.php create mode 100644 mod/scheduler/backup/moodle2/restore_scheduler_stepslib.php create mode 100644 mod/scheduler/bookingform.php create mode 100644 mod/scheduler/classes/event/appointment_base.php create mode 100644 mod/scheduler/classes/event/appointment_list_viewed.php create mode 100644 mod/scheduler/classes/event/booking_added.php create mode 100644 mod/scheduler/classes/event/booking_form_viewed.php create mode 100644 mod/scheduler/classes/event/booking_removed.php create mode 100644 mod/scheduler/classes/event/course_module_instance_list_viewed.php create mode 100644 mod/scheduler/classes/event/scheduler_base.php create mode 100644 mod/scheduler/classes/event/slot_added.php create mode 100644 mod/scheduler/classes/event/slot_base.php create mode 100644 mod/scheduler/classes/event/slot_deleted.php create mode 100644 mod/scheduler/classes/model/appointment.php create mode 100644 mod/scheduler/classes/model/appointment_factory.php create mode 100644 mod/scheduler/classes/model/mvc_child_list.php create mode 100644 mod/scheduler/classes/model/mvc_child_model_factory.php create mode 100644 mod/scheduler/classes/model/mvc_child_record_model.php create mode 100644 mod/scheduler/classes/model/mvc_model.php create mode 100644 mod/scheduler/classes/model/mvc_model_factory.php create mode 100644 mod/scheduler/classes/model/mvc_record_model.php create mode 100644 mod/scheduler/classes/model/scheduler.php create mode 100644 mod/scheduler/classes/model/slot.php create mode 100644 mod/scheduler/classes/model/slot_factory.php create mode 100644 mod/scheduler/classes/permission/permissions_manager.php create mode 100644 mod/scheduler/classes/permission/scheduler_permissions.php create mode 100644 mod/scheduler/classes/privacy/provider.php create mode 100644 mod/scheduler/classes/search/activity.php create mode 100644 mod/scheduler/classes/task/purge_unused_slots.php create mode 100644 mod/scheduler/classes/task/send_reminders.php create mode 100644 mod/scheduler/customlib.php create mode 100644 mod/scheduler/datelist.php create mode 100644 mod/scheduler/db/access.php create mode 100644 mod/scheduler/db/install.xml create mode 100644 mod/scheduler/db/messages.php create mode 100644 mod/scheduler/db/tasks.php create mode 100644 mod/scheduler/db/upgrade.php create mode 100644 mod/scheduler/export.php create mode 100644 mod/scheduler/exportform.php create mode 100644 mod/scheduler/exportlib.php create mode 100644 mod/scheduler/index.php create mode 100644 mod/scheduler/lang/en/scheduler.php create mode 100644 mod/scheduler/lib.php create mode 100644 mod/scheduler/locallib.php create mode 100644 mod/scheduler/mailtemplatelib.php create mode 100644 mod/scheduler/message_form.php create mode 100644 mod/scheduler/mod_form.php create mode 100644 mod/scheduler/pix/attachment.png create mode 100644 mod/scheduler/pix/attachment.svg create mode 100644 mod/scheduler/pix/icon.gif create mode 100644 mod/scheduler/pix/icon.png create mode 100644 mod/scheduler/pix/icon.svg create mode 100644 mod/scheduler/pix/ticked.gif create mode 100644 mod/scheduler/pix/unticked.gif create mode 100644 mod/scheduler/renderable.php create mode 100644 mod/scheduler/renderer.php create mode 100644 mod/scheduler/settings.php create mode 100644 mod/scheduler/slotforms.php create mode 100644 mod/scheduler/studentview.controller.php create mode 100644 mod/scheduler/studentview.php create mode 100644 mod/scheduler/styles.css create mode 100644 mod/scheduler/teacherview.controller.php create mode 100644 mod/scheduler/teacherview.php create mode 100644 mod/scheduler/tests/behat/add_slots.feature create mode 100644 mod/scheduler/tests/behat/behat_mod_scheduler.php create mode 100644 mod/scheduler/tests/behat/conflicts.feature create mode 100644 mod/scheduler/tests/behat/group_availability.feature create mode 100644 mod/scheduler/tests/behat/groupmode.feature create mode 100644 mod/scheduler/tests/behat/groupscheduling.feature create mode 100644 mod/scheduler/tests/behat/notes.feature create mode 100644 mod/scheduler/tests/behat/officehours.feature create mode 100644 mod/scheduler/tests/behat/studentdata.feature create mode 100644 mod/scheduler/tests/behat/teacherpermissions.feature create mode 100644 mod/scheduler/tests/behat/tutorappointments.feature create mode 100644 mod/scheduler/tests/behat/viewslots.feature create mode 100644 mod/scheduler/tests/fixtures/studentfile.txt create mode 100644 mod/scheduler/tests/generator/lib.php create mode 100644 mod/scheduler/tests/model_test.php create mode 100644 mod/scheduler/tests/permissions_test.php create mode 100644 mod/scheduler/tests/privacy_test.php create mode 100644 mod/scheduler/tests/scheduler_test.php create mode 100644 mod/scheduler/tests/slot_test.php create mode 100644 mod/scheduler/version.php create mode 100644 mod/scheduler/view.php create mode 100644 mod/scheduler/viewstatistics.php create mode 100644 mod/scheduler/viewstudent.php create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-debug.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-min.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-debug.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-min.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-debug.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-min.js create mode 100644 mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist.js create mode 100644 mod/scheduler/yui/src/delselected/build.json create mode 100644 mod/scheduler/yui/src/delselected/js/delselected.js create mode 100644 mod/scheduler/yui/src/delselected/meta/delselected.json create mode 100644 mod/scheduler/yui/src/saveseen/build.json create mode 100644 mod/scheduler/yui/src/saveseen/js/saveseen.js create mode 100644 mod/scheduler/yui/src/saveseen/meta/saveseen.json create mode 100644 mod/scheduler/yui/src/studentlist/build.json create mode 100644 mod/scheduler/yui/src/studentlist/js/studentlist.js create mode 100644 mod/scheduler/yui/src/studentlist/meta/studentlist.json create mode 100644 theme/adaptable/.eslintignore create mode 100644 theme/adaptable/.eslintrc create mode 100644 theme/adaptable/.travis.yml create mode 100644 theme/adaptable/CONTRIBUTING.txt create mode 100644 theme/adaptable/COPYING.txt create mode 100644 theme/adaptable/Changes.md create mode 100644 theme/adaptable/Gruntfile.js create mode 100644 theme/adaptable/README.md create mode 100644 theme/adaptable/amd/build/adaptable.min.js create mode 100644 theme/adaptable/amd/build/bsoptions.min.js create mode 100644 theme/adaptable/amd/build/drawer.min.js create mode 100644 theme/adaptable/amd/build/savebutton.min.js create mode 100644 theme/adaptable/amd/build/search-input.min.js create mode 100644 theme/adaptable/amd/build/showsidebar.min.js create mode 100644 theme/adaptable/amd/build/templatepreview.min.js create mode 100644 theme/adaptable/amd/build/utils.min.js create mode 100644 theme/adaptable/amd/build/zoomin.min.js create mode 100644 theme/adaptable/amd/src/adaptable.js create mode 100644 theme/adaptable/amd/src/bsoptions.js create mode 100644 theme/adaptable/amd/src/drawer.js create mode 100644 theme/adaptable/amd/src/savebutton.js create mode 100644 theme/adaptable/amd/src/search-input.js create mode 100644 theme/adaptable/amd/src/showsidebar.js create mode 100644 theme/adaptable/amd/src/templatepreview.js create mode 100644 theme/adaptable/amd/src/utils.js create mode 100644 theme/adaptable/amd/src/zoomin.js create mode 100644 theme/adaptable/classes/activity.php create mode 100644 theme/adaptable/classes/activity_meta.php create mode 100644 theme/adaptable/classes/admin_settingspage_tabs.php create mode 100644 theme/adaptable/classes/output/core/core_renderer.php create mode 100644 theme/adaptable/classes/output/core/course_renderer.php create mode 100644 theme/adaptable/classes/output/core_user/myprofile/editprofile.php create mode 100644 theme/adaptable/classes/output/core_user/myprofile/editprofile_form.php create mode 100644 theme/adaptable/classes/output/core_user/myprofile/renderer.php create mode 100644 theme/adaptable/classes/output/mod_forum/email/renderer_htmlemail.php create mode 100644 theme/adaptable/classes/output/mod_forum/email/renderer_textemail.php create mode 100644 theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_htmlemail.php create mode 100644 theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_textemail.php create mode 100644 theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_htmlemail.php create mode 100644 theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_textemail.php create mode 100644 theme/adaptable/classes/output/mustache_filesystemstring_loader.php create mode 100644 theme/adaptable/classes/output/mustache_renderer.php create mode 100644 theme/adaptable/classes/output/mustachesource_renderer.php create mode 100644 theme/adaptable/classes/output/topcoll_course_renderer.php create mode 100644 theme/adaptable/classes/privacy/provider.php create mode 100644 theme/adaptable/classes/toolbox.php create mode 100644 theme/adaptable/classes/traits/null_object.php create mode 100644 theme/adaptable/classes/traits/single_section_page.php create mode 100644 theme/adaptable/config.php create mode 100644 theme/adaptable/db/caches.php create mode 100644 theme/adaptable/db/upgrade.php create mode 100644 theme/adaptable/jquery/adaptable_v2_1_1_2.js create mode 100644 theme/adaptable/jquery/jquery-easing-min.js create mode 100644 theme/adaptable/jquery/jquery-flexslider-min.js create mode 100644 theme/adaptable/jquery/pace-min.js create mode 100644 theme/adaptable/jquery/plugins.php create mode 100644 theme/adaptable/jquery/tickerme.js create mode 100644 theme/adaptable/lang/en/theme_adaptable.php create mode 100644 theme/adaptable/layout/columns1.php create mode 100644 theme/adaptable/layout/columns2.php create mode 100644 theme/adaptable/layout/course.php create mode 100644 theme/adaptable/layout/dashboard.php create mode 100644 theme/adaptable/layout/embedded.php create mode 100644 theme/adaptable/layout/frontpage.php create mode 100644 theme/adaptable/layout/includes/footer.php create mode 100644 theme/adaptable/layout/includes/head.php create mode 100644 theme/adaptable/layout/includes/header.php create mode 100644 theme/adaptable/layout/includes/loginnofooter.php create mode 100644 theme/adaptable/layout/includes/loginnoheader.php create mode 100644 theme/adaptable/layout/login.php create mode 100644 theme/adaptable/layout/maintenance.php create mode 100644 theme/adaptable/layout/secure.php create mode 100644 theme/adaptable/lib.php create mode 100644 theme/adaptable/libs/admin_confightmleditor.php create mode 100644 theme/adaptable/package.json create mode 100644 theme/adaptable/pix/2xlogo.png create mode 100644 theme/adaptable/pix/bkg.png create mode 100644 theme/adaptable/pix/favicon.ico create mode 100644 theme/adaptable/pix/icon.png create mode 100644 theme/adaptable/pix/layout-builder/12-0-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/3-3-3-3.png create mode 100644 theme/adaptable/pix/layout-builder/3-3-6-0.png create mode 100644 theme/adaptable/pix/layout-builder/3-6-3-0.png create mode 100644 theme/adaptable/pix/layout-builder/3-9-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/4-4-4-0.png create mode 100644 theme/adaptable/pix/layout-builder/4-8-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/5-7-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/6-3-3-0.png create mode 100644 theme/adaptable/pix/layout-builder/6-6-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/7-5-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/8-4-0-0.png create mode 100644 theme/adaptable/pix/layout-builder/9-3-0-0.png create mode 100644 theme/adaptable/pix/layout.png create mode 100644 theme/adaptable/pix/next.png create mode 100644 theme/adaptable/pix/previous.png create mode 100644 theme/adaptable/pix/quiz/arrow.png create mode 100644 theme/adaptable/pix/quiz/qanswered.png create mode 100644 theme/adaptable/pix/quiz/qcheck.png create mode 100644 theme/adaptable/pix/quiz/qdash.png create mode 100644 theme/adaptable/pix/quiz/qwhite.png create mode 100644 theme/adaptable/pix/quiz/qwrong.png create mode 100644 theme/adaptable/pix/screenshot.png create mode 100644 theme/adaptable/pix/search2.png create mode 100644 theme/adaptable/pix/tile-background.png create mode 100644 theme/adaptable/pix_core/f/archive.svg create mode 100644 theme/adaptable/pix_core/f/audio.svg create mode 100644 theme/adaptable/pix_core/f/avi.svg create mode 100644 theme/adaptable/pix_core/f/bmp.svg create mode 100644 theme/adaptable/pix_core/f/calc.svg create mode 100644 theme/adaptable/pix_core/f/chart.svg create mode 100644 theme/adaptable/pix_core/f/database.svg create mode 100644 theme/adaptable/pix_core/f/document.svg create mode 100644 theme/adaptable/pix_core/f/eps.svg create mode 100644 theme/adaptable/pix_core/f/flash.svg create mode 100644 theme/adaptable/pix_core/f/folder.svg create mode 100644 theme/adaptable/pix_core/f/gif.svg create mode 100644 theme/adaptable/pix_core/f/html.svg create mode 100644 theme/adaptable/pix_core/f/image.svg create mode 100644 theme/adaptable/pix_core/f/impress.svg create mode 100644 theme/adaptable/pix_core/f/jpeg.svg create mode 100644 theme/adaptable/pix_core/f/markup.svg create mode 100644 theme/adaptable/pix_core/f/mov.svg create mode 100644 theme/adaptable/pix_core/f/mp3.svg create mode 100644 theme/adaptable/pix_core/f/mpeg.svg create mode 100644 theme/adaptable/pix_core/f/oth.svg create mode 100644 theme/adaptable/pix_core/f/pdf.svg create mode 100644 theme/adaptable/pix_core/f/png.svg create mode 100644 theme/adaptable/pix_core/f/powerpoint.svg create mode 100644 theme/adaptable/pix_core/f/quicktime.svg create mode 100644 theme/adaptable/pix_core/f/sourcecode.svg create mode 100644 theme/adaptable/pix_core/f/spreadsheet.svg create mode 100644 theme/adaptable/pix_core/f/svg.svg create mode 100644 theme/adaptable/pix_core/f/text.svg create mode 100644 theme/adaptable/pix_core/f/unknown.svg create mode 100644 theme/adaptable/pix_core/f/url.svg create mode 100644 theme/adaptable/pix_core/f/video.svg create mode 100644 theme/adaptable/pix_core/f/wav.svg create mode 100644 theme/adaptable/pix_core/f/wmv.svg create mode 100644 theme/adaptable/pix_core/f/writer.svg create mode 100644 theme/adaptable/pix_core/i/loading.gif create mode 100644 theme/adaptable/pix_core/i/loading_small.gif create mode 100644 theme/adaptable/pix_core/i/notifications-black.png create mode 100644 theme/adaptable/pix_core/i/notifications-black.svg create mode 100644 theme/adaptable/pix_core/i/notifications.png create mode 100644 theme/adaptable/pix_core/i/notifications.svg create mode 100644 theme/adaptable/pix_core/s/angry.png create mode 100644 theme/adaptable/pix_core/s/approve.png create mode 100644 theme/adaptable/pix_core/s/biggrin.png create mode 100644 theme/adaptable/pix_core/s/blackeye.png create mode 100644 theme/adaptable/pix_core/s/blush.png create mode 100644 theme/adaptable/pix_core/s/clown.png create mode 100644 theme/adaptable/pix_core/s/cool.png create mode 100644 theme/adaptable/pix_core/s/dead.png create mode 100644 theme/adaptable/pix_core/s/evil.png create mode 100644 theme/adaptable/pix_core/s/heart.png create mode 100644 theme/adaptable/pix_core/s/kiss.png create mode 100644 theme/adaptable/pix_core/s/mixed.png create mode 100644 theme/adaptable/pix_core/s/no.png create mode 100644 theme/adaptable/pix_core/s/sad.png create mode 100644 theme/adaptable/pix_core/s/shy.png create mode 100644 theme/adaptable/pix_core/s/sleepy.png create mode 100644 theme/adaptable/pix_core/s/smiley.png create mode 100644 theme/adaptable/pix_core/s/surprise.png create mode 100644 theme/adaptable/pix_core/s/thoughtful.png create mode 100644 theme/adaptable/pix_core/s/tongueout.png create mode 100644 theme/adaptable/pix_core/s/wideeyes.png create mode 100644 theme/adaptable/pix_core/s/wink.png create mode 100644 theme/adaptable/pix_core/s/yes.png create mode 100644 theme/adaptable/pix_core/t/message-black.png create mode 100644 theme/adaptable/pix_core/t/message-black.svg create mode 100644 theme/adaptable/pix_core/t/message.png create mode 100644 theme/adaptable/pix_core/t/message.svg create mode 100644 theme/adaptable/pix_plugins/mod/activequiz/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/activequiz/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/adaptivequiz/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/adaptivequiz/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/assign/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/assign/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/assignment/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/assignment/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/attendance/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/attendance/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/basiclti/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/basiclti/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/book/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/book/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/booking/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/booking/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/certificate/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/certificate/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/chat/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/chat/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/checklist/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/checklist/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/choice/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/choice/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/choicegroup/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/choicegroup/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/courseguide/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/courseguide/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/customcert/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/customcert/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/data/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/data/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/equella/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/equella/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/feedback/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/feedback/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/flashcard/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/flashcard/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/folder/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/folder/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/forum/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/forum/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/glossary/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/glossary/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/helixmedia/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/helixmedia/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/hotpot/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/hotpot/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/hotquestion/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/hotquestion/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/hsuforum/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/hsuforum/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/hvp/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/hvp/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/imscp/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/imscp/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/jclic/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/jclic/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/journal/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/journal/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/label/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/label/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/lesson/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/lesson/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/lightboxgallery/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/lightboxgallery/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/lti/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/lti/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/mapleta/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/mapleta/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/mediagallery/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/mediagallery/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/page/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/page/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/pearson/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/pearson/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/peerwork/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/peerwork/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/questionnaire/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/questionnaire/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/quiz/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/quiz/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/recordingsbn/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/recordingsbn/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/resource/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/resource/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/scheduler/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/scheduler/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/scorm/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/scorm/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/structlabel/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/structlabel/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/survey/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/survey/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/turnitintool/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/turnitintool/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/url/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/url/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/wiki/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/wiki/icon.svg create mode 100644 theme/adaptable/pix_plugins/mod/workshop/icon.png create mode 100644 theme/adaptable/pix_plugins/mod/workshop/icon.svg create mode 100644 theme/adaptable/renderers.php create mode 100644 theme/adaptable/scss/card-blocks.scss create mode 100644 theme/adaptable/settings.php create mode 100644 theme/adaptable/settings/adaptable_admin_setting_configtemplate.php create mode 100644 theme/adaptable/settings/adaptable_admin_setting_getprops.php create mode 100644 theme/adaptable/settings/adaptable_admin_setting_putprops.php create mode 100644 theme/adaptable/settings/alert_box.php create mode 100644 theme/adaptable/settings/analytics.php create mode 100644 theme/adaptable/settings/array_definitions.php create mode 100644 theme/adaptable/settings/block_regions.php create mode 100644 theme/adaptable/settings/block_settings.php create mode 100644 theme/adaptable/settings/buttons.php create mode 100644 theme/adaptable/settings/category_headers.php create mode 100644 theme/adaptable/settings/colors.php create mode 100644 theme/adaptable/settings/course_formats.php create mode 100644 theme/adaptable/settings/custom_css.php create mode 100644 theme/adaptable/settings/dash_block_regions.php create mode 100644 theme/adaptable/settings/fonts.php create mode 100644 theme/adaptable/settings/footer.php create mode 100644 theme/adaptable/settings/frontpage_courses.php create mode 100644 theme/adaptable/settings/frontpage_slider.php create mode 100644 theme/adaptable/settings/frontpage_ticker.php create mode 100644 theme/adaptable/settings/header.php create mode 100644 theme/adaptable/settings/header_menus.php create mode 100644 theme/adaptable/settings/header_navbar_menu.php create mode 100644 theme/adaptable/settings/header_social.php create mode 100644 theme/adaptable/settings/header_user.php create mode 100644 theme/adaptable/settings/importexport_settings.php create mode 100644 theme/adaptable/settings/layout.php create mode 100644 theme/adaptable/settings/layout_responsive.php create mode 100644 theme/adaptable/settings/login.php create mode 100644 theme/adaptable/settings/marketing_blocks.php create mode 100644 theme/adaptable/settings/navbar_links.php create mode 100644 theme/adaptable/settings/navbar_settings.php create mode 100644 theme/adaptable/settings/navbar_styles.php create mode 100644 theme/adaptable/settings/print.php create mode 100644 theme/adaptable/settings/templates.php create mode 100644 theme/adaptable/settings/user.php create mode 100644 theme/adaptable/style/adaptable.css create mode 100644 theme/adaptable/style/backup-restore.css create mode 100644 theme/adaptable/style/blocks.css create mode 100644 theme/adaptable/style/bootstrap.css create mode 100644 theme/adaptable/style/browser.css create mode 100644 theme/adaptable/style/button.css create mode 100644 theme/adaptable/style/cardblocks.css create mode 100644 theme/adaptable/style/categorycustom.css create mode 100644 theme/adaptable/style/core.css create mode 100644 theme/adaptable/style/course.css create mode 100644 theme/adaptable/style/custom.css create mode 100644 theme/adaptable/style/extras.css create mode 100644 theme/adaptable/style/form.css create mode 100644 theme/adaptable/style/header.css create mode 100644 theme/adaptable/style/menu.css create mode 100644 theme/adaptable/style/messages.css create mode 100644 theme/adaptable/style/navigation.css create mode 100644 theme/adaptable/style/notifications.css create mode 100644 theme/adaptable/style/print.css create mode 100644 theme/adaptable/style/responsive.css create mode 100644 theme/adaptable/style/tabs.css create mode 100644 theme/adaptable/style/user.css create mode 100644 theme/adaptable/templates/adaptable_admin_setting_configtemplate.mustache create mode 100644 theme/adaptable/templates/adaptable_admin_setting_configtemplate_nopreview.mustache create mode 100644 theme/adaptable/templates/adaptable_admin_setting_configtemplate_source.mustache create mode 100644 theme/adaptable/templates/adaptable_admin_setting_tabs.mustache create mode 100644 theme/adaptable/templates/core/modal.mustache create mode 100644 theme/adaptable/templates/core/preferences_groups.mustache create mode 100644 theme/adaptable/templates/core/progress_bar.mustache create mode 100644 theme/adaptable/templates/core_course/activity_navigation.mustache create mode 100644 theme/adaptable/templates/core_message/message_drawer.mustache create mode 100644 theme/adaptable/templates/core_message/message_popover.mustache create mode 100644 theme/adaptable/templates/header.mustache create mode 100644 theme/adaptable/templates/headerloginform.mustache create mode 100644 theme/adaptable/templates/headernavbar.mustache create mode 100644 theme/adaptable/templates/headersearch.mustache create mode 100644 theme/adaptable/templates/headersocial.mustache create mode 100644 theme/adaptable/templates/headerstyleone.mustache create mode 100644 theme/adaptable/templates/headerstyletwo.mustache create mode 100644 theme/adaptable/templates/overlaymenu.mustache create mode 100644 theme/adaptable/templates/overlaymenuitem.mustache create mode 100644 theme/adaptable/templates/savediscard.mustache create mode 100644 theme/adaptable/templates/tabs.mustache create mode 100644 theme/adaptable/templates/tool_usertours/tourstep.mustache create mode 100644 theme/adaptable/templates/usermenu.mustache create mode 100644 theme/adaptable/tests/PHPUNIT_COMMANDS.txt create mode 100644 theme/adaptable/tests/adaptabletoolbox_test.php create mode 100644 theme/adaptable/version.php diff --git a/auth/ldap_syncplus/.travis.yml b/auth/ldap_syncplus/.travis.yml new file mode 100644 index 0000000..bcab70b --- /dev/null +++ b/auth/ldap_syncplus/.travis.yml @@ -0,0 +1,48 @@ +language: php + +addons: + postgresql: "9.6" + +services: + - mysql + - postgresql + - docker + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.2 + - 7.3 + - 7.4 + +env: + global: + - MOODLE_BRANCH=MOODLE_310_STABLE + matrix: + - DB=pgsql + - DB=mysqli + +before_install: + - phpenv config-rm xdebug.ini + - cd ../.. + - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat --dump diff --git a/auth/ldap_syncplus/CHANGES.md b/auth/ldap_syncplus/CHANGES.md new file mode 100644 index 0000000..9ff8406 --- /dev/null +++ b/auth/ldap_syncplus/CHANGES.md @@ -0,0 +1,113 @@ +moodle-auth_ldap_syncplus +========================= + +Changes +------- + +### v3.10-r1 + +* 2020-12-11 - Adopt code changes from Moodle 3.10 core auth_ldap. +* 2020-12-11 - Prepare compatibility for Moodle 3.10. +* 2020-12-10 - Change in Moodle release support: + For the time being, this plugin is maintained for the most recent LTS release of Moodle as well as the most recent major release of Moodle. + Bugfixes are backported to the LTS release. However, new features and improvements are not necessarily backported to the LTS release. +* 2020-12-10 - Improvement: Declare which major stable version of Moodle this plugin supports (see MDL-59562 for details). + +### v3.9-r1 + +* 2020-09-18 - Prepare compatibility for Moodle 3.9. +* 2020-02-26 - Added Behat tests. + +### v3.8-r1 + +* 2020-02-19 - Adopt code changes Moodle 3.8 core auth_ldap. +* 2020-02-19 - Prepare compatibility for Moodle 3.8. + +### v3.7-r1 + +* 2019-08-15 - Make codechecker happy. +* 2019-08-15 - Prepare compatibility for Moodle 3.7. + +### v3.6-r1 + +* 2019-01-29 - Check compatibility for Moodle 3.6, no functionality change. + +### v3.5-r2 + +* 2019-01-29 - Adopt code changes Moodle 3.5 core auth_ldap (MDL-63887). +* 2018-12-05 - Changed travis.yml due to upstream changes. + +### v3.5-r1 + +* 2018-06-25 - Bugfix: Creating users and first logins resulted in a fatal error in 3.5 because of a visibility change of update_user_record() in Moodle core. +* 2018-06-25 - Check compatibility for Moodle 3.5, no functionality change. + +### v3.4-r4 + +* 2018-05-16 - Implement Privacy API. + +### v3.4-r3 + +* 2018-02-07 - Bugfix: Login via email for first-time LDAP logins did not work if multiple LDAP contexts were configured; Credits to derhelge. + +### v3.4-r2 + +* 2018-02-07 - Add forgotten sync_roles task definition + +### v3.4-r1 + +* 2018-02-07 - Adopt code changes in Moodle 3.4 core auth_ldap: Assign arbitrary system roles via LDAP sync. +* 2018-02-06 - Check compatibility for Moodle 3.4, no functionality change. + +### v3.3-r1 + +* 2018-02-02 - Adopt code changes in Moodle 3.3 core auth_ldap: Sync user profile fields +* 2018-02-02 - Adopt code changes in Moodle 3.3 core auth_ldap: Convert auth plugins to use settings.php. Please double-check your plugin settings after upgrading to this version. +* 2017-12-12 - Prepare compatibility for Moodle 3.3, no functionality change. +* 2017-12-05 - Added Workaround to travis.yml for fixing Behat tests with TravisCI. +* 2017-11-08 - Updated travis.yml to use newer node version for fixing TravisCI error. + +### v3.2-r4 + +* 2017-05-29 - Add Travis CI support + +### v3.2-r3 + +* 2017-05-05 - Improve README.md + +### v3.2-r2 + +* 2017-03-03 - Adopt code changes in Moodle 3.2 core auth_ldap + +### v3.2-r1 + +* 2017-01-13 - Check compatibility for Moodle 3.2, no functionality change +* 2017-01-13 - Adopt code changes in Moodle 3.2 core auth_ldap +* 2017-01-12 - Move Changelog from README.md to CHANGES.md + +### v3.1-r1 + +* 2016-07-19 - Adopt code changes in Moodle core auth_ldap, adding the possibility to sync the "suspended" attribute +* 2016-07-19 - Check compatibility for Moodle 3.1, no functionality change + +### Changes before v3.1 + +* 2016-03-20 - Edit README to reflect the current naming of the User account syncronisation setting, no functionality change +* 2016-02-10 - Change plugin version and release scheme to the scheme promoted by moodle.org, no functionality change +* 2016-01-01 - Adopt code changes in Moodle core auth_ldap, including the new scheduled task feature. If you have used a LDAP syncronization cron job before, please use the LDAP syncronisation scheduled task from now on (for details, see "Configuring LDAP synchronization task" section below) +* 2016-01-01 - Check compatibility for Moodle 3.0, no functionality change +* 2015-08-18 - Check compatibility for Moodle 2.9, no functionality change +* 2015-08-18 - Adopt a code change in Moodle core auth_ldap +* 2015-01-29 - Check compatibility for Moodle 2.8, no functionality change +* 2015-01-23 - Adopt a code change in Moodle core auth_ldap +* 2014-10-08 - Adopt a code change in Moodle core auth_ldap +* 2014-09-12 - Bugfix: Fetching user details from LDAP on manual user creation didn't work in some circumstances +* 2014-09-02 - Bugfix: Check if LDAP auth is really used on manual user creation +* 2014-08-29 - Support login via email for first-time LDAP logins (MDL-46638) +* 2014-08-29 - Update version.php +* 2014-08-29 - Update README file +* 2014-08-27 - Change line breaks to mtrace() (MDL-30589) +* 2014-08-25 - Support new event API, remove legacy event handling +* 2014-07-31 - Add event handler for "user_created" event (see "Fetching user details from LDAP on manual user creation" below for details - MDL-47029) +* 2014-06-30 - Check compatibility for Moodle 2.7, no functionality change +* 2014-03-12 - Initial version diff --git a/auth/ldap_syncplus/COPYING.txt b/auth/ldap_syncplus/COPYING.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/auth/ldap_syncplus/COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/auth/ldap_syncplus/README.md b/auth/ldap_syncplus/README.md new file mode 100644 index 0000000..0aa7bd7 --- /dev/null +++ b/auth/ldap_syncplus/README.md @@ -0,0 +1,205 @@ +moodle-auth_ldap_syncplus +========================= + +[](https://travis-ci.com/moodleuulm/moodle-auth_ldap_syncplus) + +Moodle authentication plugin which provides all functionality of auth_ldap, but supports advanced features for the LDAP synchronization task and LDAP authentication. + + +Requirements +------------ + +This plugin requires Moodle 3.10+ + + +Motivation for this plugin +-------------------------- + +Moodle core's auth_ldap authentication plugin is a great basis for authenticating users in Moodle. However, as Moodle core's auth_ldap is somehow limited in several aspects and there is no prospect to have it improved in Moodle core, we have implemented an extended version for LDAP authentication with these key features: + +* The most important part: All functions from auth_ldap are still working if you use this authentication plugin. + +* The plugin adds the possibility to the LDAP synchronization task to suspend users which have disappeared in LDAP for a configurable amount of days and delete them only after this grace period (the Moodle core LDAP synchronization task only provides you the option to suspend _or_ delete users which have disappeared in LDAP - MDL-47018). + +* You can prevent the LDAP synchronization task from creating Moodle accounts for all LDAP users if they have never logged into Moodle before (the Moodle core LDAP synchronization task always creates Moodle accounts for all LDAP users - MDL-29249). + +* You can fetch user details from LDAP on manual user creation (MDL-47029). + +* It supports login via email for first-time LDAP logins (Moodle core only supports login via email for existing Moodle users - MDL-46638) + +* It adds several line breaks to the output of the LDAP synchronization task to improve readability (MDL-30589). + + +Installation +------------ + +Install the plugin like any other plugin to folder +/auth/ldap_syncplus + +See http://docs.moodle.org/en/Installing_plugins for details on installing Moodle plugins + + +Usage & Settings +---------------- + +After installing the plugin, it does not do anything to Moodle yet. + +To configure the plugin and its behaviour, please visit: +Site administration -> Plugins -> Authentication -> Manage authentication -> LDAP server (Sync Plus) + +There, you configure the plugin with the same settings like you would configure the Moodle core LDAP authentication method. + +Please note that there are additional setting items in settings section "User account synchronisation" compared to the Moodle core LDAP authentication method: + +### 1. Removed ext user + +The setting "Removed ext user" has an additional option called "Suspend internal and fully delete internal after grace period". If you select this option, the synchronization task will suspend users which have disappeared in LDAP for a configurable amount of days and delete them only after this grace period. If the user reappears in LDAP within the grace period, his Moodle account is revived and he can login again into Moodle as he did before. + +### 2. Fully deleting grace period + +With the setting "Fully deleting grace period" (Default: 10 days), you can control the length of the grace period until a user account is fully deleted after it has disappeared from LDAP. + +### 3. Add new users + +With the setting "Add new users" (Default: yes), you can prevent the synchronization task from creating Moodle accounts for all LDAP users if they have never logged into Moodle before. + +After configuring the LDAP server (Sync Plus) authentication method, you have to activate the plugin on Site administration -> Plugins -> Authentication -> Manage authentication so that users can be authenticated with this authentication method. Afterwards, you can deactivate the Moodle core LDAP authentication method as it is not needed anymore actively. + + +Configuring LDAP User account synchronisation +--------------------------------------------- + +To leverage the additional LDAP synchronization features of auth_ldap_syncplus, you have to disable the scheduled task of the Moodle core auth_ldap plugin and activate and configure the scheduled task of auth_ldap_syncplus. This is done on Site administration -> Server -> Scheduled tasks. + +If you don't know how to setup LDAP User account synchronisation at all, see https://docs.moodle.org/en/LDAP_authentication#Enabling_the_LDAP_users_sync_job. + + +Configuring LDAP User role synchronisation +------------------------------------------ + +In addition to the LDAP user account synchronisation, there is a LDAP user role synchronisation. LDAP user role synchronisation task in auth_ldap_syncplus does not provide any benefits over the LDAP user role synchronisation in Moodle core auth_ldap yet. However, to keep things in one place and if you want to synchronize LDAP user roles, you should activate and configure the scheduled task of auth_ldap_syncplus instead of auth_ldap. This is done on Site administration -> Server -> Scheduled tasks. + +If you don't know about the LDAP user role synchronisation at all, see https://docs.moodle.org/en/LDAP_authentication#Assign_system_roles. + + +Migrating from auth_ldap to auth_ldap_syncplus +---------------------------------------------- + +If you already have users in your Moodle installation who authenticate using the auth_ldap authentication method and want to switch them to auth_ldap_syncplus, proceed this way: + +* Configure auth_ldap_syncplus as an _additional_ authentication method while keeping auth_ldap activated. + +* Create a test user and set his authentication method to auth_ldap_syncplus. Test if this user is able to log into Moodle properly. + +* Switch all existing users to the auth_ldap_syncplus authentication method by running the following SQL command in your Moodle database: +`UPDATE mdl_user SET auth='ldap_syncplus' WHERE auth='ldap'` + +* Disable auth_ldap authentication method. + + +Fetching user details from LDAP on manual user creation +------------------------------------------------------- + +Normally, when a new user logs into Moodle for the first time and a Moodle account is automatically created, Moodle pulls the user's details from LDAP and stores them in the Moodle user profile according to the LDAP plugin's settings. + +auth_ldap_syncplus extends this behaviour of pulling user details from LDAP: +With auth_ldap_syncplus, you can create an user manually on Site administration -> Users -> Accounts -> Add a new user. The only thing you have to specify correctly is the username (which corresponds to the username in LDAP). All other details like first name or email address can be filled with placeholder content. After you click the "Create user" button, Moodle pulls the other user's details from LDAP and creates the user account correctly with the details from LDAP. + +This feature is enabled automatically and can be used as soon as you are using auth_ldap_syncplus as your LDAP authentication plugin like described above. + + +How this plugin works +--------------------- + +This plugin is implemented with minimal code duplication in mind. It inherits / requires as much code as possible from auth_ldap and only implements the extended functionalities. + + +Theme support +------------- + +This plugin acts behind the scenes, therefore it should work with all Moodle themes. +This plugin is developed and tested on Moodle Core's Boost theme. +It should also work with Boost child themes, including Moodle Core's Classic theme. However, we can't support any other theme than Boost. + +Plugin repositories +------------------- + +This plugin is published and regularly updated in the Moodle plugins repository: +http://moodle.org/plugins/view/auth_ldap_syncplus + +The latest development version can be found on Github: +https://github.com/moodleuulm/moodle-auth_ldap_syncplus + + +Bug and problem reports / Support requests +------------------------------------------ + +This plugin is carefully developed and thoroughly tested, but bugs and problems can always appear. + +Please report bugs and problems on Github: +https://github.com/moodleuulm/moodle-auth_ldap_syncplus/issues + +We will do our best to solve your problems, but please note that due to limited resources we can't always provide per-case support. + + +Feature proposals +----------------- + +Due to limited resources, the functionality of this plugin is primarily implemented for our own local needs and published as-is to the community. We are aware that members of the community will have other needs and would love to see them solved by this plugin. + +Please issue feature proposals on Github: +https://github.com/moodleuulm/moodle-auth_ldap_syncplus/issues + +Please create pull requests on Github: +https://github.com/moodleuulm/moodle-auth_ldap_syncplus/pulls + +We are always interested to read about your feature proposals or even get a pull request from you, but please accept that we can handle your issues only as feature _proposals_ and not as feature _requests_. + + +Moodle release support +---------------------- + +Due to limited resources, this plugin is only maintained for the most recent major release of Moodle as well as the most recent LTS release of Moodle. Bugfixes are backported to the LTS release. However, new features and improvements are not necessarily backported to the LTS release. + +Apart from these maintained releases, previous versions of this plugin which work in legacy major releases of Moodle are still available as-is without any further updates in the Moodle Plugins repository. + +There may be several weeks after a new major release of Moodle has been published until we can do a compatibility check and fix problems if necessary. If you encounter problems with a new major release of Moodle - or can confirm that this plugin still works with a new major release - please let us know on Github. + +If you are running a legacy version of Moodle, but want or need to run the latest version of this plugin, you can get the latest version of the plugin, remove the line starting with $plugin->requires from version.php and use this latest plugin version then on your legacy Moodle. However, please note that you will run this setup completely at your own risk. We can't support this approach in any way and there is an undeniable risk for erratic behavior. + + +Translating this plugin +----------------------- + +This Moodle plugin is shipped with an english language pack only. All translations into other languages must be managed through AMOS (https://lang.moodle.org) by what they will become part of Moodle's official language pack. + +As the plugin creator, we manage the translation into german for our own local needs on AMOS. Please contribute your translation into all other languages in AMOS where they will be reviewed by the official language pack maintainers for Moodle. + + +Right-to-left support +--------------------- + +This plugin has not been tested with Moodle's support for right-to-left (RTL) languages. +If you want to use this plugin with a RTL language and it doesn't work as-is, you are free to send us a pull request on Github with modifications. + + +Contribution to Moodle Core +--------------------------- + +There is a Moodle tracker ticket on https://tracker.moodle.org/browse/MDL-47030 which proposes to add the improved features of this plugin to Moodle core auth_ldap plugin. + +Please vote for this ticket if you want to have this realized. + + +PHP7 Support +------------ + +Since Moodle 3.4 core, PHP7 is mandatory. We are developing and testing this plugin for PHP7 only. + + +Copyright +--------- + +Ulm University +Communication and Information Centre (kiz) +Alexander Bias diff --git a/auth/ldap_syncplus/auth.php b/auth/ldap_syncplus/auth.php new file mode 100644 index 0000000..7f5fe45 --- /dev/null +++ b/auth/ldap_syncplus/auth.php @@ -0,0 +1,615 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +// @codingStandardsIgnoreFile +// Let codechecker ignore this file. This code mostly re-used from auth_ldap and the problems are already there and not made by us. + +global $CFG; + +require_once($CFG->libdir.'/authlib.php'); +require_once($CFG->libdir.'/ldaplib.php'); +require_once($CFG->dirroot.'/user/lib.php'); +require_once($CFG->dirroot.'/auth/ldap/locallib.php'); +require_once(__DIR__.'/../ldap/auth.php'); +require_once(__DIR__.'/locallib.php'); + +/** + * Auth plugin "LDAP SyncPlus" - Auth class + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class auth_plugin_ldap_syncplus extends auth_plugin_ldap { + + /** + * Constructor with initialisation. + */ + public function __construct() { + $this->authtype = 'ldap_syncplus'; + $this->roleauth = 'auth_ldap'; + $this->errorlogtag = '[AUTH LDAP SYNCPLUS] '; + $this->init_plugin($this->authtype); + } + + /** + * Old syntax of class constructor. Deprecated in PHP7. + * + * @deprecated since Moodle 3.1 + */ + public function auth_plugin_ldap_syncplus() { + debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); + self::__construct(); + } + + + /** + * Syncronizes user fron external LDAP server to moodle user table + * + * Sync is now using username attribute. + * + * Syncing users removes or suspends users that dont exists anymore in external LDAP. + * Creates new users and updates coursecreator status of users. + * + * @param bool $do_updates will do pull in data updates from LDAP if relevant + */ + function sync_users($do_updates=true) { + global $CFG, $DB; + + require_once($CFG->dirroot . '/user/profile/lib.php'); + + mtrace(get_string('connectingldap', 'auth_ldap')); + $ldapconnection = $this->ldap_connect(); + + $dbman = $DB->get_manager(); + + // Define table user to be created. + $table = new xmldb_table('tmp_extuser'); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username')); + + mtrace(get_string('creatingtemptable', 'auth_ldap', 'tmp_extuser')); + $dbman->create_temp_table($table); + + // Get user's list from ldap to sql in a scalable fashion. + // Prepare some data we'll need. + $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; + $servercontrols = array(); + + $contexts = explode(';', $this->config->contexts); + + if (!empty($this->config->create_context)) { + array_push($contexts, $this->config->create_context); + } + + $ldappagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); + $ldapcookie = ''; + foreach ($contexts as $context) { + $context = trim($context); + if (empty($context)) { + continue; + } + + do { + if ($ldappagedresults) { + // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11). + if (version_compare(PHP_VERSION, '7.3.0', '<')) { + // Before 7.3, use this function that was deprecated in PHP 7.4. + ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldapcookie); + } else { + // PHP 7.3 and up, use server controls. + $servercontrols = array(array( + 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( + 'size' => $this->config->pagesize, 'cookie' => $ldapcookie))); + } + } + if ($this->config->search_sub) { + // Use ldap_search to find first user from subtree. + // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11). + if (version_compare(PHP_VERSION, '7.3.0', '<')) { + $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute)); + } else { + $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), + 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); + } + } else { + // Search only in this context. + // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11). + if (version_compare(PHP_VERSION, '7.3.0', '<')) { + $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute)); + } else { + $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), + 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); + } + } + if(!$ldapresult) { + continue; + } + if ($ldappagedresults) { + // Get next server cookie to know if we'll need to continue searching. + $ldapcookie = ''; + // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 3.11). + if (version_compare(PHP_VERSION, '7.3.0', '<')) { + // Before 7.3, use this function that was deprecated in PHP 7.4. + $pagedresp = ldap_control_paged_result_response($ldapconnection, $ldapresult, $ldapcookie); + // Function ldap_control_paged_result_response() does not overwrite $ldapcookie if it fails, by + // setting this to null we avoid an infinite loop. + if ($pagedresp === false) { + $ldapcookie = null; + } + } else { + // Get next cookie from controls. + ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn, + $errmsg, $referrals, $controls); + if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { + $ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; + } + } + } + if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) { + do { + $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); + $value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8'); + $value = trim($value); + $this->ldap_bulk_insert($value); + } while ($entry = ldap_next_entry($ldapconnection, $entry)); + } + unset($ldapresult); // Free mem. + } while ($ldappagedresults && $ldapcookie !== null && $ldapcookie != ''); + } + + // If LDAP paged results were used, the current connection must be completely + // closed and a new one created, to work without paged results from here on. + if ($ldappagedresults) { + $this->ldap_close(true); + $ldapconnection = $this->ldap_connect(); + } + + // Preserve our user database. + // If the temp table is empty, it probably means that something went wrong, exit + // so as to avoid mass deletion of users; which is hard to undo. + $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}'); + if ($count < 1) { + mtrace(get_string('didntgetusersfromldap', 'auth_ldap')); + $dbman->drop_table($table); + $this->ldap_close(); + return false; + } else { + mtrace(get_string('gotcountrecordsfromldap', 'auth_ldap', $count)); + } + + + // Non Grace Period Synchronisation. + if ($this->config->removeuser != AUTH_REMOVEUSER_DELETEWITHGRACEPERIOD) { + + // User removal. + // Find users in DB that aren't in ldap -- to be removed! + // this is still not as scalable (but how often do we mass delete?). + + if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { + $sql = "SELECT u.* + FROM {user} u + LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) + WHERE u.auth = :auth + AND u.deleted = 0 + AND e.username IS NULL"; + $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); + + if (!empty($remove_users)) { + mtrace(get_string('userentriestoremove', 'auth_ldap', count($remove_users))); + + foreach ($remove_users as $user) { + if (delete_user($user)) { + mtrace("\t".get_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))); + } else { + mtrace("\t".get_string('auth_dbdeleteusererror', 'auth_db', $user->username)); + } + } + } else { + mtrace(get_string('nouserentriestoremove', 'auth_ldap')); + } + unset($remove_users); // Free mem! + + } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { + $sql = "SELECT u.* + FROM {user} u + LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) + WHERE u.auth = :auth + AND u.deleted = 0 + AND u.suspended = 0 + AND e.username IS NULL"; + $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); + + if (!empty($remove_users)) { + mtrace(get_string('userentriestoremove', 'auth_ldap', count($remove_users))); + + foreach ($remove_users as $user) { + $updateuser = new stdClass(); + $updateuser->id = $user->id; + $updateuser->suspended = 1; + user_update_user($updateuser, false); + mtrace("\t".get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))); + \core\session\manager::kill_user_sessions($user->id); + } + } else { + mtrace(get_string('nouserentriestoremove', 'auth_ldap')); + } + unset($remove_users); // Free mem! + } + + // Revive suspended users. + if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { + $sql = "SELECT u.id, u.username + FROM {user} u + JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) + WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0"; + // Note: 'nologin' is there for backwards compatibility. + $revive_users = $DB->get_records_sql($sql, array($this->authtype)); + + if (!empty($revive_users)) { + mtrace(get_string('userentriestorevive', 'auth_ldap', count($revive_users))); + + foreach ($revive_users as $user) { + $updateuser = new stdClass(); + $updateuser->id = $user->id; + $updateuser->auth = $this->authtype; + $updateuser->suspended = 0; + user_update_user($updateuser, false); + mtrace("\t".get_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))); + } + } else { + mtrace(get_string('nouserentriestorevive', 'auth_ldap')); + } + + unset($revive_users); + } + } + + // Grace Period Synchronisation. + else if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_DELETEWITHGRACEPERIOD) { + + // Revive suspended users. + $sql = "SELECT u.id, u.username + FROM {user} u + JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) + WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0"; + // Note: 'nologin' is there for backwards compatibility. + $revive_users = $DB->get_records_sql($sql, array($this->authtype)); + + if (!empty($revive_users)) { + mtrace(get_string('userentriestorevive', 'auth_ldap', count($revive_users))); + + foreach ($revive_users as $user) { + $updateuser = new stdClass(); + $updateuser->id = $user->id; + $updateuser->auth = $this->authtype; + $updateuser->suspended = 0; + user_update_user($updateuser, false); + mtrace("\t".get_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))); + } + } else { + mtrace(get_string('nouserentriestorevive', 'auth_ldap')); + } + unset($revive_users); + + // User temporary suspending. + $sql = "SELECT u.* + FROM {user} u + LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) + WHERE u.auth = :auth + AND u.deleted = 0 + AND u.suspended = 0 + AND e.username IS NULL"; + $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); + + if (!empty($remove_users)) { + mtrace(get_string('userentriestosuspend', 'auth_ldap_syncplus', count($remove_users))); + + foreach ($remove_users as $user) { + $updateuser = new stdClass(); + $updateuser->id = $user->id; + $updateuser->suspended = 1; + $updateuser->timemodified = time(); // Remember suspend time, abuse timemodified column for this. + user_update_user($updateuser, false); + mtrace("\t".get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))); + \core\session\manager::kill_user_sessions($user->id); + } + } else { + mtrace(get_string('nouserentriestosuspend', 'auth_ldap_syncplus')); + } + unset($remove_users); // Free mem! + + // User complete removal. + $sql = "SELECT u.* + FROM {user} u + LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) + WHERE u.auth = :auth + AND u.deleted = 0 + AND e.username IS NULL"; + $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); + + if (!empty($remove_users)) { + mtrace(get_string('userentriestoremove', 'auth_ldap', count($remove_users))); + + foreach ($remove_users as $user) { + // Do only if user was suspended before grace period. + $graceperiod = max(intval($this->config->removeuser_graceperiod), 0); + // Fix problems if grace period setting was negative or no number. + if (time() - $user->timemodified >= $graceperiod * 24 * 3600) { + if (delete_user($user)) { + mtrace("\t".get_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))); + } else { + mtrace("\t".get_string('auth_dbdeleteusererror', 'auth_db', $user->username)); + } + // Otherwise inform about ongoing grace period. + } else { + mtrace("\t".get_string('waitinginremovalqueue', 'auth_ldap_syncplus', array('days'=>$graceperiod, 'name'=>$user->username, 'id'=>$user->id))); + } + } + } else { + mtrace(get_string('nouserentriestoremove', 'auth_ldap')); + } + unset($remove_users); // Free mem! + } + + // User Updates - time-consuming (optional). + if ($do_updates) { + // Narrow down what fields we need to update. + $updatekeys = $this->get_profile_keys(); + } else { + mtrace(get_string('noupdatestobedone', 'auth_ldap')); + } + if ($do_updates and !empty($updatekeys)) { // run updates only if relevant. + $users = $DB->get_records_sql('SELECT u.username, u.id + FROM {user} u + WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', + array($this->authtype, $CFG->mnet_localhost_id)); + if (!empty($users)) { + mtrace(get_string('userentriestoupdate', 'auth_ldap', count($users))); + + $transaction = $DB->start_delegated_transaction(); + $xcount = 0; + $maxxcount = 100; + + foreach ($users as $user) { + $userinfo = $this->get_userinfo($user->username); + if (!$this->update_user_record($user->username, $updatekeys, true, + $this->is_user_suspended((object) $userinfo))) { + $skipped = ' - '.get_string('skipped'); + } + else { + $skipped = ''; + } + mtrace("\t".get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)).$skipped); + $xcount++; + + // Update system roles, if needed. + $this->sync_roles($user); + } + $transaction->allow_commit(); + unset($users); // free mem. + } + } else { // end do updates. + mtrace(get_string('noupdatestobedone', 'auth_ldap')); + } + + // User Additions. + // Find users missing in DB that are in LDAP + // and gives me a nifty object I don't want. + // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin. + if (!empty($this->config->sync_script_createuser_enabled) and $this->config->sync_script_createuser_enabled == 1) { + $sql = 'SELECT e.id, e.username + FROM {tmp_extuser} e + LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid) + WHERE u.id IS NULL'; + $add_users = $DB->get_records_sql($sql); + + if (!empty($add_users)) { + mtrace(get_string('userentriestoadd', 'auth_ldap', count($add_users))); + + $transaction = $DB->start_delegated_transaction(); + foreach ($add_users as $user) { + $user = $this->get_userinfo_asobj($user->username); + + // Prep a few params. + $user->modified = time(); + $user->confirmed = 1; + $user->auth = $this->authtype; + $user->mnethostid = $CFG->mnet_localhost_id; + // get_userinfo_asobj() might have replaced $user->username with the value + // from the LDAP server (which can be mixed-case). Make sure it's lowercase. + $user->username = trim(core_text::strtolower($user->username)); + // It isn't possible to just rely on the configured suspension attribute since + // things like active directory use bit masks, other things using LDAP might + // do different stuff as well. + // + // The cast to int is a workaround for MDL-53959. + $user->suspended = (int)$this->is_user_suspended($user); + + if (empty($user->calendartype)) { + $user->calendartype = $CFG->calendartype; + } + + $id = user_create_user($user, false); + mtrace("\t".get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id))); + $euser = $DB->get_record('user', array('id' => $id)); + + if (!empty($this->config->forcechangepassword)) { + set_user_preference('auth_forcepasswordchange', 1, $id); + } + + // Save custom profile fields. + $this->update_user_record($user->username, $this->get_profile_keys(true), false); + + // Add roles if needed. + $this->sync_roles($euser); + } + $transaction->allow_commit(); + unset($add_users); // free mem + } else { + mtrace(get_string('nouserstobeadded', 'auth_ldap')); + } + } else { + mtrace(get_string('nouserstobeadded', 'auth_ldap')); + } + + $dbman->drop_table($table); + $this->ldap_close(); + + return true; + } + + + /** + * Support login via email ($CFG->authloginviaemail) for first-time LDAP logins + * @return void + */ + public function loginpage_hook() { + global $CFG, $frm, $DB; + + // If $CFG->authloginviaemail is not set, users don't want to login by mail, call parent hook and return. + if ($CFG->authloginviaemail != 1) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Get submitted form data. + $frm = data_submitted(); + + // If there is no username submitted, there's nothing to do, call parent hook and return. + if (empty($frm->username)) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Clean username parameter to make sure that its an email address. + $email = clean_param($frm->username, PARAM_EMAIL); + + // If we don't have an email adress, there's nothing to do, call parent hook and return. + if ($email == '' || strpos($email, '@') == false) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // If there is an existing useraccount with this email adress as email address (then a Moodle account already exists and + // the standard mechanism of $CFG->authloginviaemail will kick in automatically) or if there is an existing useraccount + // with this email adress as username (which is not forbidden, so this useraccount has to be used), call parent hook and + // return. + if ($DB->count_records_select('user', '(username = :p1 OR email = :p2) AND deleted = 0', + array('p1' => $email, 'p2' => $email)) > 0) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Get auth plugin. + $authplugin = get_auth_plugin('ldap_syncplus'); + + // If there is no email field mapping configured, we don't know where we can find the email adress in LDAP, + // call parent hook and return. + if (empty($authplugin->config->field_map_email)) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Prepare LDAP search. + $contexts = explode(';', $authplugin->config->contexts); + $filter = '(&('.$authplugin->config->field_map_email.'='.ldap_filter_addslashes($email).')'. + $authplugin->config->objectclass.')'; + + // Connect to LDAP. + $ldapconnection = $authplugin->ldap_connect(); + + // Array for saving the user's ids which are found in the configured LDAP contexts. + $uidsfound = array(); + + // Look for users matching the given email adress in LDAP. + foreach ($contexts as $context) { + // Verify that the given context is valid. + $context = trim($context); + if (empty($context)) { + continue; + } + + // Search LDAP. + if ($authplugin->config->search_sub) { + // Use ldap_search to find first user from subtree. + $ldapresult = ldap_search($ldapconnection, $context, $filter, array($authplugin->config->user_attribute)); + } else { + // Search only in this context. + $ldapresult = ldap_list($ldapconnection, $context, $filter, array($authplugin->config->user_attribute)); + } + + // If there is no LDAP result or if the user was not found in this context, continue with next context. + if (!$ldapresult || ldap_count_entries($ldapconnection, $ldapresult) == 0) { + continue; + } + + // If there is not exactly one matching user, we can't continue, call parent hook and return. + if (ldap_count_entries($ldapconnection, $ldapresult) != 1) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Get this one matching user entry. + if (!$ldapentry = ldap_first_entry($ldapconnection, $ldapresult)) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Get the uid attribute's value(s) from this user entry. + $values = ldap_get_values($ldapconnection, $ldapentry, $authplugin->config->user_attribute); + + // If there is not exactly one copy of the uid attribute in the LDAP user entry, we don't know which one to use, + // call parent hook and return. + if ($values['count'] != 1) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + + // Remember this one user's uid attribute. + $uidsfound[] = $values[0]; + + unset($ldapresult); // Free mem! + } + + // After we have checked all contexts, verify that we have found only one user in total. + // If not, we can't continue, call parent hook and return. + if (count($uidsfound) != 1) { + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + + // Success! + // Replace the form data's username with the user attribute from LDAP, it will be held in the global $frm variable. + } else { + $frm->username = $uidsfound[0]; + parent::loginpage_hook(); // Call parent function to retain its functionality. + return; + } + } +} diff --git a/auth/ldap_syncplus/classes/privacy/provider.php b/auth/ldap_syncplus/classes/privacy/provider.php new file mode 100644 index 0000000..8b81de0 --- /dev/null +++ b/auth/ldap_syncplus/classes/privacy/provider.php @@ -0,0 +1,47 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Privacy provider + * + * @package auth_ldap_syncplus + * @copyright 2018 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace auth_ldap_syncplus\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementing null_provider. + * + * @package auth_ldap_syncplus + * @copyright 2018 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/auth/ldap_syncplus/classes/task/sync_roles.php b/auth/ldap_syncplus/classes/task/sync_roles.php new file mode 100644 index 0000000..2902544 --- /dev/null +++ b/auth/ldap_syncplus/classes/task/sync_roles.php @@ -0,0 +1,63 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Task definition + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace auth_ldap_syncplus\task; + +defined('MOODLE_INTERNAL') || die; + +/** + * The auth_ldap_syncplus scheduled task class for LDAP roles sync + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sync_roles extends \core\task\scheduled_task { + + /** + * Return localised task name. + * + * @return string + */ + public function get_name() { + return get_string('syncroles', 'auth_ldap_syncplus'); + } + + /** + * Execute scheduled task + * + * @return boolean + */ + public function execute() { + global $DB; + if (is_enabled_auth('ldap_syncplus')) { + $auth = get_auth_plugin('ldap_syncplus'); + $users = $DB->get_records('user', array('auth' => 'ldap_syncplus')); + foreach ($users as $user) { + $auth->sync_roles($user); + } + } + } + +} diff --git a/auth/ldap_syncplus/classes/task/sync_task.php b/auth/ldap_syncplus/classes/task/sync_task.php new file mode 100644 index 0000000..e91542f --- /dev/null +++ b/auth/ldap_syncplus/classes/task/sync_task.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Task definition + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace auth_ldap_syncplus\task; + +defined('MOODLE_INTERNAL') || die; + +/** + * The auth_ldap_syncplus scheduled task class for LDAP user sync + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sync_task extends \core\task\scheduled_task { + + /** + * Return localised task name. + * + * @return string + */ + public function get_name() { + return get_string('synctask', 'auth_ldap_syncplus'); + } + + /** + * Execute scheduled task + * + * @return boolean + */ + public function execute() { + global $CFG; + if (is_enabled_auth('ldap_syncplus')) { + $auth = get_auth_plugin('ldap_syncplus'); + $auth->sync_users(true); + } + } + +} diff --git a/auth/ldap_syncplus/cli/sync_users.php b/auth/ldap_syncplus/cli/sync_users.php new file mode 100644 index 0000000..2c1f592 --- /dev/null +++ b/auth/ldap_syncplus/cli/sync_users.php @@ -0,0 +1,52 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - CLI Script + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +// @codingStandardsIgnoreFile +// Let codechecker ignore this file. This code mostly re-used from auth_ldap and the problems are already there and not made by us. + +require(__DIR__.'/../../../config.php'); // global moodle config file. +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->libdir.'/clilib.php'); + +// Ensure errors are well explained +set_debugging(DEBUG_DEVELOPER, true); + +if (!is_enabled_auth('ldap_syncplus')) { + error_log('[AUTH LDAP SYNCPLUS] '.get_string('pluginnotenabled', 'auth_ldap')); + die; +} + +cli_problem('[AUTH LDAP SYNCPLUS] The users sync cron has been deprecated. Please use the scheduled task instead.'); + +// Abort execution of the CLI script if the auth_ldap_syncplus\task\sync_task is enabled. +$taskdisabled = \core\task\manager::get_scheduled_task('auth_ldap_syncplus\task\sync_task'); +if (!$taskdisabled->get_disabled()) { + cli_error('[AUTH LDAP SYNCPLUS] The scheduled task sync_task is enabled, the cron execution has been aborted.'); +} + +$ldapauth = get_auth_plugin('ldap_syncplus'); +$ldapauth->sync_users(true); + diff --git a/auth/ldap_syncplus/db/events.php b/auth/ldap_syncplus/db/events.php new file mode 100644 index 0000000..25b6efc --- /dev/null +++ b/auth/ldap_syncplus/db/events.php @@ -0,0 +1,33 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Event definition + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = array( + array( + 'eventname' => '\core\event\user_created', + 'includefile' => '/auth/ldap_syncplus/eventhandler.php', + 'callback' => 'update_user_onevent', + ), +); diff --git a/auth/ldap_syncplus/db/tasks.php b/auth/ldap_syncplus/db/tasks.php new file mode 100644 index 0000000..33fece1 --- /dev/null +++ b/auth/ldap_syncplus/db/tasks.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Auth plugin "LDAP SyncPlus" - Scheduled tasks + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = array( + array( + 'classname' => 'auth_ldap_syncplus\task\sync_roles', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '0', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + 'disabled' => 1 + ), + array( + 'classname' => 'auth_ldap_syncplus\task\sync_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '0', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + 'disabled' => 1 + ) +); diff --git a/auth/ldap_syncplus/db/upgrade.php b/auth/ldap_syncplus/db/upgrade.php new file mode 100644 index 0000000..69a9e11 --- /dev/null +++ b/auth/ldap_syncplus/db/upgrade.php @@ -0,0 +1,58 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Upgrade script + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Function to upgrade auth_ldap_syncplus. + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_auth_ldap_syncplus_upgrade($oldversion) { + global $DB; + + if ($oldversion < 2018020200) { + // Convert info in config plugins from auth/ldap_syncplus to auth_ldap_syncplus. + upgrade_fix_config_auth_plugin_names('ldap_syncplus'); + upgrade_fix_config_auth_plugin_defaults('ldap_syncplus'); + upgrade_plugin_savepoint(true, 2018020200, 'auth', 'ldap_syncplus'); + } + + if ($oldversion < 2018020601) { + // The "auth_ldap_syncplus/coursecreators" setting was replaced with "auth_ldap_syncplus/coursecreatorcontext" (created + // dynamically from system-assignable roles) - so migrate any existing value to the first new slot. + if ($ldapcontext = get_config('auth_ldap_syncplus', 'creators')) { + // Get info about the role that the old coursecreators setting would apply. + $creatorrole = get_archetype_roles('coursecreator'); + $creatorrole = array_shift($creatorrole); // We can only use one, let's use the first. + // Create new setting. + set_config($creatorrole->shortname . 'context', $ldapcontext, 'auth_ldap_syncplus'); + // Delete old setting. + set_config('creators', null, 'auth_ldap_syncplus'); + upgrade_plugin_savepoint(true, 2018020601, 'auth', 'ldap_syncplus'); + } + } + + return true; +} diff --git a/auth/ldap_syncplus/eventhandler.php b/auth/ldap_syncplus/eventhandler.php new file mode 100644 index 0000000..db12a5b --- /dev/null +++ b/auth/ldap_syncplus/eventhandler.php @@ -0,0 +1,55 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Event handler + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Event handler function. + * + * @param object $eventdata Event data + * @return void + */ +function update_user_onevent($eventdata) { + global $DB; + + // Do only if user id is enclosed in $eventdata. + if (!empty($eventdata->relateduserid)) { + + // Get user data. + $user = $DB->get_record('user', array('id' => $eventdata->relateduserid)); + + // Do if user was found. + if (!empty($user->username)) { + + // Do only if user has ldap_syncplus authentication. + if (isset($user->auth) && $user->auth == 'ldap_syncplus') { + + // Update user. + // Actually, we would want to call auth_plugin_base::update_user_record() + // which is lighter, but this function is unfortunately protected since Moodle 3.5. + update_user_record($user->username); + } + } + } +} diff --git a/auth/ldap_syncplus/lang/en/auth_ldap_syncplus.php b/auth/ldap_syncplus/lang/en/auth_ldap_syncplus.php new file mode 100644 index 0000000..13f5e12 --- /dev/null +++ b/auth/ldap_syncplus/lang/en/auth_ldap_syncplus.php @@ -0,0 +1,41 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Language pack + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'LDAP server (Sync Plus)'; +$string['auth_ldap_syncplusdescription'] = 'This method provides authentication against an external LDAP server. + If the given username and password are valid, Moodle creates a new user + entry in its database. This module can read user attributes from LDAP and prefill + wanted fields in Moodle. For following logins only the username and + password are checked.'; +$string['auth_remove_deletewithgraceperiod'] = 'Suspend internal and fully delete internal after grace period'; +$string['nouserentriestosuspend'] = 'No user entries to be suspended'; +$string['privacy:metadata'] = 'The LDAP server (Sync Plus) authentication plugin does not store any personal data.'; +$string['removeuser_graceperiod'] = 'Fully deleting grace period'; +$string['removeuser_graceperiod_desc'] = 'After suspending a user internally, the synchronization script will wait for this number of days until the user will be fully deleted internal. If the user re-appears in LDAP within this grace period, the user will be reactivated. Note: This setting is only used if "Removed ext user" is set to "Suspend internal and fully delete internal after grace period"'; +$string['sync_script_createuser_enabled'] = 'If enabled (default), the synchronization script will create Moodle accounts for all LDAP users if they have never logged into Moodle before. If disabled, the synchronization script will not create Moodle accounts for all LDAP users.'; +$string['sync_script_createuser_enabled_key'] = 'Add new users'; +$string['syncroles'] = 'LDAP roles sync job (Sync Plus)'; +$string['synctask'] = 'LDAP users sync job (Sync Plus)'; +$string['userentriestosuspend'] = 'User entries to be suspended: {$a}'; +$string['waitinginremovalqueue'] = 'Waiting in removal queue for {$a->days} day grace period: {$a->name} ID {$a->id}'; diff --git a/auth/ldap_syncplus/locallib.php b/auth/ldap_syncplus/locallib.php new file mode 100644 index 0000000..9ba09bf --- /dev/null +++ b/auth/ldap_syncplus/locallib.php @@ -0,0 +1,27 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Local library + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +define('AUTH_REMOVEUSER_DELETEWITHGRACEPERIOD', 3); diff --git a/auth/ldap_syncplus/settings.php b/auth/ldap_syncplus/settings.php new file mode 100644 index 0000000..7dff891 --- /dev/null +++ b/auth/ldap_syncplus/settings.php @@ -0,0 +1,356 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Settings + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + + if (!function_exists('ldap_connect')) { + $notify = new \core\output\notification(get_string('auth_ldap_noextension', 'auth_ldap'), + \core\output\notification::NOTIFY_WARNING); + $settings->add(new admin_setting_heading('auth_ldap_noextension', '', $OUTPUT->render($notify))); + } else { + + // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. + require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_lowercase_configtext.php'); + require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_contexts_configtext.php'); + require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_ntlm_configtext.php'); + + // We need to use some of the Moodle LDAP constants / functions to create the list of options. + require_once($CFG->dirroot.'/auth/ldap/auth.php'); + + // We need to use some of the Moodle LDAP Syncplus constants / functions to create the list of options. + require_once($CFG->dirroot.'/auth/ldap_syncplus/locallib.php'); + + // Introductory explanation. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/pluginname', '', + new lang_string('auth_ldapdescription', 'auth_ldap'))); + + // LDAP server settings. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/ldapserversettings', + new lang_string('auth_ldap_server_settings', 'auth_ldap'), '')); + + // Host. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/host_url', + get_string('auth_ldap_host_url_key', 'auth_ldap'), + get_string('auth_ldap_host_url', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + + // Version. + $versions = array(); + $versions[2] = '2'; + $versions[3] = '3'; + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/ldap_version', + new lang_string('auth_ldap_version_key', 'auth_ldap'), + new lang_string('auth_ldap_version', 'auth_ldap'), 3, $versions)); + + // Start TLS. + $yesno = array( + new lang_string('no'), + new lang_string('yes'), + ); + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/start_tls', + new lang_string('start_tls_key', 'auth_ldap'), + new lang_string('start_tls', 'auth_ldap'), 0 , $yesno)); + + + // Encoding. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/ldapencoding', + get_string('auth_ldap_ldap_encoding_key', 'auth_ldap'), + get_string('auth_ldap_ldap_encoding', 'auth_ldap'), 'utf-8', PARAM_RAW_TRIMMED)); + + // Page Size. (Hide if not available). + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/pagesize', + get_string('pagesize_key', 'auth_ldap'), + get_string('pagesize', 'auth_ldap'), '250', PARAM_INT)); + + // Bind settings. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/ldapbindsettings', + new lang_string('auth_ldap_bind_settings', 'auth_ldap'), '')); + + // Store Password in DB. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/preventpassindb', + new lang_string('auth_ldap_preventpassindb_key', 'auth_ldap'), + new lang_string('auth_ldap_preventpassindb', 'auth_ldap'), 0 , $yesno)); + + // User ID. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/bind_dn', + get_string('auth_ldap_bind_dn_key', 'auth_ldap'), + get_string('auth_ldap_bind_dn', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + + // Password. + $settings->add(new admin_setting_configpasswordunmask('auth_ldap_syncplus/bind_pw', + get_string('auth_ldap_bind_pw_key', 'auth_ldap'), + get_string('auth_ldap_bind_pw', 'auth_ldap'), '')); + + // User Lookup settings. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/ldapuserlookup', + new lang_string('auth_ldap_user_settings', 'auth_ldap'), '')); + + // User Type. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/user_type', + new lang_string('auth_ldap_user_type_key', 'auth_ldap'), + new lang_string('auth_ldap_user_type', 'auth_ldap'), 'default', ldap_supported_usertypes())); + + // Contexts. + $settings->add(new auth_ldap_admin_setting_special_contexts_configtext('auth_ldap_syncplus/contexts', + get_string('auth_ldap_contexts_key', 'auth_ldap'), + get_string('auth_ldap_contexts', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + + // Search subcontexts. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/search_sub', + new lang_string('auth_ldap_search_sub_key', 'auth_ldap'), + new lang_string('auth_ldap_search_sub', 'auth_ldap'), 0 , $yesno)); + + // Dereference aliases. + $optderef = array(); + $optderef[LDAP_DEREF_NEVER] = get_string('no'); + $optderef[LDAP_DEREF_ALWAYS] = get_string('yes'); + + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/opt_deref', + new lang_string('auth_ldap_opt_deref_key', 'auth_ldap'), + new lang_string('auth_ldap_opt_deref', 'auth_ldap'), LDAP_DEREF_NEVER , $optderef)); + + // User attribute. + $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap_syncplus/user_attribute', + get_string('auth_ldap_user_attribute_key', 'auth_ldap'), + get_string('auth_ldap_user_attribute', 'auth_ldap'), '', PARAM_RAW)); + + // Suspended attribute. + $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap_syncplus/suspended_attribute', + get_string('auth_ldap_suspended_attribute_key', 'auth_ldap'), + get_string('auth_ldap_suspended_attribute', 'auth_ldap'), '', PARAM_RAW)); + + // Member attribute. + $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap_syncplus/memberattribute', + get_string('auth_ldap_memberattribute_key', 'auth_ldap'), + get_string('auth_ldap_memberattribute', 'auth_ldap'), '', PARAM_RAW)); + + // Member attribute uses dn. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/memberattribute_isdn', + get_string('auth_ldap_memberattribute_isdn_key', 'auth_ldap'), + get_string('auth_ldap_memberattribute_isdn', 'auth_ldap'), '', PARAM_RAW)); + + // Object class. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/objectclass', + get_string('auth_ldap_objectclass_key', 'auth_ldap'), + get_string('auth_ldap_objectclass', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + + // Force Password change Header. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/ldapforcepasswordchange', + new lang_string('forcechangepassword', 'auth'), '')); + + // Force Password change. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/forcechangepassword', + new lang_string('forcechangepassword', 'auth'), + new lang_string('forcechangepasswordfirst_help', 'auth'), 0 , $yesno)); + + // Standard Password Change. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/stdchangepassword', + new lang_string('stdchangepassword', 'auth'), new lang_string('stdchangepassword_expl', 'auth') .' '. + get_string('stdchangepassword_explldap', 'auth'), 0 , $yesno)); + + // Password Type. + $passtype = array(); + $passtype['plaintext'] = get_string('plaintext', 'auth'); + $passtype['md5'] = get_string('md5', 'auth'); + $passtype['sha1'] = get_string('sha1', 'auth'); + + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/passtype', + new lang_string('auth_ldap_passtype_key', 'auth_ldap'), + new lang_string('auth_ldap_passtype', 'auth_ldap'), 'plaintext', $passtype)); + + // Password change URL. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/changepasswordurl', + get_string('auth_ldap_changepasswordurl_key', 'auth_ldap'), + get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); + + // Password Expiration Header. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/passwordexpire', + new lang_string('auth_ldap_passwdexpire_settings', 'auth_ldap'), '')); + + // Password Expiration. + + // Create the description lang_string object. + $strno = get_string('no'); + $strldapserver = get_string('pluginname', 'auth_ldap'); + $langobject = new stdClass(); + $langobject->no = $strno; + $langobject->ldapserver = $strldapserver; + $description = new lang_string('auth_ldap_expiration_desc', 'auth_ldap', $langobject); + + // Now create the options. + $expiration = array(); + $expiration['0'] = $strno; + $expiration['1'] = $strldapserver; + + // Add the setting. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/expiration', + new lang_string('auth_ldap_expiration_key', 'auth_ldap'), + $description, 0 , $expiration)); + + // Password Expiration warning. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/expiration_warning', + get_string('auth_ldap_expiration_warning_key', 'auth_ldap'), + get_string('auth_ldap_expiration_warning_desc', 'auth_ldap'), '', PARAM_RAW)); + + // Password Expiration attribute. + $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap_syncplus/expireattr', + get_string('auth_ldap_expireattr_key', 'auth_ldap'), + get_string('auth_ldap_expireattr_desc', 'auth_ldap'), '', PARAM_RAW)); + + // Grace Logins. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/gracelogins', + new lang_string('auth_ldap_gracelogins_key', 'auth_ldap'), + new lang_string('auth_ldap_gracelogins_desc', 'auth_ldap'), 0 , $yesno)); + + // Grace logins attribute. + $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap_syncplus/graceattr', + get_string('auth_ldap_gracelogin_key', 'auth_ldap'), + get_string('auth_ldap_graceattr_desc', 'auth_ldap'), '', PARAM_RAW)); + + // User Creation. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/usercreation', + new lang_string('auth_user_create', 'auth'), '')); + + // Create users externally. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/auth_user_create', + new lang_string('auth_ldap_auth_user_create_key', 'auth_ldap'), + new lang_string('auth_user_creation', 'auth'), 0 , $yesno)); + + // Context for new users. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/create_context', + get_string('auth_ldap_create_context_key', 'auth_ldap'), + get_string('auth_ldap_create_context', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + + // System roles mapping header. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/systemrolemapping', + new lang_string('systemrolemapping', 'auth_ldap'), '')); + + // Create system role mapping field for each assignable system role. + $roles = get_ldap_assignable_role_names(); + foreach ($roles as $role) { + // Before we can add this setting we need to check a few things. + // A) It does not exceed 100 characters otherwise it will break the DB as the 'name' field + // in the 'config_plugins' table is a varchar(100). + // B) The setting name does not contain hyphens. If it does then it will fail the check + // in parse_setting_name() and everything will explode. Role short names are validated + // against PARAM_ALPHANUMEXT which is similar to the regex used in parse_setting_name() + // except it also allows hyphens. + // Instead of shortening the name and removing/replacing the hyphens we are showing a warning. + // If we were to manipulate the setting name by removing the hyphens we may get conflicts, eg + // 'thisisashortname' and 'this-is-a-short-name'. The same applies for shortening the setting name. + if (core_text::strlen($role['settingname']) > 100 || !preg_match('/^[a-zA-Z0-9_]+$/', $role['settingname'])) { + $url = new moodle_url('/admin/roles/define.php', array('action' => 'edit', 'roleid' => $role['id'])); + $a = (object)['rolename' => $role['localname'], 'shortname' => $role['shortname'], 'charlimit' => 93, + 'link' => $url->out()]; + $settings->add(new admin_setting_heading('auth_ldap_syncplus/role_not_mapped_' . sha1($role['settingname']), '', + get_string('cannotmaprole', 'auth_ldap', $a))); + } else { + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/' . $role['settingname'], + get_string('auth_ldap_rolecontext', 'auth_ldap', $role), + get_string('auth_ldap_rolecontext_help', 'auth_ldap', $role), '', PARAM_RAW_TRIMMED)); + } + } + + // User Account Sync. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/syncusers', + new lang_string('auth_sync_script', 'auth'), '')); + + // Remove external user. + $deleteopt = array(); + $deleteopt[AUTH_REMOVEUSER_KEEP] = get_string('auth_remove_keep', 'auth'); + $deleteopt[AUTH_REMOVEUSER_SUSPEND] = get_string('auth_remove_suspend', 'auth'); + $deleteopt[AUTH_REMOVEUSER_FULLDELETE] = get_string('auth_remove_delete', 'auth'); + $deleteopt[AUTH_REMOVEUSER_DELETEWITHGRACEPERIOD] = get_string('auth_remove_deletewithgraceperiod', 'auth_ldap_syncplus'); + + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/removeuser', + new lang_string('auth_remove_user_key', 'auth'), + new lang_string('auth_remove_user', 'auth'), AUTH_REMOVEUSER_KEEP, $deleteopt)); + + // Remove external user grace period. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/removeuser_graceperiod', + get_string('removeuser_graceperiod', 'auth_ldap_syncplus'), + get_string('removeuser_graceperiod_desc', 'auth_ldap_syncplus'), 10, PARAM_INT)); + + // Create users. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/sync_script_createuser_enabled', + new lang_string('sync_script_createuser_enabled_key', 'auth_ldap_syncplus'), + new lang_string('sync_script_createuser_enabled', 'auth_ldap_syncplus'), 1, $yesno)); + + // Sync Suspension. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/sync_suspended', + new lang_string('auth_sync_suspended_key', 'auth'), + new lang_string('auth_sync_suspended', 'auth'), 0 , $yesno)); + + // NTLM SSO Header. + $settings->add(new admin_setting_heading('auth_ldap_syncplus/ntlm', + new lang_string('auth_ntlmsso', 'auth_ldap'), '')); + + // Enable NTLM. + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/ntlmsso_enabled', + new lang_string('auth_ntlmsso_enabled_key', 'auth_ldap'), + new lang_string('auth_ntlmsso_enabled', 'auth_ldap'), 0 , $yesno)); + + // Subnet. + $settings->add(new admin_setting_configtext('auth_ldap_syncplus/ntlmsso_subnet', + get_string('auth_ntlmsso_subnet_key', 'auth_ldap'), + get_string('auth_ntlmsso_subnet', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + + // NTLM Fast Path. + $fastpathoptions = array(); + $fastpathoptions[AUTH_NTLM_FASTPATH_YESFORM] = get_string('auth_ntlmsso_ie_fastpath_yesform', 'auth_ldap'); + $fastpathoptions[AUTH_NTLM_FASTPATH_YESATTEMPT] = get_string('auth_ntlmsso_ie_fastpath_yesattempt', 'auth_ldap'); + $fastpathoptions[AUTH_NTLM_FASTPATH_ATTEMPT] = get_string('auth_ntlmsso_ie_fastpath_attempt', 'auth_ldap'); + + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/ntlmsso_ie_fastpath', + new lang_string('auth_ntlmsso_ie_fastpath_key', 'auth_ldap'), + new lang_string('auth_ntlmsso_ie_fastpath', 'auth_ldap'), + AUTH_NTLM_FASTPATH_ATTEMPT, $fastpathoptions)); + + // Authentication type. + $types = array(); + $types['ntlm'] = 'NTLM'; + $types['kerberos'] = 'Kerberos'; + + $settings->add(new admin_setting_configselect('auth_ldap_syncplus/ntlmsso_type', + new lang_string('auth_ntlmsso_type_key', 'auth_ldap'), + new lang_string('auth_ntlmsso_type', 'auth_ldap'), 'ntlm', $types)); + + // Remote Username format. + $settings->add(new auth_ldap_admin_setting_special_ntlm_configtext('auth_ldap_syncplus/ntlmsso_remoteuserformat', + get_string('auth_ntlmsso_remoteuserformat_key', 'auth_ldap'), + get_string('auth_ntlmsso_remoteuserformat', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); + } + + // Display locking / mapping of profile fields. + $authplugin = get_auth_plugin('ldap_syncplus'); + $help = get_string('auth_ldapextrafields', 'auth_ldap'); + $help .= get_string('auth_updatelocal_expl', 'auth'); + $help .= get_string('auth_fieldlock_expl', 'auth'); + $help .= get_string('auth_updateremote_expl', 'auth'); + $help .= '<hr />'; + $help .= get_string('auth_updateremote_ldap', 'auth'); + display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, + $help, true, true, $authplugin->get_custom_user_profile_fields()); +} diff --git a/auth/ldap_syncplus/tests/behat/auth_ldap_syncplus.feature b/auth/ldap_syncplus/tests/behat/auth_ldap_syncplus.feature new file mode 100644 index 0000000..c366688 --- /dev/null +++ b/auth/ldap_syncplus/tests/behat/auth_ldap_syncplus.feature @@ -0,0 +1,15 @@ +@auth @auth_ldap_syncplus +Feature: Checking that all settings are shown + In order to be able to configure the auth_ldap_syncplus plugin + As admin + I need to be able to see the equivalent settings + + # This is the only check that is possible to do with Behat tests. The functionality behind cannot be tested with Behat tests. + Scenario: Check if all LDAP server (Sync Plus) settings are there + Given I log in as "admin" + And I navigate to "Plugins > Authentication > Manage authentication" in site administration + And I click on "Settings" "link" in the "LDAP server (Sync Plus)" "table_row" + Then I should see "LDAP server (Sync Plus)" in the "#region-main .settingsform" "css_element" + And the "Removed ext user" select box should contain "Suspend internal and fully delete internal after grace period" + And I should see "Fully deleting grace period" in the "#admin-removeuser_graceperiod" "css_element" + And I should see "Add new users" in the "#admin-sync_script_createuser_enabled" "css_element" diff --git a/auth/ldap_syncplus/version.php b/auth/ldap_syncplus/version.php new file mode 100644 index 0000000..c8d8209 --- /dev/null +++ b/auth/ldap_syncplus/version.php @@ -0,0 +1,33 @@ +<?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/>. + +/** + * Auth plugin "LDAP SyncPlus" - Version file + * + * @package auth_ldap_syncplus + * @copyright 2014 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'auth_ldap_syncplus'; +$plugin->version = 2020121100; +$plugin->release = 'v3.10-r1'; +$plugin->requires = 2020110900; +$plugin->supported = [310, 310]; +$plugin->maturity = MATURITY_STABLE; +$plugin->dependencies = array('auth_ldap' => 2020110900); diff --git a/block/admin_presets/.github/ISSUE_TEMPLATE/bug_report.md b/block/admin_presets/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/block/admin_presets/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/block/admin_presets/.travis.yml b/block/admin_presets/.travis.yml new file mode 100644 index 0000000..7351ed0 --- /dev/null +++ b/block/admin_presets/.travis.yml @@ -0,0 +1,45 @@ +sudo: required + +language: php + +dist: xenial + +services: + - mysql + +php: + - 7.2 + +env: + global: + - MOODLE_BRANCH=master + - IGNORE_PATHS=amd/build,amd/src/bootstrap.js + - IGNORE_NAMES=*.txt,moodle.css,moodle-rtl.css,moodle_min.css,editor.css,editor_min.css,Gruntfile.js + - DB=mysqli + +matrix: + - php: 7.2 + env: DB=mysqli TASK=PHPUNIT + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +before_install: + - cd ../.. + - composer selfupdate + - composer create-project -n --no-dev moodlerooms/moodle-plugin-ci ci ^1 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci csslint + - moodle-plugin-ci jshint + #- moodle-plugin-ci phpunit \ No newline at end of file diff --git a/block/admin_presets/README.md b/block/admin_presets/README.md new file mode 100644 index 0000000..703b046 --- /dev/null +++ b/block/admin_presets/README.md @@ -0,0 +1,31 @@ +# Admin presets for Moodle + +Block to export and import Moodle administration settings + +## Build status + +[](https://travis-ci.org/DigiDago/moodle-block_admin_presets) + +## Features + +* Export system settings to XML files +* Import presets files +* Preset preview and partial load +* Allows rollback +* Option to autoexclude the sensitive data when exporting settings (you can edit the sensitive settings list in Site Administration -> Plugins -> Blocks -> Admin presets) +* Third parties plugins supported + +## See also +* Modules and Plugins entry: https://moodle.org/plugins/view.php?plugin=block_admin_presets + + +Maintainer +============ +AdminPreset was initialy developed by David Monllaó. It is currently maintained by Pimenko team. + + +Any Problems, questions, suggestions +=================== +If you have a problem with this block, suggestions for improvement, drop an email at : +- Pimenko : contact@pimenko.com +- Github : https://github.com/DigiDago/moodle-block_admin_presets diff --git a/block/admin_presets/block_admin_presets.php b/block/admin_presets/block_admin_presets.php new file mode 100644 index 0000000..2182036 --- /dev/null +++ b/block/admin_presets/block_admin_presets.php @@ -0,0 +1,95 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +class block_admin_presets extends block_list { + + /** + * @throws coding_exception + */ + public function init() { + $this->title = get_string('pluginname', 'block_admin_presets'); + } + + public function get_content() { + + global $CFG, $OUTPUT; + + if (empty($this->instance)) { + $this->content = ''; + return $this->content; + } + + $this->content = new stdClass(); + $this->content->items = array(); + $this->content->icons = array(); + $this->content->footer = ''; + + if (!has_capability('moodle/site:config', context_system::instance())) { + $this->content = ''; + return $this->content; + } + + $this->content->items[] = $OUTPUT->pix_icon("i/backup", + get_string('actionexport', 'block_admin_presets'), + "moodle", array("class" => "icon")) . '<a title="' . + get_string('actionexport', 'block_admin_presets') . + '" href="' . $CFG->wwwroot . '/blocks/admin_presets/index.php?action=export">' . + get_string('actionexport', 'block_admin_presets') . + '</a>'; + + $this->content->items[] = $OUTPUT->pix_icon("i/restore", + get_string('actionimport', 'block_admin_presets'), + "moodle", array("class" => "icon")) . + '<a title="' . get_string('actionimport', 'block_admin_presets') . + '" href="' . $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=import">' . + get_string('actionimport', 'block_admin_presets') . + '</a>'; + + $this->content->items[] = $OUTPUT->pix_icon("i/repository", + get_string('actionbase', 'block_admin_presets'), + "moodle", array("class" => "icon")) . + '<a title="' . + get_string('actionbase', 'block_admin_presets') . + '" href="' . + $CFG->wwwroot . + '/blocks/admin_presets/index.php">' + . get_string('actionbase', 'block_admin_presets') . '</a>'; + + return $this->content; + } + + public function applicable_formats() { + return array('site' => true); + } + + public function has_config() { + return true; + } + +} diff --git a/block/admin_presets/classes/event/preset_deleted.php b/block/admin_presets/classes/event/preset_deleted.php new file mode 100644 index 0000000..39b956b --- /dev/null +++ b/block/admin_presets/classes/event/preset_deleted.php @@ -0,0 +1,51 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_deleted extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetdeleted', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has deleted the preset with id {$this->objectid}."; + } + + public function get_legacy_logdata() { + return array($this->courseid, 'block_admin_presets', 'delete', '', + $this->objectid, $this->contextinstanceid); + } + + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/preset_downloaded.php b/block/admin_presets/classes/event/preset_downloaded.php new file mode 100644 index 0000000..127c8db --- /dev/null +++ b/block/admin_presets/classes/event/preset_downloaded.php @@ -0,0 +1,41 @@ +<?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/>. + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_downloaded extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetdownloaded', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has downloaded the preset with id {$this->objectid}."; + } + + public function get_url() { + return new \moodle_url('/blocks/admin_presets/index.php', + array('action' => 'export', 'mode' => 'download_xml', 'id' => $this->objectid, 'sesskey' => sesskey())); + } + + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/preset_exported.php b/block/admin_presets/classes/event/preset_exported.php new file mode 100644 index 0000000..815aa7f --- /dev/null +++ b/block/admin_presets/classes/event/preset_exported.php @@ -0,0 +1,56 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_exported extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetexported', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has exported the preset with id {$this->objectid}."; + } + + public function get_url() { + return new \moodle_url('/blocks/admin_presets/index.php', + array('action' => 'load', 'mode' => 'preview', 'id' => $this->objectid)); + } + + public function get_legacy_logdata() { + return array($this->courseid, 'block_admin_presets', 'export', '', + $this->objectid, $this->contextinstanceid); + } + + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/preset_imported.php b/block/admin_presets/classes/event/preset_imported.php new file mode 100644 index 0000000..d25c236 --- /dev/null +++ b/block/admin_presets/classes/event/preset_imported.php @@ -0,0 +1,56 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_imported extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetimported', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has imported the preset with id {$this->objectid}."; + } + + public function get_url() { + return new \moodle_url('/blocks/admin_presets/index.php', + array('action' => 'load', 'mode' => 'preview', 'id' => $this->objectid)); + } + + public function get_legacy_logdata() { + return array($this->courseid, 'block_admin_presets', 'import', '', + $this->objectid, $this->contextinstanceid); + } + + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/preset_loaded.php b/block/admin_presets/classes/event/preset_loaded.php new file mode 100644 index 0000000..7ef0662 --- /dev/null +++ b/block/admin_presets/classes/event/preset_loaded.php @@ -0,0 +1,56 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_loaded extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetloaded', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has loaded the preset with id {$this->objectid}."; + } + + public function get_url() { + return new \moodle_url('/blocks/admin_presets/index.php', + array('action' => 'load', 'mode' => 'preview', 'id' => $this->objectid)); + } + + public function get_legacy_logdata() { + return array($this->courseid, 'block_admin_presets', 'load', '', + $this->objectid, $this->contextinstanceid); + } + + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/preset_previewed.php b/block/admin_presets/classes/event/preset_previewed.php new file mode 100644 index 0000000..92bc397 --- /dev/null +++ b/block/admin_presets/classes/event/preset_previewed.php @@ -0,0 +1,51 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_previewed extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetpreviewed', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has previewed the preset with id {$this->objectid}."; + } + + public function get_url() { + return new \moodle_url('/blocks/admin_presets/index.php', + array('action' => 'load', 'mode' => 'preview', 'id' => $this->objectid)); + } + + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/preset_reverted.php b/block/admin_presets/classes/event/preset_reverted.php new file mode 100644 index 0000000..51bf427 --- /dev/null +++ b/block/admin_presets/classes/event/preset_reverted.php @@ -0,0 +1,51 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class preset_reverted extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetreverted', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} has reverted the preset with id {$this->objectid}."; + } + + public function get_legacy_logdata() { + return array($this->courseid, 'block_admin_presets', 'rollback', '', + $this->objectid, $this->contextinstanceid); + } + + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/event/presets_listed.php b/block/admin_presets/classes/event/presets_listed.php new file mode 100644 index 0000000..3625448 --- /dev/null +++ b/block/admin_presets/classes/event/presets_listed.php @@ -0,0 +1,55 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_admin_presets\event; + +defined('MOODLE_INTERNAL') || die(); + +class presets_listed extends \core\event\base { + + public static function get_name() { + return get_string('eventpresetslisted', 'block_admin_presets'); + } + + public function get_description() { + return "User {$this->userid} listed the system presets."; + } + + public function get_url() { + return new \moodle_url('/block/admin_presets/index.php'); + } + + public function get_legacy_logdata() { + return array($this->courseid, 'block_admin_presets', 'base', '', + $this->objectid, $this->contextinstanceid); + } + + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'block_admin_presets'; + } +} diff --git a/block/admin_presets/classes/privacy/provider.php b/block/admin_presets/classes/privacy/provider.php new file mode 100644 index 0000000..c1a6f4f --- /dev/null +++ b/block/admin_presets/classes/privacy/provider.php @@ -0,0 +1,36 @@ +<?php +// This file is part of The Course Module Navigation Block +// +// 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/>. + +namespace block_admin_presets\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\metadata\null_provider; + +class provider implements + // This plugin does not store any personal user data. + null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:null_reason'; + } +} diff --git a/block/admin_presets/db/access.php b/block/admin_presets/db/access.php new file mode 100644 index 0000000..35ef3c3 --- /dev/null +++ b/block/admin_presets/db/access.php @@ -0,0 +1,39 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + 'block/admin_presets:addinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ) + ) +); + + diff --git a/block/admin_presets/db/install.xml b/block/admin_presets/db/install.xml new file mode 100644 index 0000000..26633fa --- /dev/null +++ b/block/admin_presets/db/install.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="blocks/admin_presets/db" VERSION="20120314" COMMENT="Admin presets block tables, to store exported/imported presets" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"> + <TABLES> + <TABLE NAME="block_admin_presets" COMMENT="Table to store presets data" NEXT="block_admin_presets_it"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="userid"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="id" NEXT="name"/> + <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="userid" + NEXT="comments"/> + <FIELD NAME="comments" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" PREVIOUS="name" + NEXT="site"/> + <FIELD NAME="site" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="comments" + NEXT="author"/> + <FIELD NAME="author" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" PREVIOUS="site" + NEXT="moodleversion"/> + <FIELD NAME="moodleversion" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false" PREVIOUS="author" + NEXT="moodlerelease"/> + <FIELD NAME="moodlerelease" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" + PREVIOUS="moodleversion" NEXT="timecreated"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" DEFAULT="0" + SEQUENCE="false" PREVIOUS="moodlerelease" NEXT="timeimported"/> + <FIELD NAME="timeimported" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" DEFAULT="0" + SEQUENCE="false" PREVIOUS="timecreated"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + <TABLE NAME="block_admin_presets_it" COMMENT="Table to store settings" PREVIOUS="block_admin_presets" + NEXT="block_admin_presets_it_a"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" + NEXT="adminpresetid"/> + <FIELD NAME="adminpresetid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="id" NEXT="plugin"/> + <FIELD NAME="plugin" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" PREVIOUS="adminpresetid" + NEXT="name"/> + <FIELD NAME="name" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" PREVIOUS="plugin" + NEXT="value"/> + <FIELD NAME="value" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" PREVIOUS="name"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="adminpresetid" UNIQUE="false" FIELDS="adminpresetid"/> + </INDEXES> + </TABLE> + <TABLE NAME="block_admin_presets_it_a" + COMMENT="Admin presets items attributes. For settings with attributes (extra values like 'advanced')" + PREVIOUS="block_admin_presets_it" NEXT="block_admin_presets_app"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="itemid"/> + <FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="id" NEXT="name"/> + <FIELD NAME="name" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" PREVIOUS="itemid" + NEXT="value"/> + <FIELD NAME="value" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" PREVIOUS="name"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="itemid" UNIQUE="false" FIELDS="itemid"/> + </INDEXES> + </TABLE> + <TABLE NAME="block_admin_presets_app" COMMENT="Applied presets" PREVIOUS="block_admin_presets_it_a" + NEXT="block_admin_presets_app_it"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" + NEXT="adminpresetid"/> + <FIELD NAME="adminpresetid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="id" NEXT="userid"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="adminpresetid" NEXT="time"/> + <FIELD NAME="time" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="userid"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="adminpresetid" UNIQUE="false" FIELDS="adminpresetid"/> + </INDEXES> + </TABLE> + <TABLE NAME="block_admin_presets_app_it" + COMMENT="Admin presets applied items. To maintain the relation with config_log" + PREVIOUS="block_admin_presets_app" NEXT="block_admin_presets_app_it_a"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" + NEXT="adminpresetapplyid"/> + <FIELD NAME="adminpresetapplyid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="id" NEXT="configlogid"/> + <FIELD NAME="configlogid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="adminpresetapplyid"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="configlogid" UNIQUE="false" FIELDS="configlogid" NEXT="adminpresetapplyid"/> + <INDEX NAME="adminpresetapplyid" UNIQUE="false" FIELDS="adminpresetapplyid" PREVIOUS="configlogid"/> + </INDEXES> + </TABLE> + <TABLE NAME="block_admin_presets_app_it_a" COMMENT="Attributes of the applied items" + PREVIOUS="block_admin_presets_app_it"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" + NEXT="adminpresetapplyid"/> + <FIELD NAME="adminpresetapplyid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="id" NEXT="configlogid"/> + <FIELD NAME="configlogid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" + PREVIOUS="adminpresetapplyid" NEXT="itemname"/> + <FIELD NAME="itemname" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" + COMMENT="Necessary to rollback" PREVIOUS="configlogid"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="configlogid" UNIQUE="false" FIELDS="configlogid" NEXT="adminpresetapplyid"/> + <INDEX NAME="adminpresetapplyid" UNIQUE="false" FIELDS="adminpresetapplyid" PREVIOUS="configlogid"/> + </INDEXES> + </TABLE> + </TABLES> +</XMLDB> \ No newline at end of file diff --git a/block/admin_presets/db/upgrade.php b/block/admin_presets/db/upgrade.php new file mode 100644 index 0000000..0a057c2 --- /dev/null +++ b/block/admin_presets/db/upgrade.php @@ -0,0 +1,100 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * @param int $oldversion + * @param object $block + * @return bool + * @throws coding_exception + * @throws ddl_exception + * @throws ddl_field_missing_exception + * @throws ddl_table_missing_exception + * @throws downgrade_exception + * @throws moodle_exception + * @throws upgrade_exception + * @global moodle_database $DB + */ +function xmldb_block_admin_presets_upgrade($oldversion, $block) { + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2011063000) { + + // Changing type of field moodleversion on table admin_preset to char. + $table = new xmldb_table('admin_preset'); + $field = new xmldb_field('moodleversion', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, null, 'author'); + + // Launch change of type for field moodleversion. + $dbman->change_field_type($table, $field); + + upgrade_block_savepoint(true, 2011063000, 'admin_presets'); + } + + // Renaming DB tables. + if ($oldversion < 2012031401) { + + $tablenamechanges = array('admin_preset' => 'block_admin_presets', + 'admin_preset_apply' => 'block_admin_presets_app', + 'admin_preset_apply_item' => 'block_admin_presets_app_it', + 'admin_preset_apply_item_attr' => 'block_admin_presets_app_it_a', + 'admin_preset_item' => 'block_admin_presets_it', + 'admin_preset_item_attr' => 'block_admin_presets_it_a'); + + // Just in case it gets to the max number of chars defined in the XSD. + try { + + // Renaming the tables. + foreach ($tablenamechanges as $from => $to) { + + $table = new xmldb_table($from); + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, $to); + } + } + + // Print error and rollback changes. + } catch (Exception $e) { + + // Rollback tablename changes. + foreach ($tablenamechanges as $to => $from) { + + $table = new xmldb_table($from); + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, $to); + } + } + + $debuginfo = get_string('errorupgradetablenamesdebug', 'block_admin_presets'); + throw new moodle_exception('errorupgradetablenames', 'block_admin_presets', '', null, $debuginfo); + } + + upgrade_block_savepoint(true, 2012031401, 'admin_presets'); + } + return true; +} diff --git a/block/admin_presets/forms/admin_presets_export_form.php b/block/admin_presets/forms/admin_presets_export_form.php new file mode 100644 index 0000000..0fe4e06 --- /dev/null +++ b/block/admin_presets/forms/admin_presets_export_form.php @@ -0,0 +1,68 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/lib/formslib.php'); + +class admin_presets_export_form extends moodleform { + + public function definition() { + + global $USER, $OUTPUT; + + $mform = &$this->_form; + + // Preset attributes. + $mform->addElement('header', 'general', + get_string('presetsettings', 'block_admin_presets')); + + $mform->addElement('text', 'name', get_string('name'), 'maxlength="254" size="60"'); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->setType('name', PARAM_TEXT); + + $mform->addElement('editor', 'comments', get_string('comments')); + $mform->setType('comments', PARAM_CLEANHTML); + + $mform->addElement('text', 'author', + get_string('author', 'block_admin_presets'), 'maxlength="254" size="60"'); + $mform->setType('author', PARAM_TEXT); + $mform->setDefault('author', $USER->firstname . ' ' . $USER->lastname); + + $mform->addElement('checkbox', 'excludesensiblesettings', + get_string('autohidesensiblesettings', 'block_admin_presets')); + + // Moodle settings table. + $mform->addElement('header', 'general', + get_string('adminsettings', 'block_admin_presets')); + $mform->addElement('html', '<div id="settings_tree_div" class="ygtv-checkbox"><img src="' . + $OUTPUT->pix_icon('i/loading_small', get_string('loading', + 'block_admin_presets')) . '"/></div><br/>'); + + // Submit. + $mform->addElement('submit', 'admin_presets_submit', get_string('savechanges')); + } +} diff --git a/block/admin_presets/forms/admin_presets_import_form.php b/block/admin_presets/forms/admin_presets_import_form.php new file mode 100644 index 0000000..9fea1de --- /dev/null +++ b/block/admin_presets/forms/admin_presets_import_form.php @@ -0,0 +1,52 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/lib/formslib.php'); + +class admin_presets_import_form extends moodleform { + + public function definition() { + + $mform = &$this->_form; + + $mform->addElement('header', 'general', + get_string('selectfile', 'block_admin_presets')); + + // File upload + $mform->addElement('filepicker', 'xmlfile', + get_string('selectfile', 'block_admin_presets')); + $mform->addRule('xmlfile', null, 'required'); + + // Rename input + $mform->addElement('text', 'name', + get_string('renamepreset', 'block_admin_presets'), 'maxlength="254" size="40"'); + $mform->setType('name', PARAM_TEXT); + + $mform->addElement('submit', 'admin_presets_submit', get_string('savechanges')); + } +} diff --git a/block/admin_presets/forms/admin_presets_load_form.php b/block/admin_presets/forms/admin_presets_load_form.php new file mode 100644 index 0000000..d03acfd --- /dev/null +++ b/block/admin_presets/forms/admin_presets_load_form.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/lib/formslib.php'); + +class admin_presets_load_form extends moodleform { + + private $preview; + + public function __construct($url, $preview = false) { + $this->preview = $preview; + parent::__construct($url); + } + + public function definition() { + + global $OUTPUT; + + $mform = &$this->_form; + + // Moodle settings table. + $mform->addElement('header', 'general', + get_string('adminsettings', 'block_admin_presets')); + + $class = ''; + if (!$this->preview) { + $class = 'ygtv-checkbox'; + } + $mform->addElement('html', '<div id="settings_tree_div" class="' . $class . + '"><img src="' . $OUTPUT->pix_icon('i/loading_small', + get_string('loading', 'block_admin_presets')) . '"/></div>'); + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + + // Submit. + if (!$this->preview) { + $mform->addElement('submit', 'admin_presets_submit', + get_string('loadselected', 'block_admin_presets')); + } + } +} diff --git a/block/admin_presets/index.php b/block/admin_presets/index.php new file mode 100644 index 0000000..1c83fd2 --- /dev/null +++ b/block/admin_presets/index.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); + +$action = optional_param('action', 'base', PARAM_ALPHA); +$mode = optional_param('mode', 'show', PARAM_ALPHAEXT); + +require_login(); + +if (!$context = context_system::instance()) { + print_error('wrongcontext', 'error'); +} + +require_capability('moodle/site:config', $context); + +// Loads the required action class and form. +$classname = 'admin_presets_' . $action; +$formname = $classname . '_form'; +$formpath = $CFG->dirroot . '/blocks/admin_presets/forms/' . $formname . '.php'; +require_once($CFG->dirroot . '/blocks/admin_presets/lib/' . $classname . '.class.php'); +if (file_exists($formpath)) { + require_once($formpath); +} + +if (!class_exists($classname)) { + print_error('falseaction', 'block_admin_presets', $action); +} + +$url = new moodle_url('/blocks/admin_presets/index.php'); +$url->param('action', $action); +$url->param('mode', $mode); +$PAGE->set_url($url); +$PAGE->set_pagelayout('admin'); +$PAGE->set_context($context); + +// Executes the required action. +$instance = new $classname(); +if (!method_exists($instance, $mode)) { + print_error('falsemode', 'block_admin_presets', $mode); +} + +// Executes the required method and displays output. +$instance->$mode(); +$instance->log(); +$instance->display(); diff --git a/block/admin_presets/lang/en/block_admin_presets.php b/block/admin_presets/lang/en/block_admin_presets.php new file mode 100644 index 0000000..033cec5 --- /dev/null +++ b/block/admin_presets/lang/en/block_admin_presets.php @@ -0,0 +1,120 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['accessdenied'] = 'Access denied'; +$string['actionbase'] = 'Presets'; +$string['actiondelete'] = 'Delete preset'; +$string['actionexport'] = 'Export settings'; +$string['actionimport'] = 'Import settings'; +$string['actionload'] = 'Load settings'; +$string['actionrollback'] = 'Revert applied changes'; +$string['actualvalue'] = 'Actual value'; +$string['admin_presets:addinstance'] = 'Add a new admin presets block'; +$string['adminsettings'] = 'Admin settings'; +$string['author'] = 'Author'; +$string['autohidesensiblesettings'] = 'Auto exclude sensitive settings'; +$string['baseshow'] = 'list presets'; +$string['created'] = 'Created'; +$string['deletepreset'] = 'Preset {$a} will be deleted, are you sure?'; +$string['deletepreviouslyapplied'] = 'This preset has been previously applied, + if you delete it you can not return to the previous state'; +$string['deleteexecute'] = 'execution'; +$string['deleteshow'] = 'confirm'; +$string['errorupgradetablenames'] = 'admin_presets upgrade failed, + upgrade Moodle in order to upgrade admin_presets. You can restore the previous blocks/admin_presets code until then'; +$string['errorupgradetablenamesdebug'] = 'The table names exceeds the limit of allowed characters, + this is solved using the latest Moodle 2.0, Moodle 2.1 and Moodle 2.2 releases'; +$string['errordeleting'] = 'Error deleting from DB'; +$string['errorinserting'] = 'Error inserting into DB'; +$string['errornopreset'] = 'It doesn\'t exists a preset with that name'; +$string['eventpresetdeleted'] = 'Preset deleted'; +$string['eventpresetdownloaded'] = 'Preset downloaded'; +$string['eventpresetexported'] = 'Preset exported'; +$string['eventpresetimported'] = 'Preset imported'; +$string['eventpresetloaded'] = 'Preset loaded'; +$string['eventpresetpreviewed'] = 'Preset previewed'; +$string['eventpresetreverted'] = 'Preset reverted'; +$string['eventpresetslisted'] = 'Presets have been listed'; +$string['exportexecute'] = 'saving'; +$string['exportshow'] = 'select settings'; +$string['falseaction'] = 'Action not supported in this version'; +$string['falsemode'] = 'Mode not supported in this version'; +$string['headingload'] = 'Select settings to load'; +$string['imported'] = 'Imported'; +$string['importexecute'] = 'importing'; +$string['importshow'] = 'select file'; +$string['load'] = 'load'; +$string['loadexecute'] = 'applied changes'; +$string['loadpreview'] = 'preview preset'; +$string['loadselected'] = 'Load selected settings'; +$string['loadshow'] = 'select settings'; +$string['markedasadvanced'] = 'marked as advanced'; +$string['markedasforced'] = 'marked as forced'; +$string['markedaslocked'] = 'marked as locked'; +$string['markedasnonadvanced'] = 'marked as non advanced'; +$string['markedasnonforced'] = 'marked as non forced'; +$string['markedasnonlocked'] = 'marked as non locked'; +$string['newvalue'] = 'New setting value'; +$string['loading'] = 'loading'; +$string['noparamtype'] = 'There are no param type for that setting'; +$string['nopresets'] = 'You don\'t have presets'; +$string['nothingloaded'] = 'All preset settings skipped, they are already loaded'; +$string['notpreviouslyapplied'] = 'Preset not previously applied'; +$string['novalidsettings'] = 'No valid settings'; +$string['novalidsettingsselected'] = 'No valid settings selected'; +$string['oldvalue'] = 'Old setting value'; +$string['pluginname'] = 'Admin presets'; +$string['presetmoodlerelease'] = 'Moodle release'; +$string['presetname'] = 'Preset name'; +$string['presetsettings'] = 'Preset settings'; +$string['preview'] = 'preview'; +$string['previewpreset'] = 'Preview preset'; +$string['renamepreset'] = 'Rename preset'; +$string['rollback'] = 'revert'; +$string['rollbackexecute'] = 'return to previous state'; +$string['rollbackfailures'] = 'The following settings can not be restored, + the actual values differs from the values applied by the preset'; +$string['rollbackresults'] = 'Settings successfully restored'; +$string['rollbackshow'] = 'preset applications list'; +$string['selectedvalues'] = 'setting selected values'; +$string['selectfile'] = 'Select file'; +$string['sensiblesettings'] = 'Sensitive setting to skip if "Auto exclude sensitive settings" is checked'; +$string['sensiblesettingstext'] = 'Add elements separating by \',\' and with format SETTINGNAME@@PLUGINNAME'; +$string['settingname'] = 'Setting name'; +$string['settingvalue'] = 'with value'; +$string['settingsapplied'] = 'Settings applied'; +$string['settingsnotapplicable'] = 'Settings not applicable to this Moodle version'; +$string['settingsnotapplied'] = 'Settings skipped, they are all already loaded'; +$string['site'] = 'Site'; +$string['successimported'] = 'Preset imported'; +$string['timeapplied'] = 'Time applied'; +$string['toexportclick'] = 'To export your settings click {$a}'; +$string['toimportclick'] = 'To import a admin preset click {$a}'; +$string['value'] = 'setting value'; +$string['voidvalue'] = 'that setting does not have a value'; +$string['wrongfile'] = 'Wrong file'; +$string['wrongid'] = 'Wrong id'; +$string['privacy:null_reason'] = 'The admin presets block does not effect or store any user data'; \ No newline at end of file diff --git a/block/admin_presets/lib/admin_presets_base.class.php b/block/admin_presets/lib/admin_presets_base.class.php new file mode 100644 index 0000000..697a899 --- /dev/null +++ b/block/admin_presets/lib/admin_presets_base.class.php @@ -0,0 +1,650 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->dirroot . '/blocks/admin_presets/lib/admin_presets_settings_types.php'); + +class admin_presets_base { + + protected static $eventsactionsmap = array( + 'base' => 'presets_listed', + 'delete' => 'preset_deleted', + 'export' => 'preset_exported', + 'import' => 'preset_imported', + 'preview' => 'preset_previewed', + 'load' => 'preset_loaded', + 'rollback' => 'preset_reverted', + 'download_xml' => 'preset_downloaded' + ); + protected $action; + protected $mode; + protected $adminroot; + protected $outputs; + protected $moodleform; + protected $rel; + + /** + * Loads common class attributes and initializes sensible settings and DB - XML relations + */ + public function __construct() { + + $this->action = optional_param('action', 'base', PARAM_ALPHA); + $this->mode = optional_param('mode', 'show', PARAM_ALPHAEXT); + $this->id = optional_param('id', false, PARAM_INT); + + // DB - XML relations. + $this->rel = array('name' => 'NAME', 'comments' => 'COMMENTS', + 'timecreated' => 'PRESET_DATE', 'site' => 'SITE_URL', 'author' => 'AUTHOR', + 'moodleversion' => 'MOODLE_VERSION', 'moodlerelease' => 'MOODLE_RELEASE'); + + // Sensible settings. + $sensiblesettings = explode(',', + str_replace(' ', '', get_config('admin_presets', 'sensiblesettings'))); + $this->sensiblesettings = array_combine($sensiblesettings, $sensiblesettings); + } + + /** + * Method to list the presets available on the system + * + * It allows users to access the different preset + * actions (preview, load, download, delete and rollback) + */ + public function show() { + + global $CFG, $DB, $OUTPUT; + + $presets = $DB->get_records('block_admin_presets'); + $this->outputs = ''; + + if ($presets) { + + // Initialize table. + $table = $this->_create_preset_data_table(); + + foreach ($presets as $preset) { + + // Preset actions. + $previewlink = $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=load&mode=preview&id=' . $preset->id; + $loadlink = $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=load&id=' . $preset->id; + $downloadlink = $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=export&mode=download_xml&sesskey=' . + sesskey() . '&id=' . $preset->id; + $deletelink = $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=delete&id=' . $preset->id; + $rollbacklink = $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=rollback&id=' . $preset->id; + + $actions = array(); + $actions[] = html_writer::link($previewlink, strtolower(get_string("preview"))); + $actions[] = html_writer::link($loadlink, get_string("load", + "block_admin_presets")); + $actions[] = html_writer::link($downloadlink, strtolower(get_string("download"))); + $actions[] = html_writer::link($deletelink, strtolower(get_string("delete"))); + + // Look for preset applications. + if ($DB->get_records('block_admin_presets_app', array('adminpresetid' => $preset->id))) { + $actions[] = html_writer::link($rollbacklink, + get_string("rollback", "block_admin_presets")); + } + + if ($preset->timeimported) { + $timeimportedstring = userdate($preset->timeimported); + } else { + $timeimportedstring = ''; + } + + // Populate table. + $table->data[] = array(format_text($preset->name, FORMAT_PLAIN), + format_text($preset->comments, FORMAT_HTML), + format_text($preset->moodlerelease, FORMAT_PLAIN), + format_text($preset->author, FORMAT_PLAIN), + format_text(clean_text($preset->site, PARAM_URL), FORMAT_PLAIN), + userdate($preset->timecreated), + $timeimportedstring, + '<div>' . implode('</div><div>', $actions) . '</div>'); + } + + $this->outputs .= html_writer::table($table); + + // If there aren't presets notify it. + } else { + + $exportlink = '<a href="' . $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=export">' . + strtolower(get_string("actionexport", "block_admin_presets")) . '</a>'; + $importlink = '<a href="' . + $CFG->wwwroot . '/blocks/admin_presets/index.php?action=import">' . + strtolower(get_string("actionimport", "block_admin_presets")) . '</a>'; + + $this->outputs = $OUTPUT->box_start('generalbox', 'id_nopresets'); + $this->outputs .= '<ul>' . get_string('nopresets', 'block_admin_presets'); + $this->outputs .= '<li>' . get_string('toexportclick', + 'block_admin_presets', $exportlink) . '</li>'; + $this->outputs .= '<li>' . get_string('toimportclick', + 'block_admin_presets', $importlink) . '</li>'; + $this->outputs .= '</ul>'; + $this->outputs .= $OUTPUT->box_end(); + } + + } + + /** + * Table to display preset/s data + * + * @param boolean $actionstable If is set to true adds a column to display actions + * @return html_table + * @throws coding_exception + */ + protected function _create_preset_data_table($actionstable = true) { + + $table = new html_table(); + $table->attributes['class'] = 'generaltable boxaligncenter'; + $table->align = array('left', 'left', 'center', 'left', 'left', 'center', 'center'); + $table->head = array(get_string('name'), get_string('description'), + get_string('presetmoodlerelease', 'block_admin_presets'), + get_string('author', 'block_admin_presets'), + get_string('site', 'block_admin_presets'), + get_string('created', 'block_admin_presets'), + get_string('imported', 'block_admin_presets')); + + if ($actionstable) { + $table->align[] = 'left'; + $table->head[] = get_string('actions'); + $table->size = array('14%', '16%', '12%', '11%', '17%', '10%', '10%', '10%'); + } else { + $table->size = array('17%', '20%', '13%', '12%', '18%', '10%', '10%'); + } + + return $table; + } + + /** + * Main display method + * + * Prints the block header and the common block outputs, the + * selected action outputs, his form and the footer + * + * $outputs value depends on $mode and $action selected + */ + public function display() { + global $OUTPUT; + + $this->_display_header(); + + // Other outputs. + if (!empty($this->outputs)) { + echo $this->outputs; + } + + // Form. + if ($this->moodleform) { + $this->moodleform->display(); + } + + // Footer. + echo $OUTPUT->footer(); + } + + /** + * Displays the header + */ + protected function _display_header() { + + global $CFG, $PAGE, $OUTPUT, $SITE; + + // Strings. + $actionstr = get_string('action' . $this->action, 'block_admin_presets'); + $modestr = get_string($this->action . $this->mode, 'block_admin_presets'); + $titlestr = get_string('pluginname', 'block_admin_presets'); + + // Header. + $PAGE->set_title($titlestr); + $PAGE->set_heading($SITE->fullname); + + $PAGE->navbar->add(get_string('pluginname', 'block_admin_presets'), + new moodle_url($CFG->wwwroot . '/blocks/admin_presets/index.php')); + + $PAGE->navbar->add($actionstr . ': ' . $modestr); + + echo $OUTPUT->header(); + + include(dirname(dirname(__FILE__)) . '/tabs.php'); + + echo $OUTPUT->heading($actionstr . ': ' . $modestr, 1); + } + + public function log() { + // TODO please, me of the future, fix this ununderstandable code. + + // The only read action we store is list presets. + if ($this->mode != 'show' || + ($this->mode == 'show' && $this->action == 'base')) { + + $action = $this->action; + if ($this->mode != 'execute' && $this->mode != 'show') { + $action = $this->mode; + } + + $eventnamespace = '\\block_admin_presets\\event\\' . self::$eventsactionsmap[$action]; + $eventdata = array( + 'context' => context_system::instance(), + 'objectid' => $this->id + ); + $event = $eventnamespace::create($eventdata); + $event->trigger(); + } + } + + /** + * Gets the system settings + * + * Loads the DB $CFG->prefix.'config' values and the + * $CFG->prefix.'config_plugins' values and redirects + * the flow through $this->_get_settings() + * + * @return array $settings Array format $array['plugin']['settingname'] = admin_preset_setting child class + * @throws dml_exception + */ + protected function _get_site_settings() { + + global $DB; + + // Db configs (to avoid multiple queries). + $dbconfig = $DB->get_records_select('config', '', array(), '', 'name, value'); + + // Adding site settings in course table. + $frontpagevalues = $DB->get_record_select('course', 'id = 1', + array(), 'fullname, shortname, summary'); + foreach ($frontpagevalues as $field => $value) { + $dbconfig[$field] = new StdClass(); + $dbconfig[$field]->name = $field; + $dbconfig[$field]->value = $value; + } + $sitedbsettings['none'] = $dbconfig; + + // Config plugins. + $configplugins = $DB->get_records('config_plugins'); + foreach ($configplugins as $configplugin) { + $sitedbsettings[$configplugin->plugin][$configplugin->name] = new StdClass(); + $sitedbsettings[$configplugin->plugin][$configplugin->name]->name = $configplugin->name; + $sitedbsettings[$configplugin->plugin][$configplugin->name]->value = $configplugin->value; + } + // Get an array with the common format. + return $this->_get_settings($sitedbsettings, true, $settings = array()); + } + + /** + * Constructs an array with all the system settings + * + * If a setting value can't be found on the DB it considers + * the default value as the setting value + * + * Settings without plugin are marked as 'none' in the plugin field + * + * Returns an standarized settings array format, $this->_get_settings_branches + * will get the html or js to display the settings tree + * + * @param array $dbsettings Standarized array, + * format $array['plugin']['name'] = obj('name'=>'settingname', 'value'=>'settingvalue') + * @param boolean $sitedbvalues Indicates if $dbsettings comes from the site db or not + * @param array $settings Array format $array['plugin']['settingname'] = admin_preset_setting child class + * @param bool $children admin_category children + * @return array Array format $array['plugin']['settingname'] = admin_preset_setting child class + * @throws dml_exception + */ + protected function _get_settings($dbsettings, $sitedbvalues = false, $settings, $children = false) { + + global $DB; + // If there are no children, load admin tree and iterate through. + if (!$children) { + $this->adminroot = admin_get_root(false, true); + $children = $this->adminroot->children; + } + + // Iteates through children. + foreach ($children as $key => $child) { + + // We must search category children. + if (is_a($child, 'admin_category')) { + + if ($child->children) { + $settings = $this->_get_settings($dbsettings, $sitedbvalues, $settings, $child->children); + } + + // Settings page. + } else if (is_a($child, 'admin_settingpage')) { + + if ($child->settings) { + + foreach ($child->settings as $values) { + $settingname = $values->name; + + unset($settingvalue); + + // Look for his config value. + if ($values->plugin == '') { + $values->plugin = 'none'; + } + + if (!empty($dbsettings[$values->plugin][$settingname])) { + $settingvalue = $dbsettings[$values->plugin][$settingname]->value; + } + + // If no db value found default value. + if ($sitedbvalues && !isset($settingvalue)) { + // For settings with multiple values. + if (is_array($values->defaultsetting)) { + + if (isset($values->defaultsetting['value'])) { + $settingvalue = $values->defaultsetting['value']; + // Configtime case, does not have a 'value' default setting. + } else { + $settingvalue = 0; + } + } else { + $settingvalue = $values->defaultsetting; + } + } + + // If there aren't any value loaded, skip that setting. + if (!isset($settingvalue)) { + continue; + } + // If there is no setting class defined continue. + if (!$setting = $this->_get_setting($values, $settingvalue)) { + continue; + } + + // Admin_preset_setting childs with. + // attributes provides an attributes array. + if ($attributes = $setting->get_attributes()) { + + // Look for settings attributes if it is a presets. + if (!$sitedbvalues) { + $itemid = $dbsettings[$values->plugin][$settingname]->itemid; + $attrs = $DB->get_records('block_admin_presets_it_a', + array('itemid' => $itemid), '', 'name, value'); + } + foreach ($attributes as $defaultvarname => $varname) { + + unset($attributevalue); + + // Settings from site. + if ($sitedbvalues) { + if (!empty($dbsettings[$values->plugin][$varname])) { + $attributevalue = $dbsettings[$values->plugin][$varname]->value; + } + + // Settings from a preset. + } else if (!$sitedbvalues && isset($attrs[$varname])) { + $attributevalue = $attrs[$varname]->value; + } + + // If no value found, default value, + // But we may not have a default value for the attribute. + if (!isset($attributevalue) && !empty($values->defaultsetting[$defaultvarname])) { + $attributevalue = $values->defaultsetting[$defaultvarname]; + } + + // If there is no even a default for this setting will be empty. + // So we do nothing in this case. + if (isset($attributevalue)) { + $setting->set_attribute_value($varname, $attributevalue); + } + } + } + + // Setting the text. + $setting->set_text(); + + // Adding to general settings array. + $settings[$values->plugin][$settingname] = $setting; + } + } + } + } + + return $settings; + } + + /** + * Returns the class type object + * + * @param object $settingdata Setting data + * @param mixed $currentvalue + * @return bool + */ + protected function _get_setting($settingdata, $currentvalue) { + + // Getting the appropiate class to get the correct setting value. + $settingtype = get_class($settingdata); + + // Skipping admin_*. + $classname = 'admin_preset_' . $settingtype; + + // TODO: Implement all the settings types. + if (!class_exists($classname)) { + return false; + } + + $setting = new $classname($settingdata, $currentvalue); + + return $setting; + } + + /** + * Gets the javascript to populate the settings tree + * + * @param array $settings Array format $array['plugin']['settingname'] = admin_preset_setting child class + */ + protected function _get_settings_branches($settings) { + + global $PAGE; + + // Nodes should be added in hierarchical order. + $nodes = array('categories' => array(), 'pages' => array(), 'settings' => array()); + $nodes = $this->_get_settings_elements($settings, false, false, $nodes); + + $PAGE->requires->js_init_call('M.block_admin_presets.init', null, true); + + $levels = array('categories', 'pages', 'settings'); + foreach ($levels as $level) { + foreach ($nodes[$level] as $data) { + $ids[] = $data[0]; + $nodes[] = $data[1]; + $labels[] = $data[2]; + $descriptions[] = $data[3]; + $parents[] = $data[4]; + } + } + $PAGE->requires->js_init_call('M.block_admin_presets.addNodes', + array($ids, $nodes, $labels, $descriptions, $parents), true); + $PAGE->requires->js_init_call('M.block_admin_presets.render', null, true); + } + + /** + * Gets the html code to select the settings to export/import/load + * + * @param array $allsettings Array format $array['plugin']['settingname'] = admin_preset_setting child class + * @param bool $admintree The admin tree branche object or false if we are in the root + * @param bool $jsparentnode Name of the javascript parent category node + * @param array $nodes Tree nodes + * @return array Code to output + */ + protected function _get_settings_elements($allsettings, $admintree = false, $jsparentnode = false, &$nodes) { + + if (empty($this->adminroot)) { + $this->adminroot = admin_get_root(false, true); + } + + // If there are no children, load admin tree and iterate through. + if (!$admintree) { + $this->adminroot = admin_get_root(false, true); + $admintree = $this->adminroot->children; + } + + // If there are no parentnode specified the parent becomes the tree root. + if (!$jsparentnode) { + $jsparentnode = 'root'; + } + + // Iterates through children. + foreach ($admintree as $key => $child) { + $pagesettings = array(); + + // We must search category children. + if (is_a($child, 'admin_category')) { + if ($child->children) { + $categorynode = $child->name . 'Node'; + $nodehtml = '<div class="catnode">' . $child->visiblename . '</div>'; + $nodes['categories'][$categorynode] = array("category", + $categorynode, (String) $nodehtml, "", $jsparentnode); + + // Not all admin_categories have admin_settingpages. + $this->_get_settings_elements($allsettings, $child->children, $categorynode, $nodes); + } + + // Settings page. + } else if (is_a($child, 'admin_settingpage')) { + // Only if there are settings. + if ($child->settings) { + + // The name of that page tree node. + $pagenode = $child->name . 'Node'; + + foreach ($child->settings as $values) { + $settingname = $values->name; + + // IF no plugin was specified mark as 'none'. + if (!$plugin = $values->plugin) { + $plugin = 'none'; + } + + if (empty($allsettings[$plugin][$settingname])) { + continue; + } + + // Getting setting data. + $setting = $allsettings[$plugin][$settingname]; + $settingid = $setting->get_id(); + + // String to add the setting to js tree. + $pagesettings[$settingid] = array($settingid, $settingid, + $setting->get_text(), $setting->get_description(), $pagenode); + } + + // The page node only should be added if it have children. + if ($pagesettings) { + $nodehtml = '<div class="catnode">' . $child->visiblename . '</div>'; + $nodes['pages'][$pagenode] = array("page", $pagenode, (String) $nodehtml, "", $jsparentnode); + $nodes['settings'] = array_merge($nodes['settings'], $pagesettings); + } + } + } + } + + return $nodes; + } + + /** + * Gets the standarized settings array from DB records + * + * @param array $dbsettings Array of objects + * @return array Standarized array, + * format $array['plugin']['name'] = obj('name'=>'settingname', 'value'=>'settingvalue') + */ + protected function _get_settings_from_db($dbsettings) { + + if (!$dbsettings) { + return false; + } + + $settings = array(); + foreach ($dbsettings as $dbsetting) { + $settings[$dbsetting->plugin][$dbsetting->name] = new StdClass(); + $settings[$dbsetting->plugin][$dbsetting->name]->itemid = $dbsetting->id; + $settings[$dbsetting->plugin][$dbsetting->name]->name = $dbsetting->name; + $settings[$dbsetting->plugin][$dbsetting->name]->value = $dbsetting->value; + } + + return $settings; + } + + protected function _output_applied_changes($appliedchanges) { + + $appliedtable = new html_table(); + $appliedtable->attributes['class'] = 'generaltable boxaligncenter admin_presets_applied'; + $appliedtable->head = array(get_string('plugin'), + get_string('settingname', 'block_admin_presets'), + get_string('oldvalue', 'block_admin_presets'), + get_string('newvalue', 'block_admin_presets')); + + $appliedtable->align = array('center', 'center'); + + foreach ($appliedchanges as $setting) { + $appliedtable->data[] = array($setting->plugin, + $setting->visiblename, + $setting->oldvisiblevalue, + $setting->visiblevalue); + } + + $this->outputs .= html_writer::table($appliedtable); + } + + /** + * Returns a table with the preset data + * + * @param object $preset + * @return string|string + * @throws coding_exception + */ + protected function _html_writer_preset_info_table($preset) { + + if (!$preset) { + return ''; + } + + if ($preset->timeimported) { + $timeimportedstring = userdate($preset->timeimported); + } else { + $timeimportedstring = ''; + } + $infotable = $this->_create_preset_data_table(false); + $infotable->data[] = array(format_text($preset->name, FORMAT_PLAIN), + format_text($preset->comments, FORMAT_HTML), + format_text($preset->moodlerelease, FORMAT_PLAIN), + format_text($preset->author, FORMAT_PLAIN), + format_text(clean_text($preset->site, PARAM_URL), FORMAT_PLAIN), + userdate($preset->timecreated), + $timeimportedstring); + + return html_writer::table($infotable); + } +} diff --git a/block/admin_presets/lib/admin_presets_delete.class.php b/block/admin_presets/lib/admin_presets_delete.class.php new file mode 100644 index 0000000..e4b6cc5 --- /dev/null +++ b/block/admin_presets/lib/admin_presets_delete.class.php @@ -0,0 +1,116 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/blocks/admin_presets/lib/admin_presets_base.class.php'); + +class admin_presets_delete extends admin_presets_base { + + /** + * Shows a confirm box + */ + public function show() { + + global $DB, $CFG, $OUTPUT; + + // Getting the preset name. + $presetdata = $DB->get_record('block_admin_presets', array('id' => $this->id), 'name'); + + $deletetext = get_string("deletepreset", "block_admin_presets", $presetdata->name); + $confirmurl = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=' . + $this->action . '&mode=execute&id=' . $this->id . '&sesskey=' . sesskey(); + $cancelurl = $CFG->wwwroot . '/blocks/admin_presets/index.php'; + + // If the preset was applied add a warning text. + if ($previouslyapplied = $DB->get_records('block_admin_presets_app', + array('adminpresetid' => $this->id))) { + + $deletetext .= '<br/><br/><strong>' . + get_string("deletepreviouslyapplied", "block_admin_presets") . '</strong>'; + } + + $this->outputs = $OUTPUT->confirm($deletetext, $confirmurl, $cancelurl); + } + + /** + * Delete the DB preset + */ + public function execute() { + + global $DB, $CFG; + + confirm_sesskey(); + + if (!$DB->delete_records('block_admin_presets', array('id' => $this->id))) { + print_error('errordeleting', 'block_admin_presets'); + } + + // Getting items ids before deleting to delete item attributes. + $items = $DB->get_records('block_admin_presets_it', array('adminpresetid' => $this->id), 'id'); + foreach ($items as $item) { + $DB->delete_records('block_admin_presets_it_a', array('itemid' => $item->id)); + } + + if (!$DB->delete_records('block_admin_presets_it', array('adminpresetid' => $this->id))) { + print_error('errordeleting', 'block_admin_presets'); + } + + // Deleting the preset applications. + if ($previouslyapplied = $DB->get_records('block_admin_presets_app', + array('adminpresetid' => $this->id), 'id')) { + + foreach ($previouslyapplied as $application) { + + // Deleting items. + if (!$DB->delete_records('block_admin_presets_app_it', + array('adminpresetapplyid' => $application->id))) { + + print_error('errordeleting', 'block_admin_presets'); + } + + // Deleting attributes. + if (!$DB->delete_records('block_admin_presets_app_it_a', + array('adminpresetapplyid' => $application->id))) { + + print_error('errordeleting', 'block_admin_presets'); + } + } + + if (!$DB->delete_records('block_admin_presets_app', + array('adminpresetid' => $this->id))) { + + print_error('errordeleting', 'block_admin_presets'); + } + } + + // Trigger the as it is usually triggered after execute finishes. + $this->log(); + + redirect($CFG->wwwroot . '/blocks/admin_presets/index.php'); + } + +} diff --git a/block/admin_presets/lib/admin_presets_export.class.php b/block/admin_presets/lib/admin_presets_export.class.php new file mode 100644 index 0000000..972493d --- /dev/null +++ b/block/admin_presets/lib/admin_presets_export.class.php @@ -0,0 +1,235 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/blocks/admin_presets/lib/admin_presets_base.class.php'); +require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php'); +require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php'); +require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.php'); + +class admin_presets_export extends admin_presets_base { + + /** + * Shows the initial form to export/save admin settings + * + * Loads the database configuration and prints + * the settings in a hierical table + */ + public function show() { + + global $CFG; + + // Load site settings in the common format and do the js calls to populate the tree. + $settings = $this->_get_site_settings(); + $this->_get_settings_branches($settings); + + $url = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=export&mode=execute'; + $this->moodleform = new admin_presets_export_form($url); + } + + /** + * Stores the preset into the DB + */ + public function execute() { + + global $CFG, $USER, $DB; + + confirm_sesskey(); + + $url = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=export&mode=execute'; + $this->moodleform = new admin_presets_export_form($url); + + // Reload site settings. + $sitesettings = $this->_get_site_settings(); + + if ($data = $this->moodleform->get_data()) { + + // admin_preset record. + $preset = new StdClass(); + $preset->userid = $USER->id; + $preset->name = $data->name; + $preset->comments = $data->comments['text']; + $preset->site = $CFG->wwwroot; + $preset->author = $data->author; + $preset->moodleversion = $CFG->version; + $preset->moodlerelease = $CFG->release; + $preset->timecreated = time(); + $preset->timemodified = 0; + if (!$preset->id = $DB->insert_record('block_admin_presets', $preset)) { + print_error('errorinserting', 'block_admin_presets'); + } + + // Store it here for logging and other future id-oriented stuff. + $this->id = $preset->id; + + // We must ensure that there are settings selected. + foreach (filter_input_array(INPUT_POST) as $varname => $value) { + + unset($setting); + + if (strstr($varname, '@@') != false) { + + $settingsfound = true; + + // Avoid sensible data. + if (!empty($data->excludesensiblesettings) && !empty($this->sensiblesettings[$varname])) { + continue; + } + + $name = explode('@@', $varname); + $setting = new StdClass(); + $setting->adminpresetid = $preset->id; + $setting->plugin = $name[1]; + $setting->name = $name[0]; + $setting->value = $sitesettings[$setting->plugin][$setting->name]->get_value(); + + if (!$setting->id = $DB->insert_record('block_admin_presets_it', $setting)) { + print_error('errorinserting', 'block_admin_presets'); + } + + // Setting attributes must also be exported. + if ($attributes = $sitesettings[$setting->plugin][$setting->name]->get_attributes_values()) { + foreach ($attributes as $attributename => $value) { + + $attr = new StdClass(); + $attr->itemid = $setting->id; + $attr->name = $attributename; + $attr->value = $value; + + $DB->insert_record('block_admin_presets_it_a', $attr); + } + } + } + } + + // If there are no valid or selected settings we should delete the admin preset record. + if (empty($settingsfound)) { + $DB->delete_records('block_admin_presets', array('id' => $preset->id)); + redirect($CFG->wwwroot . '/blocks/admin_presets/index.php?action=export', + get_string('novalidsettingsselected', 'block_admin_presets'), 4); + } + } + + // Trigger the as it is usually triggered after execute finishes. + $this->log(); + + redirect($CFG->wwwroot . '/blocks/admin_presets/index.php'); + } + + /** + * To download system presets + * + * @return void preset file + * @throws dml_exception + * @throws moodle_exception + * @throws xml_output_exception + * @throws xml_writer_exception + */ + public function download_xml() { + + global $DB; + + confirm_sesskey(); + + if (!$preset = $DB->get_record('block_admin_presets', array('id' => $this->id))) { + print_error('errornopreset', 'block_admin_presets'); + } + + if (!$items = $DB->get_records('block_admin_presets_it', array('adminpresetid' => $this->id))) { + print_error('errornopreset', 'block_admin_presets'); + } + + // Start. + $xmloutput = new memory_xml_output(); + $xmlwriter = new xml_writer($xmloutput); + $xmlwriter->start(); + + // Preset data. + $xmlwriter->begin_tag('PRESET'); + foreach ($this->rel as $dbname => $xmlname) { + $xmlwriter->full_tag($xmlname, $preset->$dbname); + } + + // We ride through the settings array. + $allsettings = $this->_get_settings_from_db($items); + if ($allsettings) { + + $xmlwriter->begin_tag('ADMIN_SETTINGS'); + + foreach ($allsettings as $plugin => $settings) { + + $tagname = strtoupper($plugin); + + // To aviod xml slash problems. + if (strstr($tagname, '/') != false) { + $tagname = str_replace('/', '__', $tagname); + } + + $xmlwriter->begin_tag($tagname); + + // One tag for each plugin setting. + if (!empty($settings)) { + + $xmlwriter->begin_tag('SETTINGS'); + + foreach ($settings as $setting) { + + // Unset the tag attributes string. + $attributes = array(); + + // Getting setting attributes, if present. + $attrs = $DB->get_records('block_admin_presets_it_a', array('itemid' => $setting->itemid)); + if ($attrs) { + foreach ($attrs as $attr) { + $attributes[$attr->name] = $attr->value; + } + } + + $xmlwriter->full_tag(strtoupper($setting->name), $setting->value, $attributes); + } + + $xmlwriter->end_tag('SETTINGS'); + } + + $xmlwriter->end_tag(strtoupper($tagname)); + } + + $xmlwriter->end_tag('ADMIN_SETTINGS'); + } + + // End + $xmlwriter->end_tag('PRESET'); + $xmlwriter->stop(); + $xmlstr = $xmloutput->get_allcontents(); + + // Trigger the as it is usually triggered after execute finishes. + $this->log(); + + $filename = addcslashes($preset->name, '"') . '.xml'; + send_file($xmlstr, $filename, 0, 0, true, true); + } +} diff --git a/block/admin_presets/lib/admin_presets_import.class.php b/block/admin_presets/lib/admin_presets_import.class.php new file mode 100644 index 0000000..5ef2ff2 --- /dev/null +++ b/block/admin_presets/lib/admin_presets_import.class.php @@ -0,0 +1,180 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/blocks/admin_presets/lib/admin_presets_base.class.php'); + +class admin_presets_import extends admin_presets_base { + + /** + * Displays the import moodleform + */ + public function show() { + + global $CFG; + + $url = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=import&mode=execute'; + $this->moodleform = new admin_presets_import_form($url); + } + + /** + * Imports the xmlfile into DB + */ + public function execute() { + + global $CFG, $USER, $DB; + + confirm_sesskey(); + + $sitesettings = $this->_get_site_settings(); + + $url = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=import&mode=execute'; + $this->moodleform = new admin_presets_import_form($url); + + if ($data = $this->moodleform->get_data()) { + + $usercontext = context_user::instance($USER->id); + + // Getting the file. + $xmlcontent = $this->moodleform->get_file_content('xmlfile'); + $xml = simplexml_load_string($xmlcontent); + if (!$xml) { + redirect($CFG->wwwroot . '/blocks/admin_presets/index.php?action=import', + get_string('wrongfile', 'block_admin_presets'), 4); + } + + // Preset info. + $preset = new StdClass(); + foreach ($this->rel as $dbname => $xmlname) { + $preset->$dbname = (String) $xml->$xmlname; + } + $preset->userid = $USER->id; + $preset->timeimported = time(); + + // Overwrite preset name. + if ($data->name != '') { + $preset->name = $data->name; + } + + // Inserting preset. + if (!$preset->id = $DB->insert_record('block_admin_presets', $preset)) { + print_error('errorinserting', 'block_admin_presets'); + } + + // Store it here for logging and other future id-oriented stuff. + $this->id = $preset->id; + + // Plugins settings. + $xmladminsettings = $xml->ADMIN_SETTINGS[0]; + foreach ($xmladminsettings as $plugin => $settings) { + + $plugin = strtolower($plugin); + + if (strstr($plugin, '__') != false) { + $plugin = str_replace('__', '/', $plugin); + } + + $pluginsettings = $settings->SETTINGS[0]; + + if ($pluginsettings) { + foreach ($pluginsettings->children() as $name => $setting) { + + $name = strtolower($name); + + // Default to ''. + if ($setting->__toString() === false) { + $value = ''; + } else { + $value = $setting->__toString(); + } + + if (empty($sitesettings[$plugin][$name])) { + debugging('Setting ' . $plugin . '/' . $name . + ' not supported by this Moodle version', DEBUG_DEVELOPER); + continue; + } + + // Cleaning the setting value. + if (!$presetsetting = $this->_get_setting($sitesettings[$plugin][$name]->get_settingdata(), + $value)) { + debugging('Setting ' . $plugin . '/' . $name . ' not implemented', DEBUG_DEVELOPER); + continue; + } + + $settingsfound = true; + + // New item. + $item = new StdClass(); + $item->adminpresetid = $preset->id; + $item->plugin = $plugin; + $item->name = $name; + $item->value = $presetsetting->get_value(); + + // Inserting items. + if (!$item->id = $DB->insert_record('block_admin_presets_it', $item)) { + print_error('errorinserting', 'block_admin_presets'); + } + + // Adding settings attributes. + if ($setting->attributes() && ($itemattributes = $presetsetting->get_attributes())) { + + foreach ($setting->attributes() as $attrname => $attrvalue) { + + $itemattributenames = array_flip($itemattributes); + + // Check the attribute existence. + if (!isset($itemattributenames[$attrname])) { + debugging('The ' . $plugin . '/' . $name . ' attribute ' . $attrname . + ' is not supported by this Moodle version', DEBUG_DEVELOPER); + continue; + } + + $attr = new StdClass(); + $attr->itemid = $item->id; + $attr->name = $attrname; + $attr->value = $attrvalue->__toString(); + $DB->insert_record('block_admin_presets_it_a', $attr); + } + } + } + } + } + + // If there are no valid or selected settings we should delete the admin preset record. + if (empty($settingsfound)) { + $DB->delete_records('block_admin_presets', array('id' => $preset->id)); + redirect($CFG->wwwroot . '/blocks/admin_presets/index.php?action=import', + get_string('novalidsettings', 'block_admin_presets'), 4); + } + + // Trigger the as it is usually triggered after execute finishes. + $this->log(); + + redirect($CFG->wwwroot . '/blocks/admin_presets/index.php?action=load&id=' . $preset->id); + } + } +} diff --git a/block/admin_presets/lib/admin_presets_load.class.php b/block/admin_presets/lib/admin_presets_load.class.php new file mode 100644 index 0000000..18369d5 --- /dev/null +++ b/block/admin_presets/lib/admin_presets_load.class.php @@ -0,0 +1,292 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/blocks/admin_presets/lib/admin_presets_base.class.php'); + +class admin_presets_load extends admin_presets_base { + + /** + * Executes the settings load into the system + */ + public function execute() { + + global $CFG, $DB, $OUTPUT, $USER; + + confirm_sesskey(); + + $url = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=load&mode=execute'; + $this->moodleform = new admin_presets_load_form($url); + + if ($data = $this->moodleform->get_data()) { + + // Standarized format $array['plugin']['settingname'] = child class. + $siteavailablesettings = $this->_get_site_settings(); + + // Get preset settings + if (!$items = $DB->get_records('block_admin_presets_it', array('adminpresetid' => $this->id))) { + print_error('errornopreset', 'block_admin_presets'); + } + $presetdbsettings = $this->_get_settings_from_db($items); + + // Standarized format $array['plugin']['settingname'] = child class. + $presetsettings = $this->_get_settings($presetdbsettings, false, $presetsettings = array()); + + // Only for selected items. + $appliedchanges = array(); + $unnecessarychanges = array(); + foreach (filter_input_array(INPUT_POST) as $varname => $value) { + + unset($updatesetting); + + if (strstr($varname, '@@') != false) { + + // [0] => setting [1] => plugin. + $name = explode('@@', $varname); + + // Just to be sure. + if (empty($presetsettings[$name[1]][$name[0]])) { + continue; + } + if (empty($siteavailablesettings[$name[1]][$name[0]])) { + continue; + } + + // New and old values. + $presetsetting = $presetsettings[$name[1]][$name[0]]; + $sitesetting = $siteavailablesettings[$name[1]][$name[0]]; + + // Wrong setting, set_value() method has previously cleaned the value. + if ($presetsetting->get_value() === false) { + debugging($presetsetting->get_settingdata()->plugin . '/' . + $presetsetting->get_settingdata()->name . + ' setting has a wrong value!', DEBUG_DEVELOPER); + continue; + } + + // If the new value is different the setting must be updated. + if ($presetsetting->get_value() != $sitesetting->get_value()) { + $updatesetting = true; + } + + // If one of the setting attributes values is different, setting must also be updated. + if ($presetsetting->get_attributes_values()) { + + $siteattributesvalues = $sitesetting->get_attributes_values(); + foreach ($presetsetting->get_attributes_values() as $attributename => $attributevalue) { + + if ($attributevalue !== $siteattributesvalues[$attributename]) { + $updatesetting = true; + } + } + } + + // Saving data + if (!empty($updatesetting)) { + + // The preset application it's only saved when values differences are found. + if (empty($applieditem)) { + // Save the preset application and store the preset applied id. + $presetapplied = new StdClass(); + $presetapplied->adminpresetid = $this->id; + $presetapplied->userid = $USER->id; + $presetapplied->time = time(); + if (!$adminpresetapplyid = $DB->insert_record('block_admin_presets_app', + $presetapplied)) { + print_error('errorinserting', 'block_admin_presets'); + } + } + + // Implemented this way because the config_write. + // method of admin_setting class does not. + // return the config_log inserted id. + $applieditem = new StdClass(); + $applieditem->adminpresetapplyid = $adminpresetapplyid; + if ($applieditem->configlogid = $presetsetting->save_value()) { + $DB->insert_record('block_admin_presets_app_it', $applieditem); + } + + // For settings with multiple values. + if ($attributeslogids = $presetsetting->save_attributes_values()) { + foreach ($attributeslogids as $attributelogid) { + $applieditemattr = new StdClass(); + $applieditemattr->adminpresetapplyid = $applieditem->adminpresetapplyid; + $applieditemattr->configlogid = $attributelogid; + $applieditemattr->itemname = $presetsetting->get_settingdata()->name; + $DB->insert_record('block_admin_presets_app_it_a', $applieditemattr); + } + } + + // Added to changed values. + $appliedchanges[$varname] = new StdClass(); + $appliedchanges[$varname]->plugin = $presetsetting->get_settingdata()->plugin; + $appliedchanges[$varname]->visiblename = $presetsetting->get_settingdata()->visiblename; + $appliedchanges[$varname]->oldvisiblevalue = $sitesetting->get_visiblevalue(); + $appliedchanges[$varname]->visiblevalue = $presetsetting->get_visiblevalue(); + + // Unnecessary changes (actual setting value). + } else { + $unnecessarychanges[$varname] = $presetsetting; + } + } + } + } + + // Output applied changes. + if (!empty($appliedchanges)) { + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string('settingsapplied', + 'block_admin_presets'), 3, 'admin_presets_success'); + $this->_output_applied_changes($appliedchanges); + } else { + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string('nothingloaded', + 'block_admin_presets'), 3, 'admin_presets_error'); + } + + // Show skipped changes. + if (!empty($unnecessarychanges)) { + + $skippedtable = new html_table(); + $skippedtable->attributes['class'] = 'generaltable boxaligncenter admin_presets_skipped'; + $skippedtable->head = array(get_string('plugin'), + get_string('settingname', 'block_admin_presets'), + get_string('actualvalue', 'block_admin_presets') + ); + + $skippedtable->align = array('center', 'center'); + + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string('settingsnotapplied', + 'block_admin_presets'), 3); + + foreach ($unnecessarychanges as $setting) { + $skippedtable->data[] = array($setting->get_settingdata()->plugin, + $setting->get_settingdata()->visiblename, + $setting->get_visiblevalue() + ); + } + + $this->outputs .= html_writer::table($skippedtable); + } + + // Don't display the load form. + $this->moodleform = false; + } + + /** + * Lists the preset available settings + */ + public function preview() { + $this->show(1); + } + + /** + * Displays the select preset settings to select what to import + * + * Loads the preset data and displays a settings tree + * + * It checks the Moodle version, it only allows users + * to import the preset available settings + * + * @param boolean $preview If it's a preview it only lists the preset applicable settings + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + public function show($preview = false) { + + global $CFG, $DB, $OUTPUT; + + $data = new StdClass(); + $data->id = $this->id; + + // Preset data. + if (!$preset = $DB->get_record('block_admin_presets', array('id' => $data->id))) { + print_error('errornopreset', 'block_admin_presets'); + } + + if (!$items = $DB->get_records('block_admin_presets_it', array('adminpresetid' => $data->id))) { + print_error('errornopreset', 'block_admin_presets'); + } + + // Standarized format $array['pluginname']['settingname']. + // object('name' => 'settingname', 'value' => 'settingvalue'). + $presetdbsettings = $this->_get_settings_from_db($items); + + // Load site avaible settings to ensure that the settings exists on this release. + $siteavailablesettings = $this->_get_site_settings(); + + $notapplicable = array(); + if ($presetdbsettings) { + foreach ($presetdbsettings as $plugin => $elements) { + foreach ($elements as $settingname => $element) { + + // If the setting doesn't exists in that release skip it. + if (empty($siteavailablesettings[$plugin][$settingname])) { + + // Adding setting plugin. + $presetdbsettings[$plugin][$settingname]->plugin = $plugin; + + $notapplicable[] = $presetdbsettings[$plugin][$settingname]; + } + } + } + } + // Standarized format $array['plugin']['settingname'] = child class. + $presetsettings = $this->_get_settings($presetdbsettings, false, $presetsettings = array()); + + $this->_get_settings_branches($presetsettings); + + // Print preset basic data. + $this->outputs .= $this->_html_writer_preset_info_table($preset); + + // Display not applicable settings. + if (!empty($notapplicable)) { + + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string('settingsnotapplicable', + 'block_admin_presets'), 3, 'admin_presets_error'); + + $table = new html_table(); + $table->attributes['class'] = 'generaltable boxaligncenter'; + $table->head = array(get_string('plugin'), + get_string('settingname', 'block_admin_presets'), + get_string('value', 'block_admin_presets')); + + $table->align = array('center', 'center'); + + foreach ($notapplicable as $setting) { + $table->data[] = array($setting->plugin, $setting->name, $setting->value); + } + + $this->outputs .= html_writer::table($table); + + } + + $url = $CFG->wwwroot . '/blocks/admin_presets/index.php?action=load&mode=execute'; + $this->moodleform = new admin_presets_load_form($url, $preview); + $this->moodleform->set_data($data); + + } +} diff --git a/block/admin_presets/lib/admin_presets_rollback.class.php b/block/admin_presets/lib/admin_presets_rollback.class.php new file mode 100644 index 0000000..a51755f --- /dev/null +++ b/block/admin_presets/lib/admin_presets_rollback.class.php @@ -0,0 +1,230 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/blocks/admin_presets/lib/admin_presets_base.class.php'); + +class admin_presets_rollback extends admin_presets_base { + + /** + * Displays the different previous applications of the preset + */ + public function show() { + + global $CFG, $DB, $OUTPUT; + + $table = new html_table(); + $table->attributes['class'] = 'generaltable boxaligncenter'; + $table->head = array(get_string('timeapplied', + 'block_admin_presets'), get_string('user'), get_string('actions')); + $table->align = array('left', 'center', 'left'); + + // Preset data. + $preset = $DB->get_record('block_admin_presets', array('id' => $this->id)); + + // Applications data. + $applications = $DB->get_records('block_admin_presets_app', array('adminpresetid' => $this->id)); + if (!$applications) { + print_error('notpreviouslyapplied', 'block_admin_presets'); + } + + foreach ($applications as $application) { + + $format = get_string('strftimedatetime', 'langconfig'); + + $user = $DB->get_record('user', array('id' => $application->userid)); + + $rollbacklink = $CFG->wwwroot . + '/blocks/admin_presets/index.php?action=rollback&mode=execute&id=' . + $application->id . '&sesskey=' . sesskey(); + $action = html_writer::link($rollbacklink, + get_string("rollback", "block_admin_presets")); + + $table->data[] = array(strftime($format, $application->time), + $user->firstname . ' ' . $user->lastname, + '<div>' . $action . '</div>' + ); + } + + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string("presetname", + "block_admin_presets") . ': ' . $preset->name, 3); + $this->outputs .= html_writer::table($table); + } + + /** + * Executes the application rollback + * + * Each setting value is checked against the config_log->value + */ + public function execute() { + + global $DB, $OUTPUT; + + confirm_sesskey(); + + // Actual settings. + $sitesettings = $this->_get_site_settings(); + + // To store rollback results. + $rollback = array(); + $failures = array(); + + if (!$DB->get_record('block_admin_presets_app', array('id' => $this->id))) { + print_error('wrongid', 'block_admin_presets'); + } + + // Items. + $itemsql = "SELECT cl.id, cl.plugin, cl.name, cl.value, cl.oldvalue, ap.adminpresetapplyid + FROM {block_admin_presets_app_it} ap + JOIN {config_log} cl ON cl.id = ap.configlogid + WHERE ap.adminpresetapplyid = {$this->id}"; + $itemchanges = $DB->get_records_sql($itemsql); + if ($itemchanges) { + + foreach ($itemchanges as $change) { + + if ($change->plugin == '') { + $change->plugin = 'none'; + } + + // Admin setting. + if (!empty($sitesettings[$change->plugin][$change->name])) { + + $actualsetting = $sitesettings[$change->plugin][$change->name]; + $oldsetting = $this->_get_setting($actualsetting->get_settingdata(), $change->oldvalue); + $oldsetting->set_text(); + $varname = $change->plugin . '_' . $change->name; + + // Check if the actual value is the same set by the preset. + if ($change->value == $actualsetting->get_value()) { + + $oldsetting->save_value(); + + // Output table. + $rollback[$varname] = new stdClass(); + $rollback[$varname]->plugin = $oldsetting->get_settingdata()->plugin; + $rollback[$varname]->visiblename = $oldsetting->get_settingdata()->visiblename; + $rollback[$varname]->oldvisiblevalue = $actualsetting->get_visiblevalue(); + $rollback[$varname]->visiblevalue = $oldsetting->get_visiblevalue(); + + // Deleting the admin_preset_apply_item instance. + $deletewhere = array('adminpresetapplyid' => $change->adminpresetapplyid, + 'configlogid' => $change->id); + $DB->delete_records('block_admin_presets_app_it', $deletewhere); + + } else { + + $failures[$varname] = new stdClass(); + $failures[$varname]->plugin = $oldsetting->get_settingdata()->plugin; + $failures[$varname]->visiblename = $oldsetting->get_settingdata()->visiblename; + $failures[$varname]->oldvisiblevalue = $actualsetting->get_visiblevalue(); + $failures[$varname]->visiblevalue = $oldsetting->get_visiblevalue(); + } + } + } + } + + // Attributes. + $attrsql = "SELECT cl.id, cl.plugin, cl.name, cl.value, cl.oldvalue, ap.itemname, ap.adminpresetapplyid + FROM {block_admin_presets_app_it_a} ap + JOIN {config_log} cl ON cl.id = ap.configlogid + WHERE ap.adminpresetapplyid = {$this->id}"; + $attrchanges = $DB->get_records_sql($attrsql); + if ($attrchanges) { + + foreach ($attrchanges as $change) { + + if ($change->plugin == '') { + $change->plugin = 'none'; + } + + // Admin setting of the attribute item. + if (!empty($sitesettings[$change->plugin][$change->itemname])) { + + // Getting the attribute item. + $actualsetting = $sitesettings[$change->plugin][$change->itemname]; + + $oldsetting = $this->_get_setting($actualsetting->get_settingdata(), $actualsetting->get_value()); + $oldsetting->set_attribute_value($change->name, $change->oldvalue); + $oldsetting->set_text(); + + $varname = $change->plugin . '_' . $change->name; + + // Check if the actual value is the same set by the preset. + $actualattributes = $actualsetting->get_attributes_values(); + if ($change->value == $actualattributes[$change->name]) { + + $oldsetting->save_attributes_values(); + + // Output table. + $rollback[$varname] = new stdClass(); + $rollback[$varname]->plugin = $oldsetting->get_settingdata()->plugin; + $rollback[$varname]->visiblename = $oldsetting->get_settingdata()->visiblename; + $rollback[$varname]->oldvisiblevalue = $actualsetting->get_visiblevalue(); + $rollback[$varname]->visiblevalue = $oldsetting->get_visiblevalue(); + + // Deleting the admin_preset_apply_item_attr instance. + $deletewhere = array('adminpresetapplyid' => $change->adminpresetapplyid, + 'configlogid' => $change->id); + $DB->delete_records('block_admin_presets_app_it_a', $deletewhere); + + } else { + + $failures[$varname] = new stdClass(); + $failures[$varname]->plugin = $oldsetting->get_settingdata()->plugin; + $failures[$varname]->visiblename = $oldsetting->get_settingdata()->visiblename; + $failures[$varname]->oldvisiblevalue = $actualsetting->get_visiblevalue(); + $failures[$varname]->visiblevalue = $oldsetting->get_visiblevalue(); + } + } + } + } + + // Delete application if no items nor attributes of the application remains. + if (!$DB->get_record('block_admin_presets_app_it', array('adminpresetapplyid' => $this->id)) && + !$DB->get_records('block_admin_presets_app_it_a', array('adminpresetapplyid' => $this->id))) { + + $DB->delete_records('block_admin_presets_app', array('id' => $this->id)); + } + + // Display the rollback changes. + if (!empty($rollback)) { + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string('rollbackresults', + "block_admin_presets"), 3, 'admin_presets_success'); + $this->outputs .= '<br/>'; + $this->_output_applied_changes($rollback); + } + + // Display the rollback failures. + if (!empty($failures)) { + $this->outputs .= '<br/>' . $OUTPUT->heading(get_string('rollbackfailures', + 'block_admin_presets'), 3, 'admin_presets_error'); + $this->outputs .= '<br/>'; + $this->_output_applied_changes($failures); + } + } +} diff --git a/block/admin_presets/lib/admin_presets_settings_types.php b/block/admin_presets/lib/admin_presets_settings_types.php new file mode 100644 index 0000000..cc0dc66 --- /dev/null +++ b/block/admin_presets/lib/admin_presets_settings_types.php @@ -0,0 +1,1201 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +abstract class admin_preset_setting { + + /** + * @var admin_setting + */ + protected $settingdata; + + /** + * @var admin_presets_delegation + */ + protected $delegation; + + /** + * The setting DB value + * + * @var mixed + */ + protected $value; + + /** + * Stores the visible value of the setting DB value + * + * @var string + */ + protected $visiblevalue; + + /** + * Text to display on the TreeView + * + * @var string + */ + protected $text; + + /** + * For multiple value settings, used to look for the other values + * + * @var string + */ + protected $attributes = false; + + /** + * To store the setting attributes + * + * @var array + */ + protected $attributesvalues; + + /** + * Stores the setting data and the selected value + * + * @param admin_setting $settingdata admin_setting subclass + * @param mixed $dbsettingvalue Actual value + */ + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + + $this->settingdata = $settingdata; + $this->delegation = new admin_presets_delegation(); + + if ($this->settingdata->plugin == '') { + $this->settingdata->plugin = 'none'; + } + + // Applies specific children behaviors + $this->set_behaviors(); + $this->apply_behaviors(); + + // Cleaning value. + $this->set_value($dbsettingvalue); + } + + /** + * Each class can overwrite this method to specify extra processes + */ + protected function set_behaviors() { + } + + /** + * Applies the children class specific behaviors + * + * See admin_presets_delegation() for the available extra behaviors + */ + protected function apply_behaviors() { + + if (!empty($this->behaviors)) { + + foreach ($this->behaviors as $behavior => $arguments) { + + // The arguments of the behavior depends on the caller. + $methodname = 'extra_' . $behavior; + $this->delegation->{$methodname}($arguments); + } + } + } + + /** + * Returns the TreeView node identifier + */ + public function get_id() { + return $this->settingdata->name . '@@' . $this->settingdata->plugin; + } + + public function get_value() { + return $this->value; + } + + /** + * Sets the setting value cleaning it + * + * Child classes should overwrite method to clean more acurately + * + * @param mixed $value Setting value + * @return mixed Returns false if wrong param value + */ + protected function set_value($value) { + $this->value = $value; + } + + public function get_visiblevalue() { + return $this->visiblevalue; + } + + /** + * Sets the visible name for the setting selected value + * + * In most cases the child classes will overwrite + */ + protected function set_visiblevalue() { + $this->visiblevalue = $this->value; + } + + public function get_description() { + + // PARAM_TEXT clean because the alt attribute does not support html. + $description = clean_param($this->settingdata->description, PARAM_TEXT); + return $this->encode_string($description); + } + + /** + * Encodes a string to send it to js + * + * @param string $string + * @return string + */ + protected function encode_string($string) { + + $encoded = rawurlencode($string); + return $encoded; + } + + public function get_text() { + return $this->encode_string($this->text); + } + + /** + * Sets the text to display on the settings tree + * + * Default format: I'm a setting visible name (setting value: "VALUE") + */ + public function set_text() { + + $this->set_visiblevalue(); + + $namediv = '<div class="admin_presets_tree_name">' . $this->settingdata->visiblename . '</div>'; + $valuediv = '<div class="admin_presets_tree_value">' . $this->visiblevalue . '</div>'; + + $this->text = $namediv . $valuediv . '<br/>'; + } + + public function get_attributes() { + return $this->attributes; + } + + public function get_attributes_values() { + return $this->attributesvalues; + } + + public function get_settingdata() { + return $this->settingdata; + } + + public function set_attribute_value($name, $value) { + $this->attributesvalues[$name] = $value; + } + + /** + * Saves the setting attributes values + * + * @return array Array of inserted ids (in config_log) + */ + public function save_attributes_values() { + + // Plugin name or null. + $plugin = $this->settingdata->plugin; + if ($plugin == 'none' || $plugin == '') { + $plugin = null; + } + + if (!$this->attributesvalues) { + return false; + } + + // To store inserted ids. + $ids = array(); + foreach ($this->attributesvalues as $name => $value) { + + // Getting actual setting. + $actualsetting = get_config($plugin, $name); + + // If it's the actual setting get off. + if ($value == $actualsetting) { + return false; + } + + if ($id = $this->save_value($name, $value)) { + $ids[] = $id; + } + } + + return $ids; + } + + /** + * Stores the setting into database, logs the change and returns the config_log inserted id + * + * @param bool $name + * @param mixed $value + * @return integer config_log inserted id + * @throws dml_exception + * @throws moodle_exception + */ + public function save_value($name = false, $value = null) { + + // Object values if no arguments. + if ($value === null) { + $value = $this->value; + } + if (!$name) { + $name = $this->settingdata->name; + } + + // Plugin name or null. + $plugin = $this->settingdata->plugin; + if ($plugin == 'none' || $plugin == '') { + $plugin = null; + } + + // Getting the actual value. + $actualvalue = get_config($plugin, $name); + + // If it's the same it's not necessary. + if ($actualvalue == $value) { + return false; + } + + set_config($name, $value, $plugin); + + return $this->to_log($plugin, $name, $value, $actualvalue); + } + + /** + * Copy of config_write method of the admin_setting class + * + * @param string $plugin + * @param string $name + * @param mixed $value + * @param mixed $actualvalue + * @return integer The stored config_log id + */ + protected function to_log($plugin, $name, $value, $actualvalue) { + + global $DB, $USER; + + // Log the change (pasted from admin_setting class). + $log = new stdClass(); + $log->userid = during_initial_install() ? 0 : $USER->id; // 0 as user id during install. + $log->timemodified = time(); + $log->plugin = $plugin; + $log->name = $name; + $log->value = $value; + $log->oldvalue = $actualvalue; + + // Getting the inserted config_log id. + if (!$id = $DB->insert_record('config_log', $log)) { + print_error('errorinserting', 'block_admin_presets'); + } + + return $id; + } +} + +/** + * Cross-class methods + */ +class admin_presets_delegation { + + /** + * Adds a piece of string to the $type setting + * + * @param boolean $value + * @param string $type Indicates the "extra" setting + * @return string + */ + public function extra_set_visiblevalue($value, $type) { + + // Adding the advanced value to the text string if present. + if ($value) { + $string = get_string('markedas' . $type, 'block_admin_presets'); + } else { + $string = get_string('markedasnon' . $type, 'block_admin_presets'); + } + + // Adding the advanced state. + return ', ' . $string; + } + + public function extra_loadchoices(admin_setting &$adminsetting) { + $adminsetting->load_choices(); + } +} + +/** TEXT **/ + +/** + * Basic text setting, cleans the param using the admin_setting paramtext attribute + */ +class admin_preset_auth_ldap_admin_setting_special_contexts_configtext extends admin_preset_setting { + + /** + * Validates the value using paramtype attribute + * + * @param string $value + * @return boolean Cleaned or not, but always true + */ + protected function set_value($value) { + + $this->value = $value; + + if (empty($this->settingdata->paramtype)) { + + // For configfile, configpasswordunmask... + $this->settingdata->paramtype = 'RAW'; + } + + $paramtype = 'PARAM_' . strtoupper($this->settingdata->paramtype); + + // Regexp. + if (!defined($paramtype)) { + $this->value = preg_replace($this->settingdata->paramtype, '', $this->value); + + // Standard moodle param type. + } else { + $this->value = clean_param($this->value, constant($paramtype)); + } + + return true; + } +} + +/** + * Basic text setting, cleans the param using the admin_setting paramtext attribute + */ +class admin_preset_admin_setting_configtext extends admin_preset_setting { + + /** + * Validates the value using paramtype attribute + * + * @param string $value + * @return boolean Cleaned or not, but always true + */ + protected function set_value($value) { + + $this->value = $value; + + if (empty($this->settingdata->paramtype)) { + + // For configfile, configpasswordunmask... + $this->settingdata->paramtype = 'RAW'; + } + + $paramtype = 'PARAM_' . strtoupper($this->settingdata->paramtype); + + // Regexp. + if (!defined($paramtype)) { + $this->value = preg_replace($this->settingdata->paramtype, '', $this->value); + + // Standard moodle param type. + } else { + $this->value = clean_param($this->value, constant($paramtype)); + } + + return true; + } +} + +/** + * Adds the advanced attribute + */ +class admin_preset_admin_setting_configtext_with_advanced extends admin_preset_admin_setting_configtext { + + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + + // To look for other values. + $this->attributes = array('fix' => $settingdata->name . '_adv'); + parent::__construct($settingdata, $dbsettingvalue); + } + + /** + * Delegates + */ + protected function set_visiblevalue() { + parent::set_visiblevalue(); + $this->visiblevalue .= $this->delegation->extra_set_visiblevalue( + $this->attributesvalues[$this->attributes['fix']], 'advanced'); + } +} + +class admin_preset_admin_setting_configiplist extends admin_preset_admin_setting_configtext { + + protected function set_value($value) { + + // Just in wrong format case. + $this->value = ''; + + // Check ip format. + if ($this->settingdata->validate($value) !== true) { + $this->value = false; + return false; + } + + $this->value = $value; + return true; + } +} + +/** + * Reimplementation to allow human friendly view of the selected regexps + */ +class admin_preset_admin_setting_devicedetectregex extends admin_preset_admin_setting_configtext { + + public function set_visiblevalue() { + + $values = json_decode($this->get_value()); + + if (!$values) { + parent::set_visiblevalue(); + return; + } + + $this->visiblevalue = ''; + foreach ($values as $key => $value) { + $this->visiblevalue .= $key . ' = ' . $value . ', '; + } + $this->visiblevalue = rtrim($this->visiblevalue, ', '); + } +} + +/** + * Reimplemented to store values in course table, not in config or config_plugins + */ +class admin_preset_admin_setting_sitesettext extends admin_preset_admin_setting_configtext { + + /** + * Overwritten to store the value in the course table + * + * @param bool $name + * @param mixed $value + * @return integer + */ + public function save_value($name = false, $value = false) { + + global $DB; + + // Object values if no arguments. + if ($value === null) { + $value = $this->value; + } + if (!$name) { + $name = $this->settingdata->name; + } + + $sitecourse = $DB->get_record('course', array('id' => 1)); + $actualvalue = $sitecourse->{$name}; + + // If it's the same value skip. + if ($actualvalue == $this->value) { + return false; + } + + // Plugin name or ''. + $plugin = $this->settingdata->plugin; + if ($plugin == 'none' || $plugin == '') { + $plugin = null; + } + + // Updating mdl_course. + $sitecourse->{$name} = $this->value; + $DB->update_record('course', $sitecourse); + + return $this->to_log($plugin, $name, $this->value, $actualvalue); + } +} + +class admin_preset_admin_setting_configselect extends admin_preset_setting { + + /** + * $value must be one of the setting choices + * + * @return boolean true if the value one of the setting choices + */ + protected function set_value($value) { + + // When we intantiate the class we need the choices. + if (empty($this->settindata->choices) && method_exists($this->settingdata, 'load_choices')) { + $this->settingdata->load_choices(); + } + + foreach ($this->settingdata->choices as $key => $choice) { + + if ($key == $value) { + $this->value = $value; + return true; + } + } + + $this->value = false; + return false; + } + + protected function set_visiblevalue() { + + // Just to avoid heritage problems. + if (empty($this->settingdata->choices[$this->value])) { + $this->visiblevalue = ''; + } else { + $this->visiblevalue = $this->settingdata->choices[$this->value]; + } + + } +} + +class admin_preset_admin_setting_bloglevel extends admin_preset_admin_setting_configselect { + + /** + * Extended to change the block visibility + */ + public function save_value($name = false, $value = false) { + + global $DB; + + if (!$id = parent::save_value($name, $value)) { + return false; + } + + // Pasted from admin_setting_bloglevel (can't use write_config). + if ($value == 0) { + $DB->set_field('block', 'visible', 0, array('name' => 'blog_menu')); + } else { + $DB->set_field('block', 'visible', 1, array('name' => 'blog_menu')); + } + + return $id; + } +} + +/** + * Adds support for the "advanced" attribute + */ +class admin_preset_admin_setting_configselect_with_advanced extends admin_preset_admin_setting_configselect { + + protected $advancedkey; + + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + + // Getting the advanced defaultsetting attribute name. + if (is_array($settingdata->defaultsetting)) { + foreach ($settingdata->defaultsetting as $key => $defaultvalue) { + if ($key != 'value') { + $this->advancedkey = $key; + } + } + } + + // To look for other values. + $this->attributes = array($this->advancedkey => $settingdata->name . '_adv'); + parent::__construct($settingdata, $dbsettingvalue); + } + + /** + * Funcionality used by other _with_advanced settings + */ + protected function set_visiblevalue() { + parent::set_visiblevalue(); + $this->visiblevalue .= $this->delegation->extra_set_visiblevalue( + $this->attributesvalues[$this->attributes[$this->advancedkey]], 'advanced'); + } +} + +class admin_preset_mod_quiz_admin_setting_browsersecurity extends admin_preset_admin_setting_configselect_with_advanced { + + public function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } +} + +class admin_preset_mod_quiz_admin_setting_grademethod extends admin_preset_admin_setting_configselect_with_advanced { + + public function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } +} + +class admin_preset_mod_quiz_admin_setting_overduehandling extends admin_preset_admin_setting_configselect_with_advanced { + + public function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } +} + +class admin_preset_mod_quiz_admin_setting_user_image extends admin_preset_admin_setting_configselect_with_advanced { + + public function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } +} + +/** + * A select with force and advanced options + */ +class admin_preset_admin_setting_gradecat_combo extends admin_preset_admin_setting_configselect { + + /** + * One db value for two setting attributes + * + * @param admin_setting $settingdata + * @param unknown_type $dbsettingvalue + */ + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + + // set_attribute_value() will mod the VARNAME_flag value. + $this->attributes = array('forced' => $settingdata->name . '_flag', + 'adv' => $settingdata->name . '_flag'); + parent::__construct($settingdata, $dbsettingvalue); + } + + /** + * Special treatment! the value be extracted from the $value argument + */ + protected function set_visiblevalue() { + parent::set_visiblevalue(); + + $flagvalue = $this->attributesvalues[$this->settingdata->name . '_flag']; + + if (isset($flagvalue)) { + + if (($flagvalue % 2) == 1) { + $forcedvalue = '1'; + } else { + $forcedvalue = '0'; + } + + if ($flagvalue >= 2) { + $advancedvalue = '1'; + } else { + $advancedvalue = '0'; + } + $this->visiblevalue .= $this->delegation->extra_set_visiblevalue($forcedvalue, 'forced'); + $this->visiblevalue .= $this->delegation->extra_set_visiblevalue($advancedvalue, 'advanced'); + } + } +} + +/** + * Extends the base class and lists the selected values separated by comma + */ +class admin_preset_admin_setting_configmultiselect extends admin_preset_setting { + + /** + * Ensure that the $value values are setting choices + */ + protected function set_value($value) { + + if ($value) { + $options = explode(',', $value); + foreach ($options as $key => $option) { + + foreach ($this->settingdata->choices as $key => $choice) { + + if ($key == $value) { + $this->value = $value; + return true; + } + } + } + + $value = implode(',', $options); + } + + $this->value = $value; + } + + protected function set_visiblevalue() { + + $values = explode(',', $this->value); + $visiblevalues = array(); + + foreach ($values as $value) { + + if (!empty($this->settingdata->choices[$value])) { + $visiblevalues[] = $this->settingdata->choices[$value]; + } + } + + if (empty($visiblevalues)) { + $this->visiblevalue = ''; + return false; + } + + $this->visiblevalue = implode(', ', $visiblevalues); + } +} + +/** + * Extends configselect to reuse set_valuevisible + */ +class admin_preset_admin_setting_users_with_capability extends admin_preset_admin_setting_configmultiselect { + + protected function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } + + protected function set_value($value) { + + // Dirty hack (the value stored in the DB is ''). + $this->settingdata->choices[''] = $this->settingdata->choices['$@NONE@$']; + + return parent::set_value($value); + } +} + +/** + * Generalizes a configmultipleselect with load_choices() + * + * @abstract + */ +abstract class admin_preset_admin_setting_configmultiselect_with_loader extends admin_preset_admin_setting_configmultiselect { + + public function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } +} + +class admin_preset_admin_setting_configtime extends admin_preset_setting { + + /** + * To check that the value is one of the options + * + * @param string $name + * @param mixed $value + */ + public function set_attribute_value($name, $value) { + + for ($i = 0; $i < 60; $i = $i + 5) { + $minutes[$i] = $i; + } + + if (!empty($minutes[$value])) { + $this->attributesvalues[$name] = $value; + } else { + $this->attributesvalues[$name] = $this->settingdata->defaultsetting['m']; + } + } + + protected function set_value($value) { + + $this->attributes = array('m' => $this->settingdata->name2); + + for ($i = 0; $i < 24; $i++) { + $hours[$i] = $i; + } + + if (empty($hours[$value])) { + $this->value = false; + } + + $this->value = $value; + } + + protected function set_visiblevalue() { + $this->visiblevalue = $this->value . ':' . $this->attributesvalues[$this->settingdata->name2]; + } +} + +/** CHECKBOXES **/ +class admin_preset_admin_setting_configcheckbox extends admin_preset_setting { + + protected function set_value($value) { + $this->value = clean_param($value, PARAM_BOOL); + return true; + } + + protected function set_visiblevalue() { + + if ($this->value) { + $str = get_string('yes'); + } else { + $str = get_string('no'); + } + + $this->visiblevalue = $str; + } +} + +class admin_preset_admin_setting_configcheckbox_with_advanced extends admin_preset_admin_setting_configcheckbox { + + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + + // To look for other values. + $this->attributes = array('adv' => $settingdata->name . '_adv'); + parent::__construct($settingdata, $dbsettingvalue); + } + + /** + * Uses delegation + */ + protected function set_visiblevalue() { + parent::set_visiblevalue(); + $this->visiblevalue .= $this->delegation->extra_set_visiblevalue( + $this->attributesvalues[$this->attributes['adv']], 'advanced'); + } +} + +class admin_preset_admin_setting_configcheckbox_with_lock extends admin_preset_admin_setting_configcheckbox { + + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + + // To look for other values. + $this->attributes = array('locked' => $settingdata->name . '_locked'); + parent::__construct($settingdata, $dbsettingvalue); + } + + /** + * Uses delegation + */ + protected function set_visiblevalue() { + parent::set_visiblevalue(); + $this->visiblevalue .= $this->delegation->extra_set_visiblevalue( + $this->attributesvalues[$this->attributes['locked']], 'locked'); + } +} + +/** + * Abstract class to be extended by multicheckbox settings + * + * Now it's a useless class, child classes could extend admin_preset_admin_setting_configmultiselect + * + * @abstract + */ +class admin_preset_admin_setting_configmulticheckbox extends admin_preset_admin_setting_configmultiselect { + + public function set_behaviors() { + $this->behaviors['loadchoices'] = &$this->settingdata; + } +} + +/** + * It doesn't specify loadchoices behavior because is set_visiblevalue who needs it + */ +class admin_preset_admin_setting_special_backupdays extends admin_preset_setting { + + protected function set_value($value) { + $this->value = clean_param($value, PARAM_SEQUENCE); + } + + protected function set_visiblevalue() { + + // TODO Try to use $this->behaviors. + $this->settingdata->load_choices(); + + $days = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'); + + $selecteddays = array(); + + $week = str_split($this->value); + foreach ($week as $key => $day) { + if ($day) { + $index = $days[$key]; + $selecteddays[] = $this->settingdata->choices[$index]; + } + } + + $this->visiblevalue = implode(', ', $selecteddays); + } +} + +/** OTHERS **/ +class admin_preset_admin_setting_special_calendar_weekend extends admin_preset_setting { + + protected function set_visiblevalue() { + + if (!$this->value) { + parent::set_visiblevalue(); + return; + } + + $days = array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'); + for ($i = 0; $i < 7; $i++) { + if ($this->value & (1 << $i)) { + $settings[] = get_string($days[$i], 'calendar'); + } + } + + $this->visiblevalue = implode(', ', $settings); + } +} + +/** + * Backward compatibility for Moodle 2.0 + */ +class admin_preset_admin_setting_quiz_reviewoptions extends admin_preset_setting { + + // Caution VENOM! admin_setting_quiz_reviewoptions vars can't be accessed. + private static $times = array( + QUIZ_REVIEW_IMMEDIATELY => 'reviewimmediately', + QUIZ_REVIEW_OPEN => 'reviewopen', + QUIZ_REVIEW_CLOSED => 'reviewclosed'); + + private static $things = array( + QUIZ_REVIEW_RESPONSES => 'responses', + QUIZ_REVIEW_ANSWERS => 'answers', + QUIZ_REVIEW_FEEDBACK => 'feedback', + QUIZ_REVIEW_GENERALFEEDBACK => 'generalfeedback', + QUIZ_REVIEW_SCORES => 'scores', + QUIZ_REVIEW_OVERALLFEEDBACK => 'overallfeedback'); + + /** + * Stores the setting data and the selected value + * + * @param admin_setting $settingdata admin_setting subclass + * @param mixed $dbsettingvalue Actual value + */ + public function __construct(admin_setting $settingdata, $dbsettingvalue) { + $this->attributes = array('fix' => $settingdata->name . '_adv'); + parent::__construct($settingdata, $dbsettingvalue); + } + + /** + * Delegates + */ + protected function set_visiblevalue() { + + $marked = array(); + + foreach (self::$times as $timemask => $time) { + foreach (self::$things as $typemask => $type) { + if ($this->value & $timemask & $typemask) { + $marked[$time][] = get_string($type, "quiz"); + } + } + } + + foreach ($marked as $time => $types) { + $visiblevalues[] = '<strong>' . get_string($time, "quiz") . + ':</strong> ' . implode(', ', $types); + } + $this->visiblevalue = implode('<br/>', $visiblevalues); + + if ($this->attributesvalues[$this->attributes['fix']]) { + $string = get_string("markedasnonadvanced", "block_admin_presets"); + } else { + $string = get_string("markedasadvanced", "block_admin_presets"); + } + + $this->visiblevalue .= '<br/>' . ucfirst($string); + } +} + +/** + * Compatible with moodle 2.1 onwards (20120314) + */ +class admin_preset_mod_quiz_admin_review_setting extends admin_preset_setting { + + /** + * Overwrite to add the reviewoptions text + */ + public function set_text() { + + $this->set_visiblevalue(); + + $name = get_string('reviewoptionsheading', 'quiz') . + ': ' . $this->settingdata->visiblename; + $namediv = '<div class="admin_presets_tree_name">' . $name . '</div>'; + $valuediv = '<div class="admin_presets_tree_value">' . $this->visiblevalue . '</div>'; + + $this->text = $namediv . $valuediv . '<br/>'; + } + + /** + * The setting value is a sum of 'mod_quiz_admin_review_setting::times' + */ + protected function set_visiblevalue() { + + // Getting the masks descriptions (mod_quiz_admin_review_setting protected method). + $reflectiontimes = new ReflectionMethod('mod_quiz_admin_review_setting', 'times'); + $reflectiontimes->setAccessible(true); + $times = $reflectiontimes->invoke(null); + + $visiblevalue = ''; + foreach ($times as $timemask => $namestring) { + + // If the value is checked. + if ($this->value & $timemask) { + $visiblevalue .= $namestring . ', '; + } + } + $visiblevalue = rtrim($visiblevalue, ', '); + + $this->visiblevalue = $visiblevalue; + } +} + +/* We need to extend all those class */ + +class admin_preset_admin_setting_configtextarea extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_configfile extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_configexecutable extends admin_preset_admin_setting_configfile { +} + +class admin_preset_admin_setting_configdirectory extends admin_preset_admin_setting_configfile { +} + +class admin_preset_admin_setting_special_backup_auto_destination extends admin_preset_admin_setting_configdirectory { +} + +class admin_preset_admin_setting_configpasswordunmask extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_langlist extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_configcolourpicker extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_emoticons extends admin_preset_setting { +} + +class admin_preset_admin_setting_confightmleditor extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_configtext_trim_lower extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_special_gradepointmax extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_special_gradepointdefault extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_configempty extends admin_preset_admin_setting_configtext { +} + +class admin_preset_admin_setting_configtext_with_maxlength extends admin_preset_admin_setting_configtext { +} + +class admin_preset_editor_tinymce_json_setting_textarea extends admin_preset_admin_setting_configtext { +} + +/** + * I'm not overwriting set_visiblevalue() as there is a lot of logic to duplicate. + */ +class admin_preset_admin_setting_configduration extends admin_preset_admin_setting_configtext { +} + +class admin_preset_enrol_flatfile_role_setting extends admin_preset_admin_setting_configtext { +} + +/** + * I'm not overwriting set_visiblevalue() as there is a lot of logic to duplicate. + * + * @see admin_preset_admin_setting_configduration + */ +class admin_preset_admin_setting_configduration_with_advanced extends admin_preset_admin_setting_configtext_with_advanced { +} + +class admin_preset_admin_setting_special_frontpagedesc extends admin_preset_admin_setting_sitesettext { +} + +class admin_preset_admin_setting_special_selectsetup extends admin_preset_admin_setting_configselect { +} + +class admin_preset_admin_setting_sitesetselect extends admin_preset_admin_setting_configselect { +} + +class admin_preset_admin_setting_special_grademinmaxtouse extends admin_preset_admin_setting_configselect { +} + +class admin_preset_admin_setting_my_grades_report extends admin_preset_admin_setting_configselect { +} + +class admin_preset_admin_setting_servertimezone extends admin_preset_admin_setting_configselect { +} + +class admin_preset_admin_setting_forcetimezone extends admin_preset_admin_setting_configselect { +} + +class admin_preset_enrol_database_admin_setting_category extends admin_preset_admin_setting_configselect { +} + +class admin_preset_enrol_ldap_admin_setting_category extends admin_preset_admin_setting_configselect { +} + +class admin_preset_format_singleactivity_admin_setting_activitytype extends admin_preset_admin_setting_configselect { +} + +class admin_preset_admin_setting_courselist_frontpage extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_setting_configmultiselect_modules extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_settings_country_select extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_setting_special_registerauth extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_setting_special_debug extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_settings_coursecat_select extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_setting_grade_profilereport extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_settings_num_course_sections extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_setting_question_behaviour extends admin_preset_admin_setting_configmultiselect_with_loader { +} + +class admin_preset_admin_setting_sitesetcheckbox extends admin_preset_admin_setting_configcheckbox { +} + +class admin_preset_admin_setting_special_adminseesall extends admin_preset_admin_setting_configcheckbox { +} + +class admin_preset_admin_setting_regradingcheckbox extends admin_preset_admin_setting_configcheckbox { +} + +class admin_preset_admin_setting_special_gradelimiting extends admin_preset_admin_setting_configcheckbox { +} + +class admin_preset_admin_setting_enablemobileservice extends admin_preset_admin_setting_configcheckbox { +} + +class admin_preset_admin_setting_pickroles extends admin_preset_admin_setting_configmulticheckbox { +} + +class admin_preset_admin_setting_special_coursemanager extends admin_preset_admin_setting_configmulticheckbox { +} + +class admin_preset_admin_setting_special_coursecontact extends admin_preset_admin_setting_configmulticheckbox { +} + +class admin_preset_admin_setting_special_gradebookroles extends admin_preset_admin_setting_configmulticheckbox { +} + +class admin_preset_admin_setting_special_gradeexport extends admin_preset_admin_setting_configmulticheckbox { +} \ No newline at end of file diff --git a/block/admin_presets/module.js b/block/admin_presets/module.js new file mode 100644 index 0000000..c41fcb4 --- /dev/null +++ b/block/admin_presets/module.js @@ -0,0 +1,115 @@ +M.block_admin_presets = { + + tree: null, + nodes: null, + + + /** + * Initializes the TreeView object and adds the submit listener + */ + init: function (Y) { + + Y.use('yui2-treeview', function (Y) { + + var context = M.block_admin_presets; + + context.tree = new Y.YUI2.widget.TreeView("settings_tree_div"); + + context.nodes = []; + context.nodes.root = context.tree.getRoot(); + }); + }, + + /** + * Creates a tree branch + */ + addNodes: function (Y, ids, nodeids, labels, descriptions, parents) { + + var context = M.block_admin_presets; + + var nelements = ids.length; + for (var i = 0; i < nelements; i++) { + + var settingId = ids[i]; + var nodeId = nodeids[i]; + var label = decodeURIComponent(labels[i]); + var description = decodeURIComponent(descriptions[i]); + var parent = parents[i]; + + var newNode = new Y.YUI2.widget.HTMLNode(label, context.nodes[parent]); + + newNode.settingId = settingId; + newNode.setNodesProperty('title', description); + newNode.highlightState = 1; + + context.nodes[nodeId] = newNode; + } + }, + + render: function (Y) { + + var context = M.block_admin_presets; + var categories = context.tree.getNodesByProperty('settingId', 'category'); + // Cleaning categories without children. + if (categories) { + for (var i = 0; i < categories.length; i++) { + if (!categories[i].hasChildren()) { + context.tree.popNode(categories[i]); + } + } + } + categories = context.tree.getRoot().children; + if (categories) { + for (var j = 0; j < categories.length; j++) { + if (!categories[j].hasChildren()) { + context.tree.popNode(categories[j]); + } + } + } + + // Context.tree.expandAll();. + context.tree.setNodesProperty('propagateHighlightUp', true); + context.tree.setNodesProperty('propagateHighlightDown', true); + context.tree.subscribe('clickEvent', context.tree.onEventToggleHighlight); + context.tree.render(); + + // Listener to create one node for each selected setting. + Y.YUI2.util.Event.on('id_admin_presets_submit', 'click', function () { + + // We need the moodle form to add the checked settings. + var settingsPresetsForm = document.getElementById('id_admin_presets_submit').parentNode; + + var hiLit = context.tree.getNodesByProperty('highlightState', 1); + if (Y.YUI2.lang.isNull(hiLit)) { + Y.YUI2.log("Nothing selected"); + + } else { + + // Only for debugging. + var labels = []; + + for (var i = 0; i < hiLit.length; i++) { + + var treeNode = hiLit[i]; + + // Only settings not setting categories nor settings pages. + if (treeNode.settingId !== 'category' && treeNode.settingId !== 'page') { + labels.push(treeNode.settingId); + + // If the node does not exists we add it. + if (!document.getElementById(treeNode.settingId)) { + + var settingInput = document.createElement('input'); + settingInput.setAttribute('type', 'hidden'); + settingInput.setAttribute('name', treeNode.settingId); + settingInput.setAttribute('value', '1'); + settingsPresetsForm.appendChild(settingInput); + } + } + } + + Y.YUI2.log("Checked settings:\n" + labels.join("\n"), "info"); + } + }); + } +}; diff --git a/block/admin_presets/pix/check0.gif b/block/admin_presets/pix/check0.gif new file mode 100644 index 0000000000000000000000000000000000000000..193028b99361c6527f17a9056037f3d8729fada7 GIT binary patch literal 608 zcmZ?wbhEHb6krfzc;>|L^y$-=FJFHD{{8v$=l}lwYiw-%_U+rNSFb*Q{ycHwL|t9o z4<A0<y?b}lrcEs^Er0+1eemGH=FOX5ym+y5=gxcg?tT3D@%{VvckbL-vSi7nOP9WW z{W@>nyho27-Mo48_U+qq=FHi!VMBd=eR6X0+O=zU@7{g=`t?<-R$aV!QBO~=si|q# zu3gWbJ!@`mzI^%e<HwJeEnC*w+N!Uwzh}>$A3uKV-Me@C^y%BTZ(p%u#nr1<lai7Q z4GoQqjBebxaqHHtckkZazkmPJr%(UEfMI9?#h)yU3=B>TIw0qR;)H>HZ-Y})b4zPm zdq-zicTcmUtRf$uChJr_107k%CMHe`N0x;P`5omrrJC4T89CNEtasp8#oolg$!xcC z*G^_mh9((lImRPL*@bMRWts#ebZswOyr3>2&}6S{Yk&KWy@0BkeG`wFo;Jga7tBID z3Or5XCSs!Nxxa8b*r|v&i3p1^@tiSGSg@dh)kuRwD&VlggmwY>tRDsr6B@;pN)`kN kI5!@bwak+c_`=}P#wi$N7O_%^c>;TH(y2L>8UhT~0GYwkU;qFB literal 0 HcmV?d00001 diff --git a/block/admin_presets/pix/check1.gif b/block/admin_presets/pix/check1.gif new file mode 100644 index 0000000000000000000000000000000000000000..7d9ceba3847ffb41864626de755147cf2e0ccc41 GIT binary patch literal 622 zcmZ?wbhEHb6krfzcoxU7ckkYV2M?Y-d-m9|V>@>2*tTt3LPEl(O`BR;T8<t)x^m^p zLx&D+-n_Z7vGMNRyLax~*|%@s{{8#EefyT1oBQ<X(|h;s)zvlp|NsBht5*R50iQm7 z`uq3qg9i_uKYw0dU%zwb&YL%HK6>=%(xpqq#l;5>99Xq#)&2YTYiepbIy&0g+AOW@ zckS9Guc-F@`}d~imPLyeZQs6~fsu`YfvK{pX2F65U0q$Rt*sL$O-@WsOG`^DEiKK? z&hGB+UcY|*k|j<XVU0;^Z@D&KMaP9Y20NDkkph*RPC>j7?2Vb@dHfw{DGzO<2Bs z`IaqPdU|@+ty@=FS-D`r!sh1Y-Me>J*VG+3a%9=EW##4N)22;lVdr6D<<2iCGBh+i zckUbm1H=FS{~2fx6o0ZXGBCt3=zy#O#R&uZ>4vzb=9bpB_Kwc3?w)3G1vw55ciyQS zW`PRgO}cD$F-!{=I>xB7xi(2ivT(0uTp!FTFDcO^!p6$5YnNAS5G$KVlcKOH%W)Rt z*w_GXVZ|mHWqqzFy>P<_4=xjBnI<)LE|*vxtw?h}+faYCCO%DePcfrdF&|a~dpW)) zeqR9(7KSG0gcb&dEE|CkM%ETa&NdYZhe8JyIZ>9<6B8S{kN3;1eDgxVqlr)Pbda9$ irlVafj?yL_KMpK-);L?+CF91070b`hZ)aj;um%A96Yh%u literal 0 HcmV?d00001 diff --git a/block/admin_presets/pix/check2.gif b/block/admin_presets/pix/check2.gif new file mode 100644 index 0000000000000000000000000000000000000000..181317599bfd45f03a7a69784b232509171d98e9 GIT binary patch literal 609 zcmZ?wbhEHb6krfzc;?HnW5<qT$Bvyndv?o~EeQz;hYlT*RWjJLX;Vu}%g&uUH*enD z*x0yj+qV7t_a8laG$0^g`}Xa*xw-rH?R)j=)ul_9{{H>@{Q2|Z;^P1R|37;4=+mc9 z2M!#ludjdl^y!^DcW&Oi`QX8Wd-v|GTD9u_{rlg(eY<=2?(*f!7cE+}Zr!@3rlz#C zw93lLty{Ntbad?6wX3|me9f9QU0q#_jEvpg-8D5erKP3o*RL-ssmjjIK6B=bk&#hr zYwM9CN4|dj+Sb-)XlVHT`}dxnp5w=lFIlo=!GZ<LmMxnybw+b@^TC4$SFT*SckkYF z=gu)OF#P}jpJ7k}#h)yU3=F;uIw049;)H>HUxRN`b4zPmdq-zicTcl~ybK42Ciheh zYj=5xCI=R0Z>EI{ExnakY@3An8CchPtoLB$<QHy|W#RGKwR;y2i)@pmpc2E;W1IqN zf|5-N;_99kFI{vIS7=f;@zgM9yT|5c=BlmSq-w{m!~FU+kAQ)UYLlF!ko0=q@4Oyf zhH_1cQ63x*96FjAIG9al2s%w*73G`eF-Kz}J0EwG$CV2Qmn?QyGpgI8ptz!mjf+!c Wh0TFWtgO7EQYYtlehyGzum%9F#MlD> literal 0 HcmV?d00001 diff --git a/block/admin_presets/settings.php b/block/admin_presets/settings.php new file mode 100644 index 0000000..4a57c38 --- /dev/null +++ b/block/admin_presets/settings.php @@ -0,0 +1,40 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($ADMIN->fulltree) { + + $sensiblesettingsdefault = 'recaptchapublickey@@none, recaptchaprivatekey@@none, googlemapkey@@none, '; + $sensiblesettingsdefault .= 'secretphrase@@none, cronremotepassword@@none, smtpuser@@none, '; + $sensiblesettingsdefault .= 'smtppass@none, proxypassword@@none, password@@quiz, '; + $sensiblesettingsdefault .= 'enrolpassword@@moodlecourse, allowedip@@none, blockedip@@none'; + + $settings->add(new admin_setting_configtextarea('admin_presets/sensiblesettings', + get_string('sensiblesettings', 'block_admin_presets'), + get_string('sensiblesettingstext', 'block_admin_presets'), + $sensiblesettingsdefault, PARAM_TEXT)); +} diff --git a/block/admin_presets/styles.css b/block/admin_presets/styles.css new file mode 100644 index 0000000..6b86901 --- /dev/null +++ b/block/admin_presets/styles.css @@ -0,0 +1,44 @@ +.admin_presets_tree_name { + padding: 0 0 4px 2px; +} + +.admin_presets_tree_value { + border: 1px solid #ccc; + padding: 0 0 4px 2px; +} + +.admin_presets_error { + color: red; + text-align: center; +} + +.admin_presets_success { + color: green; + text-align: center; +} + +#page-blocks-admin_presets-index #settings_tree_div .catnode { + display: inline; + margin-left: 5px; +} + +#page-blocks-admin_presets-index #settings_tree_div .ygtv-checkbox .ygtv-highlight0 .ygtvcontent { + background: url([[pix:block_admin_presets|check0]]) no-repeat; + padding-left: 1em; +} + +#page-blocks-admin_presets-index #settings_tree_div .ygtv-checkbox .ygtv-highlight0 .ygtvfocus.ygtvcontent, +.ygtv-checkbox .ygtv-highlight1 .ygtvfocus.ygtvcontent, +.ygtv-checkbox .ygtv-highlight2 .ygtvfocus.ygtvcontent { + background-color: #c0e0e0; +} + +#page-blocks-admin_presets-index #settings_tree_div .ygtv-checkbox .ygtv-highlight1 .ygtvcontent { + background: url([[pix:block_admin_presets|check1]]) no-repeat; + padding-left: 1em; +} + +#page-blocks-admin_presets-index #settings_tree_div .ygtv-checkbox .ygtv-highlight2 .ygtvcontent { + background: url([[pix:block_admin_presets|check2]]) no-repeat; + padding-left: 1em; +} diff --git a/block/admin_presets/tabs.php b/block/admin_presets/tabs.php new file mode 100644 index 0000000..a68783d --- /dev/null +++ b/block/admin_presets/tabs.php @@ -0,0 +1,45 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$adminpresetsurl = $CFG->wwwroot . '/blocks/admin_presets/index.php'; + +$adminpresetstabs = array('base' => 'base', + 'export' => 'export', + 'import' => 'import'); + +if (!array_key_exists($this->action, $adminpresetstabs)) { + $row[] = new tabobject($this->action, $adminpresetsurl . + '?action=' . $this->action, get_string('action' . $this->action, 'block_admin_presets')); +} + +foreach ($adminpresetstabs as $actionname) { + $row[] = new tabobject($actionname, $adminpresetsurl . + '?action=' . $actionname, get_string('action' . $actionname, 'block_admin_presets')); +} + +print_tabs(array($row), $this->action); diff --git a/block/admin_presets/tests/behat/import_settings.feature b/block/admin_presets/tests/behat/import_settings.feature new file mode 100644 index 0000000..425b1d7 --- /dev/null +++ b/block/admin_presets/tests/behat/import_settings.feature @@ -0,0 +1,56 @@ +@block @block_admin_presets +Feature: I can export and import site settings + In order to save time + As an admin + I need to export and import settings presets + + Background: + Given I log in as "admin" + And I am on site homepage + And I follow "Turn editing on" + And I add the "Admin presets" block + And I follow "Export settings" + And I set the following fields to these values: + | Name | My preset | + And I press "Save changes" + + @javascript + Scenario: Preset settings are applied + Given I follow "Advanced features" + And I set the field "Enable portfolios" to "1" + And I set the field "Enable badges" to "0" + And I press "Save changes" + And I navigate to "Assignment settings" node in "Site administration > Plugins > Activity modules > Assignment" + And I set the field "Feedback plugin" to "File feedback" + And I press "Save changes" + And I navigate to "Course overview" node in "Site administration > Plugins > Blocks" + And I set the field "Default maximum courses" to "5" + And I press "Save changes" + When I am on site homepage + And I follow "Presets" + And I click on "load" "link" in the "My preset" "table_row" + And I press "Load selected settings" + Then I should not see "All preset settings skipped, they are already loaded" + And I should see "Settings applied" + And I should see "Enable portfolios" in the ".admin_presets_applied" "css_element" + And I should see "Enable badges" in the ".admin_presets_applied" "css_element" + And I should see "Feedback plugin" in the ".admin_presets_applied" "css_element" + And I should see "File feedback" in the ".admin_presets_applied" "css_element" + And I should see "Default maximum courses" in the ".admin_presets_applied" "css_element" + And I should see "Enable outcomes" in the ".admin_presets_skipped" "css_element" + And I should see "Show recent submissions" in the ".admin_presets_skipped" "css_element" + And I should see "Force maximum courses" in the ".admin_presets_skipped" "css_element" + And I follow "Advanced features" + And the field "Enable portfolios" matches value "0" + And the field "Enable badges" matches value "1" + And I navigate to "Assignment settings" node in "Site administration > Plugins > Activity modules > Assignment" + And the field "Feedback plugin" matches value "Feedback comments" + And I navigate to "Course overview" node in "Site administration > Plugins > Blocks" + And the field "Default maximum courses" matches value "10" + + @javascript + Scenario: Settings don't change if you import what you just exported + When I click on "load" "link" in the "My preset" "table_row" + And I press "Load selected settings" + Then I should see "All preset settings skipped, they are already loaded" + And I should not see "Settings applied" \ No newline at end of file diff --git a/block/admin_presets/tests/behat/revert_changes.feature b/block/admin_presets/tests/behat/revert_changes.feature new file mode 100644 index 0000000..9f5d9ca --- /dev/null +++ b/block/admin_presets/tests/behat/revert_changes.feature @@ -0,0 +1,46 @@ +@block @block_admin_presets +Feature: I can revert changes + In order to save time + As an admin + I need to export and import settings presets + + @javascript + Scenario: Load changes and revert them + Given I log in as "admin" + And I am on site homepage + And I follow "Turn editing on" + And I add the "Admin presets" block + And I follow "Export settings" + And I set the following fields to these values: + | Name | My preset | + And I press "Save changes" + And I follow "Advanced features" + And I set the field "Enable portfolios" to "1" + And I set the field "Enable badges" to "0" + And I press "Save changes" + And I navigate to "Assignment settings" node in "Site administration > Plugins > Activity modules > Assignment" + And I set the field "Feedback plugin" to "File feedback" + And I press "Save changes" + And I navigate to "Course overview" node in "Site administration > Plugins > Blocks" + And I set the field "Default maximum courses" to "5" + And I press "Save changes" + And I am on site homepage + And I follow "Presets" + And I click on "load" "link" in the "My preset" "table_row" + And I press "Load selected settings" + And I am on site homepage + When I follow "Presets" + And I click on "revert" "link" in the "My preset" "table_row" + And I follow "revert" + Then I should see "Settings successfully restored" + And I should see "Enable portfolios" in the ".admin_presets_applied" "css_element" + And I should see "Enable badges" in the ".admin_presets_applied" "css_element" + And I should see "Feedback plugin" in the ".admin_presets_applied" "css_element" + And I should see "File feedback" in the ".admin_presets_applied" "css_element" + And I follow "Advanced features" + And the field "Enable portfolios" matches value "1" + And the field "Enable badges" matches value "0" + And I navigate to "Assignment settings" node in "Site administration > Plugins > Activity modules > Assignment" + And the field "Feedback plugin" matches value "File feedback" + And I navigate to "Course overview" node in "Site administration > Plugins > Blocks" + And the field "Default maximum courses" matches value "5" \ No newline at end of file diff --git a/block/admin_presets/version.php b/block/admin_presets/version.php new file mode 100644 index 0000000..7520cae --- /dev/null +++ b/block/admin_presets/version.php @@ -0,0 +1,34 @@ +<?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/>. + +/** + * Admin presets block main controller + * + * @package blocks/admin_presets + * @copyright 2019 Pimenko <support@pimenko.com><pimenko.com> + * @author Jordan Kesraoui | DigiDago + * @orignalauthor David Monllaó <david.monllao@urv.cat> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020062417; +$plugin->requires = 2016052300; // Requires this Moodle version +$plugin->component = 'block_admin_presets'; +$plugin->release = '3.2'; +$plugin->cron = 0; +$plugin->maturity = MATURITY_STABLE; diff --git a/block/attendance/.travis.yml b/block/attendance/.travis.yml new file mode 100644 index 0000000..3e9e4fe --- /dev/null +++ b/block/attendance/.travis.yml @@ -0,0 +1,52 @@ +language: php + +addons: + postgresql: "9.5" + +services: + - mysql + - postgresql + - docker + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.2 + - 7.4 + +env: + global: + - MOODLE_BRANCH=master + - MUSTACHE_IGNORE_NAMES=mobile_teacher_form.mustache + matrix: + - DB=pgsql + - DB=mysqli + +before_install: + - phpenv config-rm xdebug.ini + - nvm install 14.0.0 + - nvm use 14.0.0 + - cd ../.. + - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci dev-master + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci add-plugin --branch main danmarsden/moodle-mod_attendance + - moodle-plugin-ci install + - docker run -d -p 127.0.0.1:4444:4444 --net=host --shm-size=2g -v $HOME/build/moodle:$HOME/build/moodle selenium/standalone-chrome:3 + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat --profile chrome diff --git a/block/attendance/README.md b/block/attendance/README.md new file mode 100644 index 0000000..dab05bd --- /dev/null +++ b/block/attendance/README.md @@ -0,0 +1,12 @@ +#Moodle Attendance Block [](https://travis-ci.org/danmarsden/moodle-block_attendance) + +The Attendance block supplements the Attendance activity and is supported and maintained by Dan Marsden http://danmarsden.com + +The Attendance block was previously developed by +* Human Logic Development Team, www.human-logic.com +* Dmitry Pupinin, Novosibirsk, Russia, + +#PURPOSE +The Attendance activity allows teachers to maintain a record of attendance, replacing or supplementing a paper-based attendance register. + +This block provides quick links to features such as reporting, taking of attendance and adding new sessions. diff --git a/block/attendance/block_attendance.php b/block/attendance/block_attendance.php new file mode 100644 index 0000000..9c4f482 --- /dev/null +++ b/block/attendance/block_attendance.php @@ -0,0 +1,160 @@ +<?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/>. + +/** + * Attendance Block + * + * @package block_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Displays information about Attendance Module in this course. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_attendance extends block_base { + + /** + * Set the initial properties for the block + */ + public function init() { + $this->title = get_string('blockname', 'block_attendance'); + } + + /** + * Gets the content for this block + * + * @return object $this->content + */ + public function get_content() { + global $CFG, $USER, $COURSE; + + if ($this->content !== null) { + return $this->content; + } + + $this->content = new stdClass; + $this->content->footer = ''; + $this->content->text = ''; + + $attendances = get_all_instances_in_course('attendance', $COURSE, null, true); + if (count($attendances) == 0) { + $this->content->text = get_string('needactivity', 'block_attendance');; + return $this->content; + } + + require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + require_once($CFG->dirroot.'/mod/attendance/renderhelpers.php'); + + foreach ($attendances as $attinst) { + $cmid = $attinst->coursemodule; + $cm = get_coursemodule_from_id('attendance', $cmid, $COURSE->id, false, MUST_EXIST); + if (!empty($cm->deletioninprogress)) { + // Don't display if this attendance is in recycle bin. + continue; + } + $context = context_module::instance($cmid, MUST_EXIST); + $divided = $this->divide_databasetable_and_coursemodule_data($attinst); + + $att = new mod_attendance_structure($divided->atttable, $divided->cm, $COURSE, $context); + + $this->content->text .= html_writer::link($att->url_view(), html_writer::tag('b', format_string($att->name))); + $this->content->text .= html_writer::empty_tag('br'); + + // Link to attendance. + + if (has_capability('mod/attendance:takeattendances', $context) or + has_capability('mod/attendance:changeattendances', $context)) { + $this->content->text .= html_writer::link($att->url_manage(array('from' => 'block')), + get_string('takeattendance', 'attendance')); + $this->content->text .= html_writer::empty_tag('br'); + } + if (has_capability('mod/attendance:manageattendances', $context)) { + $url = $att->url_sessions(array('action' => mod_attendance_sessions_page_params::ACTION_ADD)); + $this->content->text .= html_writer::link($url, get_string('add', 'attendance')); + $this->content->text .= html_writer::empty_tag('br'); + } + if (has_capability('mod/attendance:viewreports', $context)) { + $this->content->text .= html_writer::link($att->url_report(), get_string('report', 'attendance')); + $this->content->text .= html_writer::empty_tag('br'); + } + + if (has_capability('mod/attendance:canbelisted', $context, null, false) && + has_capability('mod/attendance:view', $context)) { + $this->content->text .= construct_full_user_stat_html_table($attinst, $USER); + } + $this->content->text .= "<br />"; + } + + $categorycontext = context_coursecat::instance($COURSE->category); + if (has_capability('mod/attendance:viewsummaryreports', $categorycontext)) { + $url = new moodle_url('/mod/attendance/coursesummary.php', + array('category' => $COURSE->category, 'fromcourse' => $COURSE->id)); + $this->content->text .= html_writer::link($url, get_string('categoryreport', 'attendance')); + $this->content->text .= html_writer::empty_tag('br'); + } + + return $this->content; + } + + /** + * parses data to pass into construct. + * @param object $alldata + * @return array + */ + private function divide_databasetable_and_coursemodule_data($alldata) { + static $cmfields; + + if (!isset($cmfields)) { + $cmfields = array( + 'coursemodule' => 'id', + 'section' => 'section', + 'visible' => 'visible', + 'groupmode' => 'groupmode', + 'groupingid' => 'groupingid', + 'groupmembersonly' => 'groupmembersonly'); + } + + $atttable = new stdClass(); + $cm = new stdClass(); + foreach ($alldata as $field => $value) { + if (array_key_exists($field, $cmfields)) { + $cm->{$cmfields[$field]} = $value; + } else { + $atttable->{$field} = $value; + } + } + + $ret = new stdClass(); + $ret->atttable = $atttable; + $ret->cm = $cm; + + return $ret; + } + + /** + * Set the applicable formats for this block + * @return array + */ + public function applicable_formats() { + return array('all' => true, 'my' => false, 'admin' => false, 'tag' => false); + } +} diff --git a/block/attendance/classes/privacy/provider.php b/block/attendance/classes/privacy/provider.php new file mode 100644 index 0000000..2f02482 --- /dev/null +++ b/block/attendance/classes/privacy/provider.php @@ -0,0 +1,46 @@ +<?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 Subsystem implementation for block_attendance. + * + * @package block_attendance + * @copyright 2018 Dan Marsden <dan@danmarsden.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_attendance\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_attendance implementing null_provider. + * + * @copyright 2018 Dan Marsden <dan@danmarsden.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/block/attendance/composer.json b/block/attendance/composer.json new file mode 100644 index 0000000..3ce5c92 --- /dev/null +++ b/block/attendance/composer.json @@ -0,0 +1,10 @@ +{ + "name": "danmarsden/moodle-block_attendance", + "type": "moodle-block", + "require": { + "composer/installers": "~1.0" + }, + "extra": { + "installer-name": "attendance" + } +} diff --git a/block/attendance/db/access.php b/block/attendance/db/access.php new file mode 100644 index 0000000..54d860b --- /dev/null +++ b/block/attendance/db/access.php @@ -0,0 +1,38 @@ +<?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/>. + +/** + * Attendance block caps. + * + * @package block_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + 'block/attendance:addinstance' => array( + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ), +); diff --git a/block/attendance/lang/en/block_attendance.php b/block/attendance/lang/en/block_attendance.php new file mode 100644 index 0000000..fe5c6f9 --- /dev/null +++ b/block/attendance/lang/en/block_attendance.php @@ -0,0 +1,29 @@ +<?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/>. + +/** + * Language file for block "attendance" + * + * @package block_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['attendance:addinstance'] = 'Add a new attendance block'; +$string['blockname'] = 'Attendance'; +$string['needactivity'] = 'This block can work only with an attendance activity. Please add the activity to this course.'; +$string['pluginname'] = 'Attendance'; +$string['privacy:metadata'] = 'The Attendance block only displays existing attendance data.'; \ No newline at end of file diff --git a/block/attendance/tests/behat/attendance_block.feature b/block/attendance/tests/behat/attendance_block.feature new file mode 100644 index 0000000..4b8cdc5 --- /dev/null +++ b/block/attendance/tests/behat/attendance_block.feature @@ -0,0 +1,32 @@ +@block @block_attendance @javascript +Feature: Test that teachers can add the attendance block and students can view reports. + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "course enrolments" exist: + | course | user | role | + | C1 | teacher1 | editingteacher | + | C1 | student1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | attendance | AttendanceTest1 | attendance description | C1 | attendance1 | + + Scenario: Teachers can add the attendance block + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Attendance" block + Then I should see "Take attendance" + + Scenario: Students can view their reports. + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Attendance" block + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + Then I should see "Taken sessions" diff --git a/block/attendance/version.php b/block/attendance/version.php new file mode 100644 index 0000000..052ec30 --- /dev/null +++ b/block/attendance/version.php @@ -0,0 +1,32 @@ +<?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/>. + +/** + * Version details + * + * @package block_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018052100; +$plugin->requires = 2017111300; // Requires 3.4. +$plugin->component = 'block_attendance'; +$plugin->dependencies = array('mod_attendance' => 2017050208); +$plugin->maturity = MATURITY_STABLE; +$plugin->release = '3.2.4'; diff --git a/block/course_overview_campus/.travis.yml b/block/course_overview_campus/.travis.yml new file mode 100644 index 0000000..64e1b3b --- /dev/null +++ b/block/course_overview_campus/.travis.yml @@ -0,0 +1,54 @@ +language: php + +sudo: true + +services: + - mysql + +addons: + firefox: "47.0.1" + postgresql: "9.4" + apt: + packages: + - openjdk-8-jre-headless + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.1 + - 7.2 + - 7.3 + +env: + global: + - MOODLE_BRANCH=MOODLE_37_STABLE + matrix: + - DB=pgsql + - DB=mysqli + +before_install: + - phpenv config-rm xdebug.ini + - nvm install 8.9 + - nvm use 8.9 + - cd ../.. + - composer create-project -n --no-dev --prefer-dist blackboard-open-source/moodle-plugin-ci ci ^2 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd || true # Output warnings but do not fail the build because of working legacy code + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat --dump diff --git a/block/course_overview_campus/CHANGES.md b/block/course_overview_campus/CHANGES.md new file mode 100644 index 0000000..9cc55d9 --- /dev/null +++ b/block/course_overview_campus/CHANGES.md @@ -0,0 +1,149 @@ +moodle-block_course_overview_campus +=================================== + +Changes +------- + +### v3.7-r1 + +* 2019-06-12 - Remove course news functionality as it does not work anymore from Moodle 3.7 on. +* 2019-06-12 - Prepare compatibility for Moodle 3.7. + +### v3.6-r1 + +* 2019-03-28 - Remove user preferences when being uninstalled. +* 2019-03-28 - Prepare the plugin that the hooks for fetching the course news will be removed in Moodle 3.7. If installed on Moodle 3.7, this plugin will silently disable the course news feature even if it is enabled in the plugin settings. +* 2019-03-28 - Check compatibility for Moodle 3.6, no functionality change. +* 2018-12-05 - Changed travis.yml due to upstream changes. + +### v3.5-r2 + +* 2018-08-26 - Bugfix: There might be a debug message when prioritizemyteachedcourses setting is enabled. + +### v3.5-r1 + +* 2018-07-02 - Fix some visual flaws and changes which came with BS 4 stable. +* 2018-05-29 - Check compatibility for Moodle 3.5, no functionality change. + +### v3.4-r2 + +* 2018-05-16 - Implement Privacy API. + +### v3.4-r1 + +* 2018-03-30 - Check compatibility for Moodle 3.4, no functionality change. + +### v3.3-r1 + +* 2018-03-07 - Add a nice slide animation when hiding a course. +* 2018-03-06 - Add a note about the future of the course news feature to README. +* 2018-03-06 - Bugfix: Add a default title string to this plugin instead of fetching it from block_course_overview which is EOL. +* 2018-03-06 - Activity icons should be rendered with image_icon() and not with pix_icon anymore. +* 2018-03-06 - pix_url() is deprecated in Moodle 3.3, change to pix_icon() and fontawesome icons. +* 2017-12-12 - Prepare compatibility for Moodle 3.3, no functionality change. + +### v3.2-r6 + +* 2017-06-16 - Bugfix: Prevent debug notice when there are no modules supporting the print_overview() function +* 2017-05-29 - Add Travis CI support + +### v3.2-r5 + +* 2017-05-05 - Improve README.md + +### v3.2-r4 + +* 2017-03-29 - Tighten parameter filtering for user preferences saved by block_course_overview_campus +* 2017-03-16 - Bugfix: Eliminate debug message about duplicate teacher role entries - Credits to Davo Smith +* 2017-03-10 - Don't show course news when hidden courses management is active +* 2017-03-10 - Bugfix: The hidden courses management box was partly broken after the styling changes in v3.2-r3 +* 2017-03-10 - Bugfix: The fallback for browsers with JavaScript disabled was broken after the styling changes in v3.2-r3 +* 2017-03-10 - Restructure code in several areas, especially to support our companion plugin local_boostcoc; No functionality change + +### v3.2-r3 + +* 2017-03-04 - Change the styling of the block even more to Bootstrap 4 + +### v3.2-r2 + +* 2017-01-27 - Bugfix: Set filter correctly after using the browser's back functionality - Credits to Davo Smith + +### v3.2-r1 + +* 2017-01-17 - Bugfix: Top level category filter did not show lower-level courses on first page load +* 2017-01-16 - Adapt course list appearance to Bootstrap 4 (used by theme_boost) +* 2017-01-16 - Check compatibility for Moodle 3.2, no functionality change +* 2017-01-16 - Convert YUI to jQuery + AMD - Credits to Davo Smith +* 2017-01-12 - Move Changelog from README.md to CHANGES.md + +### v3.1-r2 + +* 2016-11-07 - Remove a debug message about missing name fields in the DB query if teacher names are configured to be displayed according to the fullnamedisplay setting + +### v3.1-r1 + +* 2016-07-19 - Check compatibility for Moodle 3.1, no functionality change + +### Changes before v3.1 + +* 2016-06-14 - New Feature: Hide suspended teachers +* 2016-04-05 - Split the existing long settings page into multiple settings pages +* 2016-04-01 - Add feature to show top level category name in second row; rename existing feature to show parent category name +* 2016-04-01 - Add filter for top level category; rename existing category filter to parent category filter +* 2016-03-02 - Fix missing data in second row when corresponding filters are not activated; Credits to Dimitri Vorona +* 2016-02-10 - Change plugin version and release scheme to the scheme promoted by moodle.org, no functionality change +* 2016-01-01 - Add support for Shifter in YUI files, fix several JSLint errors +* 2016-01-01 - Check compatibility for Moodle 3.0, no functionality change +* 2015-09-29 - Output introduction string in course news like it's done in block_course_overview +* 2015-08-21 - Change My Moodle to Dashboard in language pack +* 2015-08-18 - Check compatibility for Moodle 2.9, no functionality change +* 2015-03-21 - Bugfix: Block couldn't be placed on MyMoodle in some circumstances +* 2015-03-20 - New Feature: Add a setting to control if the block should, when looking for teachers with the specified teacher roles, include teachers who have their role assigned in parent contexts (course category or system level) +* 2015-02-22 - Bugfix: Teacher filter showed teachers twice or even multiple times, Thanks to Mario Wehr +* 2015-02-22 - Bugfix: Term filter might have listed terms twice; Thanks to Michael Veit +* 2015-01-29 - Check compatibility for Moodle 2.8, no functionality change +* 2014-10-20 - Bugfix: There were problems with the term filter and courses which start on the term start day and / or term starting on january 1st +* 2014-10-20 - Add multilanguage support to noteachertext string +* 2014-08-29 - Update README file +* 2014-08-22 - Added setting to hide second row in course list on mobile phones to save space +* 2014-08-22 - Bootstrapbase makes h3 headings uppercase, this is not desired for this block and was overwritten in styles_bootstrapbase.css +* 2014-08-22 - Changed HTML code to leverage Bootstrap based themes, Drop support for Non-Bootstrap based themes +* 2014-08-22 - Changed HTML code for hide course management box - please check your theme, if you have styled the block in a custom way +* 2014-08-19 - Added setting to disable hiding of courses completely +* 2014-08-19 - Added setting to control the styling of the teacher's name in the second row +* 2014-06-30 - Check compatibility for Moodle 2.7, no functionality change +* 2014-02-18 - Bugfix: Second row didn't show the configured string for "timeless courses"; Credits to Sebastian Becker +* 2014-01-31 - Improve width of filters if less than all three filters are enabled +* 2014-01-31 - Added setting to skip activities when collecting and displaying course news +* 2014-01-31 - Added setting to disable course news completely and to hide course news by default +* 2014-01-31 - Added setting to define a placeholder text for course list entries if the block is configures to display teacher names but no teacher is enrolled in the course +* 2014-01-31 - Bugfix: Second row in course list was empty if the block was configured to show only teacher names in second row +* 2014-01-31 - Check compatibility for Moodle 2.6, no functionality change +* 2013-10-29 - Bugfix: block_course_overview_campus variable names interfered with other plugin's variables +* 2013-09-04 - Bugfix: Long course lists were incomplete. Sorry for the inconvenience! +* 2013-09-03 - Added ability to fine-tune the course name and meta info which will be displayed in the course overview list entries. Please revise your settings after updating the plugin +* 2013-09-03 - Added ability to fine-tune the term names which will be displayed in the term filter dropdown. Please revise your settings after updating the plugin, especially if you are running the term filter in Academic year mode +* 2013-07-30 - Transfer Github repository from github.com/abias/... to github.com/moodleuulm/...; Please update your Git paths if necessary +* 2013-07-30 - Check compatibility for Moodle 2.5, no functionality change +* 2013-06-18 - Bugfix: Fix problem with new ability to prioritize courses in which I teach. Sorry for the inconvenience +* 2013-06-18 - Re-sorted block settings page +* 2013-06-18 - Added ability to prioritize courses in which I teach in the course overview list +* 2013-06-18 - Added ability to merge homonymous categories into one category when using the category filter +* 2013-06-18 - Added ability to set the title of the block instead of using title from block_course_overview +* 2013-06-18 - Added ability to define teacher roles in block settings instead of relying on Moodle core coursecontact setting +* 2013-06-18 - Bugfix: When show teacher names setting was enabled, but teacher filter was diabled, the teacher name's list was not populated correctly +* 2013-06-12 - When managing hidden courses, now all courses are shown regardless if of the user's filter settings +* 2013-06-12 - Bugfix: Setting page should check if the configured term dates make sense and show a warning information if not. This check didn't work up to now +* 2013-04-23 - Add support for timeless courses +* 2013-03-18 - Code cleanup according to moodle codechecker +* 2013-03-06 - Bugfix: Block failed to work when wwwroot contained a subdirectory, kudos to Michael Wuttke +* 2013-03-05 - Small code change, now PHP doesn't need to be compiled with --enable-calendar option, kudos to Carsten Biemann +* 2013-02-22 - German language has been integrated into AMOS and was removed from this plugin. Please update your language packs with http://YOURMOODLEURL/admin/tool/langimport/index.php after installing this plugin version +* 2013-02-18 - Check compatibility for Moodle 2.4, add module icons to course news, fix language string names to comply with language string name convention +* 2013-01-18 - Bugfix: Block didn't read configuration from config_plugins database table properly +* 2012-12-21 - Block now uses config_plugins database table instead of config table. You will have to set all block settings again, sorry about that! +* 2012-12-21 - Small CSS improvement +* 2012-12-18 - Code cleanup +* 2012-12-18 - New feature: Short teachers' names in course list +* 2012-12-18 - New feature: Support multilang strings in term names and filter display names +* 2012-12-17 - Initial version diff --git a/block/course_overview_campus/COPYING.txt b/block/course_overview_campus/COPYING.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/block/course_overview_campus/COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/block/course_overview_campus/README.md b/block/course_overview_campus/README.md new file mode 100644 index 0000000..2d7b2c2 --- /dev/null +++ b/block/course_overview_campus/README.md @@ -0,0 +1,217 @@ +moodle-block_course_overview_campus +=================================== + +[](https://travis-ci.org/moodleuulm/moodle-block_course_overview_campus) + +Moodle block which provides all functionality of block_course_overview, provides additional filters to be used on university campuses as well as the possibility to hide courses from the course list + + +Requirements +------------ + +This plugin requires Moodle 3.7+ + + +Motivation for this plugin +-------------------------- + +Moodle installations on university campuses have certain constraints which are not completely supported by Moodle core and the course overview block in Moodle core. We implemented this course overview block to accommodate these needs as much as possible while keeping the features from the original course overview block from Moodle core as much as possible as well. + + +Installation +------------ + +Install the plugin like any other plugin to folder +/blocks/course_overview_campus + +See http://docs.moodle.org/en/Installing_plugins for details on installing Moodle plugins + + +Usage & Settings +---------------- + +After installing the plugin, it can be directly used by users and can be added to the Moodle dashboard and to the Moodle frontpage. + +Initially, it behaves like block_course_overview from moodle core. Additionally, courses can be hidden from the course list. + +To make use of the advanced features of the block, please visit: +Site administration -> Plugins -> Blocks -> Course overview on campus + +There, you find multiple settings pages: + +### 1. General + +On this settings page, you can change the block's title which is shown in the block view (multilang strings are supported, see http://docs.moodle.org/en/Multi-language_content_filter for details). + +### 2. Course overview list + +On this settings page, you can change the appearance of the course overview list, especially if the course's full name or short name should be displayed in the course overview list entries. Additionally, you can enable and style the displaying of some meta data in a second row of the course overview list entry and you can define if courses in which the user has a teacher role are listed first in the course overview list. + +### 3. Hide courses + +On this settings page, you can enable (default) or disable the system for hiding courses from the course overview list. + +### 4. Teacher roles + +On this settings page, you can define which roles in a course will be displayed besides the course's name as teacher and get listed in the teacher filter. + +### 5. Parent category filter + +On this settings page, you can activate and configure a filter which enables your users to filter their courses by parent category. As soon as the filter is activated and the setting is saved, the filter appears in the block view. + +### 6. Top level category filter + +On this settings page, you can activate and configure a filter which enables your users to filter their courses by top level category. As soon as the filter is activated and the setting is saved, the filter appears in the block view. + +### 7. Teacher filter + +On this settings page, you can activate and configure a filter which enables your users to filter their courses by teacher. As soon as the filter is activated and the setting is saved, the filter appears in the block view. + +### 8. Term filter + +On this settings page, you can activate and configure a filter which enables your users to filter their courses by term. As soon as the filter is activated and the setting is saved, the filter appears in the block view. + + +Data sources +------------ + +### 1. Parent category filter + +The parent category filter is filled with the main category of each of the user's courses. + +Example: +If the course's category path is Category A -> Category B -> Category C -> Course, the filter will contain an entry with Category C. + + +### 2. Top level category filter + +The top level filter is filled with the top level category of each of the user's courses. + +Example: +If the course's category path is Category A -> Category B -> Category C -> Course, the filter will contain an entry with Category A. + +### 3. Teacher filter + +As described in the "Usage & Settings" section of this file, you should configure the teacher roles for block_course_overview_campus according to your campus needs. After that, block_course_overview_campus takes each course member with one of the configured roles. These teachers are filled into the teacher filter. + +### 4. Term filter + +As described in the "Usage & Settings" section of this file, you should configure block_course_overview_campus according to your campus course of the year. After that, block_course_overview_campus maps each course to a term by looking at the course's start date. This term is filled into the term filter. + + +Block placement +--------------- + +block_course_overview_campus is used ideally as sticky block and placed on your frontpage (and / or Dashboard page). + +See http://docs.moodle.org/en/Block_settings#Making_a_block_sticky_throughout_the_whole_site for details about sticky blocks + + +Disregarded Moodle Features +--------------------------- + +During the development of Moodle, there have been added several features added to the moodle core block_course_overview and moodle core which would conflict with block_course_overview_campus functionality. It has been decided to disregard the following Moodle features for this block: + +* In block_course_overview in Moodle 2.4+, a user is able to sort his course list by drag and drop. We decided to not adopt this feature for block_course_overview_campus because we think this would conflict with the filtering / hiding feature and confuse users. In block_course_overview_campus, the course list remains sorted by full course name. +* In block_course_overview in Moodle 2.4+, a user is able to limit the length of his course list with a block setting. We decided to not adopt this feature for block_course_overview_campus because we think this would conflict with the filtering / hiding feature and confuse users. In block_course_overview_campus, the course list always shows all courses which have passed the selected course filters. +* In block_course_overview in Moodle 2.4+, the administrator can configure the block to show Metacourse children. We decided to not adopt this feature for block_course_overview_campus because we have no need for this. If you need this feature, please let us know on https://github.com/moodleuulm/moodle-block_course_overview_campus/issues +* In block_course_overview in Moodle 2.4+, the administrator can configure the block to show a welcome message. We decided to not adopt this feature for block_course_overview_campus because we have no need for this. If you need this feature, please let us know on https://github.com/moodleuulm/moodle-block_course_overview_campus/issues +* In Moodle core since Moodle 2.2+, there is a setting "courselistshortnames" which controls the displaying of course names. This setting is also processed in block_course_overview. We decided to ignore this core setting and to stick to block_course_overview_campus's internal course display control settings. +* In contrast to the Moodle core block_course_overview, this block doesn't support MNet courses and wasn't tested with MNet Moodle installations. + + +Companion plugin local_boostcoc +------------------------------- + +Since the release of Moodle 3.2, Moodle core ships with a shiny new theme called "Boost". While Boost does many things right and better than the legacy theme Clean, it also has some fixed behaviours which don't make sense for all Moodle installations. One of these behaviours is the fact that the mycourses list in the nav drawer (the menu which appears when you click on the hamburger menu button) is non-collapsible, always contains all of my courses and can hardly be configured by administrators. + +We have created local_boostcoc as a companion plugin to block_course_overview_campus which does its best to add support for filtering and hiding courses to the mycourses list in the nav drawer. local_boostcoc is published on http://moodle.org/plugins/view/local_boostcoc and on https://github.com/moodleuulm/moodle-local_boostcoc. + + +Theme support +------------- + +This plugin is developed and tested on Moodle Core's Boost theme. +It should also work with Boost child themes, including Moodle Core's Classic theme. However, we can't support any other theme than Boost. + +This plugin also provides a fallback for browsers with JavaScript disabled. + + +Plugin repositories +------------------- + +This plugin is published and regularly updated in the Moodle plugins repository: +http://moodle.org/plugins/view/block_course_overview_campus + +The latest development version can be found on Github: +https://github.com/moodleuulm/moodle-block_course_overview_campus + + +Bug and problem reports / Support requests +------------------------------------------ + +This plugin is carefully developed and thoroughly tested, but bugs and problems can always appear. + +Please report bugs and problems on Github: +https://github.com/moodleuulm/moodle-block_course_overview_campus/issues + +We will do our best to solve your problems, but please note that due to limited resources we can't always provide per-case support. + + +Feature proposals +----------------- + +Due to limited resources, the functionality of this plugin is primarily implemented for our own local needs and published as-is to the community. We are aware that members of the community will have other needs and would love to see them solved by this plugin. + +Please issue feature proposals on Github: +https://github.com/moodleuulm/moodle-block_course_overview_campus/issues + +Please create pull requests on Github: +https://github.com/moodleuulm/moodle-block_course_overview_campus/pulls + +We are always interested to read about your feature proposals or even get a pull request from you, but please accept that we can handle your issues only as feature _proposals_ and not as feature _requests_. + + +Moodle release support +---------------------- + +Due to limited resources, this plugin is only maintained for the most recent major release of Moodle. However, previous versions of this plugin which work in legacy major releases of Moodle are still available as-is without any further updates in the Moodle Plugins repository. + +There may be several weeks after a new major release of Moodle has been published until we can do a compatibility check and fix problems if necessary. If you encounter problems with a new major release of Moodle - or can confirm that this plugin still works with a new major relase - please let us know on Github. + +If you are running a legacy version of Moodle, but want or need to run the latest version of this plugin, you can get the latest version of the plugin, remove the line starting with $plugin->requires from version.php and use this latest plugin version then on your legacy Moodle. However, please note that you will run this setup completely at your own risk. We can't support this approach in any way and there is a undeniable risk for erratic behavior. + + +Translating this plugin +----------------------- + +This Moodle plugin is shipped with an english language pack only. All translations into other languages must be managed through AMOS (https://lang.moodle.org) by what they will become part of Moodle's official language pack. + +As the plugin creator, we manage the translation into german for our own local needs on AMOS. Please contribute your translation into all other languages in AMOS where they will be reviewed by the official language pack maintainers for Moodle. + + +Right-to-left support +--------------------- + +This plugin has not been tested with Moodle's support for right-to-left (RTL) languages. +If you want to use this plugin with a RTL language and it doesn't work as-is, you are free to send us a pull request on Github with modifications. + + +PHP7 Support +------------ + +Since Moodle 3.4 core, PHP7 is mandatory. We are developing and testing this plugin for PHP7 only. + + +Copyright +--------- + +Ulm University +Communication and Information Centre (kiz) +Alexander Bias + + +Credits +------- + +This plugin is an enhanced version of Andrew James' block_course_overview_plus (https://moodle.org/plugins/view.php?plugin=block_course_overview_plus) which was enhanced to fit the needs of university campuses. diff --git a/block/course_overview_campus/amd/build/filter.min.js b/block/course_overview_campus/amd/build/filter.min.js new file mode 100644 index 0000000..4c3d057 --- /dev/null +++ b/block/course_overview_campus/amd/build/filter.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){"use strict";function b(b){void 0!==b&&b.preventDefault();var c=a("#coc-filterterm").val();"all"===c?a(".termdiv").removeClass("coc-hidden"):(a(".termdiv").addClass("coc-hidden"),a(".coc-term-"+c).removeClass("coc-hidden")),M.util.set_user_preference("block_course_overview_campus-selectedterm",c)}function c(b){void 0!==b&&b.preventDefault();var c=a("#coc-filterteacher").val();"all"===c?a(".teacherdiv").removeClass("coc-hidden"):(a(".teacherdiv").addClass("coc-hidden"),a(".coc-teacher-"+c).removeClass("coc-hidden")),M.util.set_user_preference("block_course_overview_campus-selectedteacher",c)}function d(b){void 0!==b&&b.preventDefault();var c=a("#coc-filtercategory").val();"all"===c?a(".categorydiv").removeClass("coc-hidden"):(a(".categorydiv").addClass("coc-hidden"),a(".coc-category-"+c).removeClass("coc-hidden")),M.util.set_user_preference("block_course_overview_campus-selectedcategory",c)}function e(b){void 0!==b&&b.preventDefault();var c=a("#coc-filtertoplevelcategory").val();"all"===c?a(".toplevelcategorydiv").removeClass("coc-hidden"):(a(".toplevelcategorydiv").addClass("coc-hidden"),a(".coc-toplevelcategory-"+c).removeClass("coc-hidden")),M.util.set_user_preference("block_course_overview_campus-selectedtoplevelcategory",c)}function f(f){var g,h,i,j;for(g in f)if(f.hasOwnProperty(g)&&(h=f[g],i=a("#coc-filter"+g),i.length&&(j=i.val(),j!==h)))switch(g){case"term":b();break;case"teacher":c();break;case"category":d();break;case"toplevelcategory":e()}}function g(){var b=new Array;a(".coc-course").each(function(c,d){0==a(d).height()&&b.push(d.id.slice(11))});var c=JSON.stringify(b);M.util.set_user_preference("local_boostcoc-notshowncourses",c)}function h(){var b=new Array;a("#coc-filterterm, #coc-filtercategory, #coc-filtertoplevelcategory, #coc-filterteacher").each(function(c,d){"all"!==a(d).val()&&b.push(d.id.slice(4))});var c=parseInt(a("#coc-hiddencoursescount").html(),10);c>0&&b.push("hidecourses");var d=JSON.stringify(b);M.util.set_user_preference("local_boostcoc-activefilters",d)}return{initFilter:function(i){a("#coc-filterterm").on("change",b),a("#coc-filterteacher").on("change",c),a("#coc-filtercategory").on("change",d),a("#coc-filtertoplevelcategory").on("change",e),1==i.local_boostcoc&&a("#coc-filterterm, #coc-filterteacher, #coc-filtercategory, #coc-filtertoplevelcategory").on("change",g).on("change",h),f(i.initialsettings)}}}); \ No newline at end of file diff --git a/block/course_overview_campus/amd/build/hidecourse.min.js b/block/course_overview_campus/amd/build/hidecourse.min.js new file mode 100644 index 0000000..7025467 --- /dev/null +++ b/block/course_overview_campus/amd/build/hidecourse.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){"use strict";function b(b){var c;if(void 0!==b&&b.preventDefault(),1===b.data.manage&&(a("#coc-hidecourseicon-"+b.data.course).addClass("coc-hidden"),a("#coc-showcourseicon-"+b.data.course).removeClass("coc-hidden")),0===b.data.manage){a("#coc-hidecourseicon-"+b.data.course).addClass("coc-hidden"),a("#coc-showcourseicon-"+b.data.course).removeClass("coc-hidden");var d=b.data.course;a(".coc-hidecourse-"+b.data.course).slideUp(function(){a(".coc-hidecourse-"+d).addClass("coc-hidden"),c=parseInt(a("#coc-hiddencoursescount").html(),10),a("#coc-hiddencoursescount").html(c+1),a("#coc-hiddencoursesmanagement-bottom .row").removeClass("coc-hidden")})}M.util.set_user_preference("block_course_overview_campus-hidecourse-"+b.data.course,1)}function c(b){void 0!==b&&b.preventDefault(),1===b.data.manage&&(a("#coc-showcourseicon-"+b.data.course).addClass("coc-hidden"),a("#coc-hidecourseicon-"+b.data.course).removeClass("coc-hidden")),M.util.set_user_preference("block_course_overview_campus-hidecourse-"+b.data.course,0)}function d(){var b=new Array;a(".coc-course").each(function(c,d){0==a(d).height()&&b.push(d.id.slice(11))});var c=JSON.stringify(b);M.util.set_user_preference("local_boostcoc-notshowncourses",c)}function e(){var b=new Array;a("#coc-filterterm, #coc-filtercategory, #coc-filtertoplevelcategory, #coc-filterteacher").each(function(c,d){"all"!==a(d).val()&&b.push(d.id.slice(4))});var c=parseInt(a("#coc-hiddencoursescount").html(),10);c>0&&b.push("hidecourses");var d=JSON.stringify(b);M.util.set_user_preference("local_boostcoc-activefilters",d)}return{initHideCourse:function(f){var g,h=f.courses.split(" ");for(g=0;g<h.length;g++)a("#coc-hidecourseicon-"+h[g]).on("click",{course:h[g],manage:f.manage},b),a("#coc-showcourseicon-"+h[g]).on("click",{course:h[g],manage:f.manage},c),1==f.local_boostcoc&&0==f.manage&&a("#coc-hidecourseicon-"+h[g]).on("click",d).on("click",e)}}}); \ No newline at end of file diff --git a/block/course_overview_campus/amd/src/filter.js b/block/course_overview_campus/amd/src/filter.js new file mode 100644 index 0000000..e40d035 --- /dev/null +++ b/block/course_overview_campus/amd/src/filter.js @@ -0,0 +1,191 @@ +/** + * Block "course overview (campus)" - JS code for filtering courses + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + "use strict"; + + /** + * Function to filter the shown courses by term. + */ + function filterTerm(e) { + // Prevent the event from refreshing the page. + if (e !== undefined) { + e.preventDefault(); + } + + var value = $('#coc-filterterm').val(); + if (value === "all") { + $('.termdiv').removeClass('coc-hidden'); + } else { + $('.termdiv').addClass('coc-hidden'); + $('.coc-term-' + value).removeClass('coc-hidden'); + } + + // Store the users selection (Uses AJAX to save to the database). + M.util.set_user_preference('block_course_overview_campus-selectedterm', value); + } + + /** + * Function to filter the shown courses by term teacher. + */ + function filterTeacher(e) { + // Prevent the event from refreshing the page. + if (e !== undefined) { + e.preventDefault(); + } + + var value = $("#coc-filterteacher").val(); + if (value === "all") { + $('.teacherdiv').removeClass('coc-hidden'); + } else { + $('.teacherdiv').addClass('coc-hidden'); + $('.coc-teacher-' + value).removeClass('coc-hidden'); + } + + // Store the users selection (Uses AJAX to save to the database). + M.util.set_user_preference('block_course_overview_campus-selectedteacher', value); + } + + /** + * Function to filter the shown courses by parent category. + */ + function filterCategory(e) { + // Prevent the event from refreshing the page. + if (e !== undefined) { + e.preventDefault(); + } + + var value = $("#coc-filtercategory").val(); + if (value === "all") { + $('.categorydiv').removeClass('coc-hidden'); + } else { + $('.categorydiv').addClass('coc-hidden'); + $('.coc-category-' + value).removeClass('coc-hidden'); + } + + // Store the users selection (Uses AJAX to save to the database). + M.util.set_user_preference('block_course_overview_campus-selectedcategory', value); + } + + /** + * Function to filter the shown courses by top level category. + */ + function filterTopLevelCategory(e) { + // Prevent the event from refreshing the page. + if (e !== undefined) { + e.preventDefault(); + } + + var value = $("#coc-filtertoplevelcategory").val(); + if (value === "all") { + $('.toplevelcategorydiv').removeClass('coc-hidden'); + } else { + $('.toplevelcategorydiv').addClass('coc-hidden'); + $('.coc-toplevelcategory-' + value).removeClass('coc-hidden'); + } + + // Store the users selection (Uses AJAX to save to the database). + M.util.set_user_preference('block_course_overview_campus-selectedtoplevelcategory', value); + } + + /** + * Function to apply all filters again (used when the user has pushed the back button). + */ + function applyAllFilters(initialSettings) { + /* eslint-disable max-depth */ + var setting, value, $element, elementValue; + for (setting in initialSettings) { + if (initialSettings.hasOwnProperty(setting)) { + value = initialSettings[setting]; + $element = $('#coc-filter' + setting); + if ($element.length) { + elementValue = $element.val(); + if (elementValue !== value) { + switch (setting) { + case 'term': + filterTerm(); + break; + case 'teacher': + filterTeacher(); + break; + case 'category': + filterCategory(); + break; + case 'toplevelcategory': + filterTopLevelCategory(); + break; + } + } + } + } + } + /* eslint-enable max-depth */ + } + + /** + * Function to remember the not shown courses for local_boostcoc. + */ + function localBoostCOCRememberNotShownCourses() { + // Get all course nodes which are not shown (= invisible = their height is 0) and store their IDs in an array. + var notshowncourses = new Array(); + $('.coc-course').each(function(index, element) { + if ($(element).height() == 0) { + notshowncourses.push(element.id.slice(11)); // This will remove "coc-course-" from the id's string. + } + }); + + // Convert not shown courses array to JSON. + var jsonstring = JSON.stringify(notshowncourses); + + // Store the current status of not shown courses (Uses AJAX to save to the database). + M.util.set_user_preference('local_boostcoc-notshowncourses', jsonstring); + } + + /** + * Function to remember the active filters for local_boostcoc. + */ + function localBoostCOCRememberActiveFilters() { + // Get all active filters (value != all) and the fact that hidden courses are present and store them in an array. + var activefilters = new Array(); + $('#coc-filterterm, #coc-filtercategory, #coc-filtertoplevelcategory, #coc-filterteacher').each(function(index, element) { + if ($(element).val() !== "all") { + activefilters.push(element.id.slice(4)); // This will remove "coc-" from the id's string. + } + }); + var hiddenCount = parseInt($('#coc-hiddencoursescount').html(), 10); + if (hiddenCount > 0) { + activefilters.push('hidecourses'); + } + + // Convert not shown courses array to JSON. + var jsonstring = JSON.stringify(activefilters); + + // Store the current status of active filters (Uses AJAX to save to the database). + M.util.set_user_preference('local_boostcoc-activefilters', jsonstring); + } + + return { + initFilter: function(params) { + // Add change listener to filter widgets. + $('#coc-filterterm').on('change', filterTerm); + $('#coc-filterteacher').on('change', filterTeacher); + $('#coc-filtercategory').on('change', filterCategory); + $('#coc-filtertoplevelcategory').on('change', filterTopLevelCategory); + + // Add change listener to filter widgets for local_boostcoc. + if (params.local_boostcoc == true) { + $('#coc-filterterm, #coc-filterteacher, #coc-filtercategory, #coc-filtertoplevelcategory').on('change', + localBoostCOCRememberNotShownCourses).on('change', localBoostCOCRememberActiveFilters); + } + + // Make sure any initial filter settings are applied (may be needed if the user + // has used the browser 'back' button). + applyAllFilters(params.initialsettings); + } + }; +}); diff --git a/block/course_overview_campus/amd/src/hidecourse.js b/block/course_overview_campus/amd/src/hidecourse.js new file mode 100644 index 0000000..d53da12 --- /dev/null +++ b/block/course_overview_campus/amd/src/hidecourse.js @@ -0,0 +1,132 @@ +/** + * Block "course overview (campus)" - JS code for hiding courses + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + "use strict"; + + /** + * Function to hide a course from the course list. + */ + function hideCourse(e) { + var hiddenCount; + // Prevent the event from refreshing the page. + if (e !== undefined) { + e.preventDefault(); + } + + // When hidden course managing is active. + if (e.data.manage === 1) { + // Change the icon. + $('#coc-hidecourseicon-' + e.data.course).addClass('coc-hidden'); + $('#coc-showcourseicon-' + e.data.course).removeClass('coc-hidden'); + } + // When hidden course managing is not active. + if (e.data.manage === 0) { + // Change the icon. + $('#coc-hidecourseicon-' + e.data.course).addClass('coc-hidden'); + $('#coc-showcourseicon-' + e.data.course).removeClass('coc-hidden'); + + // Use a nice slide animation to make clear where the course is going. + var courseId = e.data.course; + $('.coc-hidecourse-' + e.data.course).slideUp(function() { + $('.coc-hidecourse-' + courseId).addClass('coc-hidden'); + hiddenCount = parseInt($('#coc-hiddencoursescount').html(), 10); + $('#coc-hiddencoursescount').html(hiddenCount + 1); + $('#coc-hiddencoursesmanagement-bottom .row').removeClass('coc-hidden'); + }); + } + + // Store the course status (Uses AJAX to save to the database). + M.util.set_user_preference('block_course_overview_campus-hidecourse-' + e.data.course, 1); + } + + /** + * Function to show a course in the course list. + */ + function showCourse(e) { + // Prevent the event from refreshing the page. + if (e !== undefined) { + e.preventDefault(); + } + + // When hidden course managing is active. + if (e.data.manage === 1) { + // Change the icon. + $('#coc-showcourseicon-' + e.data.course).addClass('coc-hidden'); + $('#coc-hidecourseicon-' + e.data.course).removeClass('coc-hidden'); + } + + // Store the course status (Uses AJAX to save to the database). + M.util.set_user_preference('block_course_overview_campus-hidecourse-' + e.data.course, 0); + } + + /** + * Function to remember the not shown courses for local_boostcoc. + */ + function localBoostCOCRememberNotShownCourses() { + // Get all course nodes which are not shown (= invisible = their height is 0) and store their IDs in an array. + var notshowncourses = new Array(); + $('.coc-course').each(function(index, element) { + if ($(element).height() == 0) { + notshowncourses.push(element.id.slice(11)); // This will remove "coc-course-" from the id's string. + } + }); + + // Convert not shown courses array to JSON. + var jsonstring = JSON.stringify(notshowncourses); + + // Store the current status of not shown courses (Uses AJAX to save to the database). + M.util.set_user_preference('local_boostcoc-notshowncourses', jsonstring); + } + + /** + * Function to remember the active filters for local_boostcoc. + */ + function localBoostCOCRememberActiveFilters() { + // Get all active filters (value != all) and the fact that hidden courses are present and store them in an array. + var activefilters = new Array(); + $('#coc-filterterm, #coc-filtercategory, #coc-filtertoplevelcategory, #coc-filterteacher').each(function(index, element) { + if ($(element).val() !== "all") { + activefilters.push(element.id.slice(4)); // This will remove "coc-" from the id's string. + } + }); + var hiddenCount = parseInt($('#coc-hiddencoursescount').html(), 10); + if (hiddenCount > 0) { + activefilters.push('hidecourses'); + } + + // Convert not shown courses array to JSON. + var jsonstring = JSON.stringify(activefilters); + + // Store the current status of active filters (Uses AJAX to save to the database). + M.util.set_user_preference('local_boostcoc-activefilters', jsonstring); + } + + return { + initHideCourse: function(params) { + var i; + var courses = params.courses.split(" "); + for (i = 0; i < courses.length; i++) { + // Add change listener to hide courses widgets. + $('#coc-hidecourseicon-' + courses[i]).on('click', {course: courses[i], manage: params.manage}, hideCourse); + // Add change listener to show courses widgets. + $('#coc-showcourseicon-' + courses[i]).on('click', {course: courses[i], manage: params.manage}, showCourse); + // Add change listener to show / hide courses widgets for local_boostcoc. + // Do this only when hidden courses management isn't active. This way, the notshowncourses will not be remembered on + // the server until the user finishes hidden courses management. While working in hidden courses management in one + // browser tab, the nav drawer in a second browser tab would still show the old status. But we accept this because + // otherwise we would have to implement a second localBoostCOCRemember detection algorithm for hidden courses + // management. + if (params.local_boostcoc == true && params.manage == false) { + $('#coc-hidecourseicon-' + courses[i]).on('click', localBoostCOCRememberNotShownCourses).on('click', + localBoostCOCRememberActiveFilters); + } + } + } + }; +}); diff --git a/block/course_overview_campus/block_course_overview_campus.php b/block/course_overview_campus/block_course_overview_campus.php new file mode 100644 index 0000000..306c63f --- /dev/null +++ b/block/course_overview_campus/block_course_overview_campus.php @@ -0,0 +1,1408 @@ +<?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/>. + +/** + * Block "course overview (campus)" + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// @codingStandardsIgnoreFile +// Let codechecker ignore this file. This legacy code is not fully compliant to Moodle coding style but working and well documented. + +/** + * Class block_course_overview_campus + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_course_overview_campus extends block_base { + + /** + * init function + * @return void + */ + public function init() { + $this->title = get_string('pluginname', 'block_course_overview_campus'); + } + + /** + * specialization function + * @return void + */ + public function specialization() { + $this->title = format_string(get_config('block_course_overview_campus', 'blocktitle')); + } + + /** + * applicable_formats function + * @return array + */ + public function applicable_formats() { + return array('my-index' => true, 'my' => true, 'site-index' => true); + } + + /** + * has_config function + * @return bool + */ + public function has_config() { + return true; + } + + /** + * instance_allow_multiple function + * @return bool + */ + public function instance_allow_multiple() { + return false; + } + + /** + * instance_can_be_hidden function + * @return bool + */ + public function instance_can_be_hidden() { + return false; + } + + /** + * get_content function + * @return string + */ + public function get_content() { + global $coc_config, $USER, $CFG, $DB, $PAGE, $OUTPUT; + + + /********************************************************************************/ + /*** PREPARE ***/ + /********************************************************************************/ + + // Don't run this function twice. + if ($this->content !== null) { + return $this->content; + } + + // Include local library. + require_once(__DIR__ . '/locallib.php'); + + // Get plugin config. + $coc_config = get_config('block_course_overview_campus'); + + + + /********************************************************************************/ + /*** PREPROCESSING ***/ + /********************************************************************************/ + + // Include local library. + require_once(__DIR__ . '/locallib.php'); + + + // Check if the configured term dates make sense, if not disable term filter. + if (!block_course_overview_campus_check_term_config()) { + $coc_config->termcoursefilter = false; + } + + + // Process GET parameters. + $param_hidecourse = optional_param('coc-hidecourse', 0, PARAM_BOOL); + $param_showcourse = optional_param('coc-showcourse', 0, PARAM_BOOL); + $param_term = optional_param('coc-term', null, PARAM_ALPHANUMEXT); + $param_category = optional_param('coc-category', null, PARAM_ALPHANUM); + $param_toplevelcategory = optional_param('coc-toplevelcategory', null, PARAM_ALPHANUM); + $param_teacher = optional_param('coc-teacher', null, PARAM_ALPHANUM); + $param_manage = optional_param('coc-manage', 0, PARAM_BOOL); + + + // Set displaying preferences when set by GET parameters. + if ($coc_config->enablehidecourses) { + if ($param_hidecourse != 0) { + set_user_preference('block_course_overview_campus-hidecourse-'.$param_hidecourse, 1); + } + if ($param_showcourse != 0) { + set_user_preference('block_course_overview_campus-hidecourse-'.$param_showcourse, 0); + } + } + + + // Set and remember term filter if GET parameter is present. + if ($coc_config->termcoursefilter == true) { + if ($param_term != null) { + $selectedterm = $param_term; + set_user_preference('block_course_overview_campus-selectedterm', $param_term); + } + // Or set term filter based on user preference with default term fallback if activated. + else if ($coc_config->defaultterm == true) { + $selectedterm = get_user_preferences('block_course_overview_campus-selectedterm', 'currentterm'); + } + // Or set term filter based on user preference with 'all' terms fallback. + else { + $selectedterm = get_user_preferences('block_course_overview_campus-selectedterm', 'all'); + } + } + + + // Set and remember parent category filter if GET parameter is present. + if ($coc_config->categorycoursefilter == true) { + if ($param_category != null) { + $selectedcategory = $param_category; + set_user_preference('block_course_overview_campus-selectedcategory', $param_category); + } + // Or set parent category filter based on user preference with 'all' categories fallback. + else { + $selectedcategory = get_user_preferences('block_course_overview_campus-selectedcategory', 'all'); + } + } + + + // Set and remember top level category filter if GET parameter is present. + if ($coc_config->toplevelcategorycoursefilter == true) { + if ($param_toplevelcategory != null) { + $selectedtoplevelcategory = $param_toplevelcategory; + set_user_preference('block_course_overview_campus-selectedtoplevelcategory', $param_toplevelcategory); + } + // Or set top level category filter based on user preference with 'all' categories fallback. + else { + $selectedtoplevelcategory = get_user_preferences('block_course_overview_campus-selectedtoplevelcategory', 'all'); + } + } + + + // Set and remember teacher filter if GET parameter is present. + if ($coc_config->teachercoursefilter == true) { + if ($param_teacher != null) { + $selectedteacher = $param_teacher; + set_user_preference('block_course_overview_campus-selectedteacher', $param_teacher); + } + // Or set teacher filter based on user preference with 'all' teachers fallback. + else { + $selectedteacher = get_user_preferences('block_course_overview_campus-selectedteacher', 'all'); + } + } + + + // Get my courses. + $courses = block_course_overview_campus_get_my_courses(); + + + + /********************************************************************************/ + /*** PROCESS MY COURSES ***/ + /********************************************************************************/ + + // No, I don't have any courses -> content is only a placeholder message. + if (empty($courses)) { + $content = get_string('nocourses', 'block_course_overview_campus'); + } + + // Yes, I have courses. + else { + // Start output buffer. + ob_start(); + + + // Get all course categories for later use. + $coursecategories = $DB->get_records('course_categories'); + + // Get teacher roles for later use. + if (!empty($coc_config->teacherroles)) { + $teacherroles = explode(',', $coc_config->teacherroles); + } + else { + $teacherroles = array(); + } + + + // Create empty filter for activated filters. + if ($coc_config->termcoursefilter == true) { + $filterterms = array(); + } + if ($coc_config->categorycoursefilter == true) { + $filtercategories = array(); + } + if ($coc_config->toplevelcategorycoursefilter == true) { + $filtertoplevelcategories = array(); + } + if ($coc_config->teachercoursefilter == true) { + $filterteachers = array(); + } + + // Create counter for hidden courses. + if ($coc_config->enablehidecourses) { + $hiddencoursescounter = 0; + } + + // Create string to remember courses for JS processing. + $js_courseslist = ' '; + + + // Now iterate over my courses and collect data about them. + foreach ($courses as $c) { + // Get course context. + $context = context_course::instance($c->id); + + // Collect information about my courses and populate filters with data about my courses. + // Term information. + if ($coc_config->termcoursefilter == true || $coc_config->secondrowshowtermname == true) { + // Create object for bufferung course term information. + $courseterm = new stdClass(); + + // If course start date is undefined, set course term to "other". + if ($c->startdate == 0) { + $courseterm->id = 'other'; + $courseterm->name = get_string('other', 'block_course_overview_campus'); + } + + // If course start date is available, if timeless courses are enabled and if course start date is before. + // timeless course threshold, set course term to "timeless". + else if ($coc_config->timelesscourses == true && date('Y', $c->startdate) < $coc_config->timelesscoursesthreshold) { + $courseterm->id = 'timeless'; + $courseterm->name = format_string($coc_config->timelesscoursesname); + } + + // If course start date is available, distinguish between term modes. + // "Academic year" mode. + else if ($coc_config->termmode == 1) { + // Prepare date information. + $coursestartyday = usergetdate($c->startdate)['yday']; + $coursestartyear = usergetdate($c->startdate)['year']; + $term1startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term1startday)[0], explode('-', $coc_config->term1startday)[1]))['yday']; + + // If term starts on January 1st, set course term to course start date's year. + if ($coc_config->term1startday == '01-01') { + $courseterm->id = $coursestartyear; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term1name, $coursestartyear); + } + // If term doesn't start on January 1st and course start date's day comes on or after term start day, + // set course term to course start date's year + next year. + else if ($coursestartyday >= $term1startyday) { + $courseterm->id = $coursestartyear.'-'.($coursestartyear + 1); + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term1name, $coursestartyear, ($coursestartyear + 1)); + } + // If term doesn't start on January 1st and course start date's day comes before term start day, + // set course term to course start date's year + former year. + else { + $courseterm->id = ($coursestartyear - 1).'-'.$coursestartyear; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term1name, ($coursestartyear - 1), $coursestartyear); + } + } + // "Semester" mode. + else if ($coc_config->termmode == 2) { + // Prepare date information. + $coursestartyday = usergetdate($c->startdate)['yday']; + $coursestartyear = usergetdate($c->startdate)['year']; + $term1startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term1startday)[0], explode('-', $coc_config->term1startday)[1]))['yday']; + $term2startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term2startday)[0], explode('-', $coc_config->term2startday)[1]))['yday']; + + // If course start date's day comes before first term start day, + // set course term to second term of former year. + if ($coursestartyday < $term1startyday) { + $courseterm->id = ($coursestartyear - 1).'-2'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term2name, ($coursestartyear - 1), $coursestartyear); + } + // If course start date's day comes on or after first term start day but before second term start day, + // set course term to first term of current year. + else if ($coursestartyday >= $term1startyday && $coursestartyday < $term2startyday) { + $courseterm->id = $coursestartyear.'-1'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term1name, $coursestartyear); + } + // If course start date's day comes on or after second term start day, + // set course term to second term of current year. + else { + $courseterm->id = $coursestartyear.'-2'; + // If first term does start on January 1st, suffix name with single year, + // otherwise suffix name with double year. + if ($coc_config->term1startday == '01-01') { + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term2name, $coursestartyear); + } else { + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term2name, $coursestartyear, ($coursestartyear + 1)); + } + } + } + // "Tertial" mode. + else if ($coc_config->termmode == 3) { + // Prepare date information. + $coursestartyday = usergetdate($c->startdate)['yday']; + $coursestartyear = usergetdate($c->startdate)['year']; + $term1startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term1startday)[0], explode('-', $coc_config->term1startday)[1]))['yday']; + $term2startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term2startday)[0], explode('-', $coc_config->term2startday)[1]))['yday']; + $term3startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term3startday)[0], explode('-', $coc_config->term3startday)[1]))['yday']; + + // If course start date's day comes before first term start day, + // set course term to third term of former year. + if ($coursestartyday < $term1startyday) { + $courseterm->id = ($coursestartyear - 1).'-3'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term3name, ($coursestartyear - 1), $coursestartyear); + } + // If course start date's day comes on or after first term start day but before second term start day, + // set course term to first term of current year. + else if ($coursestartyday >= $term1startyday && $coursestartyday < $term2startyday) { + $courseterm->id = $coursestartyear.'-1'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term1name, $coursestartyear); + } + // If course start date's day comes on or after second term start day but before third term start day, + // set course term to second term of current year. + else if ($coursestartyday >= $term2startyday && $coursestartyday < $term3startyday) { + $courseterm->id = $coursestartyear.'-2'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term2name, $coursestartyear); + } + // If course start date's day comes on or after third term start day, + // set course term to third term of current year. + else { + $courseterm->id = $coursestartyear.'-3'; + // If first term does start on January 1st, suffix name with single year, + // otherwise suffix name with double year. + if ($coc_config->term1startday == '01-01') { + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term3name, $coursestartyear); + } else { + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term3name, $coursestartyear, ($coursestartyear + 1)); + } + } + } + // "Trimester" mode. + else if ($coc_config->termmode == 4) { + // Prepare date information. + $coursestartyday = usergetdate($c->startdate)['yday']; + $coursestartyear = usergetdate($c->startdate)['year']; + $term1startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term1startday)[0], explode('-', $coc_config->term1startday)[1]))['yday']; + $term2startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term2startday)[0], explode('-', $coc_config->term2startday)[1]))['yday']; + $term3startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term3startday)[0], explode('-', $coc_config->term3startday)[1]))['yday']; + $term4startyday = usergetdate(make_timestamp($coursestartyear, explode('-', $coc_config->term4startday)[0], explode('-', $coc_config->term4startday)[1]))['yday']; + + // If course start date's day comes before first term start day, + // set course term to fourth term of former year. + if ($coursestartyday < $term1startyday) { + $courseterm->id = ($coursestartyear - 1).'-4'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term4name, ($coursestartyear - 1), $coursestartyear); + } + // If course start date's day comes on or after first term start day but before second term start day, + // set course term to first term of current year. + else if ($coursestartyday >= $term1startyday && $coursestartyday < $term2startyday) { + $courseterm->id = $coursestartyear.'-1'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term1name, $coursestartyear); + } + // If course start date's day comes on or after second term start day but before third term start day, + // set course term to second term of current year. + else if ($coursestartyday >= $term2startyday && $coursestartyday < $term3startyday) { + $courseterm->id = $coursestartyear.'-2'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term2name, $coursestartyear); + } + // If course start date's day comes on or after third term start day but before fourth term start day, + // set course term to third term of current year. + else if ($coursestartyday >= $term3startyday && $coursestartyday < $term4startyday) { + $courseterm->id = $coursestartyear.'-3'; + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term3name, $coursestartyear); + } + // If course start date's day comes on or after fourth term start day, + // set course term to fourth term of current year. + else { + $courseterm->id = $coursestartyear.'-4'; + // If first term does start on January 1st, suffix name with single year, + // otherwise suffix name with double year. + if ($coc_config->term1startday == '01-01') { + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term4name, $coursestartyear); + } else { + $courseterm->name = block_course_overview_campus_get_term_displayname($coc_config->term4name, $coursestartyear, ($coursestartyear + 1)); + } + } + } + // This should never happen. + else { + print_error('error'); + } + + // Remember course term for later use. + $c->term = $courseterm->id; + $c->termname = format_string($courseterm->name); + } + // Term filter. + if ($coc_config->termcoursefilter == true) { + // Add course term to filter list. + $filterterms[$courseterm->id] = $courseterm->name; + } + + // Parent category information. + if ($coc_config->categorycoursefilter == true || + $coc_config->secondrowshowcategoryname == true || + $coc_config->toplevelcategorycoursefilter == true || + $coc_config->secondrowshowtoplevelcategoryname == true) { + // Get course parent category name from array of all category names. + $coursecategory = $coursecategories[$c->category]; + + // Remember course parent category name for later use. + $c->categoryname = format_string($coursecategory->name); + $c->categoryid = $coursecategory->id; + + // Get course top level category name from array of all category names. + $coursecategorypath = explode('/', $coursecategory->path); + $coursetoplevelcategoryid = $coursecategorypath[1]; + $coursetoplevelcategory = $coursecategories[$coursetoplevelcategoryid]; + + // Remember course top level category name for later use. + $c->toplevelcategoryname = format_string($coursetoplevelcategory->name); + $c->toplevelcategoryid = $coursetoplevelcategory->id; + } + // Parent category filter. + if ($coc_config->categorycoursefilter == true) { + // Merge homonymous categories into one category if configured. + if ($coc_config->mergehomonymouscategories == true) { + // Check if course category name is already present in the category filter array. + if ($othercategoryid = array_search($c->categoryname, $filtercategories)) { + // If yes and if course category is different than the already present category (same name, + // but different id), modify course category id to equal the already present category id + // (poor hack, but functional). + if ($othercategoryid != $c->categoryid) { + $c->categoryid = $othercategoryid; + } + } + } + + // Add course parent category name to filter list. + $filtercategories[$c->categoryid] = $c->categoryname; + } + // Top level category filter. + if ($coc_config->toplevelcategorycoursefilter == true) { + // Add course top level category name to filter list. + $filtertoplevelcategories[$c->toplevelcategoryid] = $c->toplevelcategoryname; + } + + // Teacher information. + if ($coc_config->teachercoursefilter == true || $coc_config->secondrowshowteachername == true || $coc_config->prioritizemyteachedcourses == true) { + + // Get course teachers based on global teacher roles. + if (count($teacherroles) > 0) { + + // Get all user name fields for SQL query in a proper way. + $allnames = get_all_user_name_fields(true, 'u'); + $teacherfields = 'ra.id AS raid, u.id, '.$allnames.', r.sortorder'; // Moodle would complain about two columns called id with a "Did you remember to make the first column something unique in your call to get_records? Duplicate value 'xxx' found in column 'id'." debug message. That's why we alias one column to a name different than id. + $teachersortfields = 'u.lastname, u.firstname'; + + // Check if we have to check for suspended teachers. + if ($coc_config->teacherroleshidesuspended == 1) { + // Build extra where clause for SQL query. + $now = round(time(), -2); // Improves db caching. + $extrawhere = 'ue.status = '.ENROL_USER_ACTIVE.' AND e.status = '.ENROL_INSTANCE_ENABLED.' AND ue.timestart < '.$now.' AND (ue.timeend = 0 OR ue.timeend > '.$now.')'; + } else { + $extrawhere = ''; + } + + // Check if we have to include teacher roles from parent contexts. + // If yes. + if ($coc_config->teacherrolesparent == 1) { + // If we have to check for suspended teachers. + if ($coc_config->teacherroleshidesuspended == 1) { + $courseteachers = get_role_users($teacherroles, $context, true, + $teacherfields, $teachersortfields, false, '', '', '', $extrawhere); + } else { + $courseteachers = get_role_users($teacherroles, $context, true, + $teacherfields, $teachersortfields); + } + } + // If no. + else if ($coc_config->teacherrolesparent == 2) { + // If we have to check for suspended teachers. + if ($coc_config->teacherroleshidesuspended == 1) { + $courseteachers = get_role_users($teacherroles, $context, false, + $teacherfields, $teachersortfields, false, '', '', '', $extrawhere); + } else { + $courseteachers = get_role_users($teacherroles, $context, false, + $teacherfields, $teachersortfields); + } + } + // If depending on moodle/course:reviewotherusers capability. + else if ($coc_config->teacherrolesparent == 3) { + // If we have to check for suspended teachers. + $hasreviewotherscapability = has_capability('moodle/course:reviewotherusers', $context); + if ($coc_config->teacherroleshidesuspended == 1) { + $courseteachers = get_role_users($teacherroles, $context, $hasreviewotherscapability, + $teacherfields, $teachersortfields, false, '', '', '', $extrawhere); + } else { + $courseteachers = get_role_users($teacherroles, $context, $hasreviewotherscapability, + $teacherfields, $teachersortfields); + } + } + // Should not happen. + else { + $courseteachers = get_role_users($teacherroles, $context, true, $teacherfields, + $teachersortfields); + } + } else { + $courseteachers = array(); + } + + // The way we use get_role_users(), the teachers array may now contain duplicates as a teacher might have more + // than one role in a course and is indexed by ra.id instead of u.id (which we expect later). + // We will rewrite the array in reverse order indexed by userid, this way existing teachers will be eliminated + // by their own duplicate with higher relevance. + $courseteacherstmp = $courseteachers; + $courseteachers = []; + foreach (array_reverse($courseteacherstmp) as $teacher) { + $courseteachers[$teacher->id] = $teacher; + } + + // Remember course teachers for later use. + $c->teachers = $courseteachers; + } + // Teacher filter. + if ($coc_config->teachercoursefilter == true) { + // Add all course teacher's names to filter list. + if ($coc_config->teachercoursefilter == true) { + foreach ($courseteachers as $ct) { + $filterteachers[$ct->id] = $ct->lastname.', '.$ct->firstname; + } + } + } + + + // Check if this course is hidden according to the hide courses feature. + if ($coc_config->enablehidecourses == true) { + $courses[$c->id]->hidecourse = block_course_overview_campus_course_hidden_by_hidecourses($c); + // Increase counter for hidden courses management. + if ($courses[$c->id]->hidecourse == true) { + $hiddencoursescounter++; + } + } + + // Check if this course is hidden according to the term course filter. + if ($coc_config->termcoursefilter == true) { + $courses[$c->id]->termcoursefiltered = block_course_overview_campus_course_hidden_by_termcoursefilter($c, $selectedterm); + } + + // Check if this course is hidden according to the parent category course filter. + if ($coc_config->categorycoursefilter == true) { + $courses[$c->id]->categorycoursefiltered = block_course_overview_campus_course_hidden_by_categorycoursefilter($c, $selectedcategory); + } + + // Check if this course is hidden according to the top level category course filter. + if ($coc_config->toplevelcategorycoursefilter == true) { + $courses[$c->id]->toplevelcategorycoursefiltered = block_course_overview_campus_course_hidden_by_toplevelcategorycoursefilter($c, $selectedtoplevelcategory); + } + + // Check if this course is hidden according to the teacher course filter. + if ($coc_config->teachercoursefilter == true) { + $courses[$c->id]->teachercoursefiltered = block_course_overview_campus_course_hidden_by_teachercoursefilter($c, $selectedteacher); + } + + + // Re-sort courses to list courses in which I have a teacher role first if configured: + // First step: Removing the courses. + if ($coc_config->prioritizemyteachedcourses) { + // Check if user is teacher in this course. + if (array_key_exists($USER->id, $courseteachers)) { + // Remember the course. + $myteachercourses[] = $c; + // Remove the course from the courses array. + unset($courses[$c->id]); + } + } + } + + + // Re-sort courses to list courses in which I have a teacher role first if configured: + // Last step: Adding the courses again. + if ($coc_config->prioritizemyteachedcourses && isset ($myteachercourses) && count($myteachercourses) > 0) { + // Add the courses again at the beginning of the courses array. + $courses = $myteachercourses + $courses; + } + + + // Replace and remember currentterm placeholder with precise term based on my courses. + if ($coc_config->termcoursefilter == true && $selectedterm == 'currentterm') { + // Distinguish between term modes. + // "Academic year" mode. + if ($coc_config->termmode == '1') { + // If term starts on January 1st and there are courses this year, + // set selected term to this year. + if ($coc_config->term1startday == '1' && isset($filterterms[date('Y')])) { + $selectedterm = date('Y'); + } + // If term doesn't start on January 1st and current day comes on or after term start day and there are courses + // this term, set selected term to this year + next year. + else if (intval(date('z')) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && + isset($filterterms[date('Y').'-'.(date('Y') + 1)])) { + $selectedterm = date('Y').'-'.(date('Y') + 1); + } + // If term doesn't start on January 1st and current day comes before term start day and there are courses + // this term, set selected term to this year + former year. + else if (isset($filterterms[(date('Y') - 1).'-'.date('Y')])) { + $selectedterm = (date('Y') - 1).'-'.date('Y'); + } + // Otherwise set selected term to the latest (but not future) term possible. + else { + $selectedterm = 'all'; + arsort($filterterms); + foreach ($filterterms as $t) { + if ($t != 'other' && $t != 'timeless' && intval(substr($t, 0, 4)) <= intval(date('Y'))) { + $selectedterm = $t; + break; + } + } + } + } + // "Semester" mode. + else if ($coc_config->termmode == '2') { + // If current day comes before first term start day and there are courses this term, + // set selected term to second term of former year. + if (intval(date('z')) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && isset($filterterms[(date('Y') - 1).'-2'])) { + $selectedterm = (date('Y') - 1).'-2'; + } + // If current day comes on or after first term start day but before second term start day and there are courses + // this term, set selected term to first term of current year. + else if (intval(date('z', $c->startdate)) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && + intval(date('z', $c->startdate)) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term2startday))) && + isset($filterterms[date('Y').'-1'])) { + $selectedterm = date('Y').'-1'; + } + // If course start date's day comes on or after second term start day and there are courses this term, + // set selected term to second term of current year. + else if (isset($filterterms[date('Y').'-2'])) { + $selectedterm = date('Y').'-2'; + } + // Otherwise set selected term to the latest (but not future) term possible. + else { + $selectedterm = 'all'; + krsort($filterterms); + foreach ($filterterms as $t => $n) { + if ($t != 'other' && $t != 'timeless' && intval(substr($t, 0, 4)) <= intval(date('Y'))) { + $selectedterm = $t; + break; + } + } + } + } + // "Tertial" mode. + else if ($coc_config->termmode == '3') { + // If current day comes before first term start day and there are courses this term, + // set selected term to third term of former year. + if (intval(date('z')) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && + isset($filterterms[(date('Y') - 1).'-3'])) { + $selectedterm = (date('Y') - 1).'-2'; + } + // If current day comes on or after first term start day but before second term start day and there are courses + // this term, set selected term to first term of current year. + else if (intval(date('z', $c->startdate)) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && + intval(date('z', $c->startdate)) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term2startday))) && + isset($filterterms[date('Y').'-1'])) { + $selectedterm = date('Y').'-1'; + } + // If current day comes on or after second term start day but before third term start day and there are courses + // this term, set selected term to second term of current year. + else if (intval(date('z', $c->startdate)) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term2startday))) && + intval(date('z', $c->startdate)) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term3startday))) && + isset($filterterms[date('Y').'-2'])) { + $selectedterm = date('Y').'-2'; + } + // If course start date's day comes on or after third term start day and there are courses this term, + // set selected term to third term of current year. + else if (isset($filterterms[date('Y').'-3'])) { + $selectedterm = date('Y').'-3'; + } + // Otherwise set selected term to the latest (but not future) term possible. + else { + $selectedterm = 'all'; + krsort($filterterms); + foreach ($filterterms as $t => $n) { + if ($t != 'other' && $t != 'timeless' && intval(substr($t, 0, 4)) <= intval(date('Y'))) { + $selectedterm = $t; + break; + } + } + } + } + // "Trimester" mode. + else if ($coc_config->termmode == '4') { + // If current day comes before first term start day and there are courses this term, + // set selected term to fourth term of former year. + if (intval(date('z')) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && + isset($filterterms[(date('Y') - 1).'-4'])) { + $selectedterm = (date('Y') - 1).'-2'; + } + // If current day comes on or after first term start day but before second term start day and there are courses + // this term, set selected term to first term of current year. + else if (intval(date('z', $c->startdate)) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term1startday))) && + intval(date('z', $c->startdate)) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term2startday))) && + isset($filterterms[date('Y').'-1'])) { + $selectedterm = date('Y').'-1'; + } + // If current day comes on or after second term start day but before third term start day and there are courses + // this term, set selected term to second term of current year. + else if (intval(date('z', $c->startdate)) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term2startday))) && + intval(date('z', $c->startdate)) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term3startday))) && + isset($filterterms[date('Y').'-2'])) { + $selectedterm = date('Y').'-2'; + } + // If current day comes on or after third term start day but before fourth term start day and there are courses + // this term, set selected term to third term of current year. + else if (intval(date('z', $c->startdate)) >= intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term3startday))) && + intval(date('z', $c->startdate)) < intval(date('z', strtotime(date('Y', $c->startdate).'-'.$coc_config->term4startday))) && + isset($filterterms[date('Y').'-3'])) { + $selectedterm = date('Y').'-3'; + } + // If course start date's day comes on or after fourth term start day and there are courses this term, + // set selected term to fourth term of current year. + else if (isset($filterterms[date('Y').'-4'])) { + $selectedterm = date('Y').'-4'; + } + // Otherwise set selected term to the latest (but not future) term possible. + else { + $selectedterm = 'all'; + krsort($filterterms); + foreach ($filterterms as $t => $n) { + if ($t != 'other' && $t != 'timeless' && intval(substr($t, 0, 4)) <= intval(date('Y'))) { + $selectedterm = $t; + break; + } + } + } + } + // This should never happen. + else { + print_error('error'); + } + + // Remember selected term. + set_user_preference('block_course_overview_campus-selectedterm', $selectedterm); + } + + + + /********************************************************************************/ + /*** GENERATE OUTPUT FOR FILTER ***/ + /********************************************************************************/ + + // Show filter form if any filter is activated and if hidden courses management isn't active. + if ((!$coc_config->enablehidecourses || $param_manage == 0) && ($coc_config->categorycoursefilter == true || $coc_config->toplevelcategorycoursefilter == true || $coc_config->termcoursefilter == true || $coc_config->teachercoursefilter == true)) { + // Calculate CSS class for filter divs. + $filtercount = 0; + if ($coc_config->termcoursefilter == true) { + $filtercount++; + } + if ($coc_config->teachercoursefilter == true) { + $filtercount++; + } + if ($coc_config->categorycoursefilter == true) { + $filtercount++; + } + if ($coc_config->toplevelcategorycoursefilter == true) { + $filtercount++; + } + if ($filtercount == 1) { + $filterwidth = 'span12 col-md-12'; // Class 'span12' is used for Bootstrapbase and will be ignored by Boost. + } else if ($filtercount == 2) { + $filterwidth = 'span6 col-md-6'; // Class 'span6' is used for Bootstrapbase and will be ignored by Boost. + } else if ($filtercount == 3) { + $filterwidth = 'span4 col-md-4'; // Class 'span4' is used for Bootstrapbase and will be ignored by Boost. + } else if ($filtercount == 4) { + $filterwidth = 'span3 col-md-6 col-lg-3'; // Class 'span3' is used for Bootstrapbase and will be ignored by Boost. + } else { + $filterwidth = 'span12 col-md-12'; // Class 'span12' is used for Bootstrapbase and will be ignored by Boost. + } + + // Start form. + echo '<form method="post" action="">'; + + // Start section. + echo '<div id="coc-filterlist" class="container-fluid"><div class="row">'; + + // Show term filter. + if ($coc_config->termcoursefilter == true) { + echo '<div class="coc-filter '.$filterwidth.' mb-3">'; + + // Show filter description. + if ($coc_config->termcoursefilterdisplayname != '') { + echo '<label for="coc-filterterm">'.format_string($coc_config->termcoursefilterdisplayname).'</label>'; + } + + // Show filter widget. + echo '<select name="coc-term" id="coc-filterterm" class="input-block-level form-control">'; // Class 'input-block-level' is used for Bootstrapbase and will be ignored by Boost. + + // Remember in this variable if selected term was displayed or not. + $selectedtermdisplayed = false; + + // Sort term filter alphabetically in reverse order. + krsort($filterterms); + + // Print "All terms" option. + if ($selectedterm == 'all') { + echo '<option value="all" selected>'.get_string('all', 'block_course_overview_campus').'</option> '; + $selectedtermdisplayed = true; + } else { + echo '<option value="all">'.get_string('all', 'block_course_overview_campus').'</option> '; + } + + // Print each term in filter as an option item and select selected term. + foreach ($filterterms as $t => $n) { + // If iterated term is selected term. + if ($selectedterm == $t) { + // Handle "other" term option. + if ($selectedterm == 'other') { + echo '<option selected value="other">'.get_string('other', 'block_course_overview_campus').'</option> '; + $selectedtermdisplayed = true; + } + // Handle "timeless" term option. + else if ($selectedterm == 'timeless') { + echo '<option selected value="timeless">'.format_string($coc_config->timelesscoursesname).'</option> '; + $selectedtermdisplayed = true; + } else { + echo '<option selected value="'.$t.'">'.format_string($n).'</option> '; + $selectedtermdisplayed = true; + } + } + // If iterated term isn't selected term. + else { + // Handle "other" term option. + if ($t == 'other') { + echo '<option value="other">'.get_string('other', 'block_course_overview_campus').'</option> '; + } + // Handle "timeless" term option. + else if ($t == 'timeless') { + echo '<option value="timeless">'.format_string($coc_config->timelesscoursesname).'</option> '; + } else { + echo '<option value="'.$t.'">'.format_string($n).'</option> '; + } + } + } + + echo '</select>'; + + // If selected term couldn't be displayed, select all terms and save the new selection. + // In this case, no option item is marked as selected, but that's ok as the "all" item is at the top. + if (!$selectedtermdisplayed) { + $selectedterm = 'all'; + set_user_preference('block_course_overview_campus-selectedterm', $selectedterm); + } + + echo '</div>'; + } + + // Show top level category filter. + if ($coc_config->toplevelcategorycoursefilter == true) { + echo '<div class="coc-filter '.$filterwidth.' mb-3">'; + + // Show filter description. + if ($coc_config->toplevelcategorycoursefilterdisplayname != '') { + echo '<label for="coc-filtertoplevelcategory">'.format_string($coc_config->toplevelcategorycoursefilterdisplayname).'</label>'; + } + + // Show filter widget. + echo '<select name="coc-toplevelcategory" id="coc-filtertoplevelcategory" class="input-block-level form-control">'; // class 'input-block-level' is used for Bootstrapbase and will be ignored by Boost. + + // Remember in this variable if selected top level category was displayed or not. + $selectedtoplevelcategorydisplayed = false; + + // Sort top level category filter by category sort order. + // Fetch full category information for each category. + foreach ($filtertoplevelcategories as $ftl_key => $ftl_value) { + $filtertoplevelcategoriesfullinfo[] = $coursecategories[$ftl_key]; + } + // Sort full category information array by sortorder. + $success = usort($filtertoplevelcategoriesfullinfo, "block_course_overview_campus_compare_categories"); + // If sorting was successful, create new array with same data structure like the old one. + // Otherwise just leave the old array as it is (should not happen). + if ($success) { + $filtertoplevelcategories = array(); + foreach ($filtertoplevelcategoriesfullinfo as $ftl) { + $filtertoplevelcategories[$ftl->id] = format_string($ftl->name); + } + } + + // Print "All categories" option. + if ($selectedtoplevelcategory == 'all') { + echo '<option value="all" selected>'.get_string('all', 'block_course_overview_campus').'</option> '; + $selectedtoplevelcategorydisplayed = true; + } else { + echo '<option value="all">'.get_string('all', 'block_course_overview_campus').'</option> '; + } + + // Print each top level category in filter as an option item and select selected top level category. + foreach ($filtertoplevelcategories as $value => $cat) { + // If iterated top level category is selected top level category. + if ($selectedtoplevelcategory == $value) { + echo '<option selected value="'.$value.'">'.$cat.'</option> '; + $selectedtoplevelcategorydisplayed = true; + } + // If iterated top level category isn't selected top level category. + else { + echo '<option value="'.$value.'">'.$cat.'</option> '; + } + } + + echo '</select>'; + + // If selected top level category couldn't be displayed, select all categories and save the new selection. + // In this case, no option item is marked as selected, but that's ok as the "all" item is at the top. + if (!$selectedtoplevelcategorydisplayed) { + $selectedtoplevelcategory = 'all'; + set_user_preference('block_course_overview_campus-selectedtoplevelcategory', $selectedtoplevelcategory); + } + + echo '</div>'; + } + + // Show parent category filter. + if ($coc_config->categorycoursefilter == true) { + echo '<div class="coc-filter '.$filterwidth.' mb-3">'; + + // Show filter description. + if ($coc_config->categorycoursefilterdisplayname != '') { + echo '<label for="coc-filtercategory">'.format_string($coc_config->categorycoursefilterdisplayname).'</label>'; + } + + // Show filter widget. + echo '<select name="coc-category" id="coc-filtercategory" class="input-block-level form-control">'; // Class 'input-block-level' is used for Bootstrapbase and will be ignored by Boost. + + // Remember in this variable if selected parent category was displayed or not. + $selectedcategorydisplayed = false; + + // Sort parent category filter alphabetically. + natcasesort($filtercategories); + + // Print "All categories" option. + if ($selectedcategory == 'all') { + echo '<option value="all" selected>'.get_string('all', 'block_course_overview_campus').'</option> '; + $selectedcategorydisplayed = true; + } else { + echo '<option value="all">'.get_string('all', 'block_course_overview_campus').'</option> '; + } + + // Print each parent category in filter as an option item and select selected parent category. + foreach ($filtercategories as $value => $cat) { + // If iterated parent category is selected parent category. + if ($selectedcategory == $value) { + echo '<option selected value="'.$value.'">'.$cat.'</option> '; + $selectedcategorydisplayed = true; + } + // If iterated parent category isn't selected parent category. + else { + echo '<option value="'.$value.'">'.$cat.'</option> '; + } + } + + echo '</select>'; + + // If selected parent category couldn't be displayed, select all categories and save the new selection. + // In this case, no option item is marked as selected, but that's ok as the "all" item is at the top. + if (!$selectedcategorydisplayed) { + $selectedcategory = 'all'; + set_user_preference('block_course_overview_campus-selectedcategory', $selectedcategory); + } + + echo '</div>'; + } + + // Show teacher filter. + if ($coc_config->teachercoursefilter == true) { + echo '<div class="coc-filter '.$filterwidth.' mb-3">'; + + // Show filter description. + if ($coc_config->teachercoursefilterdisplayname != '') { + echo '<label for="coc-filterteacher">'.format_string($coc_config->teachercoursefilterdisplayname).'</label>'; + } + + // Show filter widget. + echo '<select name="coc-teacher" id="coc-filterteacher" class="input-block-level form-control">'; // Class 'input-block-level' is used for Bootstrapbase and will be ignored by Boost. + + // Remember in this variable if selected teacher was displayed or not. + $selectedteacherdisplayed = false; + + // Sort teacher filter alphabetically. + natcasesort($filterteachers); + + // Print "All teachers" option. + if ($selectedteacher == 'all') { + echo '<option value="all" selected>'.get_string('all', 'block_course_overview_campus').'</option> '; + $selectedteacherdisplayed = true; + } else { + echo '<option value="all">'.get_string('all', 'block_course_overview_campus').'</option> '; + } + + // Print each teacher in filter as an option item and select selected teacher. + foreach ($filterteachers as $id => $t) { + // If iterated teacher is selected teacher. + if ($selectedteacher == $id) { + echo '<option selected value="'.$id.'">'.$t.'</option> '; + $selectedteacherdisplayed = true; + } else { + echo '<option value="'.$id.'">'.$t.'</option> '; + } + } + + echo '</select>'; + + // If selected teacher couldn't be displayed, select all teachers and save the new selection. + // In this case, no option item is marked as selected, but that's ok as the "all" item is at the top. + if (!$selectedteacherdisplayed) { + $selectedteacher = 'all'; + set_user_preference('block_course_overview_campus-selectedteacher', $selectedteacher); + } + + echo '</div>'; + } + + // End section. + echo '</div></div>'; + + // Show submit button for Non-JavaScript interaction. + echo '<div id="coc-filtersubmit" class="container-fluid mb-3"><div class="row"><input type="submit" value="'.get_string('submitfilter', 'block_course_overview_campus').'" class="btn btn-primary" /></div></div>'; + + // End form. + echo '</form>'; + } + + + + /********************************************************************************/ + /*** GENERATE OUTPUT FOR HIDDEN COURSES MANAGEMENT TOP BOX ***/ + /********************************************************************************/ + + // Do only if course hiding is enabled. + if ($coc_config->enablehidecourses) { + // If hidden courses managing is active, output hidden courses management top box as visible. + if ($param_manage == 1) { + echo '<div id="coc-hiddencoursesmanagement-top" class="container-fluid"><div class="row"><a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => 0)).'">'.get_string('stopmanaginghiddencourses', 'block_course_overview_campus').'</a></div></div>'; + } + } + + + + /********************************************************************************/ + /*** GENERATE OUTPUT FOR COURSELIST ***/ + /********************************************************************************/ + + // Start section. + echo '<div id="coc-courselist" class="container-fluid mb-3">'; + + // Show courses. + foreach ($courses as $c) { + // Remember course ID for JS processing. + $js_courseslist .= $c->id.' '; + + // Start course div. + echo '<div id="coc-course-'.$c->id.'" class="row coc-course">'; + + // Start hide course div - later we use this div to filter the course. + if ($coc_config->enablehidecourses == true && $param_manage == 0) { + // Show course if it is visible according to the hide courses feature. + if ($c->hidecourse == false) { + echo '<div class="hidecoursediv coc-hidecourse-'.$c->id.'">'; + } + // Otherwise hide the course with CSS. + else { + echo '<div class="hidecoursediv coc-hidecourse-'.$c->id.' coc-hidden">'; + } + } + + // Start filter by term div - later we use this div to filter the course. + if ($coc_config->termcoursefilter == true && $param_manage == 0) { + // Show course if it is visible according to the term course filter. + if ($c->termcoursefiltered == false) { + echo '<div class="termdiv coc-term-'.$c->term.'">'; + } + // Otherwise hide the course with CSS. + else { + echo '<div class="termdiv coc-term-'.$c->term.' coc-hidden">'; + } + } + + // Start filter by parent category div - later we use this div to filter the course. + if ($coc_config->categorycoursefilter == true && $param_manage == 0) { + // Show course if it is visible according to the parent category course filter. + if ($c->categorycoursefiltered == false) { + echo '<div class="categorydiv coc-category-'.$c->categoryid.'">'; + } + // Otherwise hide the course with CSS. + else { + echo '<div class="categorydiv coc-category-'.$c->categoryid.' coc-hidden">'; + } + } + + // Start filter by top level category div - later we use this div to filter the course. + if ($coc_config->toplevelcategorycoursefilter == true && $param_manage == 0) { + // Show course if it is visible according to the top level category course filter. + if ($c->toplevelcategorycoursefiltered == false) { + echo '<div class="toplevelcategorydiv coc-toplevelcategory-'.$c->toplevelcategoryid.'">'; + } + // Otherwise hide the course with CSS. + else { + echo '<div class="toplevelcategorydiv coc-toplevelcategory-'.$c->toplevelcategoryid.' coc-hidden">'; + } + } + + // Start filter by teacher div - later we use this div to filter the course. + if ($coc_config->teachercoursefilter == true && $param_manage == 0) { + // Start teacher div. + echo '<div class="teacherdiv'; + + // Add all teachers. + foreach ($c->teachers as $id => $t) { + echo ' coc-teacher-'.$id; + } + + // Show course if it is visible according to the teacher course filter. + if ($c->teachercoursefiltered == false) { + echo '">'; + } + // Otherwise hide the course with CSS. + else { + echo ' coc-hidden">'; + } + } + + + // Start standard course overview coursebox. + echo $OUTPUT->box_start('coursebox'); + + // Output course visibility control icons. + if ($coc_config->enablehidecourses) { + // If course is hidden. + if (block_course_overview_campus_course_hidden_by_hidecourses($c, 0) == false) { // We can't rely on $c->hidecourse here because otherwise the icon would always be t/show. + echo '<div class="hidecourseicon"> + <a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => $param_manage, 'coc-hidecourse' => $c->id, 'coc-showcourse' => '')).'" id="coc-hidecourseicon-'.$c->id.'" title="'.get_string('hidecourse', 'block_course_overview_campus').'">'.$OUTPUT->pix_icon('hide', get_string('hidecourse', 'block_course_overview_campus'), 'block_course_overview_campus').'</a> + <a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => $param_manage, 'coc-hidecourse' => '', 'coc-showcourse' => $c->id)).'" id="coc-showcourseicon-'.$c->id.'" class="coc-hidden" title="'.get_string('showcourse', 'block_course_overview_campus').'">'.$OUTPUT->pix_icon('show', get_string('showcourse', 'block_course_overview_campus'), 'block_course_overview_campus').'</a> + </div>'; + } + // If course is visible. + else { + echo '<div class="hidecourseicon"> + <a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => $param_manage, 'coc-hidecourse' => $c->id, 'coc-showcourse' => '')).'" id="coc-hidecourseicon-'.$c->id.'" class="coc-hidden" title="'.get_string('hidecourse', 'block_course_overview_campus').'">'.$OUTPUT->pix_icon('hide', get_string('hidecourse', 'block_course_overview_campus'), 'block_course_overview_campus').'</a> + <a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => $param_manage, 'coc-hidecourse' => '', 'coc-showcourse' => $c->id)).'" id="coc-showcourseicon-'.$c->id.'" title="'.get_string('showcourse', 'block_course_overview_campus').'">'.$OUTPUT->pix_icon('show', get_string('showcourse', 'block_course_overview_campus'), 'block_course_overview_campus').'</a> + </div>'; + } + } + + // Get course attributes for use with course link. + $attributes = array('title' => format_string($c->fullname)); + if (empty($c->visible)) { + $attributes['class'] = 'dimmed'; + } + + // Check if some meta info has to be displayed in addition to the course name. + if ($coc_config->secondrowshowshortname == true || + $coc_config->secondrowshowtermname == true || + $coc_config->secondrowshowcategoryname == true || + $coc_config->secondrowshowtoplevelcategoryname == true || + ($coc_config->secondrowshowteachername == true && count($c->teachers) > 0)) { + $meta = array(); + if ($coc_config->secondrowshowshortname == true) { + $meta[] = $c->shortname; + } + if ($coc_config->secondrowshowtermname == true) { + $meta[] = $c->termname; + } + if ($coc_config->secondrowshowcategoryname == true) { + $meta[] = $c->categoryname; + } + if ($coc_config->secondrowshowtoplevelcategoryname == true) { + $meta[] = $c->toplevelcategoryname; + } + if ($coc_config->secondrowshowteachername == true) { + // Get teachers' names for use with course link. + if (count($c->teachers) > 0) { + $teachernames = block_course_overview_campus_get_teachername_string($c->teachers); + $meta[] = $teachernames; + } else if (strlen(trim($coc_config->noteachertext)) > 0) { + $teachernames = format_string($coc_config->noteachertext); + $meta[] = $teachernames; + } + } + + // Create meta info code. + // Hide metainfo on phones if configured. + if ($coc_config->secondrowhideonphones == true) { + $metainfo = '<br /><span class="coc-metainfo hidden-phone hidden-sm-down">('.implode($meta, ' | ').')</span>'; // Class 'hidden-phone' is used for Bootstrapbase and will be ignored by Boost. + } + // Otherwise. + else { + $metainfo = '<br /><span class="coc-metainfo">('.implode($meta, ' | ').')</span>'; + } + } + else { + $metainfo = ''; + } + + // Output course link. + if ($coc_config->firstrowcoursename == 2) { + echo $OUTPUT->heading(html_writer::link(new moodle_url('/course/view.php', array('id' => $c->id)), $c->shortname.$metainfo, $attributes), 3); + } + else { + echo $OUTPUT->heading(html_writer::link(new moodle_url('/course/view.php', array('id' => $c->id)), format_string($c->fullname).$metainfo, $attributes), 3); + } + + + // End standard course overview coursebox. + echo $OUTPUT->box_end(); + + // End filter by term div. + if ($coc_config->termcoursefilter == true && $param_manage == 0) { + echo '</div>'; + } + + // End filter by parent category div. + if ($coc_config->categorycoursefilter == true && $param_manage == 0) { + echo '</div>'; + } + + // End filter by top level category div. + if ($coc_config->toplevelcategorycoursefilter == true && $param_manage == 0) { + echo '</div>'; + } + + // End filter by teacher div. + if ($coc_config->teachercoursefilter == true && $param_manage == 0) { + echo '</div>'; + } + + // End hide course div. + if ($coc_config->enablehidecourses == true && $param_manage == 0) { + echo '</div>'; + } + + // End course div. + echo '</div>'; + } + + // End section. + echo '</div>'; + + + + /********************************************************************************/ + /*** GENERATE OUTPUT FOR HIDDEN COURSES MANAGEMENT BOTTOM BOX ***/ + /********************************************************************************/ + + // Do only if course hiding is enabled. + if ($coc_config->enablehidecourses) { + // If hidden courses managing is active, output the box as visible. + if ($param_manage == 1) { + echo '<div id="coc-hiddencoursesmanagement-bottom" class="container-fluid"><div class="row"><a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => 0)).'">'.get_string('stopmanaginghiddencourses', 'block_course_overview_campus').'</a></div></div>'; + } + // If hidden courses managing is not active, but I have hidden courses, output the box as visible. + else if ($param_manage == 0 && $hiddencoursescounter > 0) { + echo '<div id="coc-hiddencoursesmanagement-bottom" class="container-fluid"><div class="row">'.get_string('youhave', 'block_course_overview_campus').' <span id="coc-hiddencoursescount">'.$hiddencoursescounter.'</span> '.get_string('hiddencourses', 'block_course_overview_campus').' | <a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => 1)).'">'.get_string('managehiddencourses', 'block_course_overview_campus').'</a></div></div>'; + } + // Otherwise output the box as hidden to appear via JS as soon as a course is hidden. + else { + echo '<div id="coc-hiddencoursesmanagement-bottom" class="container-fluid"><div class="row coc-hidden">'.get_string('youhave', 'block_course_overview_campus').' <span id="coc-hiddencoursescount">'.$hiddencoursescounter.'</span> '.get_string('hiddencourses', 'block_course_overview_campus').' | <a href="'.$CFG->wwwroot.$PAGE->url->out_as_local_url(true, array('coc-manage' => 1)).'">'.get_string('managehiddencourses', 'block_course_overview_campus').'</a></div></div>'; + } + } + + + + /********************************************************************************/ + /*** OUTPUT CONTENT ***/ + /********************************************************************************/ + + // Get and end output buffer. + $content = ob_get_contents(); + ob_end_clean(); + + + + /********************************************************************************/ + /*** AJAX MANAGEMENT ***/ + /********************************************************************************/ + + // Verify that course displaying parameters are updatable by AJAX. + foreach ($courses as $c) { + if ($coc_config->enablehidecourses) { + user_preference_allow_ajax_update('block_course_overview_campus-hidecourse-'.$c->id, PARAM_BOOL); + } + } + + // Verify that filter parameters are updatable by AJAX. + if ($coc_config->termcoursefilter == true) { + user_preference_allow_ajax_update('block_course_overview_campus-selectedterm', PARAM_ALPHANUMEXT); + } + if ($coc_config->teachercoursefilter == true) { + user_preference_allow_ajax_update('block_course_overview_campus-selectedteacher', PARAM_ALPHANUM); + } + if ($coc_config->categorycoursefilter == true) { + user_preference_allow_ajax_update('block_course_overview_campus-selectedcategory', PARAM_ALPHANUM); + } + if ($coc_config->toplevelcategorycoursefilter == true) { + user_preference_allow_ajax_update('block_course_overview_campus-selectedtoplevelcategory', PARAM_ALPHANUM); + } + + // Include JS for hiding courses with AJAX. + if ($coc_config->enablehidecourses) { + $js_hidecoursesoptions = [ + 'local_boostcoc' => block_course_overview_campus_check_local_boostcoc(), + 'courses' => trim($js_courseslist), + 'manage' => $param_manage, + ]; + $PAGE->requires->js_call_amd('block_course_overview_campus/hidecourse', 'initHideCourse', [$js_hidecoursesoptions]); + } + + // Include JS for filtering courses with AJAX. + if ($coc_config->teachercoursefilter == true || $coc_config->termcoursefilter == true || $coc_config->categorycoursefilter == true || $coc_config->toplevelcategorycoursefilter == true) { + $js_filteroptions = [ + 'local_boostcoc' => block_course_overview_campus_check_local_boostcoc(), + 'initialsettings' => [ + 'term' => (isset($selectedterm)) ? $selectedterm : '', + 'teacher' => (isset($selectedteacher)) ? $selectedteacher : '', + 'category' => (isset($selectedcategory)) ? $selectedcategory : '', + 'toplevelcategory' => (isset($selectedtoplevelcategory)) ? $selectedtoplevelcategory : '', + ], + ]; // Passing the initialsettings to the JS code is necessary for filtering the course list again when using browser 'back' button. + $PAGE->requires->js_call_amd('block_course_overview_campus/filter', 'initFilter', [$js_filteroptions]); + } + + + + /********************************************************************************/ + /*** LOCAL_BOOSTCOC ***/ + /********************************************************************************/ + + // Do only if local_boostcoc is installed. + if (block_course_overview_campus_check_local_boostcoc() == true) { + // Remember the not shown courses for local_boostcoc. + block_course_overview_campus_remember_notshowncourses_for_local_boostcoc($courses); + + // Verify that we can also remember the not shown courses for local_boostcoc by AJAX. + user_preference_allow_ajax_update('local_boostcoc-notshowncourses', PARAM_RAW); + + // Remember the active filters for local_boostcoc. + block_course_overview_campus_remember_activefilters_for_local_boostcoc($hiddencoursescounter); + + // Verify that we can also remember the active filters for local_boostcoc by AJAX. + user_preference_allow_ajax_update('local_boostcoc-activefilters', PARAM_RAW); + } + } + + + + /********************************************************************************/ + /*** OUTPUT AND RETURN ***/ + /********************************************************************************/ + + // Output content. + $this->content = new stdClass(); + + if (!empty($content)) { + $this->content->text = $content; + } else { + $this->content->text = ''; + } + + return $this->content; + } +} diff --git a/block/course_overview_campus/classes/privacy/provider.php b/block/course_overview_campus/classes/privacy/provider.php new file mode 100644 index 0000000..a3ad017 --- /dev/null +++ b/block/course_overview_campus/classes/privacy/provider.php @@ -0,0 +1,141 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Privacy provider + * + * @package block_course_overview_campus + * @copyright 2018 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_course_overview_campus\privacy; + +use \core_privacy\local\request\writer; +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\transform; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementing provider. + * + * @package block_course_overview_campus + * @copyright 2018 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @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\user_preference_provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised item collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_user_preference('block_course_overview_campus-selectedterm', + 'privacy:metadata:preference:selectedterm'); + $collection->add_user_preference('block_course_overview_campus-selectedteacher', + 'privacy:metadata:preference:selectedteacher'); + $collection->add_user_preference('block_course_overview_campus-selectedcategory', + 'privacy:metadata:preference:selectedcategory'); + $collection->add_user_preference('block_course_overview_campus-selectedtoplevelcategory', + 'privacy:metadata:preference:selectedtoplevelcategory'); + $collection->add_user_preference('block_course_overview_campus-hidecourse-', + 'privacy:metadata:preference:hidecourse'); + $collection->add_user_preference('local_boostcoc-notshowncourses', + 'privacy:metadata:preference:local_boostcoc-notshowncourses'); + $collection->add_user_preference('local_boostcoc-activefilters', + 'privacy:metadata:preference:local_boostcoc-activefilters'); + + return $collection; + } + + /** + * Store all user preferences for the plugin. + * + * @param int $userid The userid of the user whose data is to be exported. + */ + public static function export_user_preferences(int $userid) { + $preferences = get_user_preferences(); + foreach ($preferences as $name => $value) { + $descriptionidentifier = null; + + // User preferences for filters. + if (strpos($name, 'block_course_overview_campus-selected') === 0) { + if ($name == 'block_course_overview_campus-selectedterm') { + $descriptionidentifier = 'privacy:request:preference:selectedterm'; + } else if ($name == 'block_course_overview_campus-selectedteacher') { + $descriptionidentifier = 'privacy:request:preference:selectedteacher'; + } else if ($name == 'block_course_overview_campus-selectedcategory') { + $descriptionidentifier = 'privacy:request:preference:selectedcategory'; + } else if ($name == 'block_course_overview_campus-selectedtoplevelcategory') { + $descriptionidentifier = 'privacy:request:preference:selectedtoplevelcategory'; + } + + if ($descriptionidentifier !== null) { + writer::export_user_preference( + 'block_course_overview_campus', + $name, + $value, + get_string($descriptionidentifier, 'block_course_overview_campus', (object) [ + 'value' => $value, + ]) + ); + } + + // User preferences for hiding stuff. + } else if (strpos($name, 'block_course_overview_campus-hide') === 0) { + if (strpos($name, 'block_course_overview_campus-hidecourse-') === 0) { + $descriptionidentifier = 'privacy:request:preference:hidecourse'; + $item = substr($name, strlen('block_course_overview_campus-hidecourse-')); + } + + if ($descriptionidentifier !== null) { + writer::export_user_preference( + 'block_course_overview_campus', + $name, + $value, + get_string($descriptionidentifier, 'block_course_overview_campus', (object) [ + 'item' => $item, + 'value' => $value, + ]) + ); + } + + // User preferences for local_boostcoc. + } else if (strpos($name, 'local_boostcoc-') === 0) { + if ($name == 'local_boostcoc-notshowncourses') { + $descriptionidentifier = 'privacy:request:preference:local_boostcoc-notshowncourses'; + } else if ($name == 'local_boostcoc-activefilters') { + $descriptionidentifier = 'privacy:request:preference:local_boostcoc-activefilters'; + } + + if ($descriptionidentifier !== null) { + writer::export_user_preference( + 'block_course_overview_campus', + $name, + $value, + get_string($descriptionidentifier, 'block_course_overview_campus', (object) [ + 'value' => $value, + ]) + ); + } + } + } + } +} diff --git a/block/course_overview_campus/db/access.php b/block/course_overview_campus/db/access.php new file mode 100644 index 0000000..9a464c8 --- /dev/null +++ b/block/course_overview_campus/db/access.php @@ -0,0 +1,46 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Capabilities + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + 'block/course_overview_campus:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + 'block/course_overview_campus:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), +); diff --git a/block/course_overview_campus/db/uninstall.php b/block/course_overview_campus/db/uninstall.php new file mode 100644 index 0000000..3f9c93d --- /dev/null +++ b/block/course_overview_campus/db/uninstall.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Block "course overview (campus)" - Uninstall file + * + * @package block_course_overview_campus + * @copyright 2019 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plugin uninstall steps. + */ +function xmldb_block_course_overview_campus_uninstall() { + global $DB; + + // The plugin uninstall process in Moodle core will take care of removing the plugin configuration, but not of removing the + // user preferences which we have set for the users. We have to remove them ourselves. + // We remove them directly from the DB table and don't use unset_user_preference() as the cache is cleared anyway directly + // after the plugin has been uninstalled. + + $like = $DB->sql_like('name', '?', true, true, false, '|'); + $params = array($DB->sql_like_escape('block_course_overview_campus-', '|') . '%'); + $DB->delete_records_select('user_preferences', $like, $params); + + $like = $DB->sql_like('name', '?', true, true, false, '|'); + $params = array($DB->sql_like_escape('local_boostcoc-', '|') . '%'); + $DB->delete_records_select('user_preferences', $like, $params); + + return true; +} + diff --git a/block/course_overview_campus/db/upgrade.php b/block/course_overview_campus/db/upgrade.php new file mode 100644 index 0000000..9eea2a3 --- /dev/null +++ b/block/course_overview_campus/db/upgrade.php @@ -0,0 +1,49 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Upgrade script + * + * @package block_course_overview_campus + * @copyright 2019 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Function to upgrade block_course_overview_campus. + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_block_course_overview_campus_upgrade($oldversion) { + global $DB; + + if ($oldversion < 2019061200) { + // After the course news functionality has been removed from the plugin, the user preferences have to be removed manually. + // We remove them directly from the DB table and don't use unset_user_preference() as the cache is cleared anyway directly + // after the plugin has been uninstalled. + + $like = $DB->sql_like('name', '?', true, true, false, '|'); + $params = array($DB->sql_like_escape('block_course_overview_campus-hidenews-', '|') . '%'); + $DB->delete_records_select('user_preferences', $like, $params); + + upgrade_plugin_savepoint(true, 2019061200, 'block', 'course_overview_campus'); + } + + return true; +} + diff --git a/block/course_overview_campus/lang/en/block_course_overview_campus.php b/block/course_overview_campus/lang/en/block_course_overview_campus.php new file mode 100644 index 0000000..6b74ccf --- /dev/null +++ b/block/course_overview_campus/lang/en/block_course_overview_campus.php @@ -0,0 +1,181 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Language pack + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['academicyear_desc'] = 'Academic year (Calendar year is not divided)'; +$string['activityoverview'] = 'You have {$a}s that need attention'; +$string['all'] = 'All'; +$string['appearancesettingheading'] = 'Appearance'; +$string['blocktitle'] = 'Block title'; +$string['blocktitle_desc'] = 'This display name is shown as the title of the block'; +$string['blocktitledefault'] = 'Course overview'; +$string['category'] = 'Parent category'; +$string['categorycoursefilter'] = 'Activate parent category filter'; +$string['categorycoursefilter_desc'] = 'Allow users to filter courses by parent category'; +$string['categorycoursefilterdisplayname'] = 'Display name for parent category filter'; +$string['categorycoursefilterdisplayname_desc'] = 'This display name is shown above the parent category filter<br /><em>This setting is only processed when the parent category filter is activated</em>'; +$string['categorycoursefiltersettingheading'] = 'Parent category filter: Filter activation'; +$string['course_overview_campus:addinstance'] = 'Add a new course overview campus block'; +$string['course_overview_campus:myaddinstance'] = 'Add a new course overview campus block to Dashboard'; +$string['defaultterm'] = 'Use default term'; +$string['defaultterm_desc'] = 'If the user has not previously selected a term for filtering terms, a term is chosen for him. The first choice is the current term. If the user is not enrolled on any courses in the current term, the most recent previous term is chosen.<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['enablehidecourses'] = 'Enable course hiding'; +$string['enablehidecourses_desc'] = 'Enable course hiding, which lets users hide courses from the course overview list'; +$string['firstrowcoursename'] = 'First row: Course name style'; +$string['firstrowcoursename_desc'] = 'Show the course\'s full name or the course\'s short name in the first row of the course overview list entry'; +$string['hiddencourses'] = 'hidden courses'; +$string['hidecourse'] = 'Hide course in course overview list'; +$string['hidecoursessettingheading'] = 'Hide courses'; +$string['listentriessettingheading'] = 'Course overview list: Entries'; +$string['managehiddencourses'] = 'Manage hidden courses'; +$string['mergehomonymouscategories'] = 'Merge homonymous parent categories'; +$string['mergehomonymouscategories_desc'] = 'If there are multiple courses with different parent categories, but with the same parent category name, the parent category filter will be filled with multiple categories with the same name by default. This can be confusing to the user. If you want to merge all homonymous parent categories into one filter entry when using the parent category filter, activate this setting<br /><em>This setting is only processed when the parent category filter is activated</em>'; +$string['mergehomonymouscategoriessettingheading'] = 'Parent category filter: Merge homonymous parent categories'; +$string['nocourses'] = 'Currently, you are not enrolled in any courses'; +$string['noteacher'] = 'No teacher enrolled'; +$string['noteachertext'] = 'No teacher enrolled text'; +$string['noteachertext_desc'] = 'This text is displayed instead of the teachers\' names if there is no teacher enrolled in the course. If you don\'t want a placeholder text to appear, just delete this text here<br /><em>This setting is only processed when show teacher name is activated</em>'; +$string['ordersettingheading'] = 'Course overview list: Order'; +$string['other'] = 'Other'; +$string['pluginname'] = 'Course overview on campus'; +$string['prioritizemyteachedcourses'] = 'Prioritize courses in which I teach'; +$string['prioritizemyteachedcourses_desc'] = 'Courses in which the user has a teacher role are listed first in course overview list'; +$string['privacy:metadata:preference:selectedterm'] = 'The current selection of the term filter.'; +$string['privacy:metadata:preference:selectedteacher'] = 'The current selection of the teacher filter.'; +$string['privacy:metadata:preference:selectedcategory'] = 'The current selection of the parent category filter.'; +$string['privacy:metadata:preference:selectedtoplevelcategory'] = 'The current selection of the top level category filter.'; +$string['privacy:metadata:preference:hidecourse'] = 'The show/hide status of a course in the course overview list.'; +$string['privacy:metadata:preference:local_boostcoc-notshowncourses'] = 'The list of currently not shown courses to be used in the companion plugin \'Boost course overview campus\'.'; +$string['privacy:metadata:preference:local_boostcoc-activefilters'] = 'The list of currently active filters to be used in the companion plugin \'Boost course overview campus\'.'; +$string['privacy:request:preference:selectedterm'] = 'The current selection of the term filter is: {$a->value}.'; +$string['privacy:request:preference:selectedteacher'] = 'The current selection of the teacher filter is: {$a->value}.'; +$string['privacy:request:preference:selectedcategory'] = 'The current selection of the parent category filter is: {$a->value}.'; +$string['privacy:request:preference:selectedtoplevelcategory'] = 'The current selection of the top level category filter is: {$a->value}.'; +$string['privacy:request:preference:hidecourse'] = 'The show/hide status of the course {$a->item} in the course overview list is: {$a->value}.'; +$string['privacy:request:preference:local_boostcoc-notshowncourses'] = 'The list of currently not shown courses to be used in the companion plugin \'Boost course overview campus\' is: {$a->value}.'; +$string['privacy:request:preference:local_boostcoc-activefilters'] = 'The list of currently active filters to be used in the companion plugin \'Boost course overview campus\' is: {$a->value}.'; +$string['secondrowhideonphones'] = 'Second row: Hide on phones'; +$string['secondrowhideonphones_desc'] = 'Hide the second row on mobile phones to save space'; +$string['secondrowshowcategoryname'] = 'Second row: Show parent category name'; +$string['secondrowshowcategoryname_desc'] = 'Show the course\'s parent category name in a second row of the course overview list entry'; +$string['secondrowshowshortname'] = 'Second row: Show short name'; +$string['secondrowshowshortname_desc'] = 'Show the course\'s short name in a second row of the course overview list entry'; +$string['secondrowshowteachername'] = 'Second row: Show teacher name'; +$string['secondrowshowteachername_desc'] = 'Show the teacher\'s name(s) in a second row of the course overview list entry. If there is more then one teacher, the names will be sorted first by the <a href="/admin/roles/manage.php">global order of roles</a> and second by the teachers\' last names'; +$string['secondrowshowteachernamestyle'] = 'Second row: Style of teacher name'; +$string['secondrowshowteachernamestyle_desc'] = 'Define how the teacher\'s name should be displayed in the second row of the course overview list entry<br /><em>This setting is only processed when show teacher name is activated</em>'; +$string['secondrowshowtermname'] = 'Second row: Show term name'; +$string['secondrowshowtermname_desc'] = 'Show the course\'s term name in a second row of the course overview list entry'; +$string['secondrowshowtoplevelcategoryname'] = 'Second row: Show top level category name'; +$string['secondrowshowtoplevelcategoryname_desc'] = 'Show the course\'s top level category name in a second row of the course overview list entry'; +$string['semester_desc'] = 'Semester (Calendar year is divided into two terms)'; +$string['settingspage_general'] = 'General'; +$string['settingspage_courseoverviewlist'] = 'Course overview list'; +$string['settingspage_hidecourses'] = 'Hide courses'; +$string['settingspage_teacherroles'] = 'Teacher roles'; +$string['settingspage_categoryfilter'] = 'Parent category filter'; +$string['settingspage_toplevelcategoryfilter'] = 'Top level category filter'; +$string['settingspage_teacherfilter'] = 'Teacher filter'; +$string['settingspage_termfilter'] = 'Term filter'; +$string['showcourse'] = 'Show course'; +$string['stopmanaginghiddencourses'] = 'Stop managing hidden courses'; +$string['submitfilter'] = 'Filter my courses!'; +$string['teachercoursefilter'] = 'Activate teacher filter'; +$string['teachercoursefilter_desc'] = 'Allow users to filter courses by teacher'; +$string['teachercoursefilterdisplayname'] = 'Display name for teacher filter'; +$string['teachercoursefilterdisplayname_desc'] = 'This display name is shown above the teacher filter<br /><em>This setting is only processed when the teacher filter is activated</em>'; +$string['teachercoursefiltersettingheading'] = 'Teacher filter: Filter activation'; +$string['teachernamestylefullname'] = 'Firstname Lastname'; +$string['teachernamestylefirstname'] = 'Firstname'; +$string['teachernamestylelastname'] = 'Lastname'; +$string['teachernamestylefullnamedisplay'] = 'Teacher name style according to Moodle core setting "fullnamedisplay</em>" (Currently set to "{$a}")'; +$string['teacherrolessettingheading'] = 'Teacher roles'; +$string['teacherroles'] = 'Teacher roles'; +$string['teacherroles_desc'] = 'Define which roles are handled as teacher roles by this plugin<br /><em>This setting is only processed when show teacher name is activated or when the teacher filter is activated or when the priorization of courses in which I teach is activated</em>'; +$string['teacherroleshidesuspended'] = 'Hide suspended teachers'; +$string['teacherroleshidesuspended_desc'] = 'When looking for teachers with the specified teacher roles, do not only check if a teacher has one of the given roles in a course, but also check if his enrolment into the course is active (i.e. the teacher\'s enrolment in the course is not suspended + the current date is within the start and end dates of the teacher\'s enrolment + the enrolment method of the teacher\'s enrolment is enabled in the course). Teachers whose enrolment in a course is not active will not be considered as teacher for this course.<br /><em>This setting is only processed when show teacher name is activated or when the teacher filter is activated.</em><br /><em>Warning: If you enable this setting, the load on the database to create the list of courses will slightly increase due to the necessary additional checks. Thus, enable this setting only if you need to.</em>'; +$string['teacherrolesparent'] = 'Include parent context teacher roles'; +$string['teacherrolesparent_desc'] = 'When looking for teachers with the specified teacher roles, include teachers who have their role assigned in parent contexts (course category or system level)<br /><em>This setting is only processed when show teacher name is activated or when the teacher filter is activated.</em><br /><em>Warning: If you set this to "No" or "Depending on the user\'s moodle/course:reviewotherusers capability", the "Prioritize courses in which I teach" function will also be influenced and will not priorize courses where the user has his teacher role assigned in parent contexts.</em>'; +$string['teacherrolesparentcapability'] = 'Depending on the user\'s moodle/course:reviewotherusers capability'; +$string['term'] = 'Term'; +$string['term1'] = 'Term 1'; +$string['term1name'] = 'Term 1 name'; +$string['term1name_desc'] = 'Descriptive name for term 1, please rename it according to your campus terminology (or leave it empty if you want to use only year numbes in Academic year mode)<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['term1startday'] = 'Term 1 start day'; +$string['term1startday_desc'] = 'Day and month when term 1 starts<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['term2'] = 'Term 2'; +$string['term2name'] = 'Term 2 name'; +$string['term2name_desc'] = 'Descriptive name for term 2, please rename it according to your campus terminology<br /><em>This setting is only processed when the term filter is activated and when term mode is set to "Semester", "Tertial" or "Trimester"</em>'; +$string['term2startday'] = 'Term 2 start day'; +$string['term2startday_desc'] = 'Day and month when term 2 starts<br /><em>This setting is only processed when the term filter is activated and when term mode is set to "Semester", "Tertial" or "Trimester"</em>'; +$string['term3'] = 'Term 3'; +$string['term3name'] = 'Term 3 name'; +$string['term3name_desc'] = 'Descriptive name for term 3, please rename it according to your campus terminology<br /><em>This setting is only processed when the term filter is activated and when term mode is set to "Tertial" or "Trimester"</em>'; +$string['term3startday'] = 'Term 3 start day'; +$string['term3startday_desc'] = 'Day and month when term 3 starts<br /><em>This setting is only processed when the term filter is activated and when term mode is set to "Tertial" or "Trimester"</em>'; +$string['term4'] = 'Term 4'; +$string['term4name'] = 'Term 4 name'; +$string['term4name_desc'] = 'Descriptive name for term 4, please rename it according to your campus terminology<br /><em>This setting is only processed when the term filter is activated and when term mode is set to "Trimester"</em>'; +$string['term4startday'] = 'Term 4 start day'; +$string['term4startday_desc'] = 'Day and month when term 4 starts<br /><em>This setting is only processed when the term filter is activated and when term mode is set to "Trimester"</em>'; +$string['termbehavioursettingheading'] = 'Term filter: Term behaviour'; +$string['termcoursefilter'] = 'Activate term filter'; +$string['termcoursefilter_desc'] = 'Allow users to filter courses by term'; +$string['termcoursefilterdisplayname'] = 'Display name for term filter'; +$string['termcoursefilterdisplayname_desc'] = 'This display name is shown above the term filter<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['termcoursefiltersettingheading'] = 'Term filter: Filter activation'; +$string['termmode'] = 'Term mode'; +$string['termmode_desc'] = 'Set the term mode for the term filter<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['termnamesettingheading'] = 'Term filter: Term names'; +$string['termsettingerror'] = 'The configured term dates don\'t make sense. Please verify that term 2 starts after term 1 and so on. Term filter will not be available to users until you fix this.'; +$string['termsettingheading'] = 'Term filter: Term definition'; +$string['termyearpos'] = 'Term name year position'; +$string['termyearpos_desc'] = 'Define if the term\'s year should be added as suffix or prefix to the term name<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['termyearposprefixspace_desc'] = 'Year is added as prefix to the term name with space (Example: "2013 Summer term")'; +$string['termyearposprefixnospace_desc'] = 'Year is added as prefix to the term name without space (Example: "2013S")'; +$string['termyearpossuffixspace_desc'] = 'Year is added as suffix to the term name with space (Example: "Summer term 2013")'; +$string['termyearpossuffixnospace_desc'] = 'Year is added as suffix to the term name without space (Example: "S2013")'; +$string['termyearseparation'] = 'Term name second year separation'; +$string['termyearseparation_desc'] = 'If the timespan of the term includes New Year\'s day, define how this second year should be separated from the first one<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['termyearseparationhyphen_desc'] = 'Separate with a hyphen (Example: "2013-2014")'; +$string['termyearseparationslash_desc'] = 'Separate with a slash (Example: "2013/2014")'; +$string['termyearseparationunderscore_desc'] = 'Separate with a underscore (Example: "2013_2014")'; +$string['termyearseparationnosecondyear_desc'] = 'Don\'t add the second year (Example: "2013")'; +$string['tertial_desc'] = 'Tertial (Calendar year is divided into three terms)'; +$string['timelesscourses'] = 'Timeless courses'; +$string['timelesscourses_desc'] = 'Enable support for timeless courses in the term filter. Timeless courses seem to be not associated to a specific term<br /><em>This setting is only processed when the term filter is activated</em>'; +$string['timelesscoursesname'] = 'Display name for timeless courses'; +$string['timelesscoursesname_desc'] = 'This display name is shown in the term filter for courses which are timeless<br /><em>This setting is only processed when the term filter is activated and when timeless courses are activated</em>'; +$string['timelesscoursessettingheading'] = 'Term filter: Timeless courses'; +$string['timelesscoursesthreshold'] = 'Timeless courses threshold'; +$string['timelesscoursesthreshold_desc'] = 'Define courses with a start year before (and not equal to) this year as timeless courses<br /><em>This setting is only processed when the term filter is activated and when timeless courses are activated</em>'; +$string['toplevelcategory'] = 'Top level category'; +$string['toplevelcategorycoursefilter'] = 'Activate top level category filter'; +$string['toplevelcategorycoursefilter_desc'] = 'Allow users to filter courses by top level category'; +$string['toplevelcategorycoursefilterdisplayname'] = 'Display name for top level category filter'; +$string['toplevelcategorycoursefilterdisplayname_desc'] = 'This display name is shown above the top level category filter<br /><em>This setting is only processed when the top level category filter is activated</em>'; +$string['toplevelcategorycoursefiltersettingheading'] = 'Top level category filter: Filter activation'; +$string['trimester_desc'] = 'Trimester (Calendar year is divided into four terms)'; +$string['youhave'] = 'You have'; diff --git a/block/course_overview_campus/lib.php b/block/course_overview_campus/lib.php new file mode 100644 index 0000000..cd4a072 --- /dev/null +++ b/block/course_overview_campus/lib.php @@ -0,0 +1,37 @@ +<?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/>. + +/** + * Block "course overview (campus)" - library + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Get icon mapping for font-awesome. + */ +function block_course_overview_campus_get_fontawesome_icon_map() { + return [ + 'block_course_overview_campus:expanded' => 'fa-minus-square', + 'block_course_overview_campus:collapsed' => 'fa-plus-square', + 'block_course_overview_campus:hide' => 'fa-toggle-on fa-lg', + 'block_course_overview_campus:show' => 'fa-toggle-off fa-lg', + ]; +} diff --git a/block/course_overview_campus/locallib.php b/block/course_overview_campus/locallib.php new file mode 100644 index 0000000..e714265 --- /dev/null +++ b/block/course_overview_campus/locallib.php @@ -0,0 +1,448 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Local library + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// @codingStandardsIgnoreFile +// Let codechecker ignore this file. This legacy code is not fully compliant to Moodle coding style but working and well documented. + +/** + * Get my courses from DB + * + * @return array + */ +function block_course_overview_campus_get_my_courses() { + // Get my courses in alphabetical order. + $courses = enrol_get_my_courses('id, shortname', 'fullname ASC'); + + // Remove frontpage course, if enrolled, from courses list. + $site = get_site(); + if (array_key_exists($site->id, $courses)) { + unset($courses[$site->id]); + } + + return $courses; +} + + +/** + * Check if course is hidden according to the hide courses feature + * + * @param course $course + * @return boolean + */ +function block_course_overview_campus_course_hidden_by_hidecourses($course) { + // Course is visible if it isn't hidden. + if (get_user_preferences('block_course_overview_campus-hidecourse-'.$course->id, 0) == 0) { + return false; + + // Otherwise it is hidden. + } else { + return true; + } +} + + +/** + * Check if course is hidden according to the term course filter + * + * @param course $course + * @param string $selectedterm + * @return boolean + */ +function block_course_overview_campus_course_hidden_by_termcoursefilter($course, $selectedterm) { + // Course is visible if it is within selected term or all terms are selected. + if ($course->term == $selectedterm || $selectedterm == 'all') { + return false; + + // Otherwise it is hidden. + } else { + return true; + } +} + + +/** + * Check if course is hidden according to the parent category course filter + * + * @param course $course + * @param string $selectedcategory + * @return boolean + */ +function block_course_overview_campus_course_hidden_by_categorycoursefilter($course, $selectedcategory) { + // Course is visible if it is within selected parent category or all categories are selected. + if ($course->categoryid == $selectedcategory || $selectedcategory == 'all') { + return false; + + // Otherwise it is hidden. + } else { + return true; + } +} + + +/** + * Check if course is hidden according to the top level category course filter + * + * @param course $course + * @param string $selectedtoplevelcategory + * @return boolean + */ +function block_course_overview_campus_course_hidden_by_toplevelcategorycoursefilter($course, $selectedtoplevelcategory) { + // Course is visible if it is within selected top level category or all categories are selected. + if ($course->toplevelcategoryid == $selectedtoplevelcategory || $selectedtoplevelcategory == 'all') { + return false; + + // Otherwise it is hidden. + } else { + return true; + } +} + + +/** + * Check if course is visible according to the teacher course filter + * + * @param course $course + * @param string $selectedteacher + * @return boolean + */ +function block_course_overview_campus_course_hidden_by_teachercoursefilter($course, $selectedteacher) { + // Course is visible if it has the selected teacher or all teachers are selected. + if (isset($course->teachers[$selectedteacher]) || $selectedteacher == 'all') { + return false; + + // Otherwise it is hidden. + } else { + return true; + } +} + + +/** + * Check if course is hidden according to any (preprocessed!) filter + * + * @param course $course + * @return boolean + */ +function block_course_overview_campus_course_hidden_by_anyfilter($course) { + // Check if there is any reason to hide the course. + $hidecourse = (isset($course->termcoursefiltered) && $course->termcoursefiltered) || + (isset($course->categorycoursefiltered) && $course->categorycoursefiltered == true) || + (isset($course->toplevelcategorycoursefiltered) && $course->toplevelcategorycoursefiltered == true) || + (isset($course->teachercoursefiltered) && $course->teachercoursefiltered == true); + + return $hidecourse; +} + + +/** + * Check if the configured term dates make sense + * + * @return bool + */ +function block_course_overview_campus_check_term_config() { + $coc_config = get_config('block_course_overview_campus'); + + if ($coc_config->termmode == 1) { + return true; + } else if ($coc_config->termmode == 2 && + intval(date('z', strtotime('2003-'.$coc_config->term1startday))) < + intval(date('z', strtotime('2003-'.$coc_config->term2startday)))) { + return true; + } else if ($coc_config->termmode == 3 && + intval(date('z', strtotime('2003-'.$coc_config->term1startday))) < + intval(date('z', strtotime('2003-'.$coc_config->term2startday))) && + intval(date('z', strtotime('2003-'.$coc_config->term2startday))) < + intval(date('z', strtotime('2003-'.$coc_config->term3startday)))) { + return true; + } else if ($coc_config->termmode == 4 && + intval(date('z', strtotime('2003-'.$coc_config->term1startday))) < + intval(date('z', strtotime('2003-'.$coc_config->term2startday))) && + intval(date('z', strtotime('2003-'.$coc_config->term2startday))) < + intval(date('z', strtotime('2003-'.$coc_config->term3startday))) && + intval(date('z', strtotime('2003-'.$coc_config->term3startday))) < + intval(date('z', strtotime('2003-'.$coc_config->term4startday)))) { + return true; + } else { + return false; + } +} + + +/** + * Take array of teacher objects and return a string of names, sorted by relevance and name + * + * @param array $teachers array of teachers + * @return string string with concatenated teacher names + */ +function block_course_overview_campus_get_teachername_string($teachers) { + $coc_config = get_config('block_course_overview_campus'); + + // If given array is empty, return empty string. + if (empty($teachers)) { + return ''; + } + + // Sort all teachers by relevance and name, return empty string when sorting fails. + $success = usort($teachers, "block_course_overview_campus_compare_teachers"); + if (!$success) { + return ''; + } + + // Get all teachers' names as an array according the teacher name style setting. + $teachernames = array_map(function($obj) { + global $coc_config; + + // Display fullname. + if ($coc_config->secondrowshowteachernamestyle == 1) { + return $obj->firstname.' '.$obj->lastname; + } + // Display lastname. + else if ($coc_config->secondrowshowteachernamestyle == 2) { + return $obj->lastname; + } + // Display firstname. + else if ($coc_config->secondrowshowteachernamestyle == 3) { + return $obj->firstname; + } + // Display fullnamedisplay. + else if ($coc_config->secondrowshowteachernamestyle == 4) { + return fullname($obj); + } + // Fallback: Display lastname. + else { + return $obj->lastname; + } + }, $teachers); + + // Implode teachers' names to a single string. + $teachernames = implode(", ", $teachernames); + + return $teachernames; +} + + +/** + * Take term name and year(s) and return displayname for term filter based on plugin configuration + * + * @param string $termname The term's name + * @param string $year The term's year + * @param string $year2 The term's second year (optional) + * @return string String with the term's displayname + */ +function block_course_overview_campus_get_term_displayname($termname, $year, $year2='') { + $coc_config = get_config('block_course_overview_campus'); + + // Build the first year - second year combination. + $displayname = $year; + if ($year2 != '') { + // Hyphen separation. + if ($coc_config->termyearseparation == 1) { + $displayname = $year.'-'.$year2; + } + // Slash separation. + else if ($coc_config->termyearseparation == 2) { + $displayname = $year.'/'.$year2; + } + // Underscore separation. + else if ($coc_config->termyearseparation == 3) { + $displayname = $year.'_'.$year2; + } + // No second year. + else if ($coc_config->termyearseparation == 4) { + $displayname = $year; + } + // This shouldn't happen. + else { + $displayname = $year.'/'.$year2; + } + } + + // Add the term name. + // Prefix with space. + if ($coc_config->termyearpos == 1) { + $displayname = $displayname.' '.$termname; + } + // Prefix without space. + else if ($coc_config->termyearpos == 2) { + $displayname = $displayname.$termname; + } + // Suffix with space. + else if ($coc_config->termyearpos == 3) { + $displayname = $termname.' '.$displayname; + } + // Suffix without space. + else if ($coc_config->termyearpos == 4) { + $displayname = $termname.$displayname; + } + // This shouldn't happen. + else { + $displayname = $termname. ' '.$termname; + } + + return $displayname; +} + + +/** + * Compare teacher by relevance helper function + * + * @param object $a Teacher A + * @param object $b Teacher B + * @return int + */ +function block_course_overview_campus_compare_teachers($a, $b) { + // Compare relevance of teachers' roles. + if ($a->sortorder < $b->sortorder) { + return -1; + } else if ($a->sortorder > $b->sortorder) { + return 1; + } else if ($a->sortorder == $b->sortorder) { + // Teachers' roles are equal, then compare lastnames. + return strcasecmp($a->lastname, $b->lastname); + } else { + // This should never happen. + return 0; + } +} + + +/** + * Compare category by sortorder helper function + * + * @param object $a Category A + * @param object $b Category B + * @return int + */ +function block_course_overview_campus_compare_categories($a, $b) { + // Compare sortorder of categories. + if ($a->sortorder < $b->sortorder) { + return -1; + } else if ($a->sortorder > $b->sortorder) { + return 1; + } else if ($a->sortorder == $b->sortorder) { + // Category sortorders are equal - this shouldn't happen, but if it does then compare category names alphabetically. + return strcasecmp(format_string($a->name), format_string($b->name)); + } else { + // This should never happen. + return 0; + } +} + + +/** + * Remember the not shown courses for local_boostcoc + * + * Basically, this is remembered by the JavaScript filters directly when they are applied in the browser, but we want a fallback + * when javascript is off + * Unfortunately, at page load local_boostcoc can only change the nav drawer _before_ this function can store its data, thus the + * fallback when javascript is off has a lag. + * + * @param array $courses + */ +function block_course_overview_campus_remember_notshowncourses_for_local_boostcoc($courses) { + // Do only if local_boostcoc is installed. + if (block_course_overview_campus_check_local_boostcoc() == true) { + // Get all courses which are not shown (because they are hidden by any filter or by the hide courses feature) + // and store their IDs in an array. + $notshowncourses = array(); + foreach ($courses as $c) { + if ((block_course_overview_campus_course_hidden_by_anyfilter($c) == true || + block_course_overview_campus_course_hidden_by_hidecourses($c)) == true) { + $notshowncourses[] = $c->id; + } + } + + // Convert not shown courses array to JSON. + $jsonstring = json_encode($notshowncourses); + + // Store the current status of not shown courses. + set_user_preference('local_boostcoc-notshowncourses', $jsonstring); + } +} + + +/** + * Remember the active filters for local_boostcoc + * + * Basically, this is remembered by the JavaScript filters directly when they are applied in the browser, but we want a fallback + * when javascript is off. + * Unfortunately, at page load local_boostcoc can only change the nav drawer _before_ this function can store its data, thus the + * fallback when javascript is off has a lag. + * + * @param int $hiddencoursescounter + */ +function block_course_overview_campus_remember_activefilters_for_local_boostcoc($hiddencoursescounter) { + // Do only if local_boostcoc is installed. + if (block_course_overview_campus_check_local_boostcoc() == true) { + $coc_config = get_config('block_course_overview_campus'); + + // Check all filters if they are enabled and active filters (value != all) and check the fact that there are hidden courses and store them in an array. + $activefilters = array(); + if ($coc_config->termcoursefilter == true && get_user_preferences('block_course_overview_campus-selectedterm') != 'all') { + $activefilters[] = 'filterterm'; + } + if ($coc_config->categorycoursefilter == true && get_user_preferences('block_course_overview_campus-selectedcategory') != 'all') { + $activefilters[] = 'filtercategory'; + } + if ($coc_config->toplevelcategorycoursefilter == true && get_user_preferences('block_course_overview_campus-selectedtoplevelcategory') != 'all') { + $activefilters[] = 'filtertoplevelcategory'; + } + if ($coc_config->teachercoursefilter == true && get_user_preferences('block_course_overview_campus-selectedteacher') != 'all') { + $activefilters[] = 'filterteacher'; + } + if ($hiddencoursescounter > 0) { + $activefilters[] = 'hidecourses'; + } + + // Convert active filters array to JSON. + $jsonstring = json_encode($activefilters); + + // Store the current status of active filters. + set_user_preference('local_boostcoc-activefilters', $jsonstring); + } +} + + +/** + * Check if our companion plugin local_boostcoc is installed + * + * @return boolean + */ +function block_course_overview_campus_check_local_boostcoc() { + global $CFG; + + static $local_boostcoc_installed; + + if (!isset($local_boostcoc_installed)) { + if (file_exists($CFG->dirroot.'/local/boostcoc/lib.php')) { + $local_boostcoc_installed = true; + } else { + $local_boostcoc_installed = false; + } + } + + return $local_boostcoc_installed; +} diff --git a/block/course_overview_campus/pix/collapsed.png b/block/course_overview_campus/pix/collapsed.png new file mode 100644 index 0000000000000000000000000000000000000000..d971d4f15af2ddada7254e9128915bbdebe7a636 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRdCT0c(hNQXTpBNYzcmjMvTp1V`X2QUer&;G1 z7#NsKg8YIR4Bm^j@*ZJeU{LdPaSW+oOm<+>QaB{!a>J5=^Eao$BhHQ=jA{?=a2lvi e3&`bQU~ur};7q!<=obS61B0ilpUXO@geCy3m?aSa literal 0 HcmV?d00001 diff --git a/block/course_overview_campus/pix/collapsed.svg b/block/course_overview_campus/pix/collapsed.svg new file mode 100644 index 0000000..77c4295 --- /dev/null +++ b/block/course_overview_campus/pix/collapsed.svg @@ -0,0 +1,3 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/"> +]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-5 -2.1 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M.7.2C.3-.2 0 0 0 .5v10.8c0 .5.3.7.7.3l5-5c.4-.4.4-1 0-1.4l-5-5z" fill="#999"/></svg> \ No newline at end of file diff --git a/block/course_overview_campus/pix/expanded.png b/block/course_overview_campus/pix/expanded.png new file mode 100644 index 0000000000000000000000000000000000000000..fb0550a6632d6c80e4c86edef4852d271120bd65 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRdCT0c(hNQXTpBNYzcmjMvTp1V`X2QUer&;G1 z7#NsKg8YIR4Bm^j@*ZJeV9@Y%aSW+oOg_N5<NyEq0|&nS|Ifm9?C*aW35iejOsq@i hGjML>+)>KNaEgtqa+`{49|HpegQu&X%Q~loCII~-D)s;X literal 0 HcmV?d00001 diff --git a/block/course_overview_campus/pix/expanded.svg b/block/course_overview_campus/pix/expanded.svg new file mode 100644 index 0000000..2f7e463 --- /dev/null +++ b/block/course_overview_campus/pix/expanded.svg @@ -0,0 +1,3 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/"> +]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-2.1 -5 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M.5 0C0 0-.2.3.2.7l5 5c.4.4 1 .4 1.4 0l5-5c.4-.4.3-.7-.3-.7H.5z" fill="#999"/></svg> \ No newline at end of file diff --git a/block/course_overview_campus/pix/hide.png b/block/course_overview_campus/pix/hide.png new file mode 100644 index 0000000000000000000000000000000000000000..290685e8a2f0a18f3dba5e5dec7be434d97bae1e GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0y~yVBi5^7G?$ph9%LCR~Q%=R04cLTp1V`W)cFI((Vf| zFfeeJ1o;Is7&s&pH1yA3fBt^-*X{)j3=Ec@E{-7;jL8kmimn2gTdTh(v9W!->kx2Y z!rtOz?{40fD4V|JUD<u!C1uQ-3m)VxPzd34VGgn8VPJ^hk(xIr&!CQhfq}u()z4*} HQ$iB}z7#_p literal 0 HcmV?d00001 diff --git a/block/course_overview_campus/pix/hide.svg b/block/course_overview_campus/pix/hide.svg new file mode 100644 index 0000000..1b717f7 --- /dev/null +++ b/block/course_overview_campus/pix/hide.svg @@ -0,0 +1,3 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/"> +]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 -1.8 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M6 0C2.8 0 0 2.6 0 4.2s2.8 4.2 6 4.2 6-2.6 6-4.2S9.2 0 6 0zM2 4.2c.2-.4 1.4-1.6 3-2-.8.3-1.3 1.1-1.3 2 0 .9.5 1.7 1.3 2.1-1.6-.4-2.8-1.6-3-2.1zm3.1-.1c-.4 0-.8-.3-.8-.8s.3-.8.8-.8.8.3.8.8-.4.8-.8.8zM7 6.3c.8-.4 1.3-1.2 1.3-2.1 0-.9-.5-1.7-1.3-2.1 1.6.5 2.8 1.7 3 2.1-.2.5-1.4 1.7-3 2.1z" fill="#999"/></svg> \ No newline at end of file diff --git a/block/course_overview_campus/pix/show.png b/block/course_overview_campus/pix/show.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3fb4bb02b3443307203bf87d73636b1207ac64 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0y~yVBi5^7G?$ph9%LCR~Q%=R04cLTp1V`W)cFI((Vf| zFfeeJ1o;Is7&s&pH1yA3fBt^-*X{)j3=E#0E{-7_vaAO;@-`@lI2^owaW`Z32@eyA z7c1@_JaXw6=O4v<%l8ZljL~U_)~#2$x?r6*zs&N$f{f|AdOl6ua{EEwCx=a`32X=W XcdV0L_~*g*7zU83u6{1-oD!M<|2|M_ literal 0 HcmV?d00001 diff --git a/block/course_overview_campus/pix/show.svg b/block/course_overview_campus/pix/show.svg new file mode 100644 index 0000000..c794d67 --- /dev/null +++ b/block/course_overview_campus/pix/show.svg @@ -0,0 +1,3 @@ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/"> +]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M11.5 4.7c.3.5.5.9.5 1.3 0 1.6-2.8 4.2-6 4.2l5.5-5.5zM12 0v2.8L2.8 12H0l2.7-2.7C1.1 8.4 0 7 0 6c0-1.6 2.8-4.2 6-4.2 1.2 0 2.3.4 3.3.9L12 0zM7.1 3.9c.2.1.4.3.5.5l.2-.2c-.2-.1-.5-.2-.7-.3zm-2 .4c-.4 0-.8.3-.8.8s.3.8.8.8.8-.3.8-.8-.4-.8-.8-.8zm-.9 3.5l.2-.2c-.5-.4-.7-1-.7-1.6 0-.9.5-1.7 1.3-2.1-1.6.4-2.8 1.6-3 2.1.2.4 1 1.3 2.2 1.8z" fill="#999"/></svg> \ No newline at end of file diff --git a/block/course_overview_campus/settings.php b/block/course_overview_campus/settings.php new file mode 100644 index 0000000..6a0bcd4 --- /dev/null +++ b/block/course_overview_campus/settings.php @@ -0,0 +1,516 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Settings + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// @codingStandardsIgnoreFile +// Let codechecker ignore this file. This legacy code is not fully compliant to Moodle coding style but working and well documented. + +if ($hassiteconfig) { + // Empty $settings to prevent a single settings page from being created by lib/classes/plugininfo/block.php + // because we will create several settings pages now. + $settings = null; + + // Create admin settings category. + $ADMIN->add('blocksettings', new admin_category('block_course_overview_campus', + get_string('pluginname', 'block_course_overview_campus', null, true))); + + + + // Create empty settings page structure to make the site administration work on non-admin pages. + if (!$ADMIN->fulltree) { + // Settings page: General. + $settingspage = new admin_settingpage('block_course_overview_campus_general', + get_string('settingspage_general', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Course overview list. + $settingspage = new admin_settingpage('block_course_overview_campus_courseoverviewlist', + get_string('settingspage_courseoverviewlist', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Hide courses. + $settingspage = new admin_settingpage('block_course_overview_campus_hidecourses', + get_string('settingspage_hidecourses', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Teacher roles. + $settingspage = new admin_settingpage('block_course_overview_campus_teacherroles', + get_string('settingspage_teacherroles', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Parent category filter. + $settingspage = new admin_settingpage('block_course_overview_campus_categoryfilter', + get_string('settingspage_categoryfilter', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Top level category filter. + $settingspage = new admin_settingpage('block_course_overview_campus_toplevelcategoryfilter', + get_string('settingspage_toplevelcategoryfilter', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Teacher filter. + $settingspage = new admin_settingpage('block_course_overview_campus_teacherfilter', + get_string('settingspage_teacherfilter', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + + // Settings page: Term filter. + $settingspage = new admin_settingpage('block_course_overview_campus_termfilter', + get_string('settingspage_termfilter', 'block_course_overview_campus', null, true)); + $ADMIN->add('block_course_overview_campus', $settingspage); + } + + + // Create full settings page structure. + else if ($ADMIN->fulltree) { + // Include local library. + require_once(__DIR__ . '/locallib.php'); + + + // Settings page: General. + $settingspage = new admin_settingpage('block_course_overview_campus_general', + get_string('settingspage_general', 'block_course_overview_campus', null, true)); + + // Appearance. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/appearancesettingheading', + get_string('appearancesettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/blocktitle', + get_string('blocktitle', 'block_course_overview_campus', null, true), + get_string('blocktitle_desc', 'block_course_overview_campus', null, true), + get_string('blocktitledefault', 'block_course_overview_campus', null, true), + PARAM_TEXT)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Course overview list. + $settingspage = new admin_settingpage('block_course_overview_campus_courseoverviewlist', + get_string('settingspage_courseoverviewlist', 'block_course_overview_campus', null, true)); + + // Course overview list entries. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/listentriessettingheading', + get_string('listentriessettingheading', 'block_course_overview_campus', null, true), + '')); + + // Possible course name modes. + $coursenamemodes[1] = get_string('fullnamecourse', 'core', null, false); // Don't use string lazy loading here because the string will be directly used and would produce a PHP warning otherwise. + $coursenamemodes[2] = get_string('shortnamecourse', 'core', null, true); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/firstrowcoursename', + get_string('firstrowcoursename', 'block_course_overview_campus', null, true), + get_string('firstrowcoursename_desc', 'block_course_overview_campus', null, true), + $coursenamemodes[1], + $coursenamemodes)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/secondrowshowshortname', + get_string('secondrowshowshortname', 'block_course_overview_campus', null, true), + get_string('secondrowshowshortname_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/secondrowshowtermname', + get_string('secondrowshowtermname', 'block_course_overview_campus', null, true), + get_string('secondrowshowtermname_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/secondrowshowcategoryname', + get_string('secondrowshowcategoryname', 'block_course_overview_campus', null, true), + get_string('secondrowshowcategoryname_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/secondrowshowtoplevelcategoryname', + get_string('secondrowshowtoplevelcategoryname', 'block_course_overview_campus', null, true), + get_string('secondrowshowtoplevelcategoryname_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/secondrowshowteachername', + get_string('secondrowshowteachername', 'block_course_overview_campus', null, true), + get_string('secondrowshowteachername_desc', 'block_course_overview_campus', null, true), + 0)); + + // Possible teacher name styles. + $teachernamestylemodes[1] = get_string('teachernamestylefullname', 'block_course_overview_campus', null, true); + $teachernamestylemodes[2] = get_string('teachernamestylelastname', 'block_course_overview_campus', null, false); // Don't use string lazy loading here because the string will be directly used and would produce a PHP warning otherwise. + $teachernamestylemodes[3] = get_string('teachernamestylefirstname', 'block_course_overview_campus', null, true); + $teachernamestylemodes[4] = get_string('teachernamestylefullnamedisplay', 'block_course_overview_campus', get_config('core', 'fullnamedisplay'), true); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/secondrowshowteachernamestyle', + get_string('secondrowshowteachernamestyle', 'block_course_overview_campus', null, true), + get_string('secondrowshowteachernamestyle_desc', 'block_course_overview_campus', null, true), + $teachernamestylemodes[2], + $teachernamestylemodes)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/secondrowhideonphones', + get_string('secondrowhideonphones', 'block_course_overview_campus', null, true), + get_string('secondrowhideonphones_desc', 'block_course_overview_campus', null, true), + 0)); + + + // Course order. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/ordersettingheading', + get_string('ordersettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/prioritizemyteachedcourses', + get_string('prioritizemyteachedcourses', 'block_course_overview_campus', null, true), + get_string('prioritizemyteachedcourses_desc', 'block_course_overview_campus', null, true), + 0)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Hide courses. + $settingspage = new admin_settingpage('block_course_overview_campus_hidecourses', + get_string('settingspage_hidecourses', 'block_course_overview_campus', null, true)); + + // Course overview list hidden courses management. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/hidecoursessettingheading', + get_string('hidecoursessettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/enablehidecourses', + get_string('enablehidecourses', 'block_course_overview_campus', null, true), + get_string('enablehidecourses_desc', 'block_course_overview_campus', null, true), + 1)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Teacher roles. + $settingspage = new admin_settingpage('block_course_overview_campus_teacherroles', + get_string('settingspage_teacherroles', 'block_course_overview_campus', null, true)); + + // Teacher roles. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/teacherrolessettingheading', + get_string('teacherrolessettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_pickroles('block_course_overview_campus/teacherroles', + get_string('teacherroles', 'block_course_overview_campus', null, true), + get_string('teacherroles_desc', 'block_course_overview_campus', null, true), + array('editingteacher'))); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/noteachertext', + get_string('noteachertext', 'block_course_overview_campus', null, true), + get_string('noteachertext_desc', 'block_course_overview_campus', null, true), + get_string('noteacher', 'block_course_overview_campus', null, true), + PARAM_TEXT)); + + // Possible settings for parent teacher roles. + $teacherrolesparentmodes[1] = get_string('yes', 'core', null, false); // Don't use string lazy loading here because the string will be directly used and would produce a PHP warning otherwise. + $teacherrolesparentmodes[2] = get_string('no', 'core', null, true); + $teacherrolesparentmodes[3] = get_string('teacherrolesparentcapability', 'block_course_overview_campus', null, true); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/teacherrolesparent', + get_string('teacherrolesparent', 'block_course_overview_campus', null, true), + get_string('teacherrolesparent_desc', 'block_course_overview_campus', null, true), + $teacherrolesparentmodes[1], + $teacherrolesparentmodes)); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/teacherroleshidesuspended', + get_string('teacherroleshidesuspended', 'block_course_overview_campus', null, true), + get_string('teacherroleshidesuspended_desc', 'block_course_overview_campus', null, true), + 0)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Parent category filter. + $settingspage = new admin_settingpage('block_course_overview_campus_categoryfilter', + get_string('settingspage_categoryfilter', 'block_course_overview_campus', null, true)); + + // Parent category filter: Activation. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/categorycoursefiltersettingheading', + get_string('categorycoursefiltersettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/categorycoursefilter', + get_string('categorycoursefilter', 'block_course_overview_campus', null, true), + get_string('categorycoursefilter_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/categorycoursefilterdisplayname', + get_string('categorycoursefilterdisplayname', 'block_course_overview_campus', null, true), + get_string('categorycoursefilterdisplayname_desc', 'block_course_overview_campus', null, true), + get_string('category', 'block_course_overview_campus', null, true), + PARAM_TEXT)); + + + // Parent category filter: Merge homonymous categories. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/mergehomonymouscategoriessettingheading', + get_string('mergehomonymouscategoriessettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/mergehomonymouscategories', + get_string('mergehomonymouscategories', 'block_course_overview_campus', null, true), + get_string('mergehomonymouscategories_desc', 'block_course_overview_campus', null, true), + 0)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Top level category filter. + $settingspage = new admin_settingpage('block_course_overview_campus_toplevelcategoryfilter', + get_string('settingspage_toplevelcategoryfilter', 'block_course_overview_campus', null, true)); + + // Top level category filter: Activation. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/toplevelcategorycoursefiltersettingheading', + get_string('toplevelcategorycoursefiltersettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/toplevelcategorycoursefilter', + get_string('toplevelcategorycoursefilter', 'block_course_overview_campus', null, true), + get_string('toplevelcategorycoursefilter_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/toplevelcategorycoursefilterdisplayname', + get_string('toplevelcategorycoursefilterdisplayname', 'block_course_overview_campus', null, true), + get_string('toplevelcategorycoursefilterdisplayname_desc', 'block_course_overview_campus', null, true), + get_string('toplevelcategory', 'block_course_overview_campus', null, true), + PARAM_TEXT)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Teacher filter. + $settingspage = new admin_settingpage('block_course_overview_campus_teacherfilter', + get_string('settingspage_teacherfilter', 'block_course_overview_campus', null, true)); + + // Teacher filter: Activation. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/teachercoursefiltersettingheading', + get_string('teachercoursefiltersettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/teachercoursefilter', + get_string('teachercoursefilter', 'block_course_overview_campus', null, true), + get_string('teachercoursefilter_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/teachercoursefilterdisplayname', + get_string('teachercoursefilterdisplayname', 'block_course_overview_campus', null, true), + get_string('teachercoursefilterdisplayname_desc', 'block_course_overview_campus', null, true), + get_string('defaultcourseteacher'), + PARAM_TEXT)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + + + + // Settings page: Term filter. + $settingspage = new admin_settingpage('block_course_overview_campus_termfilter', + get_string('settingspage_termfilter', 'block_course_overview_campus', null, true)); + + // Term filter: Activation. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/termcoursefiltersettingheading', + get_string('termcoursefiltersettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/termcoursefilter', + get_string('termcoursefilter', 'block_course_overview_campus', null, true), + get_string('termcoursefilter_desc', 'block_course_overview_campus', null, true), + 0)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/termcoursefilterdisplayname', + get_string('termcoursefilterdisplayname', 'block_course_overview_campus', null, true), + get_string('termcoursefilterdisplayname_desc', 'block_course_overview_campus', null, true), + get_string('term', 'block_course_overview_campus', null, true), + PARAM_TEXT)); + + + // Term filter: Term definition. + // Check if the configured term dates make sense, if not show warning information. + $coc_config = get_config('block_course_overview_campus'); + if (isset($coc_config->termcoursefilter) && + $coc_config->termcoursefilter == true && + !block_course_overview_campus_check_term_config($coc_config)) { + $settingspage->add(new admin_setting_heading('block_course_overview_campus/termsettingheading', + get_string('termsettingheading', 'block_course_overview_campus'), + '<span class="errormessage">'.get_string('termsettingerror', 'block_course_overview_campus', null, true).'</span>')); + } else { + $settingspage->add(new admin_setting_heading('block_course_overview_campus/termsettingheading', + get_string('termsettingheading', 'block_course_overview_campus', null, true), '')); + } + + // Possible term modes. + $termmodes[1] = get_string('academicyear_desc', 'block_course_overview_campus', null, false); // Don't use string lazy loading here because the string will be directly used and would produce a PHP warning otherwise. + $termmodes[2] = get_string('semester_desc', 'block_course_overview_campus', null, true); + $termmodes[3] = get_string('tertial_desc', 'block_course_overview_campus', null, true); + $termmodes[4] = get_string('trimester_desc', 'block_course_overview_campus', null, true); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/termmode', + get_string('termmode', 'block_course_overview_campus', null, true), + get_string('termmode_desc', 'block_course_overview_campus', null, true), + $termmodes[1], + $termmodes)); + + + // Get all calendar days for later use. + $format = get_string('strftimedateshort', 'langconfig'); + for ($i = 1; $i <= 12; $i++) { + for ($j = 1; $j <= date('t', mktime(0, 0, 0, $i, 1, 2003)); $j++) { // Use no leap year to calculate days in month to avoid providing 29th february as an option. + // Create an intermediate timestamp with each day-month-combination and format it + // according to local date format for displaying purpose. + $daystring = userdate(gmmktime(12, 0, 0, $i, $j, 2003), $format); + + // Add the day as an option. + $days[sprintf('%02d', $i).'-'.sprintf('%02d', $j)] = $daystring; + } + } + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/term1startday', + get_string('term1startday', 'block_course_overview_campus', null, true), + get_string('term1startday_desc', 'block_course_overview_campus', null, true), + $days['01-01'], + $days)); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/term2startday', + get_string('term2startday', 'block_course_overview_campus', null, true), + get_string('term2startday_desc', 'block_course_overview_campus', null, true), + $days['01-01'], + $days)); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/term3startday', + get_string('term3startday', 'block_course_overview_campus', null, true), + get_string('term3startday_desc', 'block_course_overview_campus', null, true), + $days['01-01'], + $days)); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/term4startday', + get_string('term4startday', 'block_course_overview_campus', null, true), + get_string('term4startday_desc', 'block_course_overview_campus', null, true), + $days['01-01'], + $days)); + + + // Term filter: Term names. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/termnamesettingheading', + get_string('termnamesettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/term1name', + get_string('term1name', 'block_course_overview_campus', null, true), + get_string('term1name_desc', 'block_course_overview_campus', null, true), + get_string('term1', 'block_course_overview_campus'), + PARAM_TEXT)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/term2name', + get_string('term2name', 'block_course_overview_campus', null, true), + get_string('term2name_desc', 'block_course_overview_campus', null, true), + get_string('term2', 'block_course_overview_campus'), + PARAM_TEXT)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/term3name', + get_string('term3name', 'block_course_overview_campus', null, true), + get_string('term3name_desc', 'block_course_overview_campus', null, true), + get_string('term3', 'block_course_overview_campus'), + PARAM_TEXT)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/term4name', + get_string('term4name', 'block_course_overview_campus', null, true), + get_string('term4name_desc', 'block_course_overview_campus', null, true), + get_string('term4', 'block_course_overview_campus'), + PARAM_TEXT)); + + // Possible year positions for later use. + $termyearpos[1] = get_string('termyearposprefixspace_desc', 'block_course_overview_campus', null, true); + $termyearpos[2] = get_string('termyearposprefixnospace_desc', 'block_course_overview_campus', null, true); + $termyearpos[3] = get_string('termyearpossuffixspace_desc', 'block_course_overview_campus', null, false); // Don't use string lazy loading here because the string will be directly used and would produce a PHP warning otherwise. + $termyearpos[4] = get_string('termyearpossuffixnospace_desc', 'block_course_overview_campus', null, true); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/termyearpos', + get_string('termyearpos', 'block_course_overview_campus', null, true), + get_string('termyearpos_desc', 'block_course_overview_campus', null, true), + $termyearpos[3], + $termyearpos)); + + // Possible year separators for later use. + $termyearseparation[1] = get_string('termyearseparationhyphen_desc', 'block_course_overview_campus', null, true); + $termyearseparation[2] = get_string('termyearseparationslash_desc', 'block_course_overview_campus', null, false); // Don't use string lazy loading here because the string will be directly used and would produce a PHP warning otherwise. + $termyearseparation[3] = get_string('termyearseparationunderscore_desc', 'block_course_overview_campus', null, true); + $termyearseparation[4] = get_string('termyearseparationnosecondyear_desc', 'block_course_overview_campus', null, true); + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/termyearseparation', + get_string('termyearseparation', 'block_course_overview_campus', null, true), + get_string('termyearseparation_desc', 'block_course_overview_campus', null, true), + $termyearseparation[2], + $termyearseparation)); + + + // Term filter: Term behaviour. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/termbehavioursettingheading', + get_string('termbehavioursettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/defaultterm', + get_string('defaultterm', 'block_course_overview_campus', null, true), + get_string('defaultterm_desc', 'block_course_overview_campus', null, true), + 1)); + + + // Term filter: Timeless courses. + $settingspage->add(new admin_setting_heading('block_course_overview_campus/timelesscoursessettingheading', + get_string('timelesscoursessettingheading', 'block_course_overview_campus', null, true), + '')); + + $settingspage->add(new admin_setting_configcheckbox('block_course_overview_campus/timelesscourses', + get_string('timelesscourses', 'block_course_overview_campus', null, true), + get_string('timelesscourses_desc', 'block_course_overview_campus', null, true), + 1)); + + $settingspage->add(new admin_setting_configtext('block_course_overview_campus/timelesscoursesname', + get_string('timelesscoursesname', 'block_course_overview_campus', null, true), + get_string('timelesscoursesname_desc', 'block_course_overview_campus', null, true), + get_string('timelesscourses', 'block_course_overview_campus', null, true), + PARAM_TEXT)); + + // Get all years from 1970. + for ($i = 1971; $i <= date('Y'); $i++) { + // Add the year as an option. + $years[$i] = $i; + } + + $settingspage->add(new admin_setting_configselect('block_course_overview_campus/timelesscoursesthreshold', + get_string('timelesscoursesthreshold', 'block_course_overview_campus', null, true), + get_string('timelesscoursesthreshold_desc', 'block_course_overview_campus', null, true), + $years[date('Y') - 1], + $years)); + + // Add settings page to the admin settings category. + $ADMIN->add('block_course_overview_campus', $settingspage); + } +} diff --git a/block/course_overview_campus/styles.css b/block/course_overview_campus/styles.css new file mode 100644 index 0000000..a71c9e0 --- /dev/null +++ b/block/course_overview_campus/styles.css @@ -0,0 +1,50 @@ +/* Positioning of icons */ +.block_course_overview_campus .hidecourseicon { + float: right; + margin-left: 20px; +} + +.block_course_overview_campus a:first-of-type > .icon { + margin-left: inherit; /* Get rid of a unneeded Moodle core behaviour here */ +} + + +/* Used for hiding courses in courselist */ +.coc-hidden { + display: none; +} + + +/* Hidden courses management */ +#coc-hiddencoursesmanagement-bottom, +#coc-hiddencoursesmanagement-top { + font-size: 90%; + text-align: center; /* For some reason, the Bootstrap class text-center does not work here yet */ +} + +#coc-hiddencoursescount { + color: darkred; +} + + +/* Filter submit appearance */ +#coc-filtersubmit { + text-align: center; /* For some reason, the Bootstrap class justify-content-center does not work here yet */ +} + +.jsenabled #coc-filtersubmit { + display: none; /* Hide submit button when JS is enabled */ +} + + +/* Course list appearance */ +#coc-courselist .coursebox h3 { + margin: 0; /* Remove standard margin from h3 heading*/ + padding: 0 0 0 .5rem; /* Keep some of the left padding where the course news icon was previously */ +} + +#coc-courselist .coc-metainfo { + font-size: 85%; + font-weight: normal; /* To prevent the text from being shown in bold as it's still a h3 heading*/ + white-space: pre-wrap; /* To support multiple whitespaces before and after pipe */ +} diff --git a/block/course_overview_campus/styles_boost.css b/block/course_overview_campus/styles_boost.css new file mode 100644 index 0000000..cfdb208 --- /dev/null +++ b/block/course_overview_campus/styles_boost.css @@ -0,0 +1,25 @@ +/* Below are styles which are only needed for Boost, but are not needed for Bootstrapbase */ + +/* Boost makes h3 headings quite big, this is not desired for this block. */ +.block_course_overview_campus.block .coursebox h3 { + font-size: 1.0rem; +} + + +/* Below are styles which are needed because Boost (BS 4) makes things different from Bootstrapbase (BS 2) */ + +#coc-courselist .row, +#coc-hiddencoursesmanagement-top .row, +#coc-hiddencoursesmanagement-bottom .row { + display: block; /* Revert the flex model from BS 4 stable for the course list items and controls to circumvent redesign + of this block. This isn't nice but ok for the time being. */ +} + +.path-my .block_course_overview_campus .coursebox { + margin: 0; +} + +#coc-filterlist { + padding-left: 0; + padding-right: 0; +} diff --git a/block/course_overview_campus/styles_bootstrapbase.css b/block/course_overview_campus/styles_bootstrapbase.css new file mode 100644 index 0000000..79d00a3 --- /dev/null +++ b/block/course_overview_campus/styles_bootstrapbase.css @@ -0,0 +1,18 @@ +/* Below are styles which are only needed for Bootstrapbase, but are not needed for Boost */ + +/* Bootstrapbase makes h3 headings uppercase, this is not desired for this block. */ +.block_course_overview_campus.block .coursebox h3 { + text-transform: none; +} + + +/* Below are styles which are needed because Boost (BS 4) makes things different from Bootstrapbase (BS 2) */ + +.block_course_overview_campus .row { + margin-left: 0; +} + +.block_course_overview_campus .container-fluid { + padding-left: 0; + padding-right: 0; +} diff --git a/block/course_overview_campus/version.php b/block/course_overview_campus/version.php new file mode 100644 index 0000000..324b826 --- /dev/null +++ b/block/course_overview_campus/version.php @@ -0,0 +1,31 @@ +<?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/>. + +/** + * Block "course overview (campus)" - Version file + * + * @package block_course_overview_campus + * @copyright 2013 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'block_course_overview_campus'; +$plugin->version = 2019061200; +$plugin->release = 'v3.7-r1'; +$plugin->requires = 2019052000; +$plugin->maturity = MATURITY_STABLE; diff --git a/local/navbarplus/.travis.yml b/local/navbarplus/.travis.yml new file mode 100644 index 0000000..bcab70b --- /dev/null +++ b/local/navbarplus/.travis.yml @@ -0,0 +1,48 @@ +language: php + +addons: + postgresql: "9.6" + +services: + - mysql + - postgresql + - docker + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.2 + - 7.3 + - 7.4 + +env: + global: + - MOODLE_BRANCH=MOODLE_310_STABLE + matrix: + - DB=pgsql + - DB=mysqli + +before_install: + - phpenv config-rm xdebug.ini + - cd ../.. + - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat --dump diff --git a/local/navbarplus/CHANGES.md b/local/navbarplus/CHANGES.md new file mode 100644 index 0000000..fc95d2c --- /dev/null +++ b/local/navbarplus/CHANGES.md @@ -0,0 +1,121 @@ +moodle-local_navbarplus +======================== + +Changes +------- + +### Release v3.10-r1 + +* 2021-01-09 - Fix function call to get user tours which was renamed in 3.10 with MDL-69739 - Thanks to Leon Stringer. +* 2021-01-09 - Prepare compatibility for Moodle 3.10. +* 2021-01-06 - Change in Moodle release support: + For the time being, this plugin is maintained for the most recent LTS release of Moodle as well as the most recent major release of Moodle. + Bugfixes are backported to the LTS release. However, new features and improvements are not necessarily backported to the LTS release. +* 2021-01-06 - Improvement: Declare which major stable version of Moodle this plugin supports (see MDL-59562 for details). + +### Release v3.9-r1 + +* 2020-07-17 - Fixed broken behat step. +* 2020-07-16 - Prepare compatibility for Moodle 3.9. + +### Release v3.8-r1 + +* 2020-02-18 - Prepare compatibility for Moodle 3.8. + +### Release v3.7-r2 + +* 2020-02-13 - Adapt the placement of the icons in the navbar due to Moodle core upstream changes in MDL-67577. + PLEASE NOTE: From now on, local_navbarplus icons will be placed on the _right_ side of the Moodle + core icons and not on the _left_ side anymore. +* 2019-06-26 - Removed the optional aspect from the behat tests scenarios. + +### Release v3.7-r1 + +* 2019-06-19 - Added Behat tests. +* 2019-06-17 - Improved accessibility for the icons. +* 2019-06-17 - Prepare compatibility for Moodle 3.7. + +### Release v3.6-r3 + +* 2019-06-19 - Fixed bug that added id attribute to all other items. + +### Release v3.6-r2 + +* 2019-05-31 - Target link to FontAwesome icon list to FontAwesome 4.7.0 which is still used by Moodle core. + +### Release v3.6-r1 + +* 2019-01-16 - Check compatibility for Moodle 3.6, no functionality change. + +### Release v3.5-r2 + +* 2019-01-09 - Unified CSS classes: changed local_navbarplus_resetusertour to localnavbarplus-resetusertour. +* 2019-01-05 - Bugfix: Corrected check for the user tours setting. +* 2018-12-05 - Changed travis.yml due to upstream changes. + +### Release v3.5-r1 + +* 2018-05-24 - Changed setting description and README due to changes in referred Bootstrap classes. +* 2018-05-24 - Changed CSS selectors due to changes in Boost. IMPORTANT: Theme clean is no longer supported! +* 2018-05-24 - Check compatibility for Moodle 3.5, no functionality change. + +### Release v3.4-r3 + +* 2018-05-16 - Implement Privacy API. + +### Release v3.4-r2 + +* 2018-03-05 - Fixed Bug for openinnewwindow feature. +* 2018-02-22 - Added further information to the README.md. + +### Release v3.4-r1 + +* 2017-12-21- Check compatibility for Moodle 3.4, no functionality change. + +### Release v3.3-r2 + +* 2017-12-15 - Improved HTML structure for the icons. +* 2017-12-05 - Added Workaround to travis.yml for fixing Behat tests with TravisCI. + +### Release v3.3-r1 + +* 2017-11-23 - Removed support for Moodle pix icons. +* 2017-11-23 - Small fix to prevent icons with target blank from being shown when they do not match the language setting. +* 2017-11-23 - Check compatibility for Moodle 3.3, no functionality change. +* 2017-11-08 - Updated travis.yml to use newer node version for fixing TravisCI error. + +### Release v3.2-r8 + +* 2017-10-05 - Fixed undefined property notice bug caused by the additional id parameter. + +### Release v3.2-r7 + +* 2017-10-04 - Added possibility to add an individual element id. +* 2017-10-04 - Added possibility to add individual CSS classes. +* 2017-09-18 - Added a hint to the README.md how to individually set another icon for the reset user tour feature. + +### Release v3.2-r6 + +* 2017-09-14 - Fix to prevent adding empty containers if elements shall not be displayed. + +### Release v3.2-r5 + +* 2017-08-11 - Fixed string in lang package. + +### Release v3.2-r4 + +* 2017-08-11 - Setting to place an icon in the navbar for users to restart user tours of the current page. + +### Release v3.2-r3 + +* 2017-06-26 - Added possibility to add language support and new window attribute. +* 2017-06-17 - Add Travis CI support + +### Release v3.2-r2 + +* 2017-06-02 - Make codechecker happy. + +### Release v3.2-r1 + +* 2017-05-12 - Added support for Clean theme. +* 2017-05-10 - Initial version. diff --git a/local/navbarplus/COPYING.txt b/local/navbarplus/COPYING.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/local/navbarplus/COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/local/navbarplus/README.md b/local/navbarplus/README.md new file mode 100644 index 0000000..2e246a4 --- /dev/null +++ b/local/navbarplus/README.md @@ -0,0 +1,202 @@ +moodle-local_navbarplus +======================== + +[](https://travis-ci.com/moodleuulm/moodle-local_navbarplus) + +Moodle plugin which enhances the functionality of Moodle's page header navbar. + + +Requirements +------------ + +This plugin requires Moodle 3.10+ + + +Motivation for this plugin +-------------------------- + +In Moodle, admins can easily add own menu items to the custom menu. However, this custom menu will be hidden from the page header navbar on small screens due to the responsiveness as there is not enough space for this section. Depending on the theme base, the custom menu items will be gone (in theme Clean) or will occur in the footer as plain links (in theme Boost). + +So we came up with the idea for this plugin to be able to add additional content to the page header navbar going beyond the possibilities of the existing custom menu. + +Installation +------------ + +Install the plugin like any other plugin to folder +/local/navbarplus + +See http://docs.moodle.org/en/Installing_plugins for details on installing Moodle plugins + + +Usage & Settings +---------------- + +After installing the plugin, it does not do anything to Moodle yet. + +To configure the plugin and its behaviour, please visit: +Site administration -> Appearance -> Navbar Plus. + +There, you find two settings: + +### 1. Icons with links + +With this setting you can add link icons to the header navbar left to the icons "messages" and "notifications". +Each line consists of an icon image, a link URL, a text, supported language(s) (optional) and new window setting (optional) - separated by pipe characters. Each icon needs to be written in a new line. For example: + +``` +fa-question|http://moodle.org|Moodle|en,de|true|hidden-small-down +fa-sign-out|/login/logout.php|Logout||false +``` + +Further information to the parameters: +* Image: You can add Font Awesome icon identifiers (<a href="http://fontawesome.io/icons/">See the icon list on fontawesome.io</a>). Font Awesome is included in Moodle's core Clean and Boost themes since the version 3.3. +* Link: The link target can be defined by a full web URL (e.g. https://moodle.org) or a relative path within your Moodle instance (e.g. /login/logout.php). +* Title: This text will be written in the title and alt attributes of the icon. +* Supported language(s) (optional): This setting can be used for displaying the link to users of the specified language only. Separate more than one supported language with commas. If the link should be displayed in all languages, then leave this field empty. +* New window (optional): By default the link will be opened in the same window and the value of this setting is set to false. If you want to open the link in a new window set the value to true. +* Additional classes (optional): You can add individual classes with this optional parameter. A common use case might be to add Bootstrap's responsive classes to hide an icon for specific display sizes. <br/> You can look up the definitions for the responsive Bootstrap display classes for <a href="https://getbootstrap.com/docs/4.0/utilities/display/">Bootstrap version 4</a> for all Boost based themes. +The most important classes for Boost based themes might be "d-none d-sm-block" for hiding an icon on small devices or "d-sm-none" for only displaying the icon on small screens. +* ID (optional): You can add an individual ID to your icon element. This makes it possible to address this specific icon easily with CSS (for example for the Moodle user tours). The string you enter here will always be prefixed with "localnavbarplus-". + +Please note: +* Pipe dividing for optional parameters is always needed if they are located between other options. This means that you have to separate params with the pipe character although they are empty. Also see the example for the Font Awesome icon above. +* If the icon does not show up in the navbar, please check if all mandatory params are set correctly and if the optional language setting fits to your current Moodle user language. + +### 2. Reset user tour link + +With this setting you can place a Font Awesome map icon in the navbar with which the user is able to restart the user tour for the current page. By default Boost places the link to reset the user tour within the footer. This might not be eye catching. With this setting you can place the link to the more visible navbar. + +Please note: + +If you want to change this icon, you can do this within your own Custom CSS / RAW SCSS section of your theme. This is the CSS code you need: +``` +#localnavbarplus-resetusertour i.fa::before { + content: "\f11d"; +} +``` +Please replace this example "content" code with your desired Font Awesome icon's unicode. + +If you want to hide the footer link to reset the user tour, you can add the following code to your Raw SCSS setting: +``` +#page-footer .tool_usertours-resettourcontainer { + display: none; +} +``` +The theme <a href ="https://moodle.org/plugins/theme_boost_campus">Boost Campus</a> implements a own setting to hide the standard link to reset the user tour. + + +How this plugin works / Pitfalls +-------------------------------- + +The functionality of this plugin is simply achieved by using the *_render_navbar_output() hook which allows plugins to add HTML code to the page header navbar. + +The purpose of the plugin is to place _only few_ important icons with links in the page header navbar. If a larger number of icons will be placed, the icons will be wrapped beneath the navbar on small screens. So please test this behavior in the browser when adding content to this setting by shrinking the browser window to a width equivalent to a small screen device. If the icon link container is wrapped beneath the navbar, then please consider using less icons. + + +Icon colors +----------- + +The icons will be added to the navbar with the default Moodle icon color. You can change this either in your own CSS file or in the custom CSS or the Raw SCSS section in your theme. + +Example for changing the color of the Font Awesome icon to white: +``` +header.navbar .localnavbarplus i.fa::before { + color: #fff; +} +``` + + +Icon sizes +----------- + +The icons inherit the default Moodle icon size. Unfortunately, not all Font Awesome icons are equal in their size, so the size of the added icons can vary in size from the existing Moodle icons. You can change the font size of the icons that differ in their size in your own CSS file or in the custom CSS or the Raw SCSS section in your theme. + +Example for increasing the font size for the logout icon used in the example above: +``` +header.navbar .localnavbarplus .fa-sign-out { + font-size: 19px; +} +``` + + +Theme support +------------- + +This plugin is developed and tested on Moodle Core's Boost theme. +It should also work with Boost child themes, including Moodle Core's Classic theme. However, we can't support any other theme than Boost. + + +Plugin repositories +------------------- + +This plugin is published and regularly updated in the Moodle plugins repository: +http://moodle.org/plugins/view/local_navbarplus + +The latest development version can be found on Github: +https://github.com/moodleuulm/moodle-local_navbarplus + + +Bug and problem reports / Support requests +------------------------------------------ + +This plugin is carefully developed and thoroughly tested, but bugs and problems can always appear. + +Please report bugs and problems on Github: +https://github.com/moodleuulm/moodle-local_navbarplus/issues + +We will do our best to solve your problems, but please note that due to limited resources we can't always provide per-case support. + + +Feature proposals +----------------- + +Due to limited resources, the functionality of this plugin is primarily implemented for our own local needs and published as-is to the community. We are aware that members of the community will have other needs and would love to see them solved by this plugin. + +Please issue feature proposals on Github: +https://github.com/moodleuulm/moodle-local_navbarplus/issues + +Please create pull requests on Github: +https://github.com/moodleuulm/moodle-local_navbarplus/pulls + +We are always interested to read about your feature proposals or even get a pull request from you, but please accept that we can handle your issues only as feature _proposals_ and not as feature _requests_. + + +Moodle release support +---------------------- + +Due to limited resources, this plugin is only maintained for the most recent major release of Moodle as well as the most recent LTS release of Moodle. Bugfixes are backported to the LTS release. However, new features and improvements are not necessarily backported to the LTS release. + +Apart from these maintained releases, previous versions of this plugin which work in legacy major releases of Moodle are still available as-is without any further updates in the Moodle Plugins repository. + +There may be several weeks after a new major release of Moodle has been published until we can do a compatibility check and fix problems if necessary. If you encounter problems with a new major release of Moodle - or can confirm that this plugin still works with a new major release - please let us know on Github. + +If you are running a legacy version of Moodle, but want or need to run the latest version of this plugin, you can get the latest version of the plugin, remove the line starting with $plugin->requires from version.php and use this latest plugin version then on your legacy Moodle. However, please note that you will run this setup completely at your own risk. We can't support this approach in any way and there is an undeniable risk for erratic behavior. + + +Translating this plugin +----------------------- + +This Moodle plugin is shipped with an english language pack only. All translations into other languages must be managed through AMOS (https://lang.moodle.org) by what they will become part of Moodle's official language pack. + +As the plugin creator, we manage the translation into german for our own local needs on AMOS. Please contribute your translation into all other languages in AMOS where they will be reviewed by the official language pack maintainers for Moodle. + + +Right-to-left support +--------------------- + +This plugin has not been tested with Moodle's support for right-to-left (RTL) languages. +If you want to use this plugin with a RTL language and it doesn't work as-is, you are free to send us a pull request on Github with modifications. + + +PHP7 Support +------------ + +Since Moodle 3.4 core, PHP7 is mandatory. We are developing and testing this plugin for PHP7 only. + + +Copyright +--------- + +Ulm University +Communication and Information Centre (kiz) +Kathrin Osswald diff --git a/local/navbarplus/classes/privacy/provider.php b/local/navbarplus/classes/privacy/provider.php new file mode 100644 index 0000000..c0d2d76 --- /dev/null +++ b/local/navbarplus/classes/privacy/provider.php @@ -0,0 +1,47 @@ +<?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/>. + +/** + * Local plugin "Navbar Plus" - Privacy provider + * + * @package local_navbarplus + * @copyright 2018 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_navbarplus\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementing null_provider. + * + * @package local_navbarplus + * @copyright 2018 Alexander Bias, Ulm University <alexander.bias@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/local/navbarplus/lang/en/local_navbarplus.php b/local/navbarplus/lang/en/local_navbarplus.php new file mode 100644 index 0000000..b74795c --- /dev/null +++ b/local/navbarplus/lang/en/local_navbarplus.php @@ -0,0 +1,55 @@ +<?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/>. + +/** + * Local plugin "Navbar Plus" - Language pack + * + * @package local_navbarplus + * @copyright 2017 Kathrin Osswald, Ulm University <kathrin.osswald@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Navbar Plus'; +$string['privacy:metadata'] = 'The Navbar Plus plugin provides extended functionality to Moodle users, but does not store any personal data.'; +// Setting to insert icons with links. +$string['setting_inserticonswithlinks'] = 'Icons with links'; +$string['setting_inserticonswithlinks_desc'] = 'With this setting you can add link icons to the header navbar left to the icons "messages" and "notifications".<br/> +Each line consists of an icon image, a link URL, a text, supported language(s) (optional) and new window setting (optional) - separated by pipe characters. Each icon needs to be written in a new line.<br/> +For example:<br/> +fa-question|http://moodle.org|Moodle|en,de|true|d-none d-sm-block<br/> +fa-sign-out|/login/logout.php|Logout||false<br/><br/> +Further information to the parameters: +<ul> +<li><b>Image:</b> You can add Font Awesome icon identifiers (<a href="https://fontawesome.com/v4.7.0/icons/">See the icon list on fontawesome.com</a>). Font Awesome is included in Moodle\'s core Clean and Boost themes since the version 3.3.</li> +<li><b>Link:</b> The link target can be defined by a full web URL (e.g. https://moodle.org) or a relative path within your Moodle instance (e.g. /login/logout.php). </li> +<li><b>Title:</b> This text will be written in the title and alt attributes of the icon.</li> +<li><b>Supported language(s) (optional):</b> This setting can be used for displaying the link to users of the specified language only. Separate more than one supported language with commas. If the link should be displayed in all languages, then leave this field empty.</li> +<li><b>New window (optional)</b>: By default the link will be opened in the same window and the value of this setting is set to false. If you want to open the link in a new window set the value to true.</li> +<li><b>Additional classes (optional)</b>: You can add individual classes with this optional parameter. A common use case might be to add Bootstrap\'s responsive classes to hide an icon for specific display sizes. <br/> You can look up the definitions for the responsive Bootstrap display classes for <a href="https://getbootstrap.com/docs/4.0/utilities/display/">Bootstrap version 4</a> for all Boost based themes.<br/> +The most important classes for Boost based themes might be "d-none d-sm-block" for hiding an icon on small devices or "d-sm-none" for only displaying the icon on small screens. +<li><b>ID (optional)</b>: You can add an individual ID to your icon element. This makes it possible to address this specific icon easily with CSS (for example for the Moodle user tours). The string you enter here will always be prefixed with "localnavbarplus-".</li> +</ul> +Please note: +<ul> +<li> Pipe dividing for optional parameters is always needed if they are located between other options. This means that you have to separate params with the pipe character although they are empty. Also see the example for the Font Awesome icon above. </li> +<li> If the icon does not show up in the navbar, please check if all mandatory params are set correctly and if the optional language setting fits to your current Moodle user language. </li> +</ul>'; +// Setting to place a link to be able to reset user tours. +$string['setting_resetusertours'] = 'Reset user tour link'; +$string['setting_resetusertours_desc'] = 'With this setting you can place a Font Awesome map icon in the navbar with which the user is able to restart the user tour for the current page. By default Boost places the link to reset the user tour within the footer. This might not be eye catching. With this setting you can place the link to the more visible navbar.<br/> If you want to change this icon, please have a look at the README.md file.'; +$string['resetusertours_hint'] = '(Could take a short time)'; diff --git a/local/navbarplus/lib.php b/local/navbarplus/lib.php new file mode 100644 index 0000000..3aaa08f --- /dev/null +++ b/local/navbarplus/lib.php @@ -0,0 +1,184 @@ +<?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/>. + +/** + * Local plugin "Navbar Plus" - Library + * + * @package local_navbarplus + * @copyright 2017 Kathrin Osswald, Ulm University <kathrin.osswald@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Allow plugins to provide some content to be rendered in the navbar. + * The plugin must define a PLUGIN_render_navbar_output function that returns + * the HTML they wish to add to the navbar. + * + * @return string HTML for the navbar + */ +function local_navbarplus_render_navbar_output() { + global $OUTPUT; + + // Fetch overall config. + $config = get_config('local_navbarplus'); + // Initialize output. + $output = ''; + + // Make a new array on delimiter "new line". + if (isset($config->inserticonswithlinks)) { + // Get the lines from the config. + $lines = explode("\n", $config->inserticonswithlinks); + + // Parse item settings. + foreach ($lines as $line) { + $line = trim($line); + if (strlen($line) == 0) { + continue; + } + + $itemicon = null; + $iconfaidentifier = null; + $itemurl = null; + $itemtitle = null; + $itemvisible = false; + $itemopeninnewwindow = false; + $itemadditionalclasses = null; + $itemid = null; + + // Make a new array on delimiter "|". + $settings = explode('|', $line); + // Check for the mandatory conditions first. + // If array contains too less or too many settings, do not proceed and therefore do not display the item. + // Furthermore check it at least the first three mandatory params are not an empty string. + if (count($settings) >= 3 && count($settings) <= 7 && + $settings[0] !== '' && $settings[1] !== '' && $settings[2] !== '') { + foreach ($settings as $i => $setting) { + $setting = trim($setting); + if (!empty($setting)) { + switch ($i) { + // Check for the mandatory first param: icon. + case 0: + $faiconpattern = '~^fa-[\w\d-]+$~'; + // Check if it's matching the Font Awesome pattern. + if (preg_match($faiconpattern, $setting) > 0) { + $iconfaidentifier = $setting; + $itemvisible = true; + } + break; + // Check for the mandatory second param: URL. + case 1: + // Get the URL. + try { + $itemurl = new moodle_url($setting); + $itemvisible = true; + } catch (moodle_exception $exception) { + // We're not actually worried about this, we don't want to mess up the display + // just for a wrongly entered URL. We just hide the icon in this case. + $itemurl = null; + $itemvisible = false; + } + break; + // Check for the mandatory third param: text for title and alt attribute. + case 2: + $itemtitle = $setting; + $itemvisible = true; + break; + // Check for the optional fourth param: language support. + case 3: + // Only proceed if something is entered here. This parameter is optional. + // If no language is given the icon will be displayed in the navbar by default. + $itemlanguages = array_map('trim', explode(',', $setting)); + $itemvisible &= in_array(current_language(), $itemlanguages); + break; + // Check for the optional fifth param: the target attribute. + case 4: + // Only set this value if the item is set to visible so far. + // Especially to keep the language check. + if ($setting == 'true' && $itemvisible == true) { + $itemopeninnewwindow = true; + } + break; + // Check for optional sixth parameter: additional classes. + case 5: + $itemadditionalclasses = $setting; + break; + // Check for optional seventh parameter: additional id. + case 6: + $itemid = $setting; + break; + } + } + } + } + // Add link with icon as a child to the surrounding div only if it should be displayed. + // This is if all mandatory params are set and the item matches the optional given language setting. + if ($itemvisible) { + // To address accessibility, we need to define the icon here because the title from the next pipe is needed. + $itemicon = '<i class="icon fa ' . $iconfaidentifier . ' fa-fw" aria-label="' . $itemtitle . '"></i>'; + // Set attributes for title and alt. + $linkattributes = array('title' => $itemtitle); + // If optional param for itemopeninnewwindow is set to true add a target=_blank to the link. + if ($itemopeninnewwindow) { + $linkattributes['target'] = '_blank'; + } + // Define classes for all icons. + $itemclasses = 'localnavbarplus nav-link'; + // Add optional individual classes. + if (!empty($itemadditionalclasses)) { + $itemclasses .= ' ' . $itemadditionalclasses; + } + // Initialise attribute array for the div tag. + $divattributes = []; + $divattributes['class'] = $itemclasses; + // Add optional individual id prefixed with plugin name. + if (!empty($itemid)) { + $divattributes['id'] = 'localnavbarplus-' . $itemid; + } + // Add the link to the HTML. + $output .= html_writer::start_tag('div', $divattributes); + $output .= html_writer::link($itemurl, $itemicon, $linkattributes); + $output .= html_writer::end_tag('div'); + } + } + } + // If setting resetuseertours is enabled. + if (isset($config->resetusertours) && $config->resetusertours == true) { + if (isloggedin() || !isguestuser()) { + // Get the tour for the current page. + $tour = \tool_usertours\manager::get_current_tours(); + if (!empty($tour)) { + // Open div. + $output .= html_writer::start_tag('div', array('class' => 'localnavbarplus nav-link', + 'id' => 'localnavbarplus-resetusertour')); + // Use the Font Awesome icon "map". + $itemicon = '<i class="icon fa fa-map fa-fw"></i>'; + // Use the string for resetting the tour. + $resetstring = get_string('resettouronpage', 'tool_usertours'); + $resethint = get_string('resetusertours_hint', 'local_navbarplus'); + // Set this as the alt and title attribute and set the data action for resetting the tour. + $attributes = array('alt' => $resetstring, 'title' => $resetstring . ' ' . $resethint, + 'data-action' => 'tool_usertours/resetpagetour'); + // Add the link to the HTML. + $output .= html_writer::link('#', $itemicon, $attributes); + // Close div. + $output .= html_writer::end_tag('div'); + } + } + } + return $output; +} diff --git a/local/navbarplus/settings.php b/local/navbarplus/settings.php new file mode 100644 index 0000000..4922c93 --- /dev/null +++ b/local/navbarplus/settings.php @@ -0,0 +1,52 @@ +<?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/>. + +/** + * Local plugin "Navbar Plus" - Settings + * + * @package local_navbarplus + * @copyright 2017 Kathrin Osswald, Ulm University <kathrin.osswald@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/lib.php'); + +if ($hassiteconfig) { + // New settings page. + $page = new admin_settingpage('local_navbarplus', + get_string('pluginname', 'local_navbarplus', null, true)); + + if ($ADMIN->fulltree) { + // Create insert icons with links widget. + $setting = new admin_setting_configtextarea('local_navbarplus/inserticonswithlinks', + get_string('setting_inserticonswithlinks', 'local_navbarplus', null, true), + get_string('setting_inserticonswithlinks_desc', 'local_navbarplus', null, true), '', PARAM_RAW); + $page->add($setting); + + // Setting for adding a link to reset the user tours in the navbar. + $name = 'local_navbarplus/resetusertours'; + $title = get_string('setting_resetusertours', 'local_navbarplus', null, true); + $description = get_string('setting_resetusertours_desc', 'local_navbarplus', null, true); + $setting = new admin_setting_configcheckbox($name, $title, $description, 0); + + $page->add($setting); + } + + // Add settings page to the appearance settings category. + $ADMIN->add('appearance', $page); +} diff --git a/local/navbarplus/styles.css b/local/navbarplus/styles.css new file mode 100644 index 0000000..3a2c231 --- /dev/null +++ b/local/navbarplus/styles.css @@ -0,0 +1,23 @@ +/* Float the icons left to be placed next to the existing icons "messages" and "notifications". */ +.navbar .localnavbarplus.nav-link { + float: left; +} + +/* Remove the right margin from each but the last icon to achieve the same margins for moodle and navbarplus icons. */ +.navbar .localnavbarplus.nav-link .icon { + margin-right: 0; +} +.navbar .localnavbarplus.nav-link:last-child .icon { + margin-right: .5rem; +} + +/* Do not display the icon if a user has disabled Javascript in his browser. */ +body:not(.jsenabled) #localnavbarplus-resetusertour { + display: none; +} + +/* Inherit the colors. */ +.navbar .localnavbarplus a, +.navbar .localnavbarplus .icon { + color: inherit; +} diff --git a/local/navbarplus/tests/behat/behat_local_navbarplus.php b/local/navbarplus/tests/behat/behat_local_navbarplus.php new file mode 100644 index 0000000..ffa6884 --- /dev/null +++ b/local/navbarplus/tests/behat/behat_local_navbarplus.php @@ -0,0 +1,209 @@ +<?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/>. + +/** + * Steps definitions for local_navbarplus + * + * This script is only called from Behat as part of it's integration + * in Moodle. + * + * @package local_navbarplus + * @category test + * @copyright 2019 Kathrin Osswald <kathrin.osswald@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + + +/** + * Steps definitions for local_navbarplus + * + * This script is only called from Behat as part of it's integration + * in Moodle. + * + * @package local_navbarplus + * @category test + * @copyright 2019 Kathrin Osswald <kathrin.osswald@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_local_navbarplus extends behat_base { + /** + * Checks, that the specified element with this title, link and iconclass attribute is existent on the page. + * + * @codingStandardsIgnoreLine + * @Then /^I should see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" and the iconclass "(?P<icon_string>(?:[^"]|\\")*)" and the link "(?P<link_string>(?:[^"]|\\")*)" in the navbar$/ + * + * @param string $title + * @param string $icon + * @param string $link + */ + public function assert_element_in_navbar_contains_title_iconclass_link($title, $icon, $link) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"][contains(@href, "' . $link . '")]'; + $elementxpath .= '/descendant::i[contains(@class, "' . $icon . '")]'; + + // Check if the element exists. + $this->execute("behat_general::should_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element with this title, link and iconclass attribute is not existent on the page. + * + * @codingStandardsIgnoreLine + * @Then /^I should not see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" and the iconclass "(?P<icon_string>(?:[^"]|\\")*)" and the link "(?P<link_string>(?:[^"]|\\")*)" in the navbar$/ + * + * @param string $title + * @param string $icon + * @param string $link + */ + public function assert_element_in_navbar_not_contains_title_iconclass_link($title, $icon, $link) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"][contains(@href, "' . $link . '")]'; + $elementxpath .= '/descendant::i[contains(@class, "' . $icon . '")]'; + + // Check if the element does not exist. + $this->execute("behat_general::should_not_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element with this title attribute is existent on the page. + * + * @Then /^I should see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" in the navbar$/ + * + * @param string $title + */ + public function assert_element_in_navbar_contains_title($title) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"]'; + + // Check if the element exists. + $this->execute("behat_general::should_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element with this title attribute is not existent on the page. + * + * @Then /^I should not see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" in the navbar$/ + * + * @param string $title + */ + public function assert_element_in_navbar_not_contains_title($title) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"]'; + + // Check if the element does not exist. + $this->execute("behat_general::should_not_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element is existent and has new window attribute. + * + * @Then /^I should see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" with the new window option in the navbar$/ + * + * @param string $title + */ + public function assert_element_in_navbar_has_new_window_attribute($title) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"][contains(@target, "_blank")]'; + + // Check if the element exists. + $this->execute("behat_general::should_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element is existent and has new window attribute. + * + * @Then /^I should see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" without the new window option in the navbar$/ + * + * @param string $title + */ + public function assert_element_in_navbar_not_has_new_window_attribute($title) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"][contains(@target, "_blank")]'; + + // Check if the element exists. + $this->execute("behat_general::should_not_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element is existent and has additional classes attribute. + * + * @codingStandardsIgnoreLine + * @Then /^I should see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" and the class "(?P<class_string>(?:[^"]|\\")*)" in the navbar$/ + * + * @param string $title + * @param string $class + */ + public function assert_element_in_navbar_has_additional_class($title, $class) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant::div[contains(@class, "' . $class . '")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"]'; + + // Check if the element exists. + $this->execute("behat_general::should_exist", + array($elementxpath, "xpath_element")); + } + + /** + * Checks, that the specified element is existent and has additional id attribute. + * + * @codingStandardsIgnoreLine + * @Then /^I should see the icon with the title "(?P<title_string>(?:[^"]|\\")*)" and the id "(?P<id_string>(?:[^"]|\\")*)" in the navbar$/ + * + * @param string $title + * @param string $id + */ + public function assert_element_in_navbar_has_additional_id($title, $id) { + + // We are searching for our icons in the navbar. + $elementxpath = '//ul[contains(@class, "navbar-nav")]'; + $elementxpath .= '/li[contains(@class, "nav-item")]'; + $elementxpath .= '/descendant::div[contains(@id, "' . $id . '")]'; + $elementxpath .= '/descendant-or-self::a[@title="'. $title . '"]'; + + // Check if the element exists. + $this->execute("behat_general::should_exist", + array($elementxpath, "xpath_element")); + } +} diff --git a/local/navbarplus/tests/behat/local_navbarplus.feature b/local/navbarplus/tests/behat/local_navbarplus.feature new file mode 100644 index 0000000..1fda0b2 --- /dev/null +++ b/local/navbarplus/tests/behat/local_navbarplus.feature @@ -0,0 +1,104 @@ +@local @local_navbarplus + # Please note: + # The short notation for the settings like + # Given the following config values are set as admin: + # | config | value | plugin | + # | inserticonswithlinks | fa-sign-out|/login/logout.php|Logout | local_navbarplus | + # does not work here, as the value contains pipe characters and so the table does not have same number of columns in every row. + # Escaping the pipes with backslash helped, but then the tests failed because the value is not usable anymore. + # The short notation for the settings like + # Given the following "users" exist: + # | username | lang | + # | student1 | de | + # does not work since Moodle 3.9 anymore, so the language has to be set manually. +Feature: Configuring the navbarplus plugin + In order to have custom items in the additional navbar + As admin + I need to be able to configure the navbarplus plugin + + Scenario: Configuring item with mandatory attributes + When I log in as "admin" + And I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-sign-out|/login/logout.php|Logout" + And I press "Save" + Then I should see the icon with the title "Logout" and the iconclass "fa-sign-out" and the link "/login/logout.php" in the navbar + + Scenario: Configuring item with less than mandatory attributes + When I log in as "admin" + When I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "Falsetest" + And I press "Save" + And I should not see the icon with the title "Falsetest" in the navbar + + Scenario: Configuring item with additional language attribute + Given the following "users" exist: + | username | + | student1 | + When I log in as "admin" + And I navigate to "Language > Language packs" in site administration + And I set the field "Available language packs" to "de" + And I press "Install selected language pack(s)" + When I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-language|/?redirect=0|Languagetest|de" + And I press "Save" + Then I should not see the icon with the title "Languagetest" and the iconclass "fa-language" and the link "/?redirect=0" in the navbar + And I log out + When I log in as "student1" + And I follow "Preferences" in the user menu + And I click on "Preferred language" "link" + And I set the field "Preferred language" to "Deutsch (de)" + And I press "Save changes" + Then I should see the icon with the title "Languagetest" and the iconclass "fa-language" and the link "/?redirect=0" in the navbar + + Scenario: Configuring item with the new window attribute + When I log in as "admin" + And I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-window-maximize|/?redirect=0|Newwindowtest||true" + And I press "Save" + Then I should see the icon with the title "Newwindowtest" with the new window option in the navbar + + Scenario: Configuring item with mandatory attributes and check that they don't have the new window attribute + When I log in as "admin" + When I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-sign-in|/login/logout.php|Login" + And I press "Save" + And I should see the icon with the title "Login" without the new window option in the navbar + + Scenario: Configuring item with additional class attribute + When I log in as "admin" + And I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-glass|/?redirect=0|Classtest|||optional-class" + And I press "Save" + Then I should see the icon with the title "Classtest" and the class "optional-class" in the navbar + + Scenario: Configuring item with additional id attribute + When I log in as "admin" + And I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-id-card|/?redirect=0|Idtest||||optional-id" + And I press "Save" + Then I should see the icon with the title "Idtest" and the id "optional-id" in the navbar + + Scenario: Verifying the icon position + When I log in as "admin" + When I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "id_s_local_navbarplus_inserticonswithlinks" to "fa-sign-out|/login/logout.php|Logout" + And I press "Save" + Then "div.localnavbarplus.nav-link" "css_element" should appear after "div[data-region='popover-region-messages']" "css_element" + + Scenario: Enabling the link to show the reset users tour link in the navbar if a user tour is created for that page + When I log in as "admin" + And I navigate to "Appearance > Navbar Plus" in site administration + And I set the field "Reset user tour link" to "Yes" + And I press "Save" + And I add a new user tour with: + | Name | First tour | + | Description | My first tour | + | Apply to URL match | /my/% | + | Tour is enabled | 1 | + And I add steps to the "First tour" tour: + | targettype | Title | Content | + | Display in middle of page | Welcome | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful | + When I am on homepage + Then I should see the icon with the title "Reset user tour on this page (Could take a short time)" in the navbar + When I am on site homepage + Then I should not see the icon with the title "Reset user tour on this page (Could take a short time)" in the navbar diff --git a/local/navbarplus/version.php b/local/navbarplus/version.php new file mode 100644 index 0000000..891d6cb --- /dev/null +++ b/local/navbarplus/version.php @@ -0,0 +1,32 @@ +<?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/>. + +/** + * Local plugin "Navbar Plus" - Version file + * + * @package local_navbarplus + * @copyright 2017 Kathrin Osswald, Ulm University <kathrin.osswald@uni-ulm.de> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'local_navbarplus'; +$plugin->version = 2021010900; +$plugin->release = 'v3.10-r1'; +$plugin->requires = 2020110900; +$plugin->supported = [310, 310]; +$plugin->maturity = MATURITY_STABLE; diff --git a/mod/attendance/.github/workflows/ci.yml b/mod/attendance/.github/workflows/ci.yml new file mode 100644 index 0000000..ea18f91 --- /dev/null +++ b/mod/attendance/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: Run all tests + +# Run this workflow every time a new commit pushed to your repository +on: push + +jobs: + setup: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:9.6 + env: + POSTGRES_USER: 'postgres' + POSTGRES_HOST_AUTH_METHOD: 'trust' + # Health check to wait for postgres to start. + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + mariadb: + image: mariadb:10 + env: + MYSQL_USER: 'root' + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 + strategy: + fail-fast: false + matrix: + php-versions: ['7.3', '7.4'] + database: ['pgsql', 'mariadb'] + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + # Clone in plugin subdir, so we can setup CI in default directory. + path: plugin + + - name: Install node + uses: actions/setup-node@v1 + with: + # TODO: Check if we can support .nvmrc + node-version: '14.15.0' + + - name: Setup PHP environment + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, pgsql, mysqli + tools: phpunit + + - name: Deploy moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + # Add dirs to $PATH + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + # PHPUnit depends on en_AU.UTF-8 locale + sudo locale-gen en_AU.UTF-8 + - name: Install moodle-plugin-ci + # Need explicit IP to stop mysql client fail on attempt to use unix socket. + run: moodle-plugin-ci install -vvv --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + # TODO: Omitted MOODLE_BRANCH results in regex failure, investigate. + MOODLE_BRANCH: 'master' + + - name: Run phplint + run: moodle-plugin-ci phplint + + - name: Run phpcpd + run: moodle-plugin-ci phpcpd || true + + - name: Run phpmd + run: moodle-plugin-ci phpmd + + - name: Run codechecker + run: moodle-plugin-ci codechecker + + - name: Run validate + run: moodle-plugin-ci validate + + - name: Run savepoints + run: moodle-plugin-ci savepoints + + - name: Run mustache + run: moodle-plugin-ci phpcpd || true + env: + MUSTACHE_IGNORE_NAMES: mobile_teacher_form.mustache + + - name: Run grunt + run: moodle-plugin-ci grunt + + - name: Run phpdoc + run: moodle-plugin-ci phpdoc + + - name: Run phpunit + run: moodle-plugin-ci phpunit + + - name: Run behat + run: moodle-plugin-ci behat --profile chrome \ No newline at end of file diff --git a/mod/attendance/CHANGELOG.md b/mod/attendance/CHANGELOG.md new file mode 100644 index 0000000..918c731 --- /dev/null +++ b/mod/attendance/CHANGELOG.md @@ -0,0 +1,39 @@ +### [Unreleased] +- New Feature: Allow automatic marking using site logs. +- New Feature: Warn users when attendance drops below threshold. +- Improvement: Allow default view for teachers to be set at admin level. +- Improvement: All courses user report now displays as table. +- Bug fix: Restored attendances do not create calendar events correctly. + +### Date: 2017-May-23 +### Release: 2017052301 + +- New Feature: New site Level/course category report with average course attendance. +- New Feature: Allow unmarked students to be automatically marked when session closes. + +--- + +### Date: 2017-May-11 +### Release: 2017051104 + +- New Feature: Allow subnet mask to be set at the attendance session level. +- New Feature: Allow certain statuses to be hidden from students when self-marking attendance. +- New Feature: Allow student password to be viewed on session list page. +- Improvement: Improve usablity by grouping settings on session add form. +- Bug fix - fix issue with displaying dates when site hosted on Windows server. +- Bug fix - improve compliance with Moodle coding guidelines. + +--- + +### Date: 2017-Apr-21 +### Release: 2017042100 + +- Feature: Allow a random self-marking password to be used when creating session. +- Improvement: #63 use core useridentity setting when showing list of users. +- Improvement: #258 Add link to attendance on student overview report. +- Improvement: allow student self-marking to be restricted to the session time. +- Improvement: allow admin to set default values when teachers creating new sessions. +- Bug fix - improve compliance with Moodle coding guidelines - phpdocs etc. + +--- + diff --git a/mod/attendance/README.md b/mod/attendance/README.md new file mode 100644 index 0000000..ea71d44 --- /dev/null +++ b/mod/attendance/README.md @@ -0,0 +1,28 @@ +# ABOUT [](https://travis-ci.org/danmarsden/moodle-mod_attendance) + +The Attendance module is supported and maintained by Dan Marsden http://danmarsden.com + +The Attendance module was previously developed by + Dmitry Pupinin, Novosibirsk, Russia, + Artem Andreev, Taganrog, Russia. + +Branches +-------- +The git branches here support the following versions. + +| Moodle version | Branch | +| ----------------- | ----------- | +| Mooodle 3.5 | MOODLE_35_STABLE | +| Mooodle 3.6 | MOODLE_36_STABLE | +| Moodle 3.7 | MOODLE_37_STABLE | +| Moodle 3.8 and higher | main | + +# PURPOSE +The Attendance module allows teachers to maintain a record of attendance, replacing or supplementing a paper-based attendance register. +It is primarily used in blended-learning environments where students are required to attend classes, lectures and tutorials and allows +the teacher to track and optionally provide a grade for the students attendance. + +Sessions can be configured to allow students to record their own attendance and a range of different reports are available. + +# DOCUMENTATION +https://docs.moodle.org/en/Attendance_activity diff --git a/mod/attendance/absentee.php b/mod/attendance/absentee.php new file mode 100644 index 0000000..1fcc288 --- /dev/null +++ b/mod/attendance/absentee.php @@ -0,0 +1,145 @@ +<?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/>. + +/** + * Attendance course summary report. + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot.'/mod/attendance/lib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$category = optional_param('category', 0, PARAM_INT); +$attendancecm = optional_param('id', 0, PARAM_INT); +$download = optional_param('download', '', PARAM_ALPHA); +$sort = optional_param('tsort', 'timesent', PARAM_ALPHA); + +if (!empty($category)) { + $context = context_coursecat::instance($category); + $coursecat = core_course_category::get($category); + $courses = $coursecat->get_courses(array('recursive' => true, 'idonly' => true)); + $PAGE->set_category_by_id($category); + require_login(); +} else if (!empty($attendancecm)) { + $cm = get_coursemodule_from_id('attendance', $attendancecm, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + $courses = array($course->id); + $context = context_module::instance($cm->id); + require_login($course, false, $cm); +} else { + admin_externalpage_setup('managemodules'); + $context = context_system::instance(); + $courses = array(); // Show all courses. +} +// Check permissions. +require_capability('mod/attendance:viewreports', $context); + +$exportfilename = 'attendance-absentee.csv'; + +$PAGE->set_url('/mod/attendance/absentee.php', array('category' => $category, 'id' => $attendancecm)); + +$PAGE->set_heading($SITE->fullname); + +$table = new flexible_table('attendanceabsentee'); +$table->define_baseurl($PAGE->url); + +if (!$table->is_downloading($download, $exportfilename)) { + if (!empty($attendancecm)) { + $pageparams = new mod_attendance_sessions_page_params(); + $att = new mod_attendance_structure($att, $cm, $course, $context, $pageparams); + $output = $PAGE->get_renderer('mod_attendance'); + $tabs = new attendance_tabs($att, attendance_tabs::TAB_ABSENTEE); + echo $output->header(); + echo $output->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); + echo $output->render($tabs); + } else { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('absenteereport', 'mod_attendance')); + if (empty($category)) { + // Only show tabs if displaying via the admin page. + $tabmenu = attendance_print_settings_tabs('absentee'); + echo $tabmenu; + } + } + +} + +$table->define_columns(array('coursename', 'aname', 'userid', 'numtakensessions', 'percent', 'timesent')); +$table->define_headers(array(get_string('course'), + get_string('pluginname', 'attendance'), + get_string('user'), + get_string('takensessions', 'attendance'), + get_string('averageattendance', 'attendance'), + get_string('triggered', 'attendance'))); +$table->sortable(true); +$table->set_attribute('cellspacing', '0'); +$table->set_attribute('class', 'generaltable generalbox'); +$table->show_download_buttons_at(array(TABLE_P_BOTTOM)); +$table->setup(); + +// Work out direction of sort required. +$sortcolumns = $table->get_sort_columns(); +// Now do sorting if specified. + +// Sanity check $sort var before including in sql. Make sure it matches a known column. +$allowedsort = array_diff(array_keys($table->columns), $table->column_nosort); +if (!in_array($sort, $allowedsort)) { + $sort = ''; +} + +$orderby = ' ORDER BY percent ASC'; +if (!empty($sort)) { + $direction = ' DESC'; + if (!empty($sortcolumns[$sort]) && $sortcolumns[$sort] == SORT_ASC) { + $direction = ' ASC'; + } + $orderby = " ORDER BY $sort $direction"; + +} + +$records = attendance_get_users_to_notify($courses, $orderby); +foreach ($records as $record) { + if (!$table->is_downloading($download, $exportfilename)) { + $url = new moodle_url('/mod/attendance/index.php', array('id' => $record->courseid)); + $name = html_writer::link($url, $record->coursename); + } else { + $name = $record->coursename; + } + $url = new moodle_url('/mod/attendance/view.php', array('studentid' => $record->userid, + 'id' => $record->cmid, 'view' => ATT_VIEW_ALL)); + $attendancename = html_writer::link($url, $record->aname); + + $username = html_writer::link($url, fullname($record)); + $percent = round($record->percent * 100)."%"; + $timesent = "-"; + if (!empty($record->timesent)) { + $timesent = userdate($record->timesent); + } + + $table->add_data(array($name, $attendancename, $username, $record->numtakensessions, $percent, $timesent)); +} +$table->finish_output(); + +if (!$table->is_downloading()) { + echo $OUTPUT->footer(); +} \ No newline at end of file diff --git a/mod/attendance/attendance.php b/mod/attendance/attendance.php new file mode 100644 index 0000000..3104624 --- /dev/null +++ b/mod/attendance/attendance.php @@ -0,0 +1,212 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Prints attendance info for particular user + * + * @package mod_attendance + * @copyright 2014 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); +require_once($CFG->libdir.'/formslib.php'); + +$pageparams = new mod_attendance_sessions_page_params(); + +// Check that the required parameters are present. +$id = required_param('sessid', PARAM_INT); +$qrpass = optional_param('qrpass', '', PARAM_TEXT); + +$attforsession = $DB->get_record('attendance_sessions', array('id' => $id), '*', MUST_EXIST); +$attconfig = get_config('attendance'); +$attendance = $DB->get_record('attendance', array('id' => $attforsession->attendanceid), '*', MUST_EXIST); +$cm = get_coursemodule_from_instance('attendance', $attendance->id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + +// Require the user is logged in. +require_login($course, true, $cm); + +$qrpassflag = false; + +// If the randomised code is on grab it. +if ($attforsession->rotateqrcode == 1) { + $cookiename = 'attendance_'.$attforsession->id; + $secrethash = md5($USER->id.$attforsession->rotateqrcodesecret); + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + + // Check if cookie is set and verify. + if (isset($_COOKIE[$cookiename])) { + // Check the token. + if ($secrethash !== $_COOKIE[$cookiename]) { + // Flag error. + print_error('qr_cookie_error', 'mod_attendance', $url); + } + } else { + // Check password. + $sql = 'SELECT * FROM {attendance_rotate_passwords}'. + ' WHERE attendanceid = ? AND expirytime > ? ORDER BY expirytime ASC'; + $qrpassdatabase = $DB->get_records_sql($sql, ['attendanceid' => $id, time() - $attconfig->rotateqrcodeexpirymargin], 0, 2); + + foreach ($qrpassdatabase as $qrpasselement) { + if ($qrpass == $qrpasselement->password) { + $qrpassflag = true; + } + } + + if ($qrpassflag) { + // Create and store the token. + setcookie($cookiename, $secrethash, time() + (60 * 5), "/"); + } else { + // Flag error. + print_error('qr_pass_wrong', 'mod_attendance', $url); + } + } +} + +list($canmark, $reason) = attendance_can_student_mark($attforsession); +if (!$canmark) { + redirect(new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)), get_string($reason, 'attendance')); + exit; +} + +// Check if subnet is set and if the user is in the allowed range. +if (!empty($attforsession->subnet) && !address_in_subnet(getremoteaddr(), $attforsession->subnet)) { + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + notice(get_string('subnetwrong', 'attendance'), $url); + exit; // Notice calls this anyway. +} + +$pageparams->sessionid = $id; +$att = new mod_attendance_structure($attendance, $cm, $course, $PAGE->context, $pageparams); + +if (empty($attforsession->includeqrcode)) { + $qrpass = ''; // Override qrpass if set, as it is not allowed. +} + +// Check to see if autoassignstatus is in use and no password required or Qrpass given and passed. +if ($attforsession->autoassignstatus && (empty($attforsession->studentpassword)) || $qrpassflag) { + $statusid = attendance_session_get_highest_status($att, $attforsession); + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + if (empty($statusid)) { + print_error('attendance_no_status', 'mod_attendance', $url); + } + $take = new stdClass(); + $take->status = $statusid; + $take->sessid = $attforsession->id; + $success = $att->take_from_student($take); + + if ($success) { + // Redirect back to the view page. + redirect($url, get_string('studentmarked', 'attendance')); + } else { + print_error('attendance_already_submitted', 'mod_attendance', $url); + } +} + +if (!empty($qrpass) && !empty($attforsession->autoassignstatus)) { + $fromform = new stdClass(); + + // Check if password required and if set correctly. + if (!empty($attforsession->studentpassword) && + $attforsession->studentpassword !== $qrpass) { + + $url = new moodle_url('/mod/attendance/attendance.php', array('sessid' => $id, 'sesskey' => sesskey())); + redirect($url, get_string('incorrectpassword', 'mod_attendance'), null, \core\output\notification::NOTIFY_ERROR); + } + + // Set the password and session id in the form, because they are saved in the attendance log. + $fromform->studentpassword = $qrpass; + $fromform->sessid = $attforsession->id; + + $fromform->status = attendance_session_get_highest_status($att, $attforsession); + if (empty($fromform->status)) { + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + print_error('attendance_no_status', 'mod_attendance', $url); + } + + if (!empty($fromform->status)) { + $success = $att->take_from_student($fromform); + + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + if ($success) { + // Redirect back to the view page. + redirect($url, get_string('studentmarked', 'attendance')); + } else { + print_error('attendance_already_submitted', 'mod_attendance', $url); + } + } +} + +$PAGE->set_url($att->url_sessions()); + +// Create the form. +if ($attforsession->rotateqrcode == 1) { + $mform = new mod_attendance\form\studentattendance(null, + array('course' => $course, 'cm' => $cm, 'modcontext' => $PAGE->context, 'session' => $attforsession, + 'attendance' => $att, 'password' => $attforsession->studentpassword)); +} else { + $mform = new mod_attendance\form\studentattendance(null, + array('course' => $course, 'cm' => $cm, 'modcontext' => $PAGE->context, 'session' => $attforsession, + 'attendance' => $att, 'password' => $qrpass)); +} + +if ($mform->is_cancelled()) { + // The user cancelled the form, so redirect them to the view page. + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + redirect($url); +} else if ($fromform = $mform->get_data()) { + // Check if password required and if set correctly. + if (!empty($attforsession->studentpassword) && + $attforsession->studentpassword !== $fromform->studentpassword) { + + $url = new moodle_url('/mod/attendance/attendance.php', array('sessid' => $id, 'sesskey' => sesskey())); + redirect($url, get_string('incorrectpassword', 'mod_attendance'), null, \core\output\notification::NOTIFY_ERROR); + } + if ($attforsession->autoassignstatus) { + $fromform->status = attendance_session_get_highest_status($att, $attforsession); + if (empty($fromform->status)) { + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + print_error('attendance_no_status', 'mod_attendance', $url); + } + } + + if (!empty($fromform->status)) { + $success = $att->take_from_student($fromform); + + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + if ($success) { + // Redirect back to the view page. + redirect($url, get_string('studentmarked', 'attendance')); + } else { + print_error('attendance_already_submitted', 'mod_attendance', $url); + } + } + + // The form did not validate correctly so we will set it to display the data they submitted. + $mform->set_data($fromform); +} + +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add($att->name); + +$output = $PAGE->get_renderer('mod_attendance'); +echo $output->header(); +$mform->display(); +echo $output->footer(); diff --git a/mod/attendance/backup/moodle2/backup_attendance_activity_task.class.php b/mod/attendance/backup/moodle2/backup_attendance_activity_task.class.php new file mode 100644 index 0000000..762fe71 --- /dev/null +++ b/mod/attendance/backup/moodle2/backup_attendance_activity_task.class.php @@ -0,0 +1,71 @@ +<?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 {@see backup_attendance_activity_task} definition + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/attendance/backup/moodle2/backup_attendance_stepslib.php'); + +/** + * Provides all the settings and steps to perform one complete backup of attendance activity + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_attendance_activity_task extends backup_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + $this->add_step(new backup_attendance_activity_structure_step('attendance_structure', 'attendance.xml')); + } + + /** + * Code the transformations to perform in the activity in + * order to get transportable (encoded) links + * @param string $content + * @return string + */ + static public function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot, "/"); + + // Link to attendance view by moduleid. + $search = "/(" . $base . "\/mod\/attendance\/view.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@ATTENDANCEVIEWBYID*$2@$', $content); + + // Link to attendance view by moduleid and studentid. + $search = "/(" . $base . "\/mod\/attendance\/view.php\?id\=)([0-9]+)\&studentid\=([0-9]+)/"; + $content = preg_replace($search, '$@ATTENDANCEVIEWBYIDSTUD*$2*$3@$', $content); + + return $content; + } +} diff --git a/mod/attendance/backup/moodle2/backup_attendance_stepslib.php b/mod/attendance/backup/moodle2/backup_attendance_stepslib.php new file mode 100644 index 0000000..67e90f4 --- /dev/null +++ b/mod/attendance/backup/moodle2/backup_attendance_stepslib.php @@ -0,0 +1,110 @@ +<?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/>. + +/** + * Defines all the backup steps that will be used by {@see backup_attendance_activity_task} + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Defines the complete attendance structure for backup, with file and id annotations + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_attendance_activity_structure_step extends backup_activity_structure_step { + + /** + * Define the structure of the backup workflow. + * + * @return restore_path_element $structure + */ + protected function define_structure() { + + // Are we including userinfo? + $userinfo = $this->get_setting_value('userinfo'); + + // XML nodes declaration - non-user data. + $attendance = new backup_nested_element('attendance', array('id'), array( + 'name', 'intro', 'introformat', 'grade', 'showextrauserdetails', 'showsessiondetails', 'sessiondetailspos', 'subnet')); + + $statuses = new backup_nested_element('statuses'); + $status = new backup_nested_element('status', array('id'), array( + 'acronym', 'description', 'grade', 'studentavailability', 'setunmarked', 'visible', 'deleted', 'setnumber')); + + $warnings = new backup_nested_element('warnings'); + $warning = new backup_nested_element('warning', array('id'), array('warningpercent', 'warnafter', + 'maxwarn', 'emailuser', 'emailsubject', 'emailcontent', 'emailcontentformat', 'thirdpartyemails')); + + $sessions = new backup_nested_element('sessions'); + $session = new backup_nested_element('session', array('id'), array( + 'groupid', 'sessdate', 'duration', 'lasttaken', 'lasttakenby', 'timemodified', + 'description', 'descriptionformat', 'studentscanmark', 'studentpassword', 'autoassignstatus', + 'subnet', 'automark', 'automarkcompleted', 'statusset', 'absenteereport', 'preventsharedip', + 'preventsharediptime', 'caleventid', 'calendarevent', 'includeqrcode')); + + // XML nodes declaration - user data. + $logs = new backup_nested_element('logs'); + $log = new backup_nested_element('log', array('id'), array( + 'sessionid', 'studentid', 'statusid', 'statusset', 'timetaken', 'takenby', 'remarks')); + + // Build the tree in the order needed for restore. + $attendance->add_child($statuses); + $statuses->add_child($status); + + $attendance->add_child($warnings); + $warnings->add_child($warning); + + $attendance->add_child($sessions); + $sessions->add_child($session); + + $session->add_child($logs); + $logs->add_child($log); + + // Data sources - non-user data. + + $attendance->set_source_table('attendance', array('id' => backup::VAR_ACTIVITYID)); + + $status->set_source_table('attendance_statuses', array('attendanceid' => backup::VAR_PARENTID)); + + $warning->set_source_table('attendance_warning', + array('idnumber' => backup::VAR_PARENTID)); + + $session->set_source_table('attendance_sessions', array('attendanceid' => backup::VAR_PARENTID)); + + // Data sources - user related data. + if ($userinfo) { + $log->set_source_table('attendance_log', array('sessionid' => backup::VAR_PARENTID)); + } + + // Id annotations. + $session->annotate_ids('user', 'lasttakenby'); + $session->annotate_ids('group', 'groupid'); + $log->annotate_ids('user', 'studentid'); + $log->annotate_ids('user', 'takenby'); + + // File annotations. + $session->annotate_files('mod_attendance', 'session', 'id'); + + // Return the root element (workshop), wrapped into standard activity structure. + return $this->prepare_activity_structure($attendance); + } +} diff --git a/mod/attendance/backup/moodle2/restore_attendance_activity_task.class.php b/mod/attendance/backup/moodle2/restore_attendance_activity_task.class.php new file mode 100644 index 0000000..5276988 --- /dev/null +++ b/mod/attendance/backup/moodle2/restore_attendance_activity_task.class.php @@ -0,0 +1,113 @@ +<?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/>. + +/** + * Define all the restore steps that will be used by the restore_attendance_activity_task + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/attendance/backup/moodle2/restore_attendance_stepslib.php'); + +/** + * Attendance restore task that provides all the settings and steps to perform one complete restore of the activity + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_attendance_activity_task extends restore_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + $this->add_step(new restore_attendance_activity_structure_step('attendance_structure', 'attendance.xml')); + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder + */ + static public function define_decode_contents() { + $contents = array(); + + $contents[] = new restore_decode_content('attendance_sessions', + array('description'), 'attendance_session'); + + return $contents; + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder + */ + static public function define_decode_rules() { + $rules = array(); + + $rules[] = new restore_decode_rule('ATTENDANCEVIEWBYID', + '/mod/attendance/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('ATTENDANCEVIEWBYIDSTUD', + '/mod/attendance/view.php?id=$1&studentid=$2', array('course_module', 'user')); + + // Older style backups using previous plugin name. + $rules[] = new restore_decode_rule('ATTFORBLOCKVIEWBYID', + '/mod/attendance/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('ATTFORBLOCKVIEWBYIDSTUD', + '/mod/attendance/view.php?id=$1&studentid=$2', array('course_module', 'user')); + + return $rules; + + } + + /** + * Define the restore log rules that will be applied + * by the {@see restore_logs_processor} when restoring + * attendance logs. It must return one array + * of {@see restore_log_rule} objects + */ + static public function define_restore_log_rules() { + $rules = array(); + + // TODO: log restore. + return $rules; + } + + /** + * Define the restore log rules that will be applied + * by the {@see restore_logs_processor} when restoring + * course logs. It must return one array + * of {@see restore_log_rule} objects + * + * Note this rules are applied when restoring course logs + * by the restore final task, but are defined here at + * activity level. All them are rules not linked to any module instance (cmid = 0) + */ + static public function define_restore_log_rules_for_course() { + $rules = array(); + + return $rules; + } +} diff --git a/mod/attendance/backup/moodle2/restore_attendance_stepslib.php b/mod/attendance/backup/moodle2/restore_attendance_stepslib.php new file mode 100644 index 0000000..ae7cad8 --- /dev/null +++ b/mod/attendance/backup/moodle2/restore_attendance_stepslib.php @@ -0,0 +1,187 @@ +<?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/>. + +/** + * Structure step to restore one attendance activity + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define all the restore steps that will be used by the restore_attendance_activity_task + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_attendance_activity_structure_step extends restore_activity_structure_step { + + /** + * Define the structure of the restore workflow. + * + * @return restore_path_element $structure + */ + protected function define_structure() { + + $paths = array(); + + $userinfo = $this->get_setting_value('userinfo'); // Are we including userinfo? + + // XML interesting paths - non-user data. + $paths[] = new restore_path_element('attendance', '/activity/attendance'); + + $paths[] = new restore_path_element('attendance_status', + '/activity/attendance/statuses/status'); + + $paths[] = new restore_path_element('attendance_warning', + '/activity/attendance/warnings/warning'); + + $paths[] = new restore_path_element('attendance_session', + '/activity/attendance/sessions/session'); + + // End here if no-user data has been selected. + if (!$userinfo) { + return $this->prepare_activity_structure($paths); + } + + // XML interesting paths - user data. + $paths[] = new restore_path_element('attendance_log', + '/activity/attendance/sessions/session/logs/log'); + + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * Process an attendance restore. + * + * @param object $data The data in object form + * @return void + */ + protected function process_attendance($data) { + global $DB; + + $data = (object)$data; + $data->course = $this->get_courseid(); + + // Insert the attendance record. + $newitemid = $DB->insert_record('attendance', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * Process attendance status restore + * @param object $data The data in object form + * @return void + */ + protected function process_attendance_status($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->attendanceid = $this->get_new_parentid('attendance'); + + $newitemid = $DB->insert_record('attendance_statuses', $data); + $this->set_mapping('attendance_status', $oldid, $newitemid); + } + + /** + * Process attendance warning restore + * @param object $data The data in object form + * @return void + */ + protected function process_attendance_warning($data) { + global $DB; + + $data = (object)$data; + + $data->idnumber = $this->get_new_parentid('attendance'); + + $DB->insert_record('attendance_warning', $data); + } + + /** + * Process attendance session restore + * @param object $data The data in object form + * @return void + */ + protected function process_attendance_session($data) { + global $DB; + + $userinfo = $this->get_setting_value('userinfo'); // Are we including userinfo? + + $data = (object)$data; + $oldid = $data->id; + + $data->attendanceid = $this->get_new_parentid('attendance'); + $data->groupid = $this->get_mappingid('group', $data->groupid); + $data->sessdate = $this->apply_date_offset($data->sessdate); + $data->timemodified = $this->apply_date_offset($data->timemodified); + $data->caleventid = $this->get_mappingid('event', $data->caleventid); + + if ($userinfo) { + $data->lasttaken = $this->apply_date_offset($data->lasttaken); + $data->lasttakenby = $this->get_mappingid('user', $data->lasttakenby); + } else { + $data->lasttaken = 0; + $data->lasttakenby = 0; + } + + $newitemid = $DB->insert_record('attendance_sessions', $data); + $data->id = $newitemid; + $this->set_mapping('attendance_session', $oldid, $newitemid, true); + + // Create Calendar event. + attendance_create_calendar_event($data); + } + + /** + * Process attendance log restore + * @param object $data The data in object form + * @return void + */ + protected function process_attendance_log($data) { + global $DB; + + $data = (object)$data; + + $data->sessionid = $this->get_mappingid('attendance_session', $data->sessionid); + $data->studentid = $this->get_mappingid('user', $data->studentid); + $data->statusid = $this->get_mappingid('attendance_status', $data->statusid); + $statusset = explode(',', $data->statusset); + foreach ($statusset as $st) { + $st = $this->get_mappingid('attendance_status', $st); + } + $data->statusset = implode(',', $statusset); + $data->timetaken = $this->apply_date_offset($data->timetaken); + $data->takenby = $this->get_mappingid('user', $data->takenby); + + $DB->insert_record('attendance_log', $data); + } + + /** + * Once the database tables have been fully restored, restore the files + * @return void + */ + protected function after_execute() { + $this->add_related_files('mod_attendance', 'session', 'attendance_session'); + } +} diff --git a/mod/attendance/calendar.js b/mod/attendance/calendar.js new file mode 100644 index 0000000..5ab5c76 --- /dev/null +++ b/mod/attendance/calendar.js @@ -0,0 +1,110 @@ +/* global YUI */ +// eslint-disable-next-line new-cap +YUI().use('yui2-container', 'yui2-calendar', function(Y) { + var YAHOO = Y.YUI2; + + document.body.className += ' yui-skin-sam'; + + YAHOO.util.Event.onDOMReady(function() { + + var Event = YAHOO.util.Event, + Dom = YAHOO.util.Dom, + dialog, calendar; + + var showBtn = Dom.get("show"); + + Event.on(showBtn, "click", function() { + /** + * Reset handler and set current day. + */ + function resetHandler() { + calendar.cfg.setProperty("pagedate", calendar.today); + calendar.render(); + } + + /** + * Close dialog. + */ + function closeHandler() { + dialog.hide(); + } + + // Lazy Dialog Creation - Wait to create the Dialog, and setup document click listeners, + // until the first time the button is clicked. + if (!dialog) { + + // Hide Calendar if we click anywhere in the document other than the calendar. + Event.on(document, "click", function(e) { + var el = Event.getTarget(e); + var dialogEl = dialog.element; + if (el != dialogEl && !Dom.isAncestor(dialogEl, el) && el != showBtn && !Dom.isAncestor(showBtn, el)) { + dialog.hide(); + } + }); + + dialog = new YAHOO.widget.Dialog("attcalendarcontainer", { + visible: false, + context: ["show", "tl", "bl"], + buttons: [{text: M.util.get_string('caltoday', 'attendance'), handler: resetHandler, isDefault: true}, + {text: M.util.get_string('calclose', 'attendance'), handler: closeHandler}], + draggable: false, + close: false + }); + dialog.setHeader(''); + dialog.setBody('<div id="cal"></div>'); + dialog.render(document.body); + + dialog.showEvent.subscribe(function() { + if (YAHOO.env.ua.ie) { + // Since we're hiding the table using yui-overlay-hidden, we + // want to let the dialog know that the content size has changed, when + // shown. + dialog.fireEvent("changeContent"); + } + }); + } + + // Lazy Calendar Creation - Wait to create the Calendar until the first time the button is clicked. + if (!calendar) { + + calendar = new YAHOO.widget.Calendar("cal", { + iframe: false, // Turn iframe off, since container has iframe support. + // eslint-disable-next-line camelcase + hide_blank_weeks: true // Enable, to demonstrate how we handle changing height, using changeContent. + }); + + calendar.cfg.setProperty("start_weekday", M.attendance.cal_start_weekday); + calendar.cfg.setProperty("MONTHS_LONG", M.attendance.cal_months); + calendar.cfg.setProperty("WEEKDAYS_SHORT", M.attendance.cal_week_days); + calendar.select(new Date(M.attendance.cal_cur_date * 1000)); + calendar.render(); + + calendar.selectEvent.subscribe(function() { + if (calendar.getSelectedDates().length > 0) { + + Dom.get("curdate").value = calendar.getSelectedDates()[0] / 1000; + + Dom.get("currentdate").submit(); + } + dialog.hide(); + }); + + calendar.renderEvent.subscribe(function() { + // Tell Dialog it's contents have changed, which allows + // container to redraw the underlay (for IE6/Safari2). + dialog.fireEvent("changeContent"); + }); + } + + var seldate = calendar.getSelectedDates(); + + if (seldate.length > 0) { + // Set the pagedate to show the selected date if it exists. + calendar.cfg.setProperty("pagedate", seldate[0]); + calendar.render(); + } + + dialog.show(); + }); + }); +}); diff --git a/mod/attendance/classes/analytics/indicator/activity_base.php b/mod/attendance/classes/analytics/indicator/activity_base.php new file mode 100644 index 0000000..492b4f2 --- /dev/null +++ b/mod/attendance/classes/analytics/indicator/activity_base.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Activity base class. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Activity base class. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { + + /** + * feedback_viewed_events + * + * @return string[] + */ + protected function feedback_viewed_events() { + return array('\mod_attendance\event\session_report_viewed'); + } +} diff --git a/mod/attendance/classes/analytics/indicator/cognitive_depth.php b/mod/attendance/classes/analytics/indicator/cognitive_depth.php new file mode 100644 index 0000000..a437a49 --- /dev/null +++ b/mod/attendance/classes/analytics/indicator/cognitive_depth.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Cognitive depth indicator - attendance. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Cognitive depth indicator - attendance. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cognitive_depth extends activity_base { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('indicator:cognitivedepth', 'mod_attendance'); + } + + /** + * Defines indicator type. + * + * @return string + */ + public function get_indicator_type() { + return self::INDICATOR_COGNITIVE; + } + + /** + * Returns the potential level of cognitive depth. + * + * @param \cm_info $cm + * @return int + */ + public function get_cognitive_depth_level(\cm_info $cm) { + return self::COGNITIVE_LEVEL_3; + } +} diff --git a/mod/attendance/classes/analytics/indicator/social_breadth.php b/mod/attendance/classes/analytics/indicator/social_breadth.php new file mode 100644 index 0000000..e81f6e8 --- /dev/null +++ b/mod/attendance/classes/analytics/indicator/social_breadth.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Social breadth indicator - attendance. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Social breadth indicator - attendance. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @author Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class social_breadth extends activity_base { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('indicator:socialbreadth', 'mod_attendance'); + } + + /** + * Defines indicator type. + * + * @return string + */ + public function get_indicator_type() { + return self::INDICATOR_SOCIAL; + } + + /** + * Returns the potential level of social breadth. + * + * @param \cm_info $cm + * @return int + */ + public function get_social_breadth_level(\cm_info $cm) { + return self::SOCIAL_LEVEL_2; + } +} diff --git a/mod/attendance/classes/attendance_webservices_handler.php b/mod/attendance/classes/attendance_webservices_handler.php new file mode 100644 index 0000000..c961362 --- /dev/null +++ b/mod/attendance/classes/attendance_webservices_handler.php @@ -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/>. + +/** + * Web Services for Attendance plugin. + * + * @package mod_attendance + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/../locallib.php'); +require_once(dirname(__FILE__).'/structure.php'); +require_once(dirname(__FILE__).'/../../../lib/sessionlib.php'); +require_once(dirname(__FILE__).'/../../../lib/datalib.php'); + +/** + * Class attendance_handler + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_handler { + /** + * For this user, this method searches in all the courses that this user has permission to take attendance, + * looking for today sessions and returns the courses with the sessions. + * @param int $userid + * @return array + */ + public static function get_courses_with_today_sessions($userid) { + $usercourses = enrol_get_users_courses($userid); + $attendanceinstance = get_all_instances_in_courses('attendance', $usercourses); + + $coursessessions = array(); + + foreach ($attendanceinstance as $attendance) { + $context = context_course::instance($attendance->course); + if (has_capability('mod/attendance:takeattendances', $context, $userid)) { + $course = $usercourses[$attendance->course]; + if (!isset($course->attendance_instance)) { + $course->attendance_instance = array(); + } + + $att = new stdClass(); + $att->id = $attendance->id; + $att->course = $attendance->course; + $att->name = $attendance->name; + $att->grade = $attendance->grade; + + $cm = new stdClass(); + $cm->id = $attendance->coursemodule; + + $att = new mod_attendance_structure($att, $cm, $course, $context); + $course->attendance_instance[$att->id] = array(); + $course->attendance_instance[$att->id]['name'] = $att->name; + $todaysessions = $att->get_today_sessions(); + + if (!empty($todaysessions)) { + $course->attendance_instance[$att->id]['today_sessions'] = $todaysessions; + $coursessessions[$course->id] = $course; + } + } + } + + return self::prepare_data($coursessessions); + } + + /** + * Prepare data. + * + * @param array $coursessessions + * @return array + */ + private static function prepare_data($coursessessions) { + $courses = array(); + + foreach ($coursessessions as $c) { + $courses[$c->id] = new stdClass(); + $courses[$c->id]->shortname = $c->shortname; + $courses[$c->id]->fullname = $c->fullname; + $courses[$c->id]->attendance_instances = $c->attendance_instance; + } + + return $courses; + } + + /** + * For this session, returns all the necessary data to take an attendance. + * + * @param int $sessionid + * @return mixed + */ + public static function get_session($sessionid) { + global $DB; + + $session = $DB->get_record('attendance_sessions', array('id' => $sessionid)); + $session->courseid = $DB->get_field('attendance', 'course', array('id' => $session->attendanceid)); + $session->statuses = attendance_get_statuses($session->attendanceid, true, $session->statusset); + $coursecontext = context_course::instance($session->courseid); + $session->users = get_enrolled_users($coursecontext, 'mod/attendance:canbelisted', + $session->groupid, 'u.id, u.firstname, u.lastname'); + $session->attendance_log = array(); + + if ($attendancelog = $DB->get_records('attendance_log', array('sessionid' => $sessionid), + '', 'studentid, statusid, remarks, id')) { + $session->attendance_log = $attendancelog; + } + + return $session; + } + + /** + * Update user status + * + * @param int $sessionid + * @param int $studentid + * @param int $takenbyid + * @param int $statusid + * @param int $statusset + */ + public static function update_user_status($sessionid, $studentid, $takenbyid, $statusid, $statusset) { + global $DB; + + $record = new stdClass(); + $record->statusset = $statusset; + $record->sessionid = $sessionid; + $record->timetaken = time(); + $record->takenby = $takenbyid; + $record->statusid = $statusid; + $record->studentid = $studentid; + + if ($attendancelog = $DB->get_record('attendance_log', array('sessionid' => $sessionid, 'studentid' => $studentid))) { + $record->id = $attendancelog->id; + $DB->update_record('attendance_log', $record); + } else { + $DB->insert_record('attendance_log', $record); + } + + if ($attendancesession = $DB->get_record('attendance_sessions', array('id' => $sessionid))) { + $attendancesession->lasttaken = time(); + $attendancesession->lasttakenby = $takenbyid; + $attendancesession->timemodified = time(); + + $DB->update_record('attendance_sessions', $attendancesession); + } + } +} diff --git a/mod/attendance/classes/calendar_helpers.php b/mod/attendance/classes/calendar_helpers.php new file mode 100644 index 0000000..07fd2ab --- /dev/null +++ b/mod/attendance/classes/calendar_helpers.php @@ -0,0 +1,185 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. +/** + * Calendar related functions + * + * @package mod_attendance + * @copyright 2016 Vyacheslav Strelkov + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/../../../calendar/lib.php'); + +/** + * Create single calendar event bases on session data. + * + * @param stdClass $session initial sessions to take data from + * @return bool result of calendar event creation + */ +function attendance_create_calendar_event(&$session) { + global $DB; + + // We don't want to create multiple calendar events for 1 session. + if ($session->caleventid) { + return $session->caleventid; + } + if (empty(get_config('attendance', 'enablecalendar')) || $session->calendarevent === 0) { + // Calendar events are not used, or event not required for this session. + return true; + } + + $attendance = $DB->get_record('attendance', array('id' => $session->attendanceid)); + + $caleventdata = new stdClass(); + $caleventdata->name = $attendance->name; + $caleventdata->courseid = $attendance->course; + $caleventdata->groupid = $session->groupid; + $caleventdata->instance = $session->attendanceid; + $caleventdata->timestart = $session->sessdate; + $caleventdata->timeduration = $session->duration; + $caleventdata->description = $session->description; + $caleventdata->format = $session->descriptionformat; + $caleventdata->eventtype = 'attendance'; + $caleventdata->timemodified = time(); + $caleventdata->modulename = 'attendance'; + + if (!empty($session->groupid)) { + $caleventdata->name .= " (". get_string('group', 'group') ." ". groups_get_group_name($session->groupid) .")"; + } + + $calevent = new stdClass(); + if ($calevent = calendar_event::create($caleventdata, false)) { + $session->caleventid = $calevent->id; + $DB->set_field('attendance_sessions', 'caleventid', $session->caleventid, array('id' => $session->id)); + return true; + } else { + return false; + } +} + +/** + * Create multiple calendar events based on sessions data. + * + * @param array $sessionsids array of sessions ids + */ +function attendance_create_calendar_events($sessionsids) { + global $DB; + + if (empty(get_config('attendance', 'enablecalendar'))) { + // Calendar events are not used. + return true; + } + + $sessions = $DB->get_recordset_list('attendance_sessions', 'id', $sessionsids); + + foreach ($sessions as $session) { + attendance_create_calendar_event($session); + if ($session->caleventid) { + $DB->update_record('attendance_sessions', $session); + } + } +} + +/** + * Update calendar event duration and date + * + * @param stdClass $session Session data + * @return bool result of updating + */ +function attendance_update_calendar_event($session) { + global $DB; + + $caleventid = $session->caleventid; + $timeduration = $session->duration; + $timestart = $session->sessdate; + + if (empty(get_config('attendance', 'enablecalendar'))) { + // Calendar events are not used. + return true; + } + + // Should there even be an event? + if ($session->calendarevent == 0) { + if ($session->caleventid != 0) { + // There is an existing event we should delete, calendarevent just got turned off. + $DB->delete_records_list('event', 'id', array($caleventid)); + $session->caleventid = 0; + $DB->update_record('attendance_sessions', $session); + return true; + } else { + // This should be the common case when session does not want event. + return true; + } + } + + // Do we need new event (calendarevent option has just been turned on)? + if ($session->caleventid == 0) { + return attendance_create_calendar_event($session); + } + + // Boring update. + $caleventdata = new stdClass(); + $caleventdata->timeduration = $timeduration; + $caleventdata->timestart = $timestart; + $caleventdata->timemodified = time(); + $caleventdata->description = $session->description; + + $calendarevent = calendar_event::load($caleventid); + if ($calendarevent) { + return $calendarevent->update($caleventdata) ? true : false; + } else { + return false; + } +} + +/** + * Delete calendar events for sessions + * + * @param array $sessionsids array of sessions ids + * @return bool result of updating + */ +function attendance_delete_calendar_events($sessionsids) { + global $DB; + $caleventsids = attendance_existing_calendar_events_ids($sessionsids); + if ($caleventsids) { + $DB->delete_records_list('event', 'id', $caleventsids); + } + + $sessions = $DB->get_recordset_list('attendance_sessions', 'id', $sessionsids); + foreach ($sessions as $session) { + $session->caleventid = 0; + $DB->update_record('attendance_sessions', $session); + } +} + +/** + * Check if calendar events are created for given sessions + * + * @param array $sessionsids of sessions ids + * @return array | bool array of existing calendar events or false if none found + */ +function attendance_existing_calendar_events_ids($sessionsids) { + global $DB; + $caleventsids = array_keys($DB->get_records_list('attendance_sessions', 'id', $sessionsids, '', 'caleventid')); + $existingcaleventsids = array_filter($caleventsids); + if (! empty($existingcaleventsids)) { + return $existingcaleventsids; + } else { + return false; + } +} \ No newline at end of file diff --git a/mod/attendance/classes/event/attendance_taken.php b/mod/attendance/classes/event/attendance_taken.php new file mode 100644 index 0000000..5c9385e --- /dev/null +++ b/mod/attendance/classes/event/attendance_taken.php @@ -0,0 +1,113 @@ +<?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/>. + +/** + * This file contains an event for when an attendance is taken. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance is taken. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_taken extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_log'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' took attendance with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtaken', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/take.php', array('id' => $this->contextinstanceid, + 'sessionid' => $this->other['sessionid'], + 'grouptype' => $this->other['grouptype'])); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'taken', $this->get_url(), + '', $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (empty($this->other['sessionid'])) { + throw new \coding_exception('The event mod_attendance\\event\\attendance_taken must specify sessionid.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/attendance_taken_by_student.php b/mod/attendance/classes/event/attendance_taken_by_student.php new file mode 100644 index 0000000..3335cdf --- /dev/null +++ b/mod/attendance/classes/event/attendance_taken_by_student.php @@ -0,0 +1,113 @@ +<?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/>. + +/** + * This file contains an event for when an attendance is taken. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance is taken. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_taken_by_student extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'attendance_log'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'Student with id ' . $this->userid . ' took attendance with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtakenbystudent', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/take.php', array('id' => $this->contextinstanceid, + 'sessionid' => $this->other['sessionid'], + 'grouptype' => $this->other['grouptype'])); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'taken', $this->get_url(), + '', $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (empty($this->other['sessionid'])) { + throw new \coding_exception('The event mod_attendance\\event\\attendance_taken must specify sessionid.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/course_module_instance_list_viewed.php b/mod/attendance/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 0000000..8f34039 --- /dev/null +++ b/mod/attendance/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,50 @@ +<?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/>. + +/** + * The mod_attendance instance list viewed event. + * + * @package mod_attendance + * @copyright 2018 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_attendance instance list viewed event class. + * + * @package mod_attendance + * @copyright 2018 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { + /** + * Create the event from course record. + * + * @param \stdClass $course + * @return course_module_instance_list_viewed + */ + public static function create_from_course(\stdClass $course) { + $params = array( + 'context' => \context_course::instance($course->id) + ); + $event = self::create($params); + $event->add_record_snapshot('course', $course); + return $event; + } +} diff --git a/mod/attendance/classes/event/report_viewed.php b/mod/attendance/classes/event/report_viewed.php new file mode 100644 index 0000000..004bf2a --- /dev/null +++ b/mod/attendance/classes/event/report_viewed.php @@ -0,0 +1,99 @@ +<?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/>. + +/** + * This file contains an event for when a attendance report is viewed. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when a attendance report is viewed. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_viewed extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' viewed attendance report with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventreportviewed', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/report.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'report', 'report.php?id=' . $this->contextinstanceid, + $this->objectid, $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + +} diff --git a/mod/attendance/classes/event/session_added.php b/mod/attendance/classes/event/session_added.php new file mode 100644 index 0000000..d734345 --- /dev/null +++ b/mod/attendance/classes/event/session_added.php @@ -0,0 +1,111 @@ +<?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/>. + +/** + * This file contains an event for when an attendance session is added. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance session is added + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_added extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_sessions'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' added a session to the instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventsessionadded', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/manage.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'report', 'report.php?id=' . $this->objectid, + $this->other['info'], $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (empty($this->other['info'])) { + throw new \coding_exception('The event mod_attendance\\event\\session_added must specify info.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/session_deleted.php b/mod/attendance/classes/event/session_deleted.php new file mode 100644 index 0000000..576d1a2 --- /dev/null +++ b/mod/attendance/classes/event/session_deleted.php @@ -0,0 +1,111 @@ +<?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/>. + +/** + * This file contains an event for when an attendance session is deleted. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance session is deleted. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_deleted extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_sessions'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' deleted session with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventsessiondeleted', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/manage.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'sessions deleted', $this->get_url(), + $this->other['info'], $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (empty($this->other['info'])) { + throw new \coding_exception('The event mod_attendance\\event\\session_deleted must specify info.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/session_duration_updated.php b/mod/attendance/classes/event/session_duration_updated.php new file mode 100644 index 0000000..0e43364 --- /dev/null +++ b/mod/attendance/classes/event/session_duration_updated.php @@ -0,0 +1,111 @@ +<?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/>. + +/** + * This file contains an event for when an attendance session duration is updated. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance session duration is updated. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_duration_updated extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_sessions'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' updated attendance session duration with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventdurationupdated', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/manage.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'sessions duration updated', $this->get_url(), + $this->other['info'], $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (empty($this->other['info'])) { + throw new \coding_exception('The event mod_attendance\\event\\session_duration_updated must specify info.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/session_ip_shared.php b/mod/attendance/classes/event/session_ip_shared.php new file mode 100644 index 0000000..e2e526f --- /dev/null +++ b/mod/attendance/classes/event/session_ip_shared.php @@ -0,0 +1,92 @@ +<?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/>. + +/** + * Event for when self-marking is blocked because another student used the same IP address to self-mark. + * + * @package mod_attendance + * @author Dan Marsden <dan@danmarsden.com> + * @copyright 2018 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when self-marking is blocked + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @author Dan Marsden <dan@danmarsden.com> + * @copyright 2018 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_ip_shared extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'attendance_log'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' was blocked from taking attendance for sessionid: ' . $this->other['sessionid'] . + ' because user with id '.$this->other['otheruser'] . ' previously marked attendance with the same IP address.'; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventsessionipshared', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/attendance.php'); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array( + 'db' => 'attendance', + 'restore' => 'attendance' + ); + } +} diff --git a/mod/attendance/classes/event/session_report_updated.php b/mod/attendance/classes/event/session_report_updated.php new file mode 100644 index 0000000..1c649ec --- /dev/null +++ b/mod/attendance/classes/event/session_report_updated.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * This file contains an event for when a student's attendance report is viewed. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when a student's attendance report is updated. + * + * @property-read array $other { + * Extra information about event properties. + * + * string studentid Id of student whose attendances were updated. + * string mode Mode of the report updated. + * } + * @package mod_attendance + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_report_updated extends \mod_attendance\event\session_report_viewed { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + // Objecttable and objectid can't be meaningfully specified. + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventstudentattendancesessionsupdated', 'mod_attendance'); + } +} diff --git a/mod/attendance/classes/event/session_report_viewed.php b/mod/attendance/classes/event/session_report_viewed.php new file mode 100644 index 0000000..c4946f1 --- /dev/null +++ b/mod/attendance/classes/event/session_report_viewed.php @@ -0,0 +1,140 @@ +<?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/>. + +/** + * This file contains an event for when a student's attendance report is viewed. + * + * @package mod_attendance + * @copyright 2019 Nick Phillips + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when a student's attendance report is viewed. + * + * @property-read array $other { + * Extra information about event properties. + * + * string studentid Id of student whose attendances were viewed. + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @copyright 2019 Nick Phillips + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_report_viewed extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + // Objecttable and objectid can't be meaningfully specified. + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' ' . $this->action . ' attendance sessions for student with id ' . + $this->relateduserid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventstudentattendancesessionsviewed', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + // Mode, groupby, sesscourses are optional. + $mode = empty($this->other['mode']) ? "" : $this->other['mode']; + $groupby = empty($this->other['groupby']) ? "" : $this->other['groupby']; + $sesscourses = empty($this->other['sesscourses']) ? "" : $this->other['sesscourses']; + return new \moodle_url('/mod/attendance/view.php', array('id' => $this->contextinstanceid, + 'studentid' => $this->relateduserid, + 'mode' => $mode, + 'view' => $this->other['view'], + 'groupby' => $groupby, + 'sesscourses' => $sesscourses, + 'curdate' => $this->other['curdate'])); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'student sessions ' . $this->action, $this->get_url(), + 'student id ' . $this->relateduserid, $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array(); + } + + /** + * Get other mapping + * + * @return array of parameters for object mapping for objects referenced in 'other' property. + */ + public static function get_other_mapping() { + return array(); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (!isset($this->relateduserid)) { + throw new \coding_exception('The event ' . $this->eventname . ' must specify relateduserid.'); + } + // View params can be left out as defaults will be the same when log event is viewed as when + // it was stored. + // filter params are important, but stored in session so default effectively unknown, + // hence required here. + if (!isset($this->other['view'])) { + throw new \coding_exception('The event ' . $this->eventname . ' must specify view.'); + } + if (!isset($this->other['curdate'])) { + throw new \coding_exception('The event ' . $this->eventname . ' must specify curdate.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/session_updated.php b/mod/attendance/classes/event/session_updated.php new file mode 100644 index 0000000..efa5e62 --- /dev/null +++ b/mod/attendance/classes/event/session_updated.php @@ -0,0 +1,119 @@ +<?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/>. + +/** + * This file contains an event for when an attendance session is updated. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance session is updated. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class session_updated extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_sessions'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' updated attendance session with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventsessionupdated', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/sessions.php', array('id' => $this->contextinstanceid, + 'sessionid' => $this->other['sessionid'], + 'action' => $this->other['action'])); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'session updated', $this->get_url(), + $this->other['info'], $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + if (empty($this->other['info'])) { + throw new \coding_exception('The event mod_attendance\\event\\session_updated must specify info.'); + } + if (empty($this->other['sessionid'])) { + throw new \coding_exception('The event mod_attendance\\event\\session_updated must specify sessionid.'); + } + if (empty($this->other['action'])) { + throw new \coding_exception('The event mod_attendance\\event\\session_updated must specify action.'); + } + parent::validate_data(); + } +} diff --git a/mod/attendance/classes/event/sessions_imported.php b/mod/attendance/classes/event/sessions_imported.php new file mode 100644 index 0000000..79ba3c5 --- /dev/null +++ b/mod/attendance/classes/event/sessions_imported.php @@ -0,0 +1,92 @@ +<?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/>. + +/** + * This file contains an event for when an attendance sessions is imported. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance sessions is imported + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sessions_imported extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = 'attendance_sessions'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' imported ' . $this->other['count'] . ' sessions'; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventsessionsimported', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/import/sessions.php'); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array( + 'db' => 'attendance', + 'restore' => 'attendance' + ); + } +} diff --git a/mod/attendance/classes/event/status_added.php b/mod/attendance/classes/event/status_added.php new file mode 100644 index 0000000..b411370 --- /dev/null +++ b/mod/attendance/classes/event/status_added.php @@ -0,0 +1,99 @@ +<?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/>. + +/** + * This file contains an event for when an attendance status is added. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance status is added. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class status_added extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_statuses'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' updated attendance status with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventstatusadded', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/preferences.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'status added', $this->get_url(), + $this->other['acronym'].': '.$this->other['description'].' ('.$this->other['grade'].')', $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + +} diff --git a/mod/attendance/classes/event/status_removed.php b/mod/attendance/classes/event/status_removed.php new file mode 100644 index 0000000..5f3814b --- /dev/null +++ b/mod/attendance/classes/event/status_removed.php @@ -0,0 +1,101 @@ +<?php +// This file is part of the Attendance module for 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/>. + +/** + * This file contains an event for when an attendance status is updated. + * + * @package mod_attendance + * @copyright 2015 onwards, University of Nottingham + * @author Barry Oosthuizen <barry.oosthuizen@nottingham.ac.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance status is removed. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class status_removed extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_statuses'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' deleted attendance status "' . $this->data['other']['acronym'] . + ' - ' . $this->data['other']['description'] . '" with instanceid ' . + $this->objectid . ''; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('statusdeleted', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/preferences.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'status removed', $this->get_url(), + $this->other['acronym'] . ' - ' . $this->other['description'], $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + +} diff --git a/mod/attendance/classes/event/status_updated.php b/mod/attendance/classes/event/status_updated.php new file mode 100644 index 0000000..b7ed879 --- /dev/null +++ b/mod/attendance/classes/event/status_updated.php @@ -0,0 +1,99 @@ +<?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/>. + +/** + * This file contains an event for when an attendance status is updated. + * + * @package mod_attendance + * @copyright 2014 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Event for when an attendance status is updated. + * + * @property-read array $other { + * Extra information about event properties. + * + * string mode Mode of the report viewed. + * } + * @package mod_attendance + * @since Moodle 2.7 + * @copyright 2013 onwards Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class status_updated extends \core\event\base { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'attendance_statuses'; + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return 'User with id ' . $this->userid . ' updated attendance status with instanceid ' . + $this->objectid; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventstatusupdated', 'mod_attendance'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/attendance/preferences.php', array('id' => $this->contextinstanceid)); + } + + /** + * Replace add_to_log() statement. + * + * @return array of parameters to be passed to legacy add_to_log() function. + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'attendance', 'status updated', $this->get_url(), + '', $this->contextinstanceid); + } + + /** + * Get objectid mapping + * + * @return array of parameters for object mapping. + */ + public static function get_objectid_mapping() { + return array('db' => 'attendance', 'restore' => 'attendance'); + } + +} diff --git a/mod/attendance/classes/form/addsession.php b/mod/attendance/classes/form/addsession.php new file mode 100644 index 0000000..4bf381c --- /dev/null +++ b/mod/attendance/classes/form/addsession.php @@ -0,0 +1,398 @@ +<?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/>. + +/** + * This file contains the forms to add session. + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +use moodleform; +use mod_attendance_structure; +use DateTime; +use DateInterval; +use DatePeriod; + +/** + * class for displaying add form. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class addsession extends moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + + global $CFG, $USER; + $mform =& $this->_form; + + $course = $this->_customdata['course']; + $cm = $this->_customdata['cm']; + $modcontext = $this->_customdata['modcontext']; + + $pluginconfig = get_config('attendance'); + + $mform->addElement('header', 'general', get_string('addsession', 'attendance')); + + $groupmode = groups_get_activity_groupmode($cm); + switch ($groupmode) { + case NOGROUPS: + $mform->addElement('static', 'sessiontypedescription', get_string('sessiontype', 'attendance'), + get_string('commonsession', 'attendance')); + $mform->addHelpButton('sessiontypedescription', 'sessiontype', 'attendance'); + $mform->addElement('hidden', 'sessiontype', mod_attendance_structure::SESSION_COMMON); + $mform->setType('sessiontype', PARAM_INT); + break; + case SEPARATEGROUPS: + $mform->addElement('static', 'sessiontypedescription', get_string('sessiontype', 'attendance'), + get_string('groupsession', 'attendance')); + $mform->addHelpButton('sessiontypedescription', 'sessiontype', 'attendance'); + $mform->addElement('hidden', 'sessiontype', mod_attendance_structure::SESSION_GROUP); + $mform->setType('sessiontype', PARAM_INT); + break; + case VISIBLEGROUPS: + $radio = array(); + $radio[] = &$mform->createElement('radio', 'sessiontype', '', get_string('commonsession', 'attendance'), + mod_attendance_structure::SESSION_COMMON); + $radio[] = &$mform->createElement('radio', 'sessiontype', '', get_string('groupsession', 'attendance'), + mod_attendance_structure::SESSION_GROUP); + $mform->addGroup($radio, 'sessiontype', get_string('sessiontype', 'attendance'), ' ', false); + $mform->setType('sessiontype', PARAM_INT); + $mform->addHelpButton('sessiontype', 'sessiontype', 'attendance'); + $mform->setDefault('sessiontype', mod_attendance_structure::SESSION_COMMON); + break; + } + if ($groupmode == SEPARATEGROUPS or $groupmode == VISIBLEGROUPS) { + if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $modcontext)) { + $groups = groups_get_all_groups ($course->id, $USER->id, $cm->groupingid); + } else { + $groups = groups_get_all_groups($course->id, 0, $cm->groupingid); + } + if ($groups) { + $selectgroups = array(); + foreach ($groups as $group) { + $selectgroups[$group->id] = $group->name; + } + $select = &$mform->addElement('select', 'groups', get_string('groups', 'group'), $selectgroups); + $select->setMultiple(true); + $mform->disabledIf('groups', 'sessiontype', 'eq', mod_attendance_structure::SESSION_COMMON); + } else { + if ($groupmode == VISIBLEGROUPS) { + $mform->updateElementAttr($radio, array('disabled' => 'disabled')); + } + $mform->addElement('static', 'groups', get_string('groups', 'group'), + get_string('nogroups', 'attendance')); + if ($groupmode == SEPARATEGROUPS) { + return; + } + } + } + + attendance_form_sessiondate_selector($mform); + + // Select which status set to use. + $maxstatusset = attendance_get_max_statusset($this->_customdata['att']->id); + if ($maxstatusset > 0) { + $opts = array(); + for ($i = 0; $i <= $maxstatusset; $i++) { + $opts[$i] = attendance_get_setname($this->_customdata['att']->id, $i); + } + $mform->addElement('select', 'statusset', get_string('usestatusset', 'mod_attendance'), $opts); + } else { + $mform->addElement('hidden', 'statusset', 0); + $mform->setType('statusset', PARAM_INT); + } + + $mform->addElement('editor', 'sdescription', get_string('description', 'attendance'), array('rows' => 1, 'columns' => 80), + array('maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean' => true, 'context' => $modcontext)); + $mform->setType('sdescription', PARAM_RAW); + + if (!empty($pluginconfig->enablecalendar)) { + $mform->addElement('checkbox', 'calendarevent', '', get_string('calendarevent', 'attendance')); + $mform->addHelpButton('calendarevent', 'calendarevent', 'attendance'); + if (isset($pluginconfig->calendarevent_default)) { + $mform->setDefault('calendarevent', $pluginconfig->calendarevent_default); + } + } else { + $mform->addElement('hidden', 'calendarevent', 0); + $mform->setType('calendarevent', PARAM_INT); + } + + // If warnings allow selector for reporting. + if (!empty(get_config('attendance', 'enablewarnings'))) { + $mform->addElement('checkbox', 'absenteereport', '', get_string('includeabsentee', 'attendance')); + $mform->addHelpButton('absenteereport', 'includeabsentee', 'attendance'); + if (isset($pluginconfig->absenteereport_default)) { + $mform->setDefault('absenteereport', $pluginconfig->absenteereport_default); + } + } else { + $mform->addElement('hidden', 'absenteereport', 1); + $mform->setType('absenteereport', PARAM_INT); + } + // For multiple sessions. + $mform->addElement('header', 'headeraddmultiplesessions', get_string('addmultiplesessions', 'attendance')); + if (!empty($pluginconfig->multisessionexpanded)) { + $mform->setExpanded('headeraddmultiplesessions'); + } + $mform->addElement('checkbox', 'addmultiply', '', get_string('repeatasfollows', 'attendance')); + $mform->addHelpButton('addmultiply', 'createmultiplesessions', 'attendance'); + + $sdays = array(); + if ($CFG->calendar_startwday === '0') { // Week start from sunday. + $sdays[] =& $mform->createElement('checkbox', 'Sun', '', get_string('sunday', 'calendar')); + } + $sdays[] =& $mform->createElement('checkbox', 'Mon', '', get_string('monday', 'calendar')); + $sdays[] =& $mform->createElement('checkbox', 'Tue', '', get_string('tuesday', 'calendar')); + $sdays[] =& $mform->createElement('checkbox', 'Wed', '', get_string('wednesday', 'calendar')); + $sdays[] =& $mform->createElement('checkbox', 'Thu', '', get_string('thursday', 'calendar')); + $sdays[] =& $mform->createElement('checkbox', 'Fri', '', get_string('friday', 'calendar')); + $sdays[] =& $mform->createElement('checkbox', 'Sat', '', get_string('saturday', 'calendar')); + if ($CFG->calendar_startwday !== '0') { // Week start from sunday. + $sdays[] =& $mform->createElement('checkbox', 'Sun', '', get_string('sunday', 'calendar')); + } + $mform->addGroup($sdays, 'sdays', get_string('repeaton', 'attendance'), array(' '), true); + $mform->disabledIf('sdays', 'addmultiply', 'notchecked'); + + $period = array(1 => 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36); + $periodgroup = array(); + $periodgroup[] =& $mform->createElement('select', 'period', '', $period, false, true); + $periodgroup[] =& $mform->createElement('static', 'perioddesc', '', get_string('week', 'attendance')); + $mform->addGroup($periodgroup, 'periodgroup', get_string('repeatevery', 'attendance'), array(' '), false); + $mform->disabledIf('periodgroup', 'addmultiply', 'notchecked'); + + $mform->addElement('date_selector', 'sessionenddate', get_string('repeatuntil', 'attendance')); + $mform->disabledIf('sessionenddate', 'addmultiply', 'notchecked'); + + $mform->addElement('hidden', 'coursestartdate', $course->startdate); + $mform->setType('coursestartdate', PARAM_INT); + + $mform->addElement('hidden', 'previoussessiondate', 0); + $mform->setType('previoussessiondate', PARAM_INT); + + // Students can mark own attendance. + $studentscanmark = get_config('attendance', 'studentscanmark'); + + $mform->addElement('header', 'headerstudentmarking', get_string('studentmarking', 'attendance'), true); + if (!empty($pluginconfig->studentrecordingexpanded)) { + $mform->setExpanded('headerstudentmarking'); + } + if (!empty($studentscanmark)) { + $mform->addElement('checkbox', 'studentscanmark', '', get_string('studentscanmark', 'attendance')); + $mform->addHelpButton('studentscanmark', 'studentscanmark', 'attendance'); + } else { + $mform->addElement('hidden', 'studentscanmark', '0'); + $mform->settype('studentscanmark', PARAM_INT); + } + + $options = attendance_get_automarkoptions(); + + $mform->addElement('select', 'automark', get_string('automark', 'attendance'), $options); + $mform->setType('automark', PARAM_INT); + $mform->addHelpButton('automark', 'automark', 'attendance'); + $mform->setDefault('automark', $this->_customdata['att']->automark); + + if (!empty($studentscanmark)) { + $mgroup = array(); + + $mgroup[] = & $mform->createElement('text', 'studentpassword', get_string('studentpassword', 'attendance')); + $mform->disabledif('studentpassword', 'rotateqrcode', 'checked'); + $mgroup[] = & $mform->createElement('checkbox', 'randompassword', '', get_string('randompassword', 'attendance')); + $mform->disabledif('randompassword', 'rotateqrcode', 'checked'); + $mgroup[] = & $mform->createElement('checkbox', 'includeqrcode', '', get_string('includeqrcode', 'attendance')); + $mform->disabledif('includeqrcode', 'rotateqrcode', 'checked'); + + $mform->addGroup($mgroup, 'passwordgrp', get_string('passwordgrp', 'attendance'), array(' '), false); + + $mform->setType('studentpassword', PARAM_TEXT); + $mform->addHelpButton('passwordgrp', 'passwordgrp', 'attendance'); + + $mform->addElement('checkbox', 'rotateqrcode', '', get_string('rotateqrcode', 'attendance')); + $mform->hideif('rotateqrcode', 'studentscanmark', 'notchecked'); + + $mform->hideif('passwordgrp', 'studentscanmark', 'notchecked'); + $mform->hideif('studentpassword', 'randompassword', 'checked'); + $mform->hideif('passwordgrp', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); + + $mform->addElement('checkbox', 'autoassignstatus', '', get_string('autoassignstatus', 'attendance')); + $mform->addHelpButton('autoassignstatus', 'autoassignstatus', 'attendance'); + $mform->hideif('autoassignstatus', 'studentscanmark', 'notchecked'); + if (isset($pluginconfig->autoassignstatus)) { + $mform->setDefault('autoassignstatus', $pluginconfig->autoassignstatus); + } + if (isset($pluginconfig->studentscanmark_default)) { + $mform->setDefault('studentscanmark', $pluginconfig->studentscanmark_default); + } + if (isset($pluginconfig->randompassword_default)) { + $mform->setDefault('randompassword', $pluginconfig->randompassword_default); + } + if (isset($pluginconfig->includeqrcode_default)) { + $mform->setDefault('includeqrcode', $pluginconfig->includeqrcode_default); + } + if (isset($pluginconfig->rotateqrcode_default)) { + $mform->setDefault('rotateqrcode', $pluginconfig->rotateqrcode_default); + } + if (isset($pluginconfig->automark_default)) { + $mform->setDefault('automark', $pluginconfig->automark_default); + } + } + $mgroup2 = array(); + $mgroup2[] = & $mform->createElement('text', 'subnet', get_string('requiresubnet', 'attendance')); + if (empty(get_config('attendance', 'subnetactivitylevel'))) { + $mform->setDefault('subnet', get_config('attendance', 'subnet')); + } else { + $mform->setDefault('subnet', $this->_customdata['att']->subnet); + } + + $mgroup2[] = & $mform->createElement('checkbox', 'usedefaultsubnet', get_string('usedefaultsubnet', 'attendance')); + $mform->setDefault('usedefaultsubnet', 1); + $mform->setType('subnet', PARAM_TEXT); + + $mform->addGroup($mgroup2, 'subnetgrp', get_string('requiresubnet', 'attendance'), array(' '), false); + $mform->setAdvanced('subnetgrp'); + $mform->addHelpButton('subnetgrp', 'requiresubnet', 'attendance'); + $mform->hideif('subnet', 'usedefaultsubnet', 'checked'); + + $mgroup3 = array(); + $options = attendance_get_sharedipoptions(); + $mgroup3[] = & $mform->createElement('select', 'preventsharedip', + get_string('preventsharedip', 'attendance'), $options); + $mgroup3[] = & $mform->createElement('text', 'preventsharediptime', + get_string('preventsharediptime', 'attendance'), '', 'test'); + $mform->addGroup($mgroup3, 'preventsharedgroup', get_string('preventsharedip', 'attendance'), array(' '), false); + $mform->addHelpButton('preventsharedgroup', 'preventsharedip', 'attendance'); + $mform->setAdvanced('preventsharedgroup'); + $mform->setType('preventsharedip', PARAM_INT); + $mform->setType('preventsharediptime', PARAM_INT); + + if (isset($pluginconfig->preventsharedip)) { + $mform->setDefault('preventsharedip', $pluginconfig->preventsharedip); + } + if (isset($pluginconfig->preventsharediptime)) { + $mform->setDefault('preventsharediptime', $pluginconfig->preventsharediptime); + } + + $this->add_action_buttons(true, get_string('add', 'attendance')); + } + + /** + * Perform minimal validation on the settings form + * @param array $data + * @param array $files + */ + public function validation($data, $files) { + global $DB; + $errors = parent::validation($data, $files); + + $sesstarttime = $data['sestime']['starthour'] * HOURSECS + $data['sestime']['startminute'] * MINSECS; + $sesendtime = $data['sestime']['endhour'] * HOURSECS + $data['sestime']['endminute'] * MINSECS; + if ($sesendtime < $sesstarttime) { + $errors['sestime'] = get_string('invalidsessionendtime', 'attendance'); + } + + if (!empty($data['addmultiply']) && $data['sessiondate'] != 0 && $data['sessionenddate'] != 0 && + $data['sessionenddate'] < $data['sessiondate']) { + $errors['sessionenddate'] = get_string('invalidsessionenddate', 'attendance'); + } + + if ($data['sessiontype'] == mod_attendance_structure::SESSION_GROUP and empty($data['groups'])) { + $errors['groups'] = get_string('errorgroupsnotselected', 'attendance'); + } + + $addmulti = isset($data['addmultiply']) ? (int)$data['addmultiply'] : 0; + if (($addmulti != 0) && (!array_key_exists('sdays', $data) || empty($data['sdays']))) { + $data['sdays'] = array(); + $errors['sdays'] = get_string('required', 'attendance'); + } + if (isset($data['sdays'])) { + if (!$this->checkweekdays($data['sessiondate'], $data['sessionenddate'], $data['sdays']) ) { + $errors['sdays'] = get_string('checkweekdays', 'attendance'); + } + } + if ($addmulti && ceil(($data['sessionenddate'] - $data['sessiondate']) / YEARSECS) > 1) { + $errors['sessionenddate'] = get_string('timeahead', 'attendance'); + } + $sessstart = $data['sessiondate'] + $sesstarttime; + if ($sessstart < $data['coursestartdate'] && $sessstart != $data['previoussessiondate']) { + $errors['sessiondate'] = get_string('priorto', 'attendance', + userdate($data['coursestartdate'], get_string('strftimedmyhm', 'attendance'))); + $this->_form->setConstant('previoussessiondate', $sessstart); + } + + if (!empty($data['studentscanmark']) && $data['automark'] == ATTENDANCE_AUTOMARK_CLOSE) { + $cm = $this->_customdata['cm']; + // Check that the selected statusset has a status to use when unmarked. + $sql = 'SELECT id + FROM {attendance_statuses} + WHERE deleted = 0 AND (attendanceid = 0 or attendanceid = ?) + AND setnumber = ? AND setunmarked = 1'; + $params = array($cm->instance, $data['statusset']); + if (!$DB->record_exists_sql($sql, $params)) { + $errors['automark'] = get_string('noabsentstatusset', 'attendance'); + } + } + + if (!empty($data['studentscanmark']) && !empty($data['preventsharedip']) && + empty($data['preventsharediptime'])) { + $errors['preventsharedgroup'] = get_string('iptimemissing', 'attendance'); + + } + return $errors; + } + + /** + * Check weekdays function. + * @param int $sessiondate + * @param int $sessionenddate + * @param int $sdays + * @return bool + */ + private function checkweekdays($sessiondate, $sessionenddate, $sdays) { + + $found = false; + + $daysofweek = array(0 => "Sun", 1 => "Mon", 2 => "Tue", 3 => "Wed", 4 => "Thu", 5 => "Fri", 6 => "Sat"); + $start = new DateTime( date("Y-m-d", $sessiondate) ); + $interval = new DateInterval('P1D'); + $end = new DateTime( date("Y-m-d", $sessionenddate) ); + $end->add( new DateInterval('P1D') ); + + $period = new DatePeriod($start, $interval, $end); + foreach ($period as $date) { + if (!$found) { + foreach ($sdays as $name => $value) { + $key = array_search($name, $daysofweek); + if ($date->format("w") == $key) { + $found = true; + break; + } + } + } + } + + return $found; + } +} diff --git a/mod/attendance/classes/form/addwarning.php b/mod/attendance/classes/form/addwarning.php new file mode 100644 index 0000000..45a63f5 --- /dev/null +++ b/mod/attendance/classes/form/addwarning.php @@ -0,0 +1,118 @@ +<?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/>. + +/** + * Contains class addwarning + * + * @package mod_attendance + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +use moodleform; +use context_course; + +/** + * Class addwarning + * + * @package mod_attendance + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class addwarning extends moodleform { + /** + * Form definition + */ + public function definition() { + global $COURSE; + $mform = $this->_form; + + // Load global defaults. + $config = get_config('attendance'); + + $options = array(); + for ($i = 1; $i <= 100; $i++) { + $options[$i] = "$i%"; + } + $mform->addElement('select', 'warningpercent', get_string('warningpercent', 'mod_attendance'), $options); + $mform->addHelpButton('warningpercent', 'warningpercent', 'mod_attendance'); + $mform->setType('warningpercent', PARAM_INT); + $mform->setDefault('warningpercent', $config->warningpercent); + + $options = array(); + for ($i = 1; $i <= ATTENDANCE_MAXWARNAFTER; $i++) { + $options[$i] = "$i"; + } + $mform->addElement('select', 'warnafter', get_string('warnafter', 'mod_attendance'), $options); + $mform->addHelpButton('warnafter', 'warnafter', 'mod_attendance'); + $mform->setType('warnafter', PARAM_INT); + $mform->setDefault('warnafter', $config->warnafter); + + $mform->addElement('select', 'maxwarn', get_string('maxwarn', 'mod_attendance'), $options); + $mform->addHelpButton('maxwarn', 'maxwarn', 'mod_attendance'); + $mform->setType('maxwarn', PARAM_INT); + $mform->setDefault('maxwarn', $config->maxwarn); + + $mform->addElement('checkbox', 'emailuser', get_string('emailuser', 'mod_attendance')); + $mform->addHelpButton('emailuser', 'emailuser', 'mod_attendance'); + $mform->setDefault('emailuser', $config->emailuser); + + $mform->addElement('text', 'emailsubject', get_string('emailsubject', 'mod_attendance'), array('size' => '64')); + $mform->setType('emailsubject', PARAM_TEXT); + $mform->addRule('emailsubject', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('emailsubject', 'emailsubject', 'mod_attendance'); + $mform->setDefault('emailsubject', $config->emailsubject); + + $mform->addElement('editor', 'emailcontent', get_string('emailcontent', 'mod_attendance'), null, null); + $mform->setDefault('emailcontent', array('text' => format_text($config->emailcontent))); + $mform->setType('emailcontent', PARAM_RAW); + $mform->addHelpButton('emailcontent', 'emailcontent', 'mod_attendance'); + + $users = get_users_by_capability(context_course::instance($COURSE->id), 'mod/attendance:warningemails'); + $options = array(); + foreach ($users as $user) { + $options[$user->id] = fullname($user); + } + + $select = $mform->addElement('searchableselector', 'thirdpartyemails', + get_string('thirdpartyemails', 'mod_attendance'), $options); + $mform->setType('thirdpartyemails', PARAM_TEXT); + $mform->addHelpButton('thirdpartyemails', 'thirdpartyemails', 'mod_attendance'); + $select->setMultiple(true); + + // Need to set hidden elements when adding default options. + $mform->addElement('hidden', 'idnumber', 0); // Default options use 0 as the idnumber. + $mform->setType('idnumber', PARAM_INT); + + $mform->addElement('hidden', 'notid', 0); // The id of warning record. + $mform->setType('notid', PARAM_INT); + + $mform->addElement('hidden', 'id', $this->_customdata['id']); // The id of course module record if attendance level. + $mform->setType('id', PARAM_INT); + + if (!empty($this->_customdata['notid'])) { + $btnstring = get_string('update', 'attendance'); + } else { + $btnstring = get_string('add', 'attendance'); + } + $this->add_action_buttons(true, $btnstring); + + } +} \ No newline at end of file diff --git a/mod/attendance/classes/form/duration.php b/mod/attendance/classes/form/duration.php new file mode 100644 index 0000000..f6efd66 --- /dev/null +++ b/mod/attendance/classes/form/duration.php @@ -0,0 +1,77 @@ +<?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/>. + +/** + * This file contains the forms for duration + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +use moodleform; + +/** + * class for displaying duration form. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class duration extends moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + + $mform =& $this->_form; + + $cm = $this->_customdata['cm']; + $ids = $this->_customdata['ids']; + + $mform->addElement('header', 'general', get_string('changeduration', 'attendance')); + $mform->addElement('static', 'count', get_string('countofselected', 'attendance'), count(explode('_', $ids))); + + for ($i = 0; $i <= 23; $i++) { + $hours[$i] = sprintf("%02d", $i); + } + for ($i = 0; $i < 60; $i += 5) { + $minutes[$i] = sprintf("%02d", $i); + } + $durselect[] =& $mform->createElement('select', 'hours', '', $hours); + $durselect[] =& $mform->createElement('select', 'minutes', '', $minutes, false, true); + $mform->addGroup($durselect, 'durtime', get_string('newduration', 'attendance'), array(' '), true); + + $mform->addElement('hidden', 'ids', $ids); + $mform->setType('ids', PARAM_ALPHANUMEXT); + $mform->addElement('hidden', 'id', $cm->id); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'action', \mod_attendance_sessions_page_params::ACTION_CHANGE_DURATION); + $mform->setType('action', PARAM_INT); + + $mform->setDefaults(array('durtime' => array('hours' => 0, 'minutes' => 0))); + + $submitstring = get_string('update', 'attendance'); + $this->add_action_buttons(true, $submitstring); + } + +} diff --git a/mod/attendance/classes/form/export.php b/mod/attendance/classes/form/export.php new file mode 100644 index 0000000..d3f51a0 --- /dev/null +++ b/mod/attendance/classes/form/export.php @@ -0,0 +1,179 @@ +<?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/>. + +/** + * Export attendance sessions forms + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +/** + * class for displaying export form. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class export extends \moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + global $USER, $DB, $PAGE, $CFG; + $mform =& $this->_form; + $course = $this->_customdata['course']; + $cm = $this->_customdata['cm']; + $modcontext = $this->_customdata['modcontext']; + + $mform->addElement('header', 'general', get_string('export', 'attendance')); + + $groupmode = groups_get_activity_groupmode($cm, $course); + $groups = groups_get_activity_allowed_groups($cm, $USER->id); + if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { + $grouplist[0] = get_string('allparticipants'); + } + if ($groups) { + foreach ($groups as $group) { + $grouplist[$group->id] = $group->name; + } + } + + // Restrict the export to the selected users. + $namefields = get_all_user_name_fields(true, 'u'); + $allusers = get_enrolled_users($modcontext, 'mod/attendance:canbelisted', 0, 'u.id,'.$namefields); + $userlist = array(); + foreach ($allusers as $user) { + $userlist[$user->id] = fullname($user); + } + unset($allusers); + $tempusers = $DB->get_records('attendance_tempusers', array('courseid' => $course->id), 'studentid, fullname'); + foreach ($tempusers as $user) { + $userlist[$user->studentid] = $user->fullname; + } + if (empty($userlist)) { + $mform->addElement('static', 'nousers', '', get_string('noattendanceusers', 'attendance')); + return; + } + + list($gsql, $gparams) = $DB->get_in_or_equal(array_keys($grouplist), SQL_PARAMS_NAMED); + list($usql, $uparams) = $DB->get_in_or_equal(array_keys($userlist), SQL_PARAMS_NAMED); + $params = array_merge($gparams, $uparams); + $groupmembers = $DB->get_recordset_select('groups_members', "groupid {$gsql} AND userid {$usql}", $params, + '', 'groupid, userid'); + $groupmappings = array(); + foreach ($groupmembers as $groupmember) { + if (!isset($groupmappings[$groupmember->groupid])) { + $groupmappings[$groupmember->groupid] = array(); + } + $groupmappings[$groupmember->groupid][$groupmember->userid] = $userlist[$groupmember->userid]; + } + if (isset($grouplist[0])) { + $groupmappings[0] = $userlist; + } + + $mform->addElement('select', 'group', get_string('group'), $grouplist); + + $mform->addElement('selectyesno', 'selectedusers', get_string('onlyselectedusers', 'mod_attendance')); + $sel = $mform->addElement('select', 'users', get_string('users', 'mod_attendance'), $userlist, array('size' => 12)); + $sel->setMultiple(true); + $mform->disabledIf('users', 'selectedusers', 'eq', 0); + + $opts = array('groupmappings' => $groupmappings); + $PAGE->requires->yui_module('moodle-mod_attendance-groupfilter', 'M.mod_attendance.groupfilter.init', array($opts)); + + $ident = array(); + $checkedfields = array(); + + $adminsetfields = get_config('attendance', 'customexportfields'); + if (in_array('id', explode(',', $adminsetfields))) { + $ident[] =& $mform->createElement('checkbox', 'id', '', get_string('studentid', 'attendance')); + $checkedfields['ident[id]'] = true; + } + + $extrafields = get_extra_user_fields($modcontext); + foreach ($extrafields as $field) { + $ident[] =& $mform->createElement('checkbox', $field, '', get_string( $field)); + $mform->setType($field, PARAM_NOTAGS); + $checkedfields['ident['. $field .']'] = true; + } + + require_once($CFG->dirroot . '/user/profile/lib.php'); + $customfields = profile_get_custom_fields(); + + foreach ($customfields as $field) { + if ((is_siteadmin($USER) || $field->visible == PROFILE_VISIBLE_ALL) + && in_array($field->shortname, explode(',', $adminsetfields))) { + $ident[] =& $mform->createElement('checkbox', $field->shortname, '', + format_string($field->name, true, array('context' => $modcontext))); + $mform->setType($field->shortname, PARAM_NOTAGS); + $checkedfields['ident['. $field->shortname .']'] = true; + } + } + + if (count($ident) > 0) { + $mform->addGroup($ident, 'ident', get_string('identifyby', 'attendance'), array('<br />'), true); + $mform->setDefaults($checkedfields); + } + $mform->setType('id', PARAM_INT); + + $mform->addElement('checkbox', 'includeallsessions', get_string('includeall', 'attendance'), get_string('yes')); + $mform->setDefault('includeallsessions', true); + $mform->addElement('checkbox', 'includenottaken', get_string('includenottaken', 'attendance'), get_string('yes')); + $mform->addElement('checkbox', 'includeremarks', get_string('includeremarks', 'attendance'), get_string('yes')); + $mform->addElement('checkbox', 'includedescription', get_string('includedescription', 'attendance'), get_string('yes')); + $mform->addElement('date_selector', 'sessionstartdate', get_string('startofperiod', 'attendance')); + $mform->setDefault('sessionstartdate', $course->startdate); + $mform->disabledIf('sessionstartdate', 'includeallsessions', 'checked'); + $mform->addElement('date_selector', 'sessionenddate', get_string('endofperiod', 'attendance')); + $mform->disabledIf('sessionenddate', 'includeallsessions', 'checked'); + + $formatoptions = array('excel' => get_string('downloadexcel', 'attendance'), + 'ooo' => get_string('downloadooo', 'attendance'), + 'text' => get_string('downloadtext', 'attendance')); + $mform->addElement('select', 'format', get_string('format'), $formatoptions); + + $submitstring = get_string('ok'); + $this->add_action_buttons(false, $submitstring); + + $mform->addElement('hidden', 'id', $cm->id); + } + + /** + * Validate form. + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + // Validate the 'users' field. + if ($data['selectedusers'] && empty($data['users'])) { + $errors['users'] = get_string('mustselectusers', 'mod_attendance'); + } + + return $errors; + } +} + diff --git a/mod/attendance/classes/form/import/marksessions.php b/mod/attendance/classes/form/import/marksessions.php new file mode 100644 index 0000000..c8e5690 --- /dev/null +++ b/mod/attendance/classes/form/import/marksessions.php @@ -0,0 +1,97 @@ +<?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/>. + +/** + * This file contains the form used to upload a csv attendance file to automatically update attendance records. + * + * @package mod_attendance + * @copyright 2019 Jonathan Chan <jonathan.chan@sta.uwi.edu> + * @copyright based on work by 2012 NetSpot {@link http://www.netspot.com.au} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace mod_attendance\form\import; + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + +use core_text; +use moodleform; +require_once($CFG->libdir.'/formslib.php'); + +/** + * Class for displaying the csv upload form. + * + * @package mod_attendance + * @copyright 2019 Jonathan Chan <jonathan.chan@sta.uwi.edu> + * @copyright based on work by 2012 NetSpot {@link http://www.netspot.com.au} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class marksessions extends moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + global $COURSE; + + $mform = $this->_form; + $params = $this->_customdata; + + $mform->addElement('header', 'uploadattendance', get_string('uploadattendance', 'attendance')); + + $fileoptions = array('subdirs' => 0, + 'maxbytes' => $COURSE->maxbytes, + 'accepted_types' => 'csv', + 'maxfiles' => 1); + + $mform->addElement('filepicker', 'attendancefile', get_string('uploadafile'), null, $fileoptions); + $mform->addRule('attendancefile', get_string('uploadnofilefound'), 'required', null, 'client'); + $mform->addHelpButton('attendancefile', 'attendancefile', 'attendance'); + + $encodings = core_text::get_encodings(); + $mform->addElement('select', 'encoding', get_string('encoding', 'grades'), $encodings); + $mform->addHelpButton('encoding', 'encoding', 'grades'); + + $radio = array(); + $radio[] = $mform->createElement('radio', 'separator', null, get_string('septab', 'grades'), 'tab'); + $radio[] = $mform->createElement('radio', 'separator', null, get_string('sepcomma', 'grades'), 'comma'); + $radio[] = $mform->createElement('radio', 'separator', null, get_string('sepcolon', 'grades'), 'colon'); + $radio[] = $mform->createElement('radio', 'separator', null, get_string('sepsemicolon', 'grades'), 'semicolon'); + $mform->addGroup($radio, 'separator', get_string('separator', 'grades'), ' ', false); + $mform->addHelpButton('separator', 'separator', 'grades'); + $mform->setDefault('separator', 'comma'); + + $mform->addElement('hidden', 'id', $params['id']); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'sessionid', $params['sessionid']); + $mform->setType('sessionid', PARAM_INT); + $mform->addElement('hidden', 'grouptype', $params['grouptype']); + $mform->setType('grouptype', PARAM_INT); + $mform->addElement('hidden', 'confirm', 0); + $mform->setType('confirm', PARAM_BOOL); + $this->add_action_buttons(true, get_string('uploadattendance', 'attendance')); + } + /** + * Display an error on the import form. + * + * @param string $msg + */ + public function set_import_error($msg) { + $mform = $this->_form; + + $mform->setElementError('attendancefile', $msg); + } +} diff --git a/mod/attendance/classes/form/import/marksessions_confirm.php b/mod/attendance/classes/form/import/marksessions_confirm.php new file mode 100644 index 0000000..b0805bc --- /dev/null +++ b/mod/attendance/classes/form/import/marksessions_confirm.php @@ -0,0 +1,132 @@ +<?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/>. + +/** + * This file contains the form used to upload a csv attendance file to automatically update attendance records. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace mod_attendance\form\import; + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + +use core_text; +use moodleform; +require_once($CFG->libdir.'/formslib.php'); + +/** + * Mark attendance sessions confirm csv upload. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class marksessions_confirm extends moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + $params = $this->_customdata; + $importer = $this->_customdata['importer']; + + $mform = $this->_form; + $mform->addElement('hidden', 'confirm', 1); + $mform->setType('confirm', PARAM_BOOL); + + $foundheaders = $importer->list_found_headers(); + + // Add user mapping. + $mform->addElement('select', 'userfrom', get_string('userimportfield', 'attendance'), $foundheaders); + $mform->addHelpButton('userfrom', 'userimportfield', 'attendance'); + // This allows the user to choose which field in the user database the identifying column will map to. + $useroptions = array( + 'userid' => get_string('userid', 'attendance'), + 'username' => get_string('username'), + 'idnumber' => get_string('idnumber'), + 'email' => get_string('email') + ); + $mform->addElement('select', 'userto', get_string('userimportto', 'attendance'), $useroptions); + + // Check if we can set an easy default value. + foreach (array_keys($useroptions) as $o) { + if (in_array($o, $foundheaders)) { + $mform->setDefault('userto', $o); + $mform->setDefault('userfrom', $o); + break; + } + } + + $mform->addHelpButton('userto', 'userimportto', 'attendance'); + + // Below options need a "none" option in the headers. + $foundheaders[- 1] = get_string('notset', 'mod_attendance'); + ksort($foundheaders); + + // Add scan time mapping. + $mform->addElement('select', 'scantime', get_string('scantime', 'attendance'), $foundheaders); + $mform->addHelpButton('scantime', 'scantime', 'attendance'); + $mform->setDefault('scantime', -1); + + // Add status mapping. + $mform->addElement('select', 'status', get_string('importstatus', 'attendance'), $foundheaders); + $mform->addHelpButton('status', 'importstatus', 'attendance'); + $mform->disabledif('status', 'scantime', 'noteq', -1); + $mform->disabledif('scantime', 'status', 'noteq', -1); + + // Try to set a useful default value for scantime or status. + $key = array_search('status', $foundheaders); + + if ($key !== false) { + // Status is passed in CSV - set that as default. + $mform->setDefault('status', $key); + $mform->setDefault('scantime', -1); + } else { + $keyscan = array_search('scantime', $foundheaders); + if ($keyscan !== false) { + // The Scantime var exists in the csv. + $mform->setDefault('status', -1); + $mform->setDefault('scantime', $keyscan); + } else { + $mform->setDefault('status', -1); + $mform->setDefault('scantime', -1); + } + + } + foreach (array_keys($useroptions) as $o) { + if (in_array($o, $foundheaders)) { + $mform->setDefault('userto', $o); + $mform->setDefault('userfrom', $o); + break; + } + } + + $mform->addElement('hidden', 'id', $params['id']); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'sessionid', $params['sessionid']); + $mform->setType('sessionid', PARAM_INT); + $mform->addElement('hidden', 'grouptype', $params['grouptype']); + $mform->setType('grouptype', PARAM_INT); + $mform->addElement('hidden', 'importid', $importer->get_importid()); + $mform->setType('importid', PARAM_INT); + $mform->setConstant('importid', $importer->get_importid()); + + $this->add_action_buttons(true, get_string('uploadattendance', 'attendance')); + } +} diff --git a/mod/attendance/classes/form/import/sessions.php b/mod/attendance/classes/form/import/sessions.php new file mode 100644 index 0000000..d35ed90 --- /dev/null +++ b/mod/attendance/classes/form/import/sessions.php @@ -0,0 +1,85 @@ +<?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/>. + +/** + * This file contains the form for importing sessions from a file. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\form\import; + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + +use core_text; +use csv_import_reader; +use moodleform; +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/csvlib.class.php'); + +/** + * Import attendance sessions. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sessions extends moodleform { + + /** + * Define the form - called by parent constructor + */ + public function definition() { + + $mform = $this->_form; + $element = $mform->createElement('filepicker', 'importfile', get_string('importfile', 'mod_attendance')); + $mform->addElement($element); + $mform->addHelpButton('importfile', 'importfile', 'mod_attendance'); + $mform->addRule('importfile', null, 'required'); + $mform->addElement('hidden', 'confirm', 0); + $mform->setType('confirm', PARAM_BOOL); + + $choices = csv_import_reader::get_delimiter_list(); + $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'mod_attendance'), $choices); + if (array_key_exists('cfg', $choices)) { + $mform->setDefault('delimiter_name', 'cfg'); + } else if (get_string('listsep', 'langconfig') == ';') { + $mform->setDefault('delimiter_name', 'semicolon'); + } else { + $mform->setDefault('delimiter_name', 'comma'); + } + + $choices = core_text::get_encodings(); + $mform->addElement('select', 'encoding', get_string('encoding', 'mod_attendance'), $choices); + $mform->setDefault('encoding', 'UTF-8'); + + $this->add_action_buttons(false, get_string('import', 'mod_attendance')); + } + + /** + * Display an error on the import form. + * + * @param string $msg + */ + public function set_import_error($msg) { + $mform = $this->_form; + + $mform->setElementError('importfile', $msg); + } +} diff --git a/mod/attendance/classes/form/import/sessions_confirm.php b/mod/attendance/classes/form/import/sessions_confirm.php new file mode 100644 index 0000000..513bfd3 --- /dev/null +++ b/mod/attendance/classes/form/import/sessions_confirm.php @@ -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/>. + +/** + * Import attendance sessions. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\form\import; + +defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); + +use moodleform; +require_once($CFG->libdir . '/formslib.php'); + +/** + * Import attendance sessions. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sessions_confirm extends moodleform { + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $importer = $this->_customdata; + + $mform = $this->_form; + $mform->addElement('hidden', 'confirm', 1); + $mform->setType('confirm', PARAM_BOOL); + $mform->addElement('hidden', 'importid', $importer->get_importid()); + $mform->setType('importid', PARAM_INT); + + $requiredheaders = $importer->list_required_headers(); + $foundheaders = $importer->list_found_headers(); + + if (empty($foundheaders)) { + $foundheaders = range(0, count($requiredheaders)); + } + $foundheaders[- 1] = get_string('none'); + + foreach ($requiredheaders as $index => $requiredheader) { + $mform->addElement('select', 'header' . $index, $requiredheader, $foundheaders); + if (isset($foundheaders[$index])) { + $mform->setDefault('header' . $index, $index); + } else { + $mform->setDefault('header' . $index, - 1); + } + } + + $this->add_action_buttons(true, get_string('confirm', 'mod_attendance')); + } +} diff --git a/mod/attendance/classes/form/studentattendance.php b/mod/attendance/classes/form/studentattendance.php new file mode 100644 index 0000000..eba5302 --- /dev/null +++ b/mod/attendance/classes/form/studentattendance.php @@ -0,0 +1,125 @@ +<?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/>. + +/** + * Student form class. + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class studentattendance + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class studentattendance extends \moodleform { + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + global $USER; + + $mform =& $this->_form; + + $attforsession = $this->_customdata['session']; + $attblock = $this->_customdata['attendance']; + $password = $this->_customdata['password']; + + $statuses = $attblock->get_statuses(); + // Check if user has access to all statuses. + $disabledduetotime = false; + foreach ($statuses as $status) { + if ($status->studentavailability === '0') { + unset($statuses[$status->id]); + } + if (!empty($status->studentavailability) && + time() > $attforsession->sessdate + ($status->studentavailability * 60)) { + unset($statuses[$status->id]); + $disabledduetotime = true; + } + } + + $mform->addElement('hidden', 'sessid', null); + $mform->setType('sessid', PARAM_INT); + $mform->setConstant('sessid', $attforsession->id); + + $mform->addElement('hidden', 'sesskey', null); + $mform->setType('sesskey', PARAM_INT); + $mform->setConstant('sesskey', sesskey()); + + // Set a title as the date and time of the session. + $sesstiontitle = userdate($attforsession->sessdate, get_string('strftimedate')).' ' + .attendance_strftimehm($attforsession->sessdate); + + $mform->addElement('header', 'session', $sesstiontitle); + + // If a session description is set display it. + if (!empty($attforsession->description)) { + $mform->addElement('html', $attforsession->description); + } + if (!empty($attforsession->studentpassword)) { + $mform->addElement('text', 'studentpassword', get_string('password', 'attendance')); + $mform->setType('studentpassword', PARAM_TEXT); + $mform->addRule('studentpassword', get_string('passwordrequired', 'attendance'), 'required'); + $mform->setDefault('studentpassword', $password); + } + if (!$attforsession->autoassignstatus) { + + // Create radio buttons for setting the attendance status. + $radioarray = array(); + foreach ($statuses as $status) { + $name = \html_writer::span($status->description, 'statusdesc'); + $radioarray[] =& $mform->createElement('radio', 'status', '', $name, $status->id, array()); + } + if ($disabledduetotime) { + $warning = \html_writer::span(get_string('somedisabledstatus', 'attendance'), 'somedisabledstatus'); + $radioarray[] =& $mform->createElement('static', '', '', $warning); + } + // Add the radio buttons as a control with the user's name in front. + $radiogroup = $mform->addGroup($radioarray, 'statusarray', fullname($USER).':', array(''), false); + $radiogroup->setAttributes(array('class' => 'statusgroup')); + $mform->addRule('statusarray', get_string('attendancenotset', 'attendance'), 'required', '', 'client', false, false); + } + $this->add_action_buttons(); + } + + /** + * Validate Form. + * + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + $errors = array(); + if (!($this->_customdata['session']->autoassignstatus)) { + // Check if this status is allowed to be set. + if (empty($data['status'])) { + $errors['statusarray'] = get_string('invalidstatus', 'attendance'); + } + } + + return $errors; + } +} \ No newline at end of file diff --git a/mod/attendance/classes/form/tempmerge.php b/mod/attendance/classes/form/tempmerge.php new file mode 100644 index 0000000..8f24c92 --- /dev/null +++ b/mod/attendance/classes/form/tempmerge.php @@ -0,0 +1,72 @@ +<?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/>. + +/** + * Temp merge form class. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Temp merge form class. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tempmerge extends \moodleform { + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + global $COURSE; + + $context = \context_course::instance($COURSE->id); + $namefields = get_all_user_name_fields(true, 'u'); + $students = get_enrolled_users($context, 'mod/attendance:canbelisted', 0, 'u.id,'.$namefields.',u.email', + 'u.lastname, u.firstname', 0, 0, true); + $partarray = array(); + foreach ($students as $student) { + $partarray[$student->id] = fullname($student).' ('.$student->email.')'; + } + + $mform = $this->_form; + $description = $this->_customdata['description']; + + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'userid', 0); + $mform->setType('userid', PARAM_INT); + + $mform->addElement('header', 'attheader', get_string('tempusermerge', 'attendance')); + $mform->addElement('static', 'description', get_string('tempuser', 'attendance'), $description); + + $mform->addElement('select', 'participant', get_string('participant', 'attendance'), $partarray); + + $mform->addElement('static', 'requiredentries', '', get_string('requiredentries', 'attendance')); + $mform->addHelpButton('requiredentries', 'requiredentry', 'attendance'); + + $this->add_action_buttons(true, get_string('mergeuser', 'attendance')); + } +} \ No newline at end of file diff --git a/mod/attendance/classes/form/tempuser.php b/mod/attendance/classes/form/tempuser.php new file mode 100644 index 0000000..4b81181 --- /dev/null +++ b/mod/attendance/classes/form/tempuser.php @@ -0,0 +1,82 @@ +<?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/>. + +/** + * Form for creating temporary users. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tempuser + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tempuser extends \moodleform { + /** + * Define form. + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + + $mform->addElement('header', 'attheader', get_string('tempaddform', 'attendance')); + $mform->addElement('text', 'tname', get_string('tusername', 'attendance')); + $mform->addRule('tname', 'Required', 'required', null, 'client'); + $mform->setType('tname', PARAM_TEXT); + + $mform->addElement('text', 'temail', get_string('tuseremail', 'attendance')); + $mform->addRule('temail', 'Email', 'email', null, 'client'); + $mform->addRule('temail', '', 'callback', null, 'server'); + $mform->setType('temail', PARAM_EMAIL); + + $mform->addElement('submit', 'submitbutton', get_string('adduser', 'attendance')); + $mform->closeHeaderBefore('submit'); + } + + /** + * Do stuff to form after creation. + */ + public function definition_after_data() { + $mform = $this->_form; + $mform->applyFilter('tname', 'trim'); + } + + /** + * Form validation. + * + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if ($err = \mod_attendance_structure::check_existing_email($data['temail'])) { + $errors['temail'] = $err; + } + + return $errors; + } +} diff --git a/mod/attendance/classes/form/tempuseredit.php b/mod/attendance/classes/form/tempuseredit.php new file mode 100644 index 0000000..307f1f8 --- /dev/null +++ b/mod/attendance/classes/form/tempuseredit.php @@ -0,0 +1,89 @@ +<?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/>. + +/** + * Form for editing temporary users. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +/** + * class for displaying tempedit form. + * + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tempuseredit extends \moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + + $mform = $this->_form; + + $mform->addElement('hidden', 'userid', 0); + $mform->setType('userid', PARAM_INT); + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + + $mform->addElement('header', 'attheader', get_string('tempusersedit', 'attendance')); + $mform->addElement('text', 'tname', get_string('tusername', 'attendance')); + $mform->addRule('tname', 'Required', 'required', null, 'client'); + $mform->setType('tname', PARAM_TEXT); + + $mform->addElement('text', 'temail', get_string('tuseremail', 'attendance')); + $mform->addRule('temail', 'Email', 'email', null, 'client'); + $mform->setType('temail', PARAM_EMAIL); + + $buttonarray = array( + $mform->createElement('submit', 'submitbutton', get_string('edituser', 'attendance')), + $mform->createElement('cancel'), + ); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('submit'); + } + + /** + * Apply filter to form + * + */ + public function definition_after_data() { + $mform = $this->_form; + $mform->applyFilter('tname', 'trim'); + } + + /** + * Perform validation on the form + * @param array $data + * @param array $files + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if ($err = \mod_attendance_structure::check_existing_email($data['temail'], $data['userid'])) { + $errors['temail'] = $err; + } + return $errors; + } +} diff --git a/mod/attendance/classes/form/updatesession.php b/mod/attendance/classes/form/updatesession.php new file mode 100644 index 0000000..0dcaf11 --- /dev/null +++ b/mod/attendance/classes/form/updatesession.php @@ -0,0 +1,235 @@ +<?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/>. + +/** + * Update form + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\form; + +defined('MOODLE_INTERNAL') || die(); + +/** + * class for displaying update session form. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class updatesession extends \moodleform { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + + global $DB; + $mform =& $this->_form; + + $modcontext = $this->_customdata['modcontext']; + $sessionid = $this->_customdata['sessionid']; + + if (!$sess = $DB->get_record('attendance_sessions', array('id' => $sessionid) )) { + error('No such session in this course'); + } + $attendancesubnet = $DB->get_field('attendance', 'subnet', array('id' => $sess->attendanceid)); + $defopts = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean' => true, 'context' => $modcontext); + $sess = file_prepare_standard_editor($sess, 'description', $defopts, $modcontext, 'mod_attendance', 'session', $sess->id); + + $starttime = $sess->sessdate - usergetmidnight($sess->sessdate); + $starthour = floor($starttime / HOURSECS); + $startminute = floor(($starttime - $starthour * HOURSECS) / MINSECS); + + $enddate = $sess->sessdate + $sess->duration; + $endtime = $enddate - usergetmidnight($enddate); + $endhour = floor($endtime / HOURSECS); + $endminute = floor(($endtime - $endhour * HOURSECS) / MINSECS); + + $data = array( + 'sessiondate' => $sess->sessdate, + 'sestime' => array('starthour' => $starthour, 'startminute' => $startminute, + 'endhour' => $endhour, 'endminute' => $endminute), + 'sdescription' => $sess->description_editor, + 'calendarevent' => $sess->calendarevent, + 'studentscanmark' => $sess->studentscanmark, + 'studentpassword' => $sess->studentpassword, + 'autoassignstatus' => $sess->autoassignstatus, + 'subnet' => $sess->subnet, + 'automark' => $sess->automark, + 'absenteereport' => $sess->absenteereport, + 'automarkcompleted' => 0, + 'preventsharedip' => $sess->preventsharedip, + 'preventsharediptime' => $sess->preventsharediptime, + 'includeqrcode' => $sess->includeqrcode, + 'rotateqrcode' => $sess->rotateqrcode, + ); + if ($sess->subnet == $attendancesubnet) { + $data['usedefaultsubnet'] = 1; + } else { + $data['usedefaultsubnet'] = 0; + } + + $mform->addElement('header', 'general', get_string('changesession', 'attendance')); + + if ($sess->groupid == 0) { + $strtype = get_string('commonsession', 'attendance'); + } else { + $groupname = $DB->get_field('groups', 'name', array('id' => $sess->groupid)); + $strtype = get_string('group') . ': ' . $groupname; + } + $mform->addElement('static', 'sessiontypedescription', get_string('sessiontype', 'attendance'), $strtype); + + $olddate = construct_session_full_date_time($sess->sessdate, $sess->duration); + $mform->addElement('static', 'olddate', get_string('olddate', 'attendance'), $olddate); + + attendance_form_sessiondate_selector($mform); + + // Show which status set is in use. + $maxstatusset = attendance_get_max_statusset($this->_customdata['att']->id); + if ($maxstatusset > 0) { + $mform->addElement('static', 'statussetstring', get_string('usestatusset', 'mod_attendance'), + attendance_get_setname($this->_customdata['att']->id, $sess->statusset)); + } + $mform->addElement('hidden', 'statusset', $sess->statusset); + $mform->setType('statusset', PARAM_INT); + + $mform->addElement('editor', 'sdescription', get_string('description', 'attendance'), + array('rows' => 1, 'columns' => 80), $defopts); + $mform->setType('sdescription', PARAM_RAW); + + if (!empty(get_config('attendance', 'enablecalendar'))) { + $mform->addElement('checkbox', 'calendarevent', '', get_string('calendarevent', 'attendance')); + $mform->addHelpButton('calendarevent', 'calendarevent', 'attendance'); + } else { + $mform->addElement('hidden', 'calendarevent', 0); + $mform->setType('calendarevent', PARAM_INT); + } + + // If warnings allow selector for reporting. + if (!empty(get_config('attendance', 'enablewarnings'))) { + $mform->addElement('checkbox', 'absenteereport', '', get_string('includeabsentee', 'attendance')); + $mform->addHelpButton('absenteereport', 'includeabsentee', 'attendance'); + } + + // Students can mark own attendance. + $studentscanmark = get_config('attendance', 'studentscanmark'); + + $mform->addElement('header', 'headerstudentmarking', get_string('studentmarking', 'attendance'), true); + $mform->setExpanded('headerstudentmarking'); + if (!empty($studentscanmark)) { + $mform->addElement('checkbox', 'studentscanmark', '', get_string('studentscanmark', 'attendance')); + $mform->addHelpButton('studentscanmark', 'studentscanmark', 'attendance'); + } else { + $mform->addElement('hidden', 'studentscanmark', '0'); + $mform->settype('studentscanmark', PARAM_INT); + } + + $options2 = attendance_get_automarkoptions(); + + $mform->addElement('select', 'automark', get_string('automark', 'attendance'), $options2); + $mform->setType('automark', PARAM_INT); + $mform->addHelpButton('automark', 'automark', 'attendance'); + + if (!empty($studentscanmark)) { + $mform->addElement('text', 'studentpassword', get_string('studentpassword', 'attendance')); + $mform->setType('studentpassword', PARAM_TEXT); + $mform->addHelpButton('studentpassword', 'passwordgrp', 'attendance'); + $mform->disabledif('studentpassword', 'rotateqrcode', 'checked'); + $mform->hideif('studentpassword', 'studentscanmark', 'notchecked'); + $mform->hideif('studentpassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); + $mform->hideif('randompassword', 'automark', 'eq', ATTENDANCE_AUTOMARK_ALL); + $mform->addElement('checkbox', 'includeqrcode', '', get_string('includeqrcode', 'attendance')); + $mform->hideif('includeqrcode', 'studentscanmark', 'notchecked'); + $mform->disabledif('includeqrcode', 'rotateqrcode', 'checked'); + $mform->addElement('checkbox', 'rotateqrcode', '', get_string('rotateqrcode', 'attendance')); + $mform->hideif('rotateqrcode', 'studentscanmark', 'notchecked'); + $mform->addElement('checkbox', 'autoassignstatus', '', get_string('autoassignstatus', 'attendance')); + $mform->addHelpButton('autoassignstatus', 'autoassignstatus', 'attendance'); + $mform->hideif('autoassignstatus', 'studentscanmark', 'notchecked'); + } + + $mgroup = array(); + $mgroup[] = & $mform->createElement('text', 'subnet', get_string('requiresubnet', 'attendance')); + $mform->setDefault('subnet', $this->_customdata['att']->subnet); + $mgroup[] = & $mform->createElement('checkbox', 'usedefaultsubnet', get_string('usedefaultsubnet', 'attendance')); + $mform->setDefault('usedefaultsubnet', 1); + $mform->setType('subnet', PARAM_TEXT); + + $mform->addGroup($mgroup, 'subnetgrp', get_string('requiresubnet', 'attendance'), array(' '), false); + $mform->setAdvanced('subnetgrp'); + $mform->addHelpButton('subnetgrp', 'requiresubnet', 'attendance'); + $mform->hideif('subnet', 'usedefaultsubnet', 'checked'); + + $mform->addElement('hidden', 'automarkcompleted', '0'); + $mform->settype('automarkcompleted', PARAM_INT); + + $mgroup3 = array(); + $options = attendance_get_sharedipoptions(); + $mgroup3[] = & $mform->createElement('select', 'preventsharedip', + get_string('preventsharedip', 'attendance'), $options); + $mgroup3[] = & $mform->createElement('text', 'preventsharediptime', + get_string('preventsharediptime', 'attendance'), '', 'test'); + $mform->addGroup($mgroup3, 'preventsharedgroup', + get_string('preventsharedip', 'attendance'), array(' '), false); + $mform->addHelpButton('preventsharedgroup', 'preventsharedip', 'attendance'); + $mform->setAdvanced('preventsharedgroup'); + $mform->setType('preventsharediptime', PARAM_INT); + + $mform->setDefaults($data); + $this->add_action_buttons(true); + } + + /** + * Perform minimal validation on the settings form + * @param array $data + * @param array $files + */ + public function validation($data, $files) { + global $DB; + $errors = parent::validation($data, $files); + + $sesstarttime = $data['sestime']['starthour'] * HOURSECS + $data['sestime']['startminute'] * MINSECS; + $sesendtime = $data['sestime']['endhour'] * HOURSECS + $data['sestime']['endminute'] * MINSECS; + if ($sesendtime < $sesstarttime) { + $errors['sestime'] = get_string('invalidsessionendtime', 'attendance'); + } + + if (!empty($data['studentscanmark']) && $data['automark'] == ATTENDANCE_AUTOMARK_CLOSE) { + $cm = $this->_customdata['cm']; + // Check that the selected statusset has a status to use when unmarked. + $sql = 'SELECT id + FROM {attendance_statuses} + WHERE deleted = 0 AND (attendanceid = 0 or attendanceid = ?) + AND setnumber = ? AND setunmarked = 1'; + $params = array($cm->instance, $data['statusset']); + if (!$DB->record_exists_sql($sql, $params)) { + $errors['automark'] = get_string('noabsentstatusset', 'attendance'); + } + } + + if (!empty($data['studentscanmark']) && !empty($data['preventsharedip']) && + empty($data['preventsharediptime'])) { + $errors['preventsharedgroup'] = get_string('iptimemissing', 'attendance'); + + } + return $errors; + } +} diff --git a/mod/attendance/classes/header.php b/mod/attendance/classes/header.php new file mode 100644 index 0000000..612e6a3 --- /dev/null +++ b/mod/attendance/classes/header.php @@ -0,0 +1,80 @@ +<?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 definition for mod_attendance_header + * + * @package mod_attendance + * @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net> + * @copyright 2017 Catalyst IT Australia {@link http://www.catalyst-au.net} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Used to render the page header. + * + * @package mod_attendance + * @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net> + * @copyright 2017 Catalyst IT Australia {@link http://www.catalyst-au.net} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_header implements renderable { + /** @var mod_attendance_structure */ + private $attendance; + + /** @var string */ + private $title; + + /** + * mod_attendance_header constructor. + * + * @param mod_attendance_structure $attendance + * @param null $title + */ + public function __construct(mod_attendance_structure $attendance, $title = null) { + $this->attendance = $attendance; + $this->title = $title; + } + + /** + * Gets the attendance data. + * + * @return mod_attendance_structure + */ + public function get_attendance() { + return $this->attendance; + } + + /** + * Gets the title. If title was not provided, use the module name. + * + * @return string + */ + public function get_title() { + return is_null($this->title) ? $this->attendance->name : $this->title; + } + + /** + * Checks if the header should be rendered. + * + * @return bool + */ + public function should_render() { + return !is_null($this->title) || !empty($this->attendance->intro); + } +} diff --git a/mod/attendance/classes/import/marksessions.php b/mod/attendance/classes/import/marksessions.php new file mode 100644 index 0000000..7da34df --- /dev/null +++ b/mod/attendance/classes/import/marksessions.php @@ -0,0 +1,302 @@ +<?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/>. + +/** + * Import attendance sessions class. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\import; + +defined('MOODLE_INTERNAL') || die(); + +use csv_import_reader; +use mod_attendance_notifyqueue; +use mod_attendance_structure; +use stdClass; + +/** + * Import attendance sessions. + * + * @package mod_attendance + * @copyright 2020 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class marksessions { + + /** @var string $error The errors message from reading the xml */ + protected $error = ''; + + /** @var array $sessions The sessions info */ + protected $sessions = array(); + + /** @var array $mappings The mappings info */ + protected $mappings = array(); + + /** @var int The id of the csv import */ + protected $importid = 0; + + /** @var csv_import_reader|null $importer */ + protected $importer = null; + + /** @var array $foundheaders */ + protected $foundheaders = array(); + + /** @var bool $useprogressbar Control whether importing should use progress bars or not. */ + protected $useprogressbar = false; + + /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */ + protected $progress = null; + + /** @var mod_attendance_structure $att - the mod_attendance_structure class */ + private $att; + + /** + * Store an error message for display later + * + * @param string $msg + */ + public function fail($msg) { + $this->error = $msg; + return false; + } + + /** + * Get the CSV import id + * + * @return string The import id. + */ + public function get_importid() { + return $this->importid; + } + + /** + * Get the list of headers found in the import. + * + * @return array The found headers (names from import) + */ + public function list_found_headers() { + return $this->foundheaders; + } + + /** + * Read the data from the mapping form. + * + * @param array $data The mapping data. + */ + protected function read_mapping_data($data) { + if ($data) { + return array( + 'user' => $data->userfrom, + 'scantime' => $data->scantime, + 'status' => $data->status + ); + } else { + return array( + 'user' => 0, + 'scantime' => 1, + 'status' => 2 + ); + } + } + + /** + * Get the a column from the imported data. + * + * @param array $row The imported raw row + * @param int $index The column index we want + * @return string The column data. + */ + protected function get_column_data($row, $index) { + if ($index < 0) { + return ''; + } + return isset($row[$index]) ? $row[$index] : ''; + } + + /** + * Constructor - parses the raw text for sanity. + * + * @param string $text The raw csv text. + * @param mod_attendance_structure $att The current assignment + * @param string $encoding The encoding of the csv file. + * @param string $delimiter The specified delimiter for the file. + * @param string $importid The id of the csv import. + * @param array $mappingdata The mapping data from the import form. + * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI. + */ + public function __construct($text = null, $att, $encoding = null, $delimiter = null, $importid = 0, + $mappingdata = null, $useprogressbar = false) { + global $CFG, $USER; + + require_once($CFG->libdir . '/csvlib.class.php'); + + $type = 'marksessions'; + + $this->att = $att; + + if (! $importid) { + if ($text === null) { + return; + } + $this->importid = csv_import_reader::get_new_iid($type); + + $this->importer = new csv_import_reader($this->importid, $type); + + if (! $this->importer->load_csv_content($text, $encoding, $delimiter)) { + $this->fail(get_string('invalidimportfile', 'attendance')); + $this->importer->cleanup(); + echo $text; + return; + } + } else { + $this->importid = $importid; + + $this->importer = new csv_import_reader($this->importid, $type); + } + + if (! $this->importer->init()) { + $this->fail(get_string('invalidimportfile', 'attendance')); + $this->importer->cleanup(); + return; + } + + $this->foundheaders = $this->importer->get_columns(); + + $this->useprogressbar = $useprogressbar; + + $sesslog = array(); + + $validusers = $this->att->get_users($this->att->pageparams->grouptype, 0); + $users = array(); + + // Re-key validusers based on the identifier used by import. + if (!empty($mappingdata) && $mappingdata->userto !== 'id') { + foreach ($validusers as $u) { + if (!empty($u->{$mappingdata->userto})) { + $users[strtolower($u->{$mappingdata->userto})] = $u; + } + } + } else { + $users = $validusers; + } + + $statuses = $this->att->get_statuses(); + $statusmap = array(); + foreach ($statuses as $st) { + $statusmap[$st->acronym] = $st->id; + } + + $sessioninfo = $this->att->get_session_info($this->att->pageparams->sessionid); + + while ($row = $this->importer->next()) { + // This structure mimics what the UI form returns. + if (empty($mappingdata)) { + // Precheck - just return for now - would be nice to look at adding preview option in future. + return; + } + $mapping = $this->read_mapping_data($mappingdata); + + // Get user. + $extuser = strtolower($this->get_column_data($row, $mapping['user'])); + if (empty($users[$extuser])) { + $a = new \stdClass(); + $a->extuser = $extuser; + $a->userfield = $mappingdata->userto; + \mod_attendance_notifyqueue::notify_problem(get_string('error:usernotfound', 'attendance', $a)); + continue; + } + $userid = $users[$extuser]->id; + if (isset($sesslog[$userid])) { + \mod_attendance_notifyqueue::notify_problem(get_string('error:userduplicate', 'attendance', $extuser)); + continue; + } + $sesslog[$userid] = new stdClass(); + $sesslog[$userid]->studentid = $userid; + $sesslog[$userid]->statusset = $statuses; + $sesslog[$userid]->remarks = ''; + $sesslog[$userid]->sessionid = $this->att->pageparams->sessionid; + $sesslog[$userid]->timetaken = time(); + $sesslog[$userid]->takenby = $USER->id; + + $scantime = $this->get_column_data($row, $mapping['scantime']); + if (!empty($scantime)) { + $t = strtotime($scantime); + if ($t === false) { + $a = new \stdClass(); + $a->extuser = $extuser; + $a->scantime = $scantime; + \mod_attendance_notifyqueue::notify_problem(get_string('error:timenotreadable', 'attendance', $a)); + continue; + } + + $sesslog[$userid]->statusid = attendance_session_get_highest_status($this->att, $sessioninfo, $t); + } else { + $status = $this->get_column_data($row, $mapping['status']); + if (!empty($statusmap[$status])) { + $sesslog[$userid]->statusid = $statusmap[$status]; + } else { + $a = new \stdClass(); + $a->extuser = $extuser; + $a->status = $status; + \mod_attendance_notifyqueue::notify_problem(get_string('error:statusnotfound', 'attendance', $a)); + continue; + } + } + } + $this->sessions = $sesslog; + + $this->importer->close(); + if (empty($sesslog)) { + $this->fail(get_string('invalidimportfile', 'attendance')); + return; + } else { + raise_memory_limit(MEMORY_EXTRA); + + // We are calling from browser, display progress bar. + if ($this->useprogressbar === true) { + $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'attendance')); + $this->progress->start_html(); + } else { + $this->progress = new \core\progress\none(); + } + $this->progress->start_progress('', count($this->sessions)); + $this->progress->end_progress(); + } + } + + /** + * Get parse errors. + * + * @return array of errors from parsing the xml. + */ + public function get_error() { + return $this->error; + } + + /** + * Create sessions using the CSV data. + * + * @return void + */ + public function import() { + $this->att->save_log($this->sessions); + \mod_attendance_notifyqueue::notify_success(get_string('sessionsupdated', 'mod_attendance')); + } +} diff --git a/mod/attendance/classes/import/sessions.php b/mod/attendance/classes/import/sessions.php new file mode 100644 index 0000000..5b13b61 --- /dev/null +++ b/mod/attendance/classes/import/sessions.php @@ -0,0 +1,525 @@ +<?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/>. + +/** + * Import attendance sessions class. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\import; + +defined('MOODLE_INTERNAL') || die(); + +use csv_import_reader; +use mod_attendance_notifyqueue; +use mod_attendance_structure; +use stdClass; + +/** + * Import attendance sessions. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sessions { + + /** @var string $error The errors message from reading the xml */ + protected $error = ''; + + /** @var array $sessions The sessions info */ + protected $sessions = array(); + + /** @var array $mappings The mappings info */ + protected $mappings = array(); + + /** @var int The id of the csv import */ + protected $importid = 0; + + /** @var csv_import_reader|null $importer */ + protected $importer = null; + + /** @var array $foundheaders */ + protected $foundheaders = array(); + + /** @var bool $useprogressbar Control whether importing should use progress bars or not. */ + protected $useprogressbar = false; + + /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */ + protected $progress = null; + + /** + * Store an error message for display later + * + * @param string $msg + */ + public function fail($msg) { + $this->error = $msg; + return false; + } + + /** + * Get the CSV import id + * + * @return string The import id. + */ + public function get_importid() { + return $this->importid; + } + + /** + * Get the list of headers required for import. + * + * @return array The headers (lang strings) + */ + public static function list_required_headers() { + return array( + get_string('courseshortname', 'attendance'), + get_string('groups', 'attendance'), + get_string('sessiondate', 'attendance'), + get_string('from', 'attendance'), + get_string('to', 'attendance'), + get_string('description', 'attendance'), + get_string('repeaton', 'attendance'), + get_string('repeatevery', 'attendance'), + get_string('repeatuntil', 'attendance'), + get_string('studentscanmark', 'attendance'), + get_string('passwordgrp', 'attendance'), + get_string('randompassword', 'attendance'), + get_string('subnet', 'attendance'), + get_string('automark', 'attendance'), + get_string('autoassignstatus', 'attendance'), + get_string('absenteereport', 'attendance'), + get_string('preventsharedip', 'attendance'), + get_string('preventsharediptime', 'attendance'), + get_string('calendarevent', 'attendance'), + get_string('includeqrcode', 'attendance'), + get_string('rotateqrcode', 'attendance'), + ); + } + + /** + * Get the list of headers found in the import. + * + * @return array The found headers (names from import) + */ + public function list_found_headers() { + return $this->foundheaders; + } + + /** + * Read the data from the mapping form. + * + * @param array $data The mapping data. + */ + protected function read_mapping_data($data) { + if ($data) { + return array( + 'course' => $data->header0, + 'groups' => $data->header1, + 'sessiondate' => $data->header2, + 'from' => $data->header3, + 'to' => $data->header4, + 'description' => $data->header5, + 'repeaton' => $data->header6, + 'repeatevery' => $data->header7, + 'repeatuntil' => $data->header8, + 'studentscanmark' => $data->header9, + 'passwordgrp' => $data->header10, + 'randompassword' => $data->header11, + 'subnet' => $data->header12, + 'automark' => $data->header13, + 'autoassignstatus' => $data->header14, + 'absenteereport' => $data->header15, + 'preventsharedip' => $data->header16, + 'preventsharediptime' => $data->header17, + 'calendarevent' => $data->header18, + 'includeqrcode' => $data->header19, + 'rotateqrcode' => $data->header20, + ); + } else { + return array( + 'course' => 0, + 'groups' => 1, + 'sessiondate' => 2, + 'from' => 3, + 'to' => 4, + 'description' => 5, + 'repeaton' => 6, + 'repeatevery' => 7, + 'repeatuntil' => 8, + 'studentscanmark' => 9, + 'passwordgrp' => 10, + 'randompassword' => 11, + 'subnet' => 12, + 'automark' => 13, + 'autoassignstatus' => 14, + 'absenteereport' => 15, + 'preventsharedip' => 16, + 'preventsharediptime' => 17, + 'calendarevent' => 18, + 'includeqrcode' => 19, + 'rotateqrcode' => 20 + ); + } + } + + /** + * Get the a column from the imported data. + * + * @param array $row The imported raw row + * @param int $index The column index we want + * @return string The column data. + */ + protected function get_column_data($row, $index) { + if ($index < 0) { + return ''; + } + return isset($row[$index]) ? $row[$index] : ''; + } + + /** + * Constructor - parses the raw text for sanity. + * + * @param string $text The raw csv text. + * @param string $encoding The encoding of the csv file. + * @param string $delimiter The specified delimiter for the file. + * @param string $importid The id of the csv import. + * @param array $mappingdata The mapping data from the import form. + * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI. + */ + public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, + $mappingdata = null, $useprogressbar = false) { + global $CFG; + + require_once($CFG->libdir . '/csvlib.class.php'); + + $pluginconfig = get_config('attendance'); + + $type = 'sessions'; + + if (! $importid) { + if ($text === null) { + return; + } + $this->importid = csv_import_reader::get_new_iid($type); + + $this->importer = new csv_import_reader($this->importid, $type); + + if (! $this->importer->load_csv_content($text, $encoding, $delimiter)) { + $this->fail(get_string('invalidimportfile', 'attendance')); + $this->importer->cleanup(); + return; + } + } else { + $this->importid = $importid; + + $this->importer = new csv_import_reader($this->importid, $type); + } + + if (! $this->importer->init()) { + $this->fail(get_string('invalidimportfile', 'attendance')); + $this->importer->cleanup(); + return; + } + + $this->foundheaders = $this->importer->get_columns(); + $this->useprogressbar = $useprogressbar; + $domainid = 1; + + $sessions = array(); + + while ($row = $this->importer->next()) { + // This structure mimics what the UI form returns. + $mapping = $this->read_mapping_data($mappingdata); + + $session = new stdClass(); + $session->course = $this->get_column_data($row, $mapping['course']); + if (empty($session->course)) { + \mod_attendance_notifyqueue::notify_problem(get_string('error:sessioncourseinvalid', 'attendance')); + continue; + } + + // Handle multiple group assignments per session. Expect semicolon separated group names. + $groups = $this->get_column_data($row, $mapping['groups']); + if (! empty($groups)) { + $session->groups = explode(';', $groups); + $session->sessiontype = \mod_attendance_structure::SESSION_GROUP; + } else { + $session->sessiontype = \mod_attendance_structure::SESSION_COMMON; + } + + // Expect standardised date format, eg YYYY-MM-DD. + $sessiondate = strtotime($this->get_column_data($row, $mapping['sessiondate'])); + if ($sessiondate === false) { + \mod_attendance_notifyqueue::notify_problem(get_string('error:sessiondateinvalid', 'attendance')); + continue; + } + $session->sessiondate = $sessiondate; + + // Expect standardised time format, eg HH:MM. + $from = $this->get_column_data($row, $mapping['from']); + if (empty($from)) { + \mod_attendance_notifyqueue::notify_problem(get_string('error:sessionstartinvalid', 'attendance')); + continue; + } + $from = explode(':', $from); + $session->sestime['starthour'] = $from[0]; + $session->sestime['startminute'] = $from[1]; + + $to = $this->get_column_data($row, $mapping['to']); + if (empty($to)) { + \mod_attendance_notifyqueue::notify_problem(get_string('error:sessionendinvalid', 'attendance')); + continue; + } + $to = explode(':', $to); + $session->sestime['endhour'] = $to[0]; + $session->sestime['endminute'] = $to[1]; + + // Wrap the plain text description in html tags. + $session->sdescription['text'] = '<p>' . $this->get_column_data($row, $mapping['description']) . '</p>'; + $session->sdescription['format'] = FORMAT_HTML; + $session->sdescription['itemid'] = 0; + $session->passwordgrp = $this->get_column_data($row, $mapping['passwordgrp']); + $session->subnet = $this->get_column_data($row, $mapping['subnet']); + // Set session subnet restriction. Use the default activity level subnet if there isn't one set for this session. + if (empty($session->subnet)) { + $session->usedefaultsubnet = '1'; + } else { + $session->usedefaultsubnet = ''; + } + + if ($mapping['studentscanmark'] == -1) { + $session->studentscanmark = $pluginconfig->studentscanmark_default; + } else { + $session->studentscanmark = $this->get_column_data($row, $mapping['studentscanmark']); + } + if ($mapping['randompassword'] == -1) { + $session->randompassword = $pluginconfig->randompassword_default; + } else { + $session->randompassword = $this->get_column_data($row, $mapping['randompassword']); + } + if ($mapping['automark'] == -1) { + $session->automark = $pluginconfig->automark_default; + } else { + $session->automark = $this->get_column_data($row, $mapping['automark']); + } + if ($mapping['autoassignstatus'] == -1) { + $session->autoassignstatus = $pluginconfig->autoassignstatus; + } else { + $session->autoassignstatus = $this->get_column_data($row, $mapping['autoassignstatus']); + } + if ($mapping['absenteereport'] == -1) { + $session->absenteereport = $pluginconfig->absenteereport_default; + } else { + $session->absenteereport = $this->get_column_data($row, $mapping['absenteereport']); + } + if ($mapping['preventsharedip'] == -1) { + $session->preventsharedip = $pluginconfig->preventsharedip; + } else { + $session->preventsharedip = $this->get_column_data($row, $mapping['preventsharedip']); + } + if ($mapping['preventsharediptime'] == -1) { + $session->preventsharediptime = $pluginconfig->preventsharediptime; + } else { + $session->preventsharediptime = $this->get_column_data($row, $mapping['preventsharediptime']); + } + + if ($mapping['calendarevent'] == -1) { + $session->calendarevent = $pluginconfig->calendarevent_default; + } else { + $session->calendarevent = $this->get_column_data($row, $mapping['calendarevent']); + } + + if ($mapping['includeqrcode'] == -1) { + $session->includeqrcode = $pluginconfig->includeqrcode_default; + } else { + $session->includeqrcode = $this->get_column_data($row, $mapping['includeqrcode']); + + if ($session->includeqrcode == 1 && $session->studentscanmark != 1) { + \mod_attendance_notifyqueue::notify_problem(get_string('error:qrcode', 'attendance')); + continue; + } + + } + if ($mapping['rotateqrcode'] == -1) { + $session->rotateqrcode = $pluginconfig->rotateqrcode_default; + } else { + $session->rotateqrcode = $this->get_column_data($row, $mapping['rotateqrcode']); + } + + $session->statusset = 0; + + $sessions[] = $session; + } + $this->sessions = $sessions; + + $this->importer->close(); + if ($this->sessions == null) { + $this->fail(get_string('invalidimportfile', 'attendance')); + return; + } else { + // We are calling from browser, display progress bar. + if ($this->useprogressbar === true) { + $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'attendance')); + $this->progress->start_html(); + } else { + // Avoid html output on CLI scripts. + $this->progress = new \core\progress\none(); + } + $this->progress->start_progress('', count($this->sessions)); + raise_memory_limit(MEMORY_EXTRA); + $this->progress->end_progress(); + } + } + + /** + * Get parse errors. + * + * @return array of errors from parsing the xml. + */ + public function get_error() { + return $this->error; + } + + /** + * Create sessions using the CSV data. + * + * @return void + */ + public function import() { + global $DB; + + // Count of sessions added. + $okcount = 0; + + foreach ($this->sessions as $session) { + $groupids = array(); + // Check course shortname matches. + if ($DB->record_exists('course', array( + 'shortname' => $session->course + ))) { + // Get course. + $course = $DB->get_record('course', array( + 'shortname' => $session->course + ), '*', MUST_EXIST); + + // Check course has activities. + if ($DB->record_exists('attendance', array( + 'course' => $course->id + ))) { + // Translate group names to group IDs. They are unique per course. + if ($session->sessiontype === \mod_attendance_structure::SESSION_GROUP) { + foreach ($session->groups as $groupname) { + $gid = groups_get_group_by_name($course->id, $groupname); + if ($gid === false) { + \mod_attendance_notifyqueue::notify_problem(get_string('sessionunknowngroup', + 'attendance', $groupname)); + } else { + $groupids[] = $gid; + } + } + $session->groups = $groupids; + } + + // Get activities in course. + $activities = $DB->get_recordset('attendance', array( + 'course' => $course->id + ), 'id', 'id'); + + foreach ($activities as $activity) { + // Build the session data. + $cm = get_coursemodule_from_instance('attendance', $activity->id, $course->id); + if (!empty($cm->deletioninprogress)) { + // Don't do anything if this attendance is in recycle bin. + continue; + } + $att = new mod_attendance_structure($activity, $cm, $course); + $sessions = attendance_construct_sessions_data_for_add($session, $att); + + foreach ($sessions as $index => $sess) { + // Check for duplicate sessions. + if ($this->session_exists($sess)) { + mod_attendance_notifyqueue::notify_message(get_string('sessionduplicate', 'attendance', (array( + 'course' => $session->course, + 'activity' => $cm->name + )))); + unset($sessions[$index]); + } else { + $okcount ++; + } + } + if (! empty($sessions)) { + $att->add_sessions($sessions); + } + } + $activities->close(); + } else { + mod_attendance_notifyqueue::notify_problem(get_string('error:coursehasnoattendance', + 'attendance', $session->course)); + } + } else { + mod_attendance_notifyqueue::notify_problem(get_string('error:coursenotfound', 'attendance', $session->course)); + } + } + + $message = get_string('sessionsgenerated', 'attendance', $okcount); + if ($okcount < 1) { + mod_attendance_notifyqueue::notify_message($message); + } else { + mod_attendance_notifyqueue::notify_success($message); + } + + // Trigger a sessions imported event. + $event = \mod_attendance\event\sessions_imported::create(array( + 'objectid' => 0, + 'context' => \context_system::instance(), + 'other' => array( + 'count' => $okcount + ) + )); + + $event->trigger(); + } + + /** + * Check if an identical session exists. + * + * @param stdClass $session + * @return boolean + */ + private function session_exists(stdClass $session) { + global $DB; + + $check = clone $session; + + // Remove the properties that aren't useful to check. + unset($check->description); + unset($check->descriptionitemid); + unset($check->timemodified); + $check = (array) $check; + + if ($DB->record_exists('attendance_sessions', $check)) { + return true; + } + return false; + } +} diff --git a/mod/attendance/classes/manage_page_params.php b/mod/attendance/classes/manage_page_params.php new file mode 100644 index 0000000..48e5a54 --- /dev/null +++ b/mod/attendance/classes/manage_page_params.php @@ -0,0 +1,47 @@ +<?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 definition for mod_attendance_manage_page_params + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +/** + * stores constants/data passed depending on view. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_manage_page_params extends mod_attendance_page_with_filter_controls { + /** + * mod_attendance_manage_page_params constructor. + */ + public function __construct() { + $this->selectortype = mod_attendance_page_with_filter_controls::SELECTOR_SESS_TYPE; + } + + /** + * Get page params. + * @return array + */ + public function get_significant_params() { + return array(); + } +} \ No newline at end of file diff --git a/mod/attendance/classes/notifyqueue.php b/mod/attendance/classes/notifyqueue.php new file mode 100644 index 0000000..26e9069 --- /dev/null +++ b/mod/attendance/classes/notifyqueue.php @@ -0,0 +1,93 @@ +<?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/>. + +/** + * Notify queue + * + * @package mod_attendance + * @copyright 2015 Antonio Carlos Mariani <antonio.c.mariani@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Notify Queue class + * + * @copyright 2015 Antonio Carlos Mariani <antonio.c.mariani@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_notifyqueue { + + /** + * Show (print) the pending messages and clear them + */ + public static function show() { + global $SESSION, $OUTPUT; + + if (isset($SESSION->mod_attendance_notifyqueue)) { + foreach ($SESSION->mod_attendance_notifyqueue as $message) { + echo $OUTPUT->notification($message->message, 'notify'.$message->type); + } + unset($SESSION->mod_attendance_notifyqueue); + } + } + + /** + * Queue a text as a problem message to be shown latter by show() method + * + * @param string $message a text with a message + */ + public static function notify_problem($message) { + self::queue_message($message, \core\output\notification::NOTIFY_ERROR); + } + + /** + * Queue a text as a simple message to be shown latter by show() method + * + * @param string $message a text with a message + */ + public static function notify_message($message) { + self::queue_message($message, \core\output\notification::NOTIFY_INFO); + } + + /** + * queue a text as a suceess message to be shown latter by show() method + * + * @param string $message a text with a message + */ + public static function notify_success($message) { + self::queue_message($message, \core\output\notification::NOTIFY_SUCCESS); + } + + /** + * queue a text as a message of some type to be shown latter by show() method + * + * @param string $message a text with a message + * @param string $messagetype one of the \core\output\notification messages ('message', 'suceess' or 'problem') + */ + private static function queue_message($message, $messagetype=\core\output\notification::NOTIFY_INFO) { + global $SESSION; + + if (!isset($SESSION->mod_attendance_notifyqueue)) { + $SESSION->mod_attendance_notifyqueue = array(); + } + $m = new stdclass(); + $m->type = $messagetype; + $m->message = $message; + $SESSION->mod_attendance_notifyqueue[] = $m; + } +} diff --git a/mod/attendance/classes/observer.php b/mod/attendance/classes/observer.php new file mode 100644 index 0000000..150a540 --- /dev/null +++ b/mod/attendance/classes/observer.php @@ -0,0 +1,57 @@ +<?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/>. + +/** + * Event observers supported by this module + * + * @package mod_attendance + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event observers supported by this module + * + * @package mod_attendance + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_observer { + + /** + * Observer for the event course_content_deleted - delete all attendance stuff. + * + * @param \core\event\course_content_deleted $event + */ + public static function course_content_deleted(\core\event\course_content_deleted $event) { + global $DB; + + $attids = array_keys($DB->get_records('attendance', array('course' => $event->objectid), '', 'id')); + $sessids = array_keys($DB->get_records_list('attendance_sessions', 'attendanceid', $attids, '', 'id')); + if (attendance_existing_calendar_events_ids($sessids)) { + attendance_delete_calendar_events($sessids); + } + if ($sessids) { + $DB->delete_records_list('attendance_log', 'sessionid', $sessids); + } + if ($attids) { + $DB->delete_records_list('attendance_statuses', 'attendanceid', $attids); + $DB->delete_records_list('attendance_sessions', 'attendanceid', $attids); + } + } +} diff --git a/mod/attendance/classes/output/mobile.php b/mod/attendance/classes/output/mobile.php new file mode 100644 index 0000000..13ea491 --- /dev/null +++ b/mod/attendance/classes/output/mobile.php @@ -0,0 +1,455 @@ +<?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/>. + +/** + * Contains the mobile output class for the attendance + * + * @package mod_attendance + * @copyright 2018 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\output; + +defined('MOODLE_INTERNAL') || die(); +/** + * Mobile output class for the attendance. + * + * @copyright 2018 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mobile { + + /** + * Subnet warning. - constants used to prevent warnings from showing multiple times. + */ + const MESSAGE_SUBNET = 10; + + /** + * Prevent shared warning. used to prevent warnings from showing multiple times. + */ + const MESSAGE_PREVENTSHARED = 30; + + /** + * Returns the initial page when viewing the activity for the mobile app. + * + * @param array $args Arguments from tool_mobile_get_content WS + * @return array HTML, javascript and other data + */ + public static function mobile_view_activity($args) { + global $OUTPUT, $DB, $USER, $USER, $CFG; + + require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + + $cmid = $args['cmid']; + $courseid = $args['courseid']; + $takenstatus = empty($args['status']) ? '' : $args['status']; + $sessid = empty($args['sessid']) ? '' : $args['sessid']; + $password = empty($args['studentpass']) ? '' : $args['studentpass']; + + // Capabilities check. + $cm = get_coursemodule_from_id('attendance', $cmid); + + require_login($courseid, false , $cm, true, true); + + $context = \context_module::instance($cm->id); + require_capability('mod/attendance:view', $context); + + $attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $config = get_config('attendance'); + + $data = array(); // Data to pass to renderer. + $data['cmid'] = $cmid; + $data['courseid'] = $courseid; + $data['attendance'] = $attendance; + $data['timestamp'] = time(); // Used to prevent attendance session marking page to be cached. + + $data['attendancefunction'] = 'mobile_user_form'; + $isteacher = false; + if (has_capability('mod/attendance:takeattendances', $context)) { + $isteacher = true; + $data['attendancefunction'] = 'mobile_teacher_form'; + } + + // Add stats for this use to output. + $pageparams = new \mod_attendance_view_page_params(); + $pageparams->studentid = $USER->id; + $pageparams->group = groups_get_activity_group($cm, true); + $canseegroupsession = true; + if (!empty($sessid) && (!empty($takenstatus) || $isteacher)) { + $session = $DB->get_record('attendance_sessions', array('id' => $sessid)); + $pageparams->grouptype = $session->groupid; + $pageparams->sessionid = $sessid; + + if ($isteacher && !empty($session->groupid)) { + $allowedgroups = groups_get_activity_allowed_groups($cm); + if (!array_key_exists($session->groupid, $allowedgroups)) { + $canseegroupsession = false; + } + } + } + $pageparams->mode = \mod_attendance_view_page_params::MODE_THIS_COURSE; + $pageparams->view = 5; // Show all sessions for this course? + + $att = new \mod_attendance_structure($attendance, $cm, $course, $context, $pageparams); + + // Check if this teacher is allowed to view/mark this group session. + + if ($isteacher && $canseegroupsession) { + $keys = array_keys($args); + $userkeys = preg_grep("/status\d+/", $keys); + if (!empty($userkeys)) { // If this is a post from the teacher form. + // Build data to pass to take_from_form_data. + $formdata = new \stdClass(); + foreach ($userkeys as $uk) { + $userid = str_replace('status', '', $uk); + $status = $args[$uk]; + $formdata->{'remarks'.$userid} = ''; + $formdata->{'user'.$userid} = $status; + } + $att->take_from_form_data($formdata); + $data['showmessage'] = true; + $data['messages'][]['string'] = 'attendancesuccess'; + } + } + + // Get list of sessions based on site level settings. default = the next 24hrs and in last 6hrs. + $timefrom = time() - $config->mobilesessionfrom; + $timeto = time() + $config->mobilesessionto; + + $data['sessions'] = array(); + + $sessions = $DB->get_records_select('attendance_sessions', + 'attendanceid = ? AND sessdate > ? AND sessdate < ? ORDER BY sessdate', + array($attendance->id, $timefrom, $timeto)); + + if (!empty($sessions)) { + $userdata = new \attendance_user_data($att, $USER->id, true); + foreach ($sessions as $sess) { + if (!$isteacher && empty($userdata->sessionslog['c'.$sess->id])) { + // This session isn't viewable to this student - probably a group session. + continue; + } + + // Check if this teacher is allowed to view this group session. + if ($isteacher && !empty($sess->groupid)) { + $allowedgroups = groups_get_activity_allowed_groups($cm); + if (!array_key_exists($sess->groupid, $allowedgroups)) { + continue; + } + } + list($canmark, $reason) = attendance_can_student_mark($sess); + if (!$isteacher && $reason == 'preventsharederror') { + $data['showmessage'] = true; + // Lang string to show as a message. + $data['messages'][self::MESSAGE_PREVENTSHARED]['string'] = 'preventsharederror'; + } + + if ($isteacher || $canmark) { + $html = array('time' => strip_tags(construct_session_full_date_time($sess->sessdate, $sess->duration)), + 'groupname' => ''); + if (!empty($sess->groupid)) { + // TODO In-efficient way to get group name - we should get all groups in one query. + $html['groupname'] = $DB->get_field('groups', 'name', array('id' => $sess->groupid)); + } + + // Check if Status already recorded. + if (!$isteacher && !empty($userdata->sessionslog['c'.$sess->id]->statusid)) { + $html['currentstatus'] = $userdata->statuses[$userdata->sessionslog['c'.$sess->id]->statusid]->description; + } else { + // Status has not been recorded - If student, check auto-assign and form data. + $html['sessid'] = $sess->id; + + if (!$isteacher) { + if (!empty($sess->subnet) && !address_in_subnet(getremoteaddr(), $sess->subnet)) { + $data['showmessage'] = true; + // Lang string to show as a message. + $data['messages'][self::MESSAGE_SUBNET]['string'] = 'subnetwrong'; + $html['sessid'] = null; // Unset sessid as we cannot record session on this ip. + } else if ($sess->autoassignstatus && empty($sess->studentpassword)) { + $statusid = attendance_session_get_highest_status($att, $sess); + if (empty($statusid)) { + $data['showmessage'] = true; + $data['messages'][]['string'] = 'attendance_no_status'; + } + $take = new \stdClass(); + $take->status = $statusid; + $take->sessid = $sess->id; + $success = $att->take_from_student($take); + + if ($success) { + $html['currentstatus'] = $userdata->statuses[$statusid]->description; + $html['sessid'] = null; // Unset sessid as we have recorded session. + } + } else if ($sess->id == $sessid) { + if (!empty($sess->studentpassword) && $password != $sess->studentpassword) { + // Password incorrect. + $data['showmessage'] = true; + $data['messages'][]['string'] = 'incorrectpasswordshort'; + } else { + $statuses = $att->get_statuses(); + // Check if user has access to all statuses. + foreach ($statuses as $status) { + if ($status->studentavailability === '0') { + unset($statuses[$status->id]); + continue; + } + if (!empty($status->studentavailability) && + time() > $sess->sessdate + ($status->studentavailability * 60)) { + unset($statuses[$status->id]); + continue; + } + } + if ($sess->autoassignstatus) { + // If this is an auto-assign, get the highest status available. + $takenstatus = attendance_session_get_highest_status($att, $sess); + } + + if (empty($statuses[$takenstatus])) { + // This status has probably expired and is not available - they need to choose a new one. + $data['showmessage'] = true; + $data['messages'][]['string'] = 'invalidstatus'; + } else { + $take = new \stdClass(); + $take->status = $takenstatus; + $take->sessid = $sess->id; + $success = $att->take_from_student($take); + + if ($success) { + $html['currentstatus'] = $userdata->statuses[$takenstatus]->description; + $html['sessid'] = null; // Unset sessid as we have recorded session. + } + } + } + } + } + } + + $data['sessions'][] = $html; + } + } + } + + $summary = new \mod_attendance_summary($att->id, array($USER->id), $att->pageparams->startdate, + $att->pageparams->enddate); + $data['summary'] = $summary->get_all_sessions_summary_for($USER->id); + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_attendance/mobile_view_page', $data), + ], + ], + 'javascript' => '', + 'otherdata' => '' + ]; + } + + /** + * Returns the form to take attendance for the mobile app. + * + * @param array $args Arguments from tool_mobile_get_content WS + * @return array HTML, javascript and other data + */ + public static function mobile_user_form($args) { + global $OUTPUT, $DB, $CFG; + + require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + + $args = (object) $args; + $cmid = $args->cmid; + $courseid = $args->courseid; + $sessid = $args->sessid; + + // Capabilities check. + $cm = get_coursemodule_from_id('attendance', $cmid); + + require_login($courseid, false , $cm, true, true); + + $context = \context_module::instance($cm->id); + require_capability('mod/attendance:view', $context); + + $attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + $attforsession = $DB->get_record('attendance_sessions', array('id' => $sessid), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + $pageparams = new \mod_attendance_sessions_page_params(); + $pageparams->sessionid = $sessid; + $att = new \mod_attendance_structure($attendance, $cm, $course, $context, $pageparams); + + $data = array(); // Data to pass to renderer. + $data['attendance'] = $attendance; + $data['cmid'] = $cmid; + $data['courseid'] = $courseid; + $data['sessid'] = $sessid; + $data['messages'] = array(); + $data['showmessage'] = false; + $data['showstatuses'] = true; + $data['showpassword'] = false; + $data['statuses'] = array(); + $data['disabledduetotime'] = false; + + list($canmark, $reason) = attendance_can_student_mark($attforsession, false); + // Check if subnet is set and if the user is in the allowed range. + if (!$canmark) { + $data['messages'][]['string'] = $reason; // Lang string to show as a message. + $data['showstatuses'] = false; // Hide all statuses. + } else if (!empty($attforsession->subnet) && !address_in_subnet(getremoteaddr(), $attforsession->subnet)) { + $data['messages'][self::MESSAGE_SUBNET]['string'] = 'subnetwrong'; // Lang string to show as a message. + $data['showstatuses'] = false; // Hide all statuses. + } else if ($attforsession->autoassignstatus && empty($attforsession->studentpassword)) { + // This shouldn't happen as the main function should handle this scenario. + // Hide all status just in case the user manages to hit this page accidentally. + $data['showstatuses'] = false; // Hide all statuses. + } else { + // Show user form for submitting a status. + $statuses = $att->get_statuses(); + // Check if user has access to all statuses. + foreach ($statuses as $status) { + if ($status->studentavailability === '0') { + unset($statuses[$status->id]); + continue; + } + if (!empty($status->studentavailability) && + time() > $attforsession->sessdate + ($status->studentavailability * 60)) { + unset($statuses[$status->id]); + continue; + $data['disabledduetotime'] = true; + } + $data['statuses'][] = array('stid' => $status->id, 'description' => $status->description); + } + if (empty($data['statuses'])) { + $data['messages'][]['string'] = 'attendance_no_status'; + $data['showstatuses'] = false; // Hide all statuses. + } else if (!empty($attforsession->studentpassword)) { + $data['showpassword'] = true; + if ($attforsession->autoassignstatus) { + // If this is an auto status - don't show the statuses, but show the form. + $data['statuses'] = array(); + } + } + } + if (!empty($data['messages'])) { + $data['showmessage'] = true; + } + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_attendance/mobile_user_form', $data), + 'cache-view' => false + ], + ], + 'javascript' => '', + 'otherdata' => '' + ]; + } + + /** + * Returns the form to take attendance for the mobile app. + * + * @param array $args Arguments from tool_mobile_get_content WS + * @return array HTML, javascript and other data + */ + public static function mobile_teacher_form($args) { + global $OUTPUT, $DB, $CFG, $PAGE; + + require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + + $args = (object) $args; + $cmid = $args->cmid; + $courseid = $args->courseid; + $sessid = $args->sessid; + + // Capabilities check. + $cm = get_coursemodule_from_id('attendance', $cmid); + + require_login($courseid, false , $cm, true, true); + + $context = \context_module::instance($cm->id); + require_capability('mod/attendance:takeattendances', $context); + + $attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + $pageparams = new \mod_attendance_sessions_page_params(); + $pageparams->sessionid = $sessid; + $att = new \mod_attendance_structure($attendance, $cm, $course, $context, $pageparams); + + $data = array(); // Data to pass to renderer. + $data['attendance'] = $attendance; + $data['cmid'] = $cmid; + $data['courseid'] = $courseid; + $data['sessid'] = $sessid; + $data['messages'] = array(); + $data['showmessage'] = false; + $data['statuses'] = array(); + $data['btnargs'] = ''; // Stores list of userid status args that should be added to form post. + + $statuses = $att->get_statuses(); + $otherdata = array(); + $existinglog = $DB->get_records('attendance_log', + array('sessionid' => $sessid), '', 'studentid,statusid'); + foreach ($existinglog as $log) { + if (!empty($log->statusid)) { + $otherdata['status'.$log->studentid] = $log->statusid; + } + } + + foreach ($statuses as $status) { + $data['statuses'][] = array('stid' => $status->id, 'acronym' => $status->acronym, + 'description' => $status->description, 'selectall' => ''); + } + + $data['users'] = array(); + $users = $att->get_users($att->get_session_info($sessid)->groupid, 0); + foreach ($users as $user) { + $userpicture = new \user_picture($user); + $userpicture->size = 1; // Size f1. + $profileimageurl = $userpicture->get_url($PAGE)->out(false); + $data['users'][] = array('userid' => $user->id, 'fullname' => $user->fullname, 'profileimageurl' => $profileimageurl); + // Generate args to use in submission button here. + $data['btnargs'] .= ', status'. $user->id. ': CONTENT_OTHERDATA.status'. $user->id; + // Really Hacky way to do a select-all. This really needs to be moved into a JS function within the app. + foreach ($statuses as $status) { + foreach ($data['statuses'] as $id => $st) { // Statuses not ordered by statusid. + if ($st['stid'] == $status->id) { // Find the item that we need to add to. + $data['statuses'][$id]['selectall'] .= "CONTENT_OTHERDATA.status".$user->id."=".$status->id.";"; + } + } + } + } + if (!empty($data['messages'])) { + $data['showmessage'] = true; + } + + return [ + 'templates' => [ + [ + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_attendance/mobile_teacher_form', $data), + 'cache-view' => false + ], + ], + 'javascript' => '', + 'otherdata' => $otherdata + ]; + } + +} \ No newline at end of file diff --git a/mod/attendance/classes/page_with_filter_controls.php b/mod/attendance/classes/page_with_filter_controls.php new file mode 100644 index 0000000..94f78e6 --- /dev/null +++ b/mod/attendance/classes/page_with_filter_controls.php @@ -0,0 +1,283 @@ +<?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 definition for mod_attendance_page_with_filter_controls + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * Base filter controls class - overridden by different views where needed. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_page_with_filter_controls { + /** No filter. */ + const SELECTOR_NONE = 1; + + /** Filter by group. */ + const SELECTOR_GROUP = 2; + + /** Filter by session type. */ + const SELECTOR_SESS_TYPE = 3; + + /** Common. */ + const SESSTYPE_COMMON = 0; + + /** All. */ + const SESSTYPE_ALL = -1; + + /** No value. */ + const SESSTYPE_NO_VALUE = -2; + + /** @var int current view mode */ + public $view; + + /** @var int $view and $curdate specify displaed date range */ + public $curdate; + + /** @var int start date of displayed date range */ + public $startdate; + + /** @var int end date of displayed date range */ + public $enddate; + + /** @var int type. */ + public $selectortype = self::SELECTOR_NONE; + + /** @var int default view. */ + protected $defaultview; + + /** @var stdClass course module record. */ + private $cm; + + /** @var array */ + private $sessgroupslist; + + /** @var int */ + private $sesstype; + + /** + * initialise stuff. + * + * @param stdClass $cm + */ + public function init($cm) { + $this->cm = $cm; + if (empty($this->defaultview)) { + $this->defaultview = get_config('attendance', 'defaultview'); + } + $this->init_view(); + $this->init_curdate(); + $this->init_start_end_date(); + } + + /** + * Initialise the view. + */ + private function init_view() { + global $SESSION; + + if (isset($this->view)) { + $SESSION->attcurrentattview[$this->cm->course] = $this->view; + } else if (isset($SESSION->attcurrentattview[$this->cm->course])) { + $this->view = $SESSION->attcurrentattview[$this->cm->course]; + } else { + $this->view = $this->defaultview; + } + } + + /** + * Initialise the current date. + */ + private function init_curdate() { + global $SESSION; + + if (isset($this->curdate)) { + $SESSION->attcurrentattdate[$this->cm->course] = $this->curdate; + } else if (isset($SESSION->attcurrentattdate[$this->cm->course])) { + $this->curdate = $SESSION->attcurrentattdate[$this->cm->course]; + } else { + $this->curdate = time(); + } + } + + /** + * Initialise the end date. + */ + public function init_start_end_date() { + global $CFG; + + // HOURSECS solves issue for weeks view with Daylight saving time and clocks adjusting by one hour backward. + $date = usergetdate($this->curdate + HOURSECS); + $mday = $date['mday']; + $wday = $date['wday'] - $CFG->calendar_startwday; + if ($wday < 0) { + $wday += 7; + } + $mon = $date['mon']; + $year = $date['year']; + + switch ($this->view) { + case ATT_VIEW_DAYS: + $this->startdate = make_timestamp($year, $mon, $mday); + $this->enddate = make_timestamp($year, $mon, $mday + 1); + break; + case ATT_VIEW_WEEKS: + $this->startdate = make_timestamp($year, $mon, $mday - $wday); + $this->enddate = make_timestamp($year, $mon, $mday + 7 - $wday) - 1; + break; + case ATT_VIEW_MONTHS: + $this->startdate = make_timestamp($year, $mon); + $this->enddate = make_timestamp($year, $mon + 1); + break; + case ATT_VIEW_ALLPAST: + $this->startdate = 1; + $this->enddate = time(); + break; + case ATT_VIEW_ALL: + case ATT_VIEW_NOTPRESENT: + $this->startdate = 0; + $this->enddate = 0; + break; + case ATT_VIEW_SUMMARY: + $this->startdate = 1; + $this->enddate = 1; + break; + } + } + + /** + * Calculate the session group list type. + */ + private function calc_sessgroupslist_sesstype() { + global $SESSION; + + if (!property_exists($SESSION, 'attsessiontype')) { + $SESSION->attsessiontype = array($this->cm->course => self::SESSTYPE_ALL); + } else if (!array_key_exists($this->cm->course, $SESSION->attsessiontype)) { + $SESSION->attsessiontype[$this->cm->course] = self::SESSTYPE_ALL; + } + + $group = optional_param('group', self::SESSTYPE_NO_VALUE, PARAM_INT); + if ($this->selectortype == self::SELECTOR_SESS_TYPE) { + if ($group > self::SESSTYPE_NO_VALUE) { + $SESSION->attsessiontype[$this->cm->course] = $group; + if ($group > self::SESSTYPE_ALL) { + // Set activegroup in $SESSION. + groups_get_activity_group($this->cm, true); + } else { + // Reset activegroup in $SESSION. + unset($SESSION->activegroup[$this->cm->course][VISIBLEGROUPS][$this->cm->groupingid]); + unset($SESSION->activegroup[$this->cm->course]['aag'][$this->cm->groupingid]); + unset($SESSION->activegroup[$this->cm->course][SEPARATEGROUPS][$this->cm->groupingid]); + } + $this->sesstype = $group; + } else { + $this->sesstype = $SESSION->attsessiontype[$this->cm->course]; + } + } else if ($this->selectortype == self::SELECTOR_GROUP) { + if ($group == 0) { + $SESSION->attsessiontype[$this->cm->course] = self::SESSTYPE_ALL; + $this->sesstype = self::SESSTYPE_ALL; + } else if ($group > 0) { + $SESSION->attsessiontype[$this->cm->course] = $group; + $this->sesstype = $group; + } else { + $this->sesstype = $SESSION->attsessiontype[$this->cm->course]; + } + } + + if (is_null($this->sessgroupslist)) { + $this->calc_sessgroupslist(); + } + // For example, we set SESSTYPE_ALL but user can access only to limited set of groups. + if (!array_key_exists($this->sesstype, $this->sessgroupslist)) { + reset($this->sessgroupslist); + $this->sesstype = key($this->sessgroupslist); + } + } + + /** + * Calculate the session group list + */ + private function calc_sessgroupslist() { + global $USER, $PAGE; + + $this->sessgroupslist = array(); + $groupmode = groups_get_activity_groupmode($this->cm); + if ($groupmode == NOGROUPS) { + return; + } + + if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $PAGE->context)) { + $allowedgroups = groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid); + } else { + $allowedgroups = groups_get_all_groups($this->cm->course, $USER->id, $this->cm->groupingid); + } + + if ($allowedgroups) { + if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $PAGE->context)) { + $this->sessgroupslist[self::SESSTYPE_ALL] = get_string('all', 'attendance'); + } + // Show Common groups always. + $this->sessgroupslist[self::SESSTYPE_COMMON] = get_string('commonsessions', 'attendance'); + foreach ($allowedgroups as $group) { + $this->sessgroupslist[$group->id] = get_string('group') . ': ' . format_string($group->name); + } + } + } + + /** + * Return the session groups. + * + * @return array + */ + public function get_sess_groups_list() { + if (is_null($this->sessgroupslist)) { + $this->calc_sessgroupslist_sesstype(); + } + + return $this->sessgroupslist; + } + + /** + * Get the current session type. + * + * @return int + */ + public function get_current_sesstype() { + if (is_null($this->sesstype)) { + $this->calc_sessgroupslist_sesstype(); + } + + return $this->sesstype; + } + + /** + * Set the current session type. + * + * @param int $sesstype + */ + public function set_current_sesstype($sesstype) { + $this->sesstype = $sesstype; + } +} diff --git a/mod/attendance/classes/preferences_page_params.php b/mod/attendance/classes/preferences_page_params.php new file mode 100644 index 0000000..9be3a29 --- /dev/null +++ b/mod/attendance/classes/preferences_page_params.php @@ -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/>. + +/** + * Class definition for mod_attendance_preferences_page_params + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * base preferences page param class + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_preferences_page_params { + /** Add */ + const ACTION_ADD = 1; + /** Delete */ + const ACTION_DELETE = 2; + /** Hide */ + const ACTION_HIDE = 3; + /** Show */ + const ACTION_SHOW = 4; + /** Save */ + const ACTION_SAVE = 5; + + /** @var int view mode of taking attendance page*/ + public $action; + + /** @var int */ + public $statusid; + + /** @var array */ + public $statusset; + + /** + * Get params for this page. + * + * @return array + */ + public function get_significant_params() { + $params = array(); + + if (isset($this->action)) { + $params['action'] = $this->action; + } + if (isset($this->statusid)) { + $params['statusid'] = $this->statusid; + } + if (isset($this->statusset)) { + $params['statusset'] = $this->statusset; + } + + return $params; + } +} \ No newline at end of file diff --git a/mod/attendance/classes/privacy/provider.php b/mod/attendance/classes/privacy/provider.php new file mode 100644 index 0000000..130fee0 --- /dev/null +++ b/mod/attendance/classes/privacy/provider.php @@ -0,0 +1,572 @@ +<?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/>. + +/** + * mod_attendance Data provider. + * + * @package mod_attendance + * @copyright 2018 Cameron Ball <cameron@cameron1729.xyz> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\privacy; +defined('MOODLE_INTERNAL') || die(); + +use context; +use context_module; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\{writer, transform, helper, contextlist, approved_contextlist, approved_userlist, userlist}; +use stdClass; + +/** + * Data provider for mod_attendance. + * + * @copyright 2018 Cameron Ball <cameron@cameron1729.xyz> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class provider implements + \core_privacy\local\request\plugin\provider, + \core_privacy\local\request\core_userlist_provider, + \core_privacy\local\metadata\provider +{ + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_database_table( + 'attendance_log', + [ + 'sessionid' => 'privacy:metadata:sessionid', + 'studentid' => 'privacy:metadata:studentid', + 'statusid' => 'privacy:metadata:statusid', + 'statusset' => 'privacy:metadata:statusset', + 'timetaken' => 'privacy:metadata:timetaken', + 'takenby' => 'privacy:metadata:takenby', + 'remarks' => 'privacy:metadata:remarks', + 'ipaddress' => 'privacy:metadata:ipaddress' + ], + 'privacy:metadata:attendancelog' + ); + + $collection->add_database_table( + 'attendance_sessions', + [ + 'groupid' => 'privacy:metadata:groupid', + 'sessdate' => 'privacy:metadata:sessdate', + 'duration' => 'privacy:metadata:duration', + 'lasttaken' => 'privacy:metadata:lasttaken', + 'lasttakenby' => 'privacy:metadata:lasttakenby', + 'timemodified' => 'privacy:metadata:timemodified' + ], + 'privacy:metadata:attendancesessions' + ); + + $collection->add_database_table( + 'attendance_warning_done', + [ + 'notifyid' => 'privacy:metadata:notifyid', + 'userid' => 'privacy:metadata:userid', + 'timesent' => 'privacy:metadata:timesent' + ], + 'privacy:metadata:attendancewarningdone' + ); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * In the case of attendance, that is any attendance where a student has had their + * attendance taken or has taken attendance for someone else. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + return (new contextlist)->add_from_sql( + "SELECT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {attendance} a ON cm.instance = a.id + JOIN {attendance_sessions} asess ON asess.attendanceid = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {attendance_log} al ON asess.id = al.sessionid AND (al.studentid = :userid OR al.takenby = :takenbyid)", + [ + 'modulename' => 'attendance', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + 'takenbyid' => $userid + ] + ); + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!is_a($context, \context_module::class)) { + return; + } + + $sql = "SELECT al.studentid + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = 'attendance' + JOIN {attendance} a ON cm.instance = a.id + JOIN {attendance_sessions} asess ON asess.attendanceid = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {attendance_log} al ON asess.id = al.sessionid + WHERE ctx.id = :contextid"; + + $params = [ + 'contextlevel' => CONTEXT_MODULE, + 'contextid' => $context->id, + ]; + + $userlist->add_from_sql('studentid', $sql, $params); + + $sql = "SELECT al.takenby + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = 'attendance' + JOIN {attendance} a ON cm.instance = a.id + JOIN {attendance_sessions} asess ON asess.attendanceid = a.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel + JOIN {attendance_log} al ON asess.id = al.sessionid + WHERE ctx.id = :contextid"; + + $userlist->add_from_sql('takenby', $sql, $params); + + } + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(context $context) { + global $DB; + + if (!$context instanceof context_module) { + return; + } + + if (!$cm = get_coursemodule_from_id('attendance', $context->instanceid)) { + return; + } + + // Delete all information recorded against sessions associated with this module. + $DB->delete_records_select( + 'attendance_log', + "sessionid IN (SELECT id FROM {attendance_sessions} WHERE attendanceid = :attendanceid", + [ + 'attendanceid' => $cm->instance + ] + ); + + // Delete all completed warnings associated with a warning associated with this module. + $DB->delete_records_select( + 'attendance_warning_done', + "notifyid IN (SELECT id from {attendance_warning} WHERE idnumber = :attendanceid)", + ['attendanceid' => $cm->instance] + ); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + $userid = (int)$contextlist->get_user()->id; + + foreach ($contextlist as $context) { + if (!$context instanceof context_module) { + continue; + } + + if (!$cm = get_coursemodule_from_id('attendance', $context->instanceid)) { + continue; + } + + $attendanceid = (int)$DB->get_record('attendance', ['id' => $cm->instance])->id; + $sessionids = array_keys( + $DB->get_records('attendance_sessions', ['attendanceid' => $attendanceid]) + ); + + self::delete_user_from_session_attendance_log($userid, $sessionids); + self::delete_user_from_sessions($userid, $sessionids); + self::delete_user_from_attendance_warnings_log($userid, $attendanceid); + } + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + $context = $userlist->get_context(); + + if (!is_a($context, \context_module::class)) { + return; + } + + // Prepare SQL to gather all completed IDs. + $userids = $userlist->get_userids(); + list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + + // Delete records where user was marked as attending. + $DB->delete_records_select( + 'attendance_log', + "studentid $insql", + $inparams + ); + + // Get list of warning_done records and check if this user is set in thirdpartyusers. + foreach ($userids as $userid) { + $sql = 'SELECT DISTINCT w.* + FROM {attendance_warning} w + JOIN {attendance_warning_done} d ON d.notifyid = w.id AND d.userid = ?'; + $warnings = $DB->get_records_sql($sql, array($userid)); + if (!empty($warnings)) { + attendance_remove_user_from_thirdpartyemails($warnings, $userid); + } + + } + + $DB->delete_records_select( + 'attendance_warning_done', + "userid $insql", + $inparams + ); + + // Now for teachers remove relation for marking. + $DB->set_field_select( + 'attendance_log', + 'takenby', + 2, + "takenby $insql", + $inparams); + + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + $params = [ + 'modulename' => 'attendance', + 'contextlevel' => CONTEXT_MODULE, + 'studentid' => $contextlist->get_user()->id, + 'takenby' => $contextlist->get_user()->id + ]; + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT + al.*, + asess.id as session, + asess.description, + ctx.id as contextid, + a.name as attendancename, + a.id as attendanceid, + statuses.description as statusdesc, statuses.grade as statusgrade + FROM {course_modules} cm + JOIN {attendance} a ON cm.instance = a.id + JOIN {attendance_sessions} asess ON asess.attendanceid = a.id + JOIN {attendance_log} al on (al.sessionid = asess.id AND (studentid = :studentid OR al.takenby = :takenby)) + JOIN {context} ctx ON cm.id = ctx.instanceid + JOIN {attendance_statuses} statuses ON statuses.id = al.statusid + WHERE (ctx.id {$contextsql})"; + + $attendances = $DB->get_records_sql($sql, $params + $contextparams); + + self::export_attendance_logs( + get_string('attendancestaken', 'mod_attendance'), + array_filter( + $attendances, + function(stdClass $attendance) use ($contextlist) : bool { + return $attendance->takenby == $contextlist->get_user()->id; + } + ) + ); + + self::export_attendance_logs( + get_string('attendanceslogged', 'mod_attendance'), + array_filter( + $attendances, + function(stdClass $attendance) use ($contextlist) : bool { + return $attendance->studentid == $contextlist->get_user()->id; + } + ) + ); + + self::export_attendances( + $contextlist->get_user(), + $attendances, + self::group_by_property( + $DB->get_records_sql( + "SELECT + *, + a.id as attendanceid + FROM {attendance_warning_done} awd + JOIN {attendance_warning} aw ON awd.notifyid = aw.id + JOIN {attendance} a on aw.idnumber = a.id + WHERE userid = :userid", + ['userid' => $contextlist->get_user()->id] + ), + 'notifyid' + ) + ); + } + + /** + * Delete a user from session logs. + * + * @param int $userid The id of the user to remove. + * @param array $sessionids Array of session ids from which to remove the student from the relevant logs. + */ + private static function delete_user_from_session_attendance_log(int $userid, array $sessionids) { + global $DB; + + // Delete records where user was marked as attending. + list($sessionsql, $sessionparams) = $DB->get_in_or_equal($sessionids, SQL_PARAMS_NAMED); + $DB->delete_records_select( + 'attendance_log', + "(studentid = :studentid) AND sessionid $sessionsql", + ['studentid' => $userid] + $sessionparams + ); + + // Get every log record where user took the attendance. + $attendancetakenids = array_keys( + $DB->get_records_sql( + "SELECT * from {attendance_log} + WHERE takenby = :takenbyid AND sessionid $sessionsql", + ['takenbyid' => $userid] + $sessionparams + ) + ); + + if (!$attendancetakenids) { + return; + } + + // Don't delete the record from the log, but update to site admin taking attendance. + list($attendancetakensql, $attendancetakenparams) = $DB->get_in_or_equal($attendancetakenids, SQL_PARAMS_NAMED); + $DB->set_field_select( + 'attendance_log', + 'takenby', + 2, + "id $attendancetakensql", + $attendancetakenparams + ); + } + + /** + * Delete a user from sessions. + * + * Not much user data is stored in a session, but it's possible that a user id is saved + * in the "lasttakenby" field. + * + * @param int $userid The id of the user to remove. + * @param array $sessionids Array of session ids from which to remove the student. + */ + private static function delete_user_from_sessions(int $userid, array $sessionids) { + global $DB; + + // Get all sessions where user was last to mark attendance. + list($sessionsql, $sessionparams) = $DB->get_in_or_equal($sessionids, SQL_PARAMS_NAMED); + $sessionstaken = $DB->get_records_sql( + "SELECT * from {attendance_sessions} + WHERE lasttakenby = :lasttakenbyid AND id $sessionsql", + ['lasttakenbyid' => $userid] + $sessionparams + ); + + if (!$sessionstaken) { + return; + } + + // Don't delete the session, but update last taken by to the site admin. + list($sessionstakensql, $sessionstakenparams) = $DB->get_in_or_equal(array_keys($sessionstaken), SQL_PARAMS_NAMED); + $DB->set_field_select( + 'attendance_sessions', + 'lasttakenby', + 2, + "id $sessionstakensql", + $sessionstakenparams + ); + } + + /** + * Delete a user from the attendance waring log. + * + * @param int $userid The id of the user to remove. + * @param int $attendanceid The id of the attendance instance to remove the relevant warnings from. + */ + private static function delete_user_from_attendance_warnings_log(int $userid, int $attendanceid) { + global $DB, $CFG; + require_once($CFG->dirroot.'/mod/attendance/lib.php'); + + // Get all warnings because the user could have their ID listed in the thirdpartyemails column as a comma delimited string. + $warnings = $DB->get_records( + 'attendance_warning', + ['idnumber' => $attendanceid] + ); + + if (!$warnings) { + return; + } + + attendance_remove_user_from_thirdpartyemails($warnings, $userid); + + // Delete any record of the user being notified. + list($warningssql, $warningsparams) = $DB->get_in_or_equal(array_keys($warnings), SQL_PARAMS_NAMED); + $DB->delete_records_select( + 'attendance_warning_done', + "userid = :userid AND notifyid $warningssql", + ['userid' => $userid] + $warningsparams + ); + } + + /** + * Helper function to group an array of stdClasses by a common property. + * + * @param array $classes An array of classes to group. + * @param string $property A common property to group the classes by. + */ + private static function group_by_property(array $classes, string $property) : array { + return array_reduce( + $classes, + function (array $classes, stdClass $class) use ($property) : array { + $classes[$class->{$property}][] = $class; + return $classes; + }, + [] + ); + } + + /** + * Helper function to transform a row from the database in to session data to export. + * + * The properties of the "dbrow" are very specific to the result of the SQL from + * the export_user_data function. + * + * @param stdClass $dbrow A row from the database containing session information. + * @return stdClass The transformed row. + */ + private static function transform_db_row_to_session_data(stdClass $dbrow) : stdClass { + return (object) [ + 'name' => $dbrow->attendancename, + 'session' => $dbrow->session, + 'takenbyid' => $dbrow->takenby, + 'studentid' => $dbrow->studentid, + 'status' => $dbrow->statusdesc, + 'grade' => $dbrow->statusgrade, + 'sessiondescription' => $dbrow->description, + 'timetaken' => transform::datetime($dbrow->timetaken), + 'remarks' => $dbrow->remarks, + 'ipaddress' => $dbrow->ipaddress + ]; + } + + /** + * Helper function to transform a row from the database in to warning data to export. + * + * The properties of the "dbrow" are very specific to the result of the SQL from + * the export_user_data function. + * + * @param stdClass $warning A row from the database containing warning information. + * @return stdClass The transformed row. + */ + private static function transform_warning_data(stdClass $warning) : stdClass { + return (object) [ + 'timesent' => transform::datetime($warning->timesent), + 'thirdpartyemails' => $warning->thirdpartyemails, + 'subject' => $warning->emailsubject, + 'body' => $warning->emailcontent + ]; + } + + /** + * Helper function to export attendance logs. + * + * The array of "attendances" is actually the result returned by the SQL in export_user_data. + * It is more of a list of sessions. Which is why it needs to be grouped by context id. + * + * @param string $path The path in the export (relative to the current context). + * @param array $attendances Array of attendances to export the logs for. + */ + private static function export_attendance_logs(string $path, array $attendances) { + $attendancesbycontextid = self::group_by_property($attendances, 'contextid'); + + foreach ($attendancesbycontextid as $contextid => $sessions) { + $context = context::instance_by_id($contextid); + $sessionsbyid = self::group_by_property($sessions, 'sessionid'); + + foreach ($sessionsbyid as $sessionid => $sessions) { + writer::with_context($context)->export_data( + [get_string('session', 'attendance') . ' ' . $sessionid, $path], + (object)[array_map([self::class, 'transform_db_row_to_session_data'], $sessions)] + ); + }; + } + } + + /** + * Helper function to export attendances (and associated warnings for the user). + * + * The array of "attendances" is actually the result returned by the SQL in export_user_data. + * It is more of a list of sessions. Which is why it needs to be grouped by context id. + * + * @param stdClass $user The user to export attendances for. This is needed to retrieve context data. + * @param array $attendances Array of attendances to export. + * @param array $warningsmap Mapping between an attendance id and warnings. + */ + private static function export_attendances(stdClass $user, array $attendances, array $warningsmap) { + $attendancesbycontextid = self::group_by_property($attendances, 'contextid'); + + foreach ($attendancesbycontextid as $contextid => $attendance) { + $context = context::instance_by_id($contextid); + + // It's "safe" to get the attendanceid from the first element in the array - since they're grouped by context. + // i.e., module context. + // The reason there can be more than one "attendance" is that the attendances array will contain multiple records + // for the same attendance instance if there are multiple sessions. It is not the same as a raw record from the + // attendances table. See the SQL in export_user_data. + $warnings = array_map([self::class, 'transform_warning_data'], $warningsmap[$attendance[0]->attendanceid] ?? []); + + writer::with_context($context)->export_data( + [], + (object)array_merge( + (array) helper::get_context_data($context, $user), + ['warnings' => $warnings] + ) + ); + } + } +} diff --git a/mod/attendance/classes/report_page_params.php b/mod/attendance/classes/report_page_params.php new file mode 100644 index 0000000..0a68861 --- /dev/null +++ b/mod/attendance/classes/report_page_params.php @@ -0,0 +1,92 @@ +<?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 definition for mod_attendance_report_page_params + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * contains specific data/functions for report_page. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_report_page_params extends mod_attendance_page_with_filter_controls { + /** @var int */ + public $group; + /** @var int */ + public $sort; + /** @var int */ + public $showextrauserdetails; + /** @var int */ + public $showsessiondetails; + /** @var int */ + public $sessiondetailspos; + + /** + * mod_attendance_report_page_params constructor. + */ + public function __construct() { + $this->selectortype = self::SELECTOR_GROUP; + } + + /** + * Initialise params. + * + * @param stdClass $cm + */ + public function init($cm) { + parent::init($cm); + + if (!isset($this->group)) { + $this->group = $this->get_current_sesstype() > 0 ? $this->get_current_sesstype() : 0; + } + if (!isset($this->sort)) { + $this->sort = ATT_SORT_DEFAULT; + } + } + + /** + * Get params for this page. + * @return array + */ + public function get_significant_params() { + $params = array(); + + if ($this->sort != ATT_SORT_DEFAULT) { + $params['sort'] = $this->sort; + } + + if (empty($this->showextrauserdetails)) { + $params['showextrauserdetails'] = 0; + } + + if (empty($this->showsessiondetails)) { + $params['showsessiondetails'] = 0; + } + + if ($this->sessiondetailspos != 'left') { + $params['sessiondetailspos'] = $this->sessiondetailspos; + } + + return $params; + } +} diff --git a/mod/attendance/classes/search/activity.php b/mod/attendance/classes/search/activity.php new file mode 100644 index 0000000..d71ce49 --- /dev/null +++ b/mod/attendance/classes/search/activity.php @@ -0,0 +1,37 @@ +<?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/>. + +/** + * Search area for mod_attendance activities. + * + * @package mod_attendance + * @copyright 2016 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\search; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Search area for mod_attendance activities. + * + * @package mod_attendance + * @copyright 2016 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity extends \core_search\base_activity { +} diff --git a/mod/attendance/classes/sessions_page_params.php b/mod/attendance/classes/sessions_page_params.php new file mode 100644 index 0000000..a638811 --- /dev/null +++ b/mod/attendance/classes/sessions_page_params.php @@ -0,0 +1,66 @@ +<?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 definition for mod_attendance_sessions_page_params + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * stores constants/data used by sessions page params. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_sessions_page_params { + /** + * Add Session. + */ + const ACTION_ADD = 1; + + /** + * Update Session. + */ + const ACTION_UPDATE = 2; + + /** + * Delete Session + */ + const ACTION_DELETE = 3; + + /** + * Delete selected Sessions. + */ + const ACTION_DELETE_SELECTED = 4; + + /** + * Change duration of a session. + */ + const ACTION_CHANGE_DURATION = 5; + + /** + * Delete a hidden session. + */ + const ACTION_DELETE_HIDDEN = 6; + + /** @var int view mode of taking attendance page*/ + public $action; +} \ No newline at end of file diff --git a/mod/attendance/classes/structure.php b/mod/attendance/classes/structure.php new file mode 100644 index 0000000..c5d349f --- /dev/null +++ b/mod/attendance/classes/structure.php @@ -0,0 +1,1339 @@ +<?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 definition for mod_attendance_structure + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +global $CFG; // This class is included inside existing functions. +require_once(dirname(__FILE__) . '/calendar_helpers.php'); +require_once($CFG->libdir .'/filelib.php'); + +/** + * Main class with all Attendance related info. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_structure { + /** Common sessions */ + const SESSION_COMMON = 0; + /** Group sessions */ + const SESSION_GROUP = 1; + + /** @var stdclass course module record */ + public $cm; + + /** @var int cmid - needed for calendar internal tests (see Issue #473) */ + public $cmid; + + /** @var stdclass course record */ + public $course; + + /** @var stdclass context object */ + public $context; + + /** @var int attendance instance identifier */ + public $id; + + /** @var string attendance activity name */ + public $name; + + /** @var float number (10, 5) unsigned, the maximum grade for attendance */ + public $grade; + + /** @var int last time attendance was modified - used for global search */ + public $timemodified; + + /** @var string required field for activity modules and searching */ + public $intro; + + /** @var int format of the intro (see above) */ + public $introformat; + + /** @var array current page parameters */ + public $pageparams; + + /** @var string subnets (IP range) for student self selection. */ + public $subnet; + + /** @var string subnets (IP range) for student self selection. */ + public $automark; + + /** @var boolean flag set when automarking is complete. */ + public $automarkcompleted; + + /** @var int Define if extra user details should be shown in reports */ + public $showextrauserdetails; + + /** @var int Define if session details should be shown in reports */ + public $showsessiondetails; + + /** @var int Position for the session detail columns related to summary columns.*/ + public $sessiondetailspos; + + /** @var int groupmode */ + private $groupmode; + + /** @var array */ + private $statuses; + /** @var array Cache list of all statuses (not just one used by current session). */ + private $allstatuses; + + /** @var array of sessionid. */ + private $sessioninfo = array(); + + /** @var float number [0..1], the threshold for student to be shown at low grade report */ + private $lowgradethreshold; + + + /** + * Initializes the attendance API instance using the data from DB + * + * Makes deep copy of all passed records properties. Replaces integer $course attribute + * with a full database record (course should not be stored in instances table anyway). + * + * @param stdClass $dbrecord Attandance instance data from {attendance} table + * @param stdClass $cm Course module record as returned by {@see get_coursemodule_from_id()} + * @param stdClass $course Course record from {course} table + * @param stdClass $context The context of the attendance instance + * @param stdClass $pageparams + */ + public function __construct(stdClass $dbrecord, stdClass $cm, stdClass $course, stdClass $context=null, $pageparams=null) { + global $DB; + + foreach ($dbrecord as $field => $value) { + if (property_exists('mod_attendance_structure', $field)) { + $this->{$field} = $value; + } else { + throw new coding_exception('The attendance table has a field with no property in the attendance class'); + } + } + $this->cm = $cm; + if (empty($this->cmid)) { + $this->cmid = $cm->id; + } + $this->course = $course; + if (is_null($context)) { + $this->context = context_module::instance($this->cm->id); + } else { + $this->context = $context; + } + + $this->pageparams = $pageparams; + + if (isset($pageparams->showextrauserdetails) && $pageparams->showextrauserdetails != $this->showextrauserdetails) { + $DB->set_field('attendance', 'showextrauserdetails', $pageparams->showextrauserdetails, array('id' => $this->id)); + } + if (isset($pageparams->showsessiondetails) && $pageparams->showsessiondetails != $this->showsessiondetails) { + $DB->set_field('attendance', 'showsessiondetails', $pageparams->showsessiondetails, array('id' => $this->id)); + } + if (isset($pageparams->sessiondetailspos) && $pageparams->sessiondetailspos != $this->sessiondetailspos) { + $DB->set_field('attendance', 'sessiondetailspos', $pageparams->sessiondetailspos, array('id' => $this->id)); + } + } + + /** + * Get group mode. + * + * @return int + */ + public function get_group_mode() : int { + if (is_null($this->groupmode)) { + $this->groupmode = groups_get_activity_groupmode($this->cm, $this->course); + } + return $this->groupmode; + } + + /** + * Returns current sessions for this attendance + * + * Fetches data from {attendance_sessions} + * + * @return array of records or an empty array + */ + public function get_current_sessions() : array { + global $DB; + + $today = time(); // Because we compare with database, we don't need to use usertime(). + + $sql = "SELECT * + FROM {attendance_sessions} + WHERE :time BETWEEN sessdate AND (sessdate + duration) + AND attendanceid = :aid"; + $params = array( + 'time' => $today, + 'aid' => $this->id); + + return $DB->get_records_sql($sql, $params); + } + + /** + * Returns today sessions for this attendance + * + * Fetches data from {attendance_sessions} + * + * @return array of records or an empty array + */ + public function get_today_sessions() : array { + global $DB; + + $start = usergetmidnight(time()); + $end = $start + DAYSECS; + + $sql = "SELECT * + FROM {attendance_sessions} + WHERE sessdate >= :start AND sessdate < :end + AND attendanceid = :aid"; + $params = array( + 'start' => $start, + 'end' => $end, + 'aid' => $this->id); + + return $DB->get_records_sql($sql, $params); + } + + /** + * Returns today sessions suitable for copying attendance log + * + * Fetches data from {attendance_sessions} + * @param stdClass $sess + * @return array of records or an empty array + */ + public function get_today_sessions_for_copy($sess) : array { + global $DB; + + $start = usergetmidnight($sess->sessdate); + + $sql = "SELECT * + FROM {attendance_sessions} + WHERE sessdate >= :start AND sessdate <= :end AND + (groupid = 0 OR groupid = :groupid) AND + lasttaken > 0 AND attendanceid = :aid"; + $params = array( + 'start' => $start, + 'end' => $sess->sessdate, + 'groupid' => $sess->groupid, + 'aid' => $this->id); + + return $DB->get_records_sql($sql, $params); + } + + /** + * Returns count of hidden sessions for this attendance + * + * Fetches data from {attendance_sessions} + * + * @return int count of hidden sessions + */ + public function get_hidden_sessions_count() : int { + global $DB; + + $where = "attendanceid = :aid AND sessdate < :csdate"; + $params = array( + 'aid' => $this->id, + 'csdate' => $this->course->startdate); + + return $DB->count_records_select('attendance_sessions', $where, $params); + } + + /** + * Returns the hidden sessions for this attendance + * + * Fetches data from {attendance_sessions} + * + * @return array hidden sessions + */ + public function get_hidden_sessions() : array { + global $DB; + + $where = "attendanceid = :aid AND sessdate < :csdate"; + $params = array( + 'aid' => $this->id, + 'csdate' => $this->course->startdate); + + return $DB->get_records_select('attendance_sessions', $where, $params); + } + + /** + * Get filtered sessions. + * + * @return array + */ + public function get_filtered_sessions() : array { + global $DB; + + if ($this->pageparams->startdate && $this->pageparams->enddate) { + $where = "attendanceid = :aid AND sessdate >= :csdate AND sessdate >= :sdate AND sessdate < :edate"; + } else if ($this->pageparams->enddate) { + $where = "attendanceid = :aid AND sessdate >= :csdate AND sessdate < :edate"; + } else { + $where = "attendanceid = :aid AND sessdate >= :csdate"; + } + + if ($this->pageparams->get_current_sesstype() > mod_attendance_page_with_filter_controls::SESSTYPE_ALL) { + $where .= " AND (groupid = :cgroup OR groupid = 0)"; + } + $params = array( + 'aid' => $this->id, + 'csdate' => $this->course->startdate, + 'sdate' => $this->pageparams->startdate, + 'edate' => $this->pageparams->enddate, + 'cgroup' => $this->pageparams->get_current_sesstype()); + $sessions = $DB->get_records_select('attendance_sessions', $where, $params, 'sessdate asc'); + $statussetmaxpoints = attendance_get_statusset_maxpoints($this->get_statuses(true, true)); + foreach ($sessions as $sess) { + if (empty($sess->description)) { + $sess->description = get_string('nodescription', 'attendance'); + } else { + $sess->description = file_rewrite_pluginfile_urls($sess->description, + 'pluginfile.php', $this->context->id, 'mod_attendance', 'session', $sess->id); + } + $sess->maxpoints = $statussetmaxpoints[$sess->statusset]; + } + + return $sessions; + } + + /** + * Get manage url. + * @param array $params + * @return moodle_url of manage.php for attendance instance + */ + public function url_manage($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/manage.php', $params); + } + + /** + * Get manage temp users url. + * @param array $params optional + * @return moodle_url of tempusers.php for attendance instance + */ + public function url_managetemp($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/tempusers.php', $params); + } + + /** + * Get temp delete url. + * + * @param array $params optional + * @return moodle_url of tempdelete.php for attendance instance + */ + public function url_tempdelete($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id, 'action' => 'delete'), $params); + return new moodle_url('/mod/attendance/tempedit.php', $params); + } + + /** + * Get temp edit url. + * + * @param array $params optional + * @return moodle_url of tempedit.php for attendance instance + */ + public function url_tempedit($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/tempedit.php', $params); + } + + /** + * Get temp merge url + * + * @param array $params optional + * @return moodle_url of tempedit.php for attendance instance + */ + public function url_tempmerge($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/tempmerge.php', $params); + } + + /** + * Get url for sessions. + * @param array $params + * @return moodle_url of sessions.php for attendance instance + */ + public function url_sessions($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/sessions.php', $params); + } + + /** + * Get url for report. + * @param array $params + * @return moodle_url of report.php for attendance instance + */ + public function url_report($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/report.php', $params); + } + + /** + * Get url for report. + * @param array $params + * @return moodle_url of report.php for attendance instance + */ + public function url_absentee($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/absentee.php', $params); + } + + /** + * Get url for export. + * + * @return moodle_url of export.php for attendance instance + */ + public function url_export() : moodle_url { + $params = array('id' => $this->cm->id); + return new moodle_url('/mod/attendance/export.php', $params); + } + + /** + * Get preferences url + * @param array $params + * @return moodle_url of attsettings.php for attendance instance + */ + public function url_preferences($params=array()) : moodle_url { + // Add the statusset params. + if (isset($this->pageparams->statusset) && !isset($params['statusset'])) { + $params['statusset'] = $this->pageparams->statusset; + } + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/preferences.php', $params); + } + + /** + * Get preferences url + * @param array $params + * @return moodle_url of attsettings.php for attendance instance + */ + public function url_warnings($params=array()) : moodle_url { + // Add the statusset params. + if (isset($this->pageparams->statusset) && !isset($params['statusset'])) { + $params['statusset'] = $this->pageparams->statusset; + } + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/warnings.php', $params); + } + + /** + * Get take url. + * @param array $params + * @return moodle_url of attendances.php for attendance instance + */ + public function url_take($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/take.php', $params); + } + + /** + * Get view url. + * @param array $params + * @return moodle_url + */ + public function url_view($params=array()) : moodle_url { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/view.php', $params); + } + + /** + * Add sessions. + * + * @param array $sessions + */ + public function add_sessions($sessions) { + foreach ($sessions as $sess) { + $this->add_session($sess); + } + } + + /** + * Add single session. + * + * @param stdClass $sess + * @return int $sessionid + */ + public function add_session($sess) : int { + global $DB; + $config = get_config('attendance'); + + $sess->attendanceid = $this->id; + $sess->automarkcompleted = 0; + if (!isset($sess->automark)) { + $sess->automark = 0; + } + if (empty($config->enablecalendar)) { + // If calendard disabled at site level, don't use it. + $sess->calendarevent = 0; + } + $sess->id = $DB->insert_record('attendance_sessions', $sess); + $description = file_save_draft_area_files($sess->descriptionitemid, + $this->context->id, 'mod_attendance', 'session', $sess->id, + array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0), + $sess->description); + $DB->set_field('attendance_sessions', 'description', $description, array('id' => $sess->id)); + + $sess->caleventid = 0; + attendance_create_calendar_event($sess); + + $infoarray = array(); + $infoarray[] = construct_session_full_date_time($sess->sessdate, $sess->duration); + + // Trigger a session added event. + $event = \mod_attendance\event\session_added::create(array( + 'objectid' => $this->id, + 'context' => $this->context, + 'other' => array('info' => implode(',', $infoarray)) + )); + $event->add_record_snapshot('course_modules', $this->cm); + $sess->description = $description; + $sess->lasttaken = 0; + $sess->lasttakenby = 0; + if (!isset($sess->studentscanmark)) { + $sess->studentscanmark = 0; + } + if (!isset($sess->autoassignstatus)) { + $sess->autoassignstatus = 0; + } + if (!isset($sess->studentpassword)) { + $sess->studentpassword = ''; + } + if (!isset($sess->subnet)) { + $sess->subnet = ''; + } + + if (!isset($sess->preventsharedip)) { + $sess->preventsharedip = 0; + } + + if (!isset($sess->preventsharediptime)) { + $sess->preventsharediptime = ''; + } + if (!isset($sess->includeqrcode)) { + $sess->includeqrcode = 0; + } + if (!isset($sess->rotateqrcode)) { + $sess->rotateqrcode = 0; + $sess->rotateqrcodesecret = ''; + } + $event->add_record_snapshot('attendance_sessions', $sess); + $event->trigger(); + + return $sess->id; + } + + /** + * Update session from form. + * + * @param stdClass $formdata + * @param int $sessionid + */ + public function update_session_from_form_data($formdata, $sessionid) { + global $DB; + + if (!$sess = $DB->get_record('attendance_sessions', array('id' => $sessionid) )) { + print_error('No such session in this course'); + } + + $sesstarttime = $formdata->sestime['starthour'] * HOURSECS + $formdata->sestime['startminute'] * MINSECS; + $sesendtime = $formdata->sestime['endhour'] * HOURSECS + $formdata->sestime['endminute'] * MINSECS; + + $sess->sessdate = $formdata->sessiondate + $sesstarttime; + $sess->duration = $sesendtime - $sesstarttime; + + $description = file_save_draft_area_files($formdata->sdescription['itemid'], + $this->context->id, 'mod_attendance', 'session', $sessionid, + array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0), $formdata->sdescription['text']); + $sess->description = $description; + $sess->descriptionformat = $formdata->sdescription['format']; + $sess->calendarevent = empty($formdata->calendarevent) ? 0 : $formdata->calendarevent; + + $sess->studentscanmark = 0; + $sess->autoassignstatus = 0; + $sess->studentpassword = ''; + $sess->subnet = ''; + $sess->automark = 0; + $sess->automarkcompleted = 0; + $sess->preventsharedip = 0; + $sess->preventsharediptime = ''; + $sess->includeqrcode = 0; + $sess->rotateqrcode = 0; + $sess->rotateqrcodesecret = ''; + + if (!empty(get_config('attendance', 'enablewarnings'))) { + $sess->absenteereport = empty($formdata->absenteereport) ? 0 : 1; + } + if (!empty($formdata->autoassignstatus)) { + $sess->autoassignstatus = $formdata->autoassignstatus; + } + $studentscanmark = get_config('attendance', 'studentscanmark'); + + if (!empty($studentscanmark) && + !empty($formdata->studentscanmark)) { + $sess->studentscanmark = $formdata->studentscanmark; + $sess->studentpassword = $formdata->studentpassword; + $sess->autoassignstatus = $formdata->autoassignstatus; + if (!empty($formdata->includeqrcode)) { + $sess->includeqrcode = $formdata->includeqrcode; + } + if (!empty($formdata->rotateqrcode)) { + $sess->rotateqrcode = $formdata->rotateqrcode; + $sess->studentpassword = attendance_random_string(); + $sess->rotateqrcodesecret = attendance_random_string(); + } + } + if (!empty($formdata->usedefaultsubnet)) { + $sess->subnet = $this->subnet; + } else { + $sess->subnet = $formdata->subnet; + } + + if (!empty($formdata->automark)) { + $sess->automark = $formdata->automark; + } + if (!empty($formdata->preventsharedip)) { + $sess->preventsharedip = $formdata->preventsharedip; + } + if (!empty($formdata->preventsharediptime)) { + $sess->preventsharediptime = $formdata->preventsharediptime; + } + + $sess->timemodified = time(); + $DB->update_record('attendance_sessions', $sess); + + if (empty($sess->caleventid)) { + // This shouldn't really happen, but just in case to prevent fatal error. + attendance_create_calendar_event($sess); + } else { + attendance_update_calendar_event($sess); + } + + $info = construct_session_full_date_time($sess->sessdate, $sess->duration); + $event = \mod_attendance\event\session_updated::create(array( + 'objectid' => $this->id, + 'context' => $this->context, + 'other' => array('info' => $info, 'sessionid' => $sessionid, + 'action' => mod_attendance_sessions_page_params::ACTION_UPDATE))); + $event->add_record_snapshot('course_modules', $this->cm); + $event->add_record_snapshot('attendance_sessions', $sess); + $event->trigger(); + } + + /** + * Used to record attendance submitted by the student. + * + * @param stdClass $mformdata + * @return boolean + */ + public function take_from_student($mformdata) : bool { + global $DB, $USER; + + $statuses = implode(',', array_keys( (array)$this->get_statuses() )); + $now = time(); + + $record = new stdClass(); + $record->studentid = $USER->id; + $record->statusid = $mformdata->status; + $record->statusset = $statuses; + $record->remarks = get_string('set_by_student', 'mod_attendance'); + $record->sessionid = $mformdata->sessid; + $record->timetaken = $now; + $record->takenby = $USER->id; + $record->ipaddress = getremoteaddr(null); + + $existingattendance = $DB->record_exists('attendance_log', + array('sessionid' => $mformdata->sessid, 'studentid' => $USER->id)); + + if ($existingattendance) { + // Already recorded do not save. + return false; + } + + $logid = $DB->insert_record('attendance_log', $record, false); + $record->id = $logid; + + // Update the session to show that a register has been taken, or staff may overwrite records. + $session = $this->get_session_info($mformdata->sessid); + $session->lasttaken = $now; + $session->lasttakenby = $USER->id; + $DB->update_record('attendance_sessions', $session); + + // Update the users grade. + $this->update_users_grade(array($USER->id)); + + /* create url for link in log screen + * need to set grouptype to 0 to allow take attendance page to be called + * from report/log page */ + + $params = array( + 'sessionid' => $this->pageparams->sessionid, + 'grouptype' => 0); + + // Log the change. + $event = \mod_attendance\event\attendance_taken_by_student::create(array( + 'objectid' => $this->id, + 'context' => $this->context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $this->cm); + $event->add_record_snapshot('attendance_sessions', $session); + $event->add_record_snapshot('attendance_log', $record); + $event->trigger(); + + return true; + } + + /** + * Take attendance from form data. + * + * @param stdClass $data + */ + public function take_from_form_data($data) { + global $USER; + // WARNING - $data is unclean - comes from direct $_POST - ideally needs a rewrite but we do some cleaning below. + + $statuses = implode(',', array_keys( (array)$this->get_statuses() )); + $now = time(); + $sesslog = array(); + + $formdata = (array)$data; + + foreach ($formdata as $key => $value) { + // Look at Remarks field because the user options may not be passed if empty. + if (substr($key, 0, 7) == 'remarks') { + $sid = substr($key, 7); + if (!(is_numeric($sid))) { // Sanity check on $sid. + print_error('nonnumericid', 'attendance'); + } + $sesslog[$sid] = new stdClass(); + $sesslog[$sid]->studentid = $sid; // We check is_numeric on this above. + if (array_key_exists('user' . $sid, $formdata) && is_numeric($formdata['user' . $sid])) { + $sesslog[$sid]->statusid = $formdata['user' . $sid]; + } + $sesslog[$sid]->statusset = $statuses; + $sesslog[$sid]->remarks = $value; + $sesslog[$sid]->sessionid = $this->pageparams->sessionid; + $sesslog[$sid]->timetaken = $now; + $sesslog[$sid]->takenby = $USER->id; + } + } + + $this->save_log($sesslog); + } + + /** + * Helper function to save attendance and trigger events. + * + * @param array $sesslog + * @throws coding_exception + * @throws dml_exception + */ + public function save_log($sesslog) { + global $DB, $USER; + // Get existing session log. + $dbsesslog = $this->get_session_log($this->pageparams->sessionid); + foreach ($sesslog as $log) { + // Don't save a record if no statusid or remark. + if (!empty($log->statusid) || !empty($log->remarks)) { + if (array_key_exists($log->studentid, $dbsesslog)) { + // Check if anything important has changed before updating record. + // Don't update timetaken/takenby records if nothing has changed. + if ($dbsesslog[$log->studentid]->remarks <> $log->remarks || + $dbsesslog[$log->studentid]->statusid <> $log->statusid || + $dbsesslog[$log->studentid]->statusset <> $log->statusset) { + + $log->id = $dbsesslog[$log->studentid]->id; + $DB->update_record('attendance_log', $log); + } + } else { + $DB->insert_record('attendance_log', $log, false); + } + } + } + + $session = $this->get_session_info($this->pageparams->sessionid); + $session->lasttaken = time(); + $session->lasttakenby = $USER->id; + + $DB->update_record('attendance_sessions', $session); + + if ($this->grade != 0) { + $this->update_users_grade(array_keys($sesslog)); + } + + // Create url for link in log screen. + $params = array( + 'sessionid' => $this->pageparams->sessionid, + 'grouptype' => $this->pageparams->grouptype); + $event = \mod_attendance\event\attendance_taken::create(array( + 'objectid' => $this->id, + 'context' => $this->context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $this->cm); + $event->add_record_snapshot('attendance_sessions', $session); + $event->trigger(); + } + + /** + * Get users with enrolment status (Feature request MDL-27591) + * + * @param int $groupid + * @param int $page + * @return array + */ + public function get_users($groupid = 0, $page = 1) : array { + global $DB; + + $fields = array('username' , 'idnumber' , 'institution' , 'department', 'city', 'country'); + // Get user identity fields if required - doesn't return original $fields array. + $extrafields = get_extra_user_fields($this->context, $fields); + $fields = array_merge($fields, $extrafields); + + $userfields = user_picture::fields('u', $fields); + + if (empty($this->pageparams->sort)) { + $this->pageparams->sort = ATT_SORT_DEFAULT; + } + if ($this->pageparams->sort == ATT_SORT_FIRSTNAME) { + $orderby = $DB->sql_fullname('u.firstname', 'u.lastname') . ', u.id'; + } else if ($this->pageparams->sort == ATT_SORT_LASTNAME) { + $orderby = 'u.lastname, u.firstname, u.id'; + } else { + list($orderby, $sortparams) = users_order_by_sql('u'); + } + + if ($page) { + $usersperpage = $this->pageparams->perpage; + if (!empty($this->cm->groupingid)) { + $startusers = ($page - 1) * $usersperpage; + if ($groupid == 0) { + $groups = array_keys(groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid, 'g.id')); + } else { + $groups = $groupid; + } + $users = get_users_by_capability($this->context, 'mod/attendance:canbelisted', + $userfields, + $orderby, $startusers, $usersperpage, $groups, + '', false, true); + } else { + $startusers = ($page - 1) * $usersperpage; + $users = get_enrolled_users($this->context, 'mod/attendance:canbelisted', $groupid, $userfields, + $orderby, $startusers, $usersperpage); + } + } else { + if (!empty($this->cm->groupingid)) { + if ($groupid == 0) { + $groups = array_keys(groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid, 'g.id')); + } else { + $groups = $groupid; + } + $users = get_users_by_capability($this->context, 'mod/attendance:canbelisted', + $userfields, + $orderby, '', '', $groups, + '', false, true); + } else { + $users = get_enrolled_users($this->context, 'mod/attendance:canbelisted', $groupid, $userfields, $orderby); + } + } + + // Add a flag to each user indicating whether their enrolment is active. + if (!empty($users)) { + list($sql, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED, 'usid0'); + + // See CONTRIB-4868. + $mintime = 'MIN(CASE WHEN (ue.timestart > :zerotime) THEN ue.timestart ELSE ue.timecreated END)'; + $maxtime = 'CASE WHEN MIN(ue.timeend) = 0 THEN 0 ELSE MAX(ue.timeend) END'; + + // See CONTRIB-3549. + $sql = "SELECT ue.userid, MIN(ue.status) as status, + $mintime AS mintime, + $maxtime AS maxtime + FROM {user_enrolments} ue + JOIN {enrol} e ON e.id = ue.enrolid + WHERE ue.userid $sql + AND e.status = :estatus + AND e.courseid = :courseid + GROUP BY ue.userid"; + $params += array('zerotime' => 0, 'estatus' => ENROL_INSTANCE_ENABLED, 'courseid' => $this->course->id); + $enrolments = $DB->get_records_sql($sql, $params); + + foreach ($users as $user) { + $users[$user->id]->fullname = fullname($user); + $users[$user->id]->enrolmentstatus = $enrolments[$user->id]->status; + $users[$user->id]->enrolmentstart = $enrolments[$user->id]->mintime; + $users[$user->id]->enrolmentend = $enrolments[$user->id]->maxtime; + $users[$user->id]->type = 'standard'; // Mark as a standard (not a temporary) user. + } + } + + // Add the 'temporary' users to this list. + $tempusers = $DB->get_records('attendance_tempusers', array('courseid' => $this->course->id)); + foreach ($tempusers as $tempuser) { + $users[$tempuser->studentid] = self::tempuser_to_user($tempuser); + } + + return $users; + } + + /** + * Convert a tempuser record into a user object. + * + * @param stdClass $tempuser + * @return object + */ + protected static function tempuser_to_user($tempuser) { + global $CFG; + + $ret = (object)array( + 'id' => $tempuser->studentid, + 'firstname' => $tempuser->fullname, + 'email' => $tempuser->email, + 'username' => '', + 'enrolmentstatus' => 0, + 'enrolmentstart' => 0, + 'enrolmentend' => 0, + 'picture' => 0, + 'type' => 'temporary', + ); + $allfields = get_all_user_name_fields(); + if (!empty($CFG->showuseridentity)) { + $allfields = array_merge($allfields, explode(',', $CFG->showuseridentity)); + } + + foreach ($allfields as $namefield) { + if (!isset($ret->$namefield)) { + $ret->$namefield = ''; + } + } + + return $ret; + } + + /** + * Get user and include extra info. + * + * @param int $userid + * @return mixed|object + */ + public function get_user($userid) { + global $DB; + + $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); + + // Look for 'temporary' users and return their details from the attendance_tempusers table. + if ($user->idnumber == 'tempghost') { + $tempuser = $DB->get_record('attendance_tempusers', array('studentid' => $userid), '*', MUST_EXIST); + return self::tempuser_to_user($tempuser); + } + + $user->type = 'standard'; + + // See CONTRIB-4868. + $mintime = 'MIN(CASE WHEN (ue.timestart > :zerotime) THEN ue.timestart ELSE ue.timecreated END)'; + $maxtime = 'CASE WHEN MIN(ue.timeend) = 0 THEN 0 ELSE MAX(ue.timeend) END'; + + $sql = "SELECT ue.userid, ue.status, + $mintime AS mintime, + $maxtime AS maxtime + FROM {user_enrolments} ue + JOIN {enrol} e ON e.id = ue.enrolid + WHERE ue.userid = :uid + AND e.status = :estatus + AND e.courseid = :courseid + GROUP BY ue.userid, ue.status"; + $params = array('zerotime' => 0, 'uid' => $userid, 'estatus' => ENROL_INSTANCE_ENABLED, 'courseid' => $this->course->id); + $enrolments = $DB->get_record_sql($sql, $params); + if (!empty($enrolments)) { + $user->enrolmentstatus = $enrolments->status; + $user->enrolmentstart = $enrolments->mintime; + $user->enrolmentend = $enrolments->maxtime; + } else { + $user->enrolmentstatus = ''; + $user->enrolmentstart = 0; + $user->enrolmentend = 0; + } + + return $user; + } + + /** + * Get possible statuses. + * + * @param bool $onlyvisible + * @param bool $allsets + * @return array + */ + public function get_statuses($onlyvisible = true, $allsets = false) : array { + if (!isset($this->statuses)) { + // Get the statuses for the current set only. + $statusset = 0; + if (isset($this->pageparams->statusset)) { + $statusset = $this->pageparams->statusset; + } else if (isset($this->pageparams->sessionid)) { + $sessioninfo = $this->get_session_info($this->pageparams->sessionid); + $statusset = $sessioninfo->statusset; + } + $this->statuses = attendance_get_statuses($this->id, $onlyvisible, $statusset); + $this->allstatuses = attendance_get_statuses($this->id, $onlyvisible); + } + + // Return all sets, if requested. + if ($allsets) { + return $this->allstatuses; + } + return $this->statuses; + } + + /** + * Get session info. + * @param int $sessionid + * @return mixed + */ + public function get_session_info($sessionid) { + global $DB; + + if (!array_key_exists($sessionid, $this->sessioninfo)) { + $this->sessioninfo[$sessionid] = $DB->get_record('attendance_sessions', array('id' => $sessionid)); + } + if (empty($this->sessioninfo[$sessionid]->description)) { + $this->sessioninfo[$sessionid]->description = get_string('nodescription', 'attendance'); + } else { + $this->sessioninfo[$sessionid]->description = file_rewrite_pluginfile_urls($this->sessioninfo[$sessionid]->description, + 'pluginfile.php', $this->context->id, 'mod_attendance', 'session', $this->sessioninfo[$sessionid]->id); + } + return $this->sessioninfo[$sessionid]; + } + + /** + * Get sessions info + * + * @param array $sessionids + * @return array + */ + public function get_sessions_info($sessionids) : array { + global $DB; + + list($sql, $params) = $DB->get_in_or_equal($sessionids); + $sessions = $DB->get_records_select('attendance_sessions', "id $sql", $params, 'sessdate asc'); + + foreach ($sessions as $sess) { + if (empty($sess->description)) { + $sess->description = get_string('nodescription', 'attendance'); + } else { + $sess->description = file_rewrite_pluginfile_urls($sess->description, + 'pluginfile.php', $this->context->id, 'mod_attendance', 'session', $sess->id); + } + } + + return $sessions; + } + + /** + * Get log. + * + * @param int $sessionid + * @return array + */ + public function get_session_log($sessionid) : array { + global $DB; + + return $DB->get_records('attendance_log', array('sessionid' => $sessionid), '', 'studentid,statusid,remarks,id,statusset'); + } + + /** + * Update user grade. + * @param array $userids + */ + public function update_users_grade($userids) { + attendance_update_users_grade($this, $userids); + } + + /** + * Get filtered log. + * @param int $userid + * @return array + */ + public function get_user_filtered_sessions_log($userid) : array { + global $DB; + + if ($this->pageparams->startdate && $this->pageparams->enddate) { + $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate AND + ats.sessdate >= :sdate AND ats.sessdate < :edate"; + } else { + $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate"; + } + if ($this->get_group_mode()) { + $sql = "SELECT ats.id, ats.sessdate, ats.groupid, al.statusid, al.remarks, + ats.preventsharediptime, ats.preventsharedip + FROM {attendance_sessions} ats + JOIN {attendance_log} al ON ats.id = al.sessionid AND al.studentid = :uid + LEFT JOIN {groups_members} gm ON gm.userid = al.studentid AND gm.groupid = ats.groupid + WHERE $where AND (ats.groupid = 0 or gm.id is NOT NULL) + ORDER BY ats.sessdate ASC"; + + $params = array( + 'uid' => $userid, + 'aid' => $this->id, + 'csdate' => $this->course->startdate, + 'sdate' => $this->pageparams->startdate, + 'edate' => $this->pageparams->enddate); + + } else { + $sql = "SELECT ats.id, ats.sessdate, ats.groupid, al.statusid, al.remarks, + ats.preventsharediptime, ats.preventsharedip + FROM {attendance_sessions} ats + JOIN {attendance_log} al + ON ats.id = al.sessionid AND al.studentid = :uid + WHERE $where + ORDER BY ats.sessdate ASC"; + + $params = array( + 'uid' => $userid, + 'aid' => $this->id, + 'csdate' => $this->course->startdate, + 'sdate' => $this->pageparams->startdate, + 'edate' => $this->pageparams->enddate); + } + $sessions = $DB->get_records_sql($sql, $params); + + return $sessions; + } + + /** + * Get filtered log extended. + * @param int $userid + * @return array + */ + public function get_user_filtered_sessions_log_extended($userid) : array { + global $DB; + // All taked sessions (including previous groups). + + if ($this->pageparams->startdate && $this->pageparams->enddate) { + $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate AND + ats.sessdate >= :sdate AND ats.sessdate < :edate"; + } else { + $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate"; + } + + // We need to add this concatination so that moodle will use it as the array index that is a string. + // If the array's index is a number it will not merge entries. + // It would be better as a UNION query but unfortunatly MS SQL does not seem to support doing a + // DISTINCT on a the description field. + $id = $DB->sql_concat(':value', 'ats.id'); + if ($this->get_group_mode()) { + $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, + al.statusid, al.remarks, ats.studentscanmark, ats.autoassignstatus, + ats.preventsharedip, ats.preventsharediptime, ats.rotateqrcode + FROM {attendance_sessions} ats + RIGHT JOIN {attendance_log} al + ON ats.id = al.sessionid AND al.studentid = :uid + LEFT JOIN {groups_members} gm ON gm.userid = al.studentid AND gm.groupid = ats.groupid + WHERE $where AND (ats.groupid = 0 or gm.id is NOT NULL) + ORDER BY ats.sessdate ASC"; + } else { + $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, ats.statusset, + al.statusid, al.remarks, ats.studentscanmark, ats.autoassignstatus, + ats.preventsharedip, ats.preventsharediptime, ats.rotateqrcode + FROM {attendance_sessions} ats + RIGHT JOIN {attendance_log} al + ON ats.id = al.sessionid AND al.studentid = :uid + WHERE $where + ORDER BY ats.sessdate ASC"; + } + + $params = array( + 'uid' => $userid, + 'aid' => $this->id, + 'csdate' => $this->course->startdate, + 'sdate' => $this->pageparams->startdate, + 'edate' => $this->pageparams->enddate, + 'value' => 'c'); + $sessions = $DB->get_records_sql($sql, $params); + + // All sessions for current groups. + + $groups = array_keys(groups_get_all_groups($this->course->id, $userid)); + $groups[] = 0; + list($gsql, $gparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'gid0'); + + if ($this->pageparams->startdate && $this->pageparams->enddate) { + $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate AND + ats.sessdate >= :sdate AND ats.sessdate < :edate AND ats.groupid $gsql"; + } else { + $where = "ats.attendanceid = :aid AND ats.sessdate >= :csdate AND ats.groupid $gsql"; + } + $sql = "SELECT $id, ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, ats.statusset, + al.statusid, al.remarks, ats.studentscanmark, ats.autoassignstatus, + ats.preventsharedip, ats.preventsharediptime, ats.rotateqrcode + FROM {attendance_sessions} ats + LEFT JOIN {attendance_log} al + ON ats.id = al.sessionid AND al.studentid = :uid + WHERE $where + ORDER BY ats.sessdate ASC"; + + $params = array_merge($params, $gparams); + $sessions = array_merge($sessions, $DB->get_records_sql($sql, $params)); + + foreach ($sessions as $sess) { + if (empty($sess->description)) { + $sess->description = get_string('nodescription', 'attendance'); + } else { + $sess->description = file_rewrite_pluginfile_urls($sess->description, + 'pluginfile.php', $this->context->id, 'mod_attendance', 'session', $sess->id); + } + } + + return $sessions; + } + + /** + * Delete sessions. + * @param array $sessionsids + */ + public function delete_sessions($sessionsids) { + global $DB; + if (attendance_existing_calendar_events_ids($sessionsids)) { + attendance_delete_calendar_events($sessionsids); + } + + list($sql, $params) = $DB->get_in_or_equal($sessionsids); + $DB->delete_records_select('attendance_log', "sessionid $sql", $params); + $DB->delete_records_list('attendance_sessions', 'id', $sessionsids); + $event = \mod_attendance\event\session_deleted::create(array( + 'objectid' => $this->id, + 'context' => $this->context, + 'other' => array('info' => implode(', ', $sessionsids)))); + $event->add_record_snapshot('course_modules', $this->cm); + $event->trigger(); + } + + /** + * Update duration. + * + * @param array $sessionsids + * @param int $duration + */ + public function update_sessions_duration($sessionsids, $duration) { + global $DB; + + $now = time(); + $sessions = $DB->get_recordset_list('attendance_sessions', 'id', $sessionsids); + foreach ($sessions as $sess) { + $sess->duration = $duration; + $sess->timemodified = $now; + $DB->update_record('attendance_sessions', $sess); + if ($sess->caleventid) { + attendance_update_calendar_event($sess); + } + $event = \mod_attendance\event\session_duration_updated::create(array( + 'objectid' => $this->id, + 'context' => $this->context, + 'other' => array('info' => implode(', ', $sessionsids)))); + $event->add_record_snapshot('course_modules', $this->cm); + $event->add_record_snapshot('attendance_sessions', $sess); + $event->trigger(); + } + $sessions->close(); + } + + /** + * Check if the email address is already in use by either another temporary user, + * or a real user. + * + * @param string $email the address to check for + * @param int $tempuserid optional the ID of the temporary user (to avoid matching against themself) + * @return null|string the error message to display, null if there is no error + */ + public static function check_existing_email($email, $tempuserid = 0) { + global $DB; + + if (empty($email)) { + return null; // Fine to create temporary users without an email address. + } + if ($tempuser = $DB->get_record('attendance_tempusers', array('email' => $email), 'id')) { + if ($tempuser->id != $tempuserid) { + return get_string('tempexists', 'attendance'); + } + } + if ($DB->record_exists('user', array('email' => $email))) { + return get_string('userexists', 'attendance'); + } + + return null; + } + + /** + * Gets the status to use when auto-marking. + * + * @param int $time the time the user first accessed the course. + * @param int $sessionid the related sessionid to check. + * @return int the statusid to assign to this user. + */ + public function get_automark_status($time, $sessionid) { + $statuses = $this->get_statuses(); + // Statuses are returned highest grade first, find the first high grade we can assign to this user. + + // Get status to use when unmarked. + $session = $this->sessioninfo[$sessionid]; + $duration = $session->duration; + if (empty($duration)) { + $duration = get_config('attendance', 'studentscanmarksessiontimeend') * 60; + } + if ($time > $session->sessdate + $duration) { + // This session closed after the users access - use the unmarked state. + foreach ($statuses as $status) { + if (!empty($status->setunmarked)) { + return $status->id; + } + } + } else { + foreach ($statuses as $status) { + if ($status->studentavailability !== '0' && + $this->sessioninfo[$sessionid]->sessdate + ($status->studentavailability * 60) > $time) { + + // Found first status we could set. + return $status->id; + } + } + } + return; + } + + /** + * Gets the lowgrade threshold to use. + * + */ + public function get_lowgrade_threshold() { + if (!isset($this->lowgradethreshold)) { + $this->lowgradethreshold = 1; + + if ($this->grade > 0) { + $gradeitem = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', + 'itemmodule' => 'attendance', 'iteminstance' => $this->id)); + if ($gradeitem->gradepass > 0 && $gradeitem->grademax != $gradeitem->grademin) { + $this->lowgradethreshold = ($gradeitem->gradepass - $gradeitem->grademin) / + ($gradeitem->grademax - $gradeitem->grademin); + } + } + } + + return $this->lowgradethreshold; + } +} diff --git a/mod/attendance/classes/summary.php b/mod/attendance/classes/summary.php new file mode 100644 index 0000000..2e83fac --- /dev/null +++ b/mod/attendance/classes/summary.php @@ -0,0 +1,367 @@ +<?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 that computes summary of users points + * + * @package mod_attendance + * @copyright 2016 Antonio Carlos Mariani http://antonio.c.mariani@gmail.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/mod/attendance/locallib.php'); + +/** + * Class that computes summary of users points + * + * @package mod_attendance + * @copyright 2016 Antonio Carlos Mariani http://antonio.c.mariani@gmail.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_summary { + + /** @var int attendance instance identifier */ + private $attendanceid; + + /** @var stdclass course course data*/ + private $course; + + /** @var int groupmode*/ + private $groupmode; + + /** @var array userspoints (userid, numtakensessions, points, maxpoints) */ + private $userspoints; + + /** @var array pointsbygroup (groupid, numsessions, maxpoints) */ + private $maxpointsbygroupsessions; + + /** @var array userstakensessionsbyacronym */ + private $userstakensessionsbyacronym; + + /** + * Initializes the class + * + * @param int $attendanceid instance identifier + * @param array $userids user instances identifier + * @param int $startdate Attendance sessions startdate + * @param int $enddate Attendance sessions enddate + */ + public function __construct($attendanceid, $userids=array(), $startdate = '', $enddate = '') { + $this->attendanceid = $attendanceid; + + $this->compute_users_points($userids, $startdate, $enddate); + $this->compute_users_taken_sessions_by_acronym($userids, $startdate, $enddate); + } + + /** + * Returns true if the user has some session with points + * + * @param int $userid User instance id + * + * @return boolean + */ + public function has_taken_sessions($userid) { + return isset($this->userspoints[$userid]); + } + + /** + * Returns true if the corresponding attendance instance is currently configure to work with grades (points) + * + * @return boolean + */ + public function with_groups() { + return $this->groupmode > 0; + } + + /** + * Returns the groupmode of the corresponding attendance instance + * + * @return int + */ + public function get_groupmode() { + return $this->groupmode; + } + + /** + * Returns the percentages of each user related to the taken sessions + * + * @return array + */ + public function get_user_taken_sessions_percentages() { + $percentages = array(); + + foreach ($this->userspoints as $userid => $userpoints) { + $percentages[$userid] = attendance_calc_fraction($userpoints->points, $userpoints->maxpoints); + } + + return $percentages; + } + + /** + * Returns a summary of the points assigned to the user related to the taken sessions + * + * @param int $userid User instance id + * + * @return array + */ + public function get_taken_sessions_summary_for($userid) { + $usersummary = new stdClass(); + if ($this->has_taken_sessions($userid)) { + $usersummary->numtakensessions = $this->userspoints[$userid]->numtakensessions; + $usersummary->takensessionspoints = $this->userspoints[$userid]->points; + $usersummary->takensessionsmaxpoints = $this->userspoints[$userid]->maxpoints; + } else { + $usersummary->numtakensessions = 0; + $usersummary->takensessionspoints = 0; + $usersummary->takensessionsmaxpoints = 0; + } + $usersummary->takensessionspercentage = attendance_calc_fraction($usersummary->takensessionspoints, + $usersummary->takensessionsmaxpoints); + if (isset($this->userstakensessionsbyacronym[$userid])) { + $usersummary->userstakensessionsbyacronym = $this->userstakensessionsbyacronym[$userid]; + } else { + $usersummary->userstakensessionsbyacronym = array(); + } + + $usersummary->pointssessionscompleted = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' . + format_float($usersummary->takensessionsmaxpoints, 1, true, true); + + $usersummary->percentagesessionscompleted = format_float($usersummary->takensessionspercentage * 100) . '%'; + + return $usersummary; + } + + /** + * Returns a summary of the points assigned to the user, both related to taken sessions and related to all sessions + * + * @param int $userid User instance id + * + * @return array + */ + public function get_all_sessions_summary_for($userid) { + $usersummary = $this->get_taken_sessions_summary_for($userid); + + if (!isset($this->maxpointsbygroupsessions)) { + $this->compute_maxpoints_by_group_session(); + } + + $usersummary->numallsessions = $this->maxpointsbygroupsessions[0]->numsessions; + $usersummary->allsessionsmaxpoints = $this->maxpointsbygroupsessions[0]->maxpoints; + + if ($this->with_groups()) { + $groupids = array_keys(groups_get_all_groups($this->course->id, $userid)); + foreach ($groupids as $gid) { + if (isset($this->maxpointsbygroupsessions[$gid])) { + $usersummary->numallsessions += $this->maxpointsbygroupsessions[$gid]->numsessions; + $usersummary->allsessionsmaxpoints += $this->maxpointsbygroupsessions[$gid]->maxpoints; + } + } + } + $usersummary->allsessionspercentage = attendance_calc_fraction($usersummary->takensessionspoints, + $usersummary->allsessionsmaxpoints); + $usersummary->allsessionspercentage = format_float($usersummary->allsessionspercentage * 100) . '%'; + + $deltapoints = $usersummary->allsessionsmaxpoints - $usersummary->takensessionsmaxpoints; + + $usersummary->maxpossiblepoints = $usersummary->takensessionspoints + $deltapoints; + $usersummary->maxpossiblepoints = format_float($usersummary->maxpossiblepoints, 1, true, true) . ' / ' . + format_float($usersummary->allsessionsmaxpoints, 1, true, true); + + $usersummary->maxpossiblepercentage = attendance_calc_fraction(($usersummary->takensessionspoints + $deltapoints), + $usersummary->allsessionsmaxpoints); + $usersummary->maxpossiblepercentage = format_float($usersummary->maxpossiblepercentage * 100) . '%'; + + $usersummary->pointssessionscompleted = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' . + format_float($usersummary->takensessionsmaxpoints, 1, true, true); + + $usersummary->percentagesessionscompleted = format_float($usersummary->takensessionspercentage * 100) . '%'; + + $usersummary->pointsallsessions = format_float($usersummary->takensessionspoints, 1, true, true) . ' / ' . + format_float($usersummary->allsessionsmaxpoints, 1, true, true); + + return $usersummary; + } + + /** + * Computes the summary of points for the users that have some taken session + * + * @param array $userids user instances identifier + * @param int $startdate Attendance sessions startdate + * @param int $enddate Attendance sessions enddate + * @return (userid, numtakensessions, points, maxpoints) + */ + private function compute_users_points($userids=array(), $startdate = '', $enddate = '') { + global $DB; + + list($this->course, $cm) = get_course_and_cm_from_instance($this->attendanceid, 'attendance'); + $this->groupmode = $cm->effectivegroupmode; + + $params = array( + 'attid' => $this->attendanceid, + 'attid2' => $this->attendanceid, + 'cstartdate' => $this->course->startdate, + ); + + $where = ''; + if (!empty($userids)) { + list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $where .= ' AND atl.studentid ' . $insql; + $params = array_merge($params, $inparams); + } + if (!empty($startdate)) { + $where .= ' AND ats.sessdate >= :startdate'; + $params['startdate'] = $startdate; + } + if (!empty($enddate)) { + $where .= ' AND ats.sessdate < :enddate '; + $params['enddate'] = $enddate; + } + + $joingroup = ''; + if ($this->with_groups()) { + $joingroup = 'LEFT JOIN {groups_members} gm ON (gm.userid = atl.studentid AND gm.groupid = ats.groupid)'; + $where .= ' AND (ats.groupid = 0 or gm.id is NOT NULL)'; + } else { + $where .= ' AND ats.groupid = 0'; + } + + $sql = " SELECT atl.studentid AS userid, COUNT(DISTINCT ats.id) AS numtakensessions, + SUM(stg.grade) AS points, SUM(stm.maxgrade) AS maxpoints + FROM {attendance_sessions} ats + JOIN {attendance_log} atl ON (atl.sessionid = ats.id) + JOIN {attendance_statuses} stg ON (stg.id = atl.statusid AND stg.deleted = 0 AND stg.visible = 1) + JOIN (SELECT setnumber, MAX(grade) AS maxgrade + FROM {attendance_statuses} + WHERE attendanceid = :attid2 + AND deleted = 0 + AND visible = 1 + GROUP BY setnumber) stm + ON (stm.setnumber = ats.statusset) + {$joingroup} + WHERE ats.attendanceid = :attid + AND ats.sessdate >= :cstartdate + AND ats.lasttaken != 0 + {$where} + GROUP BY atl.studentid"; + $this->userspoints = $DB->get_records_sql($sql, $params); + } + + /** + * Computes the summary of taken sessions by acronym + * + * @param array $userids user instances identifier + * @param int $startdate Attendance sessions startdate + * @param int $enddate Attendance sessions enddate + * @return null + */ + private function compute_users_taken_sessions_by_acronym($userids=array(), $startdate = '', $enddate = '') { + global $DB; + + list($this->course, $cm) = get_course_and_cm_from_instance($this->attendanceid, 'attendance'); + $this->groupmode = $cm->effectivegroupmode; + + $params = array( + 'attid' => $this->attendanceid, + 'cstartdate' => $this->course->startdate, + ); + + $where = ''; + if (!empty($userids)) { + list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $where .= ' AND atl.studentid ' . $insql; + $params = array_merge($params, $inparams); + } + if (!empty($startdate)) { + $where .= ' AND ats.sessdate >= :startdate'; + $params['startdate'] = $startdate; + } + if (!empty($enddate)) { + $where .= ' AND ats.sessdate < :enddate '; + $params['enddate'] = $enddate; + } + + if ($this->with_groups()) { + $joingroup = 'LEFT JOIN {groups_members} gm ON (gm.userid = atl.studentid AND gm.groupid = ats.groupid)'; + $where .= ' AND (ats.groupid = 0 or gm.id is NOT NULL)'; + } else { + $joingroup = ''; + $where .= ' AND ats.groupid = 0'; + } + + $sql = "SELECT atl.studentid AS userid, sts.setnumber, sts.acronym, COUNT(*) AS numtakensessions + FROM {attendance_sessions} ats + JOIN {attendance_log} atl ON (atl.sessionid = ats.id) + JOIN {attendance_statuses} sts + ON (sts.attendanceid = ats.attendanceid AND + sts.id = atl.statusid AND + sts.deleted = 0 AND sts.visible = 1) + {$joingroup} + WHERE ats.attendanceid = :attid + AND ats.sessdate >= :cstartdate + AND ats.lasttaken != 0 + {$where} + GROUP BY atl.studentid, sts.setnumber, sts.acronym"; + $this->userstakensessionsbyacronym = array(); + $records = $DB->get_recordset_sql($sql, $params); + foreach ($records as $rec) { + $this->userstakensessionsbyacronym[$rec->userid][$rec->setnumber][$rec->acronym] = $rec->numtakensessions; + } + $records->close(); + } + + /** + * Computes and store the maximum points possible for each group session + * + * @return null + */ + private function compute_maxpoints_by_group_session() { + global $DB; + + $params = array( + 'attid' => $this->attendanceid, + 'attid2' => $this->attendanceid, + 'cstartdate' => $this->course->startdate, + ); + + $where = ''; + if (!$this->with_groups()) { + $where = 'AND sess.groupid = 0'; + } + + $sql = "SELECT sess.groupid, COUNT(*) AS numsessions, SUM(stamax.maxgrade) AS maxpoints + FROM {attendance_sessions} sess + JOIN (SELECT setnumber, MAX(grade) AS maxgrade + FROM {attendance_statuses} + WHERE attendanceid = :attid2 + AND deleted = 0 + AND visible = 1 + GROUP BY setnumber) stamax + ON (stamax.setnumber = sess.statusset) + WHERE sess.attendanceid = :attid + AND sess.sessdate >= :cstartdate + {$where} + GROUP BY sess.groupid"; + $this->maxpointsbygroupsessions = $DB->get_records_sql($sql, $params); + + if (!isset($this->maxpointsbygroupsessions[0])) { + $gpoints = new stdClass(); + $gpoints->numsessions = 0; + $gpoints->maxpoints = 0; + $this->maxpointsbygroupsessions[0] = $gpoints; + } + } +} diff --git a/mod/attendance/classes/take_page_params.php b/mod/attendance/classes/take_page_params.php new file mode 100644 index 0000000..38f29ec --- /dev/null +++ b/mod/attendance/classes/take_page_params.php @@ -0,0 +1,116 @@ +<?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 definition for mod_attendance_take_page_params + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * contains functions/constants used by take attendance page. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_take_page_params { + /** Sorted list. */ + const SORTED_LIST = 1; + /** Sorted grid. */ + const SORTED_GRID = 2; + + /** Default view */ + const DEFAULT_VIEW_MODE = self::SORTED_LIST; + + /** @var int */ + public $sessionid; + /** @var int */ + public $grouptype; + /** @var int */ + public $group; + /** @var int */ + public $sort; + /** @var int */ + public $copyfrom; + + /** @var int view mode of taking attendance page*/ + public $viewmode; + + /** @var int */ + public $gridcols; + + /** + * Initialize params. + */ + public function init() { + if (!isset($this->group)) { + $this->group = 0; + } + if (!isset($this->sort)) { + $this->sort = ATT_SORT_DEFAULT; + } + $this->init_view_mode(); + $this->init_gridcols(); + } + + /** + * Initialise view mode params. + */ + private function init_view_mode() { + if (isset($this->viewmode)) { + set_user_preference("attendance_take_view_mode", $this->viewmode); + } else { + $this->viewmode = get_user_preferences("attendance_take_view_mode", self::DEFAULT_VIEW_MODE); + } + } + + /** + * Initilise grid columns. + */ + private function init_gridcols() { + if (isset($this->gridcols)) { + set_user_preference("attendance_gridcolumns", $this->gridcols); + } else { + $this->gridcols = get_user_preferences("attendance_gridcolumns", 5); + } + } + + /** + * Get main page params. + * @return array + */ + public function get_significant_params() { + $params = array(); + + $params['sessionid'] = $this->sessionid; + $params['grouptype'] = $this->grouptype; + if ($this->group) { + $params['group'] = $this->group; + } + if ($this->sort != ATT_SORT_DEFAULT) { + $params['sort'] = $this->sort; + } + if (isset($this->copyfrom)) { + $params['copyfrom'] = $this->copyfrom; + } + + return $params; + } +} diff --git a/mod/attendance/classes/task/auto_mark.php b/mod/attendance/classes/task/auto_mark.php new file mode 100644 index 0000000..40be6d5 --- /dev/null +++ b/mod/attendance/classes/task/auto_mark.php @@ -0,0 +1,230 @@ +<?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/>. + +/** + * Attendance task - auto mark. + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\task; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/attendance/locallib.php'); +/** + * get_scores class, used to get scores for submitted files. + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class auto_mark extends \core\task\scheduled_task { + /** + * Returns localised general event name. + * + * @return string + */ + public function get_name() { + // Shown in admin screens. + return get_string('automarktask', 'mod_attendance'); + } + + /** + * Execte the task. + */ + public function execute() { + global $DB; + // Create some cache vars - might be nice to restructure this and make a smaller number of sql calls. + $cachecm = array(); + $cacheatt = array(); + $cachecourse = array(); + $now = time(); // Store current time to use in queries so they all match nicely. + + $sessions = $DB->get_recordset_select('attendance_sessions', + 'automark > 0 AND automarkcompleted < 2 AND sessdate < ? ', array($now)); + + foreach ($sessions as $session) { + if ($session->sessdate + $session->duration < $now || // If session is over. + // OR if session is currently open and automark is set to do all. + ($session->sessdate < $now && $session->automark == 1)) { + + $userfirstaccess = array(); + $donesomething = false; // Only trigger grades/events when an update actually occurs. + $sessionover = false; // Is this session over? + if ($session->sessdate + $session->duration < $now) { + $sessionover = true; + } + + // Store cm/att/course in cachefields so we don't make unnecessary db calls. + // Would probably be nice to grab this stuff outside of the loop. + // Make sure this status set has something to setunmarked. + $setunmarked = $DB->get_field('attendance_statuses', 'id', + array('attendanceid' => $session->attendanceid, 'setnumber' => $session->statusset, + 'setunmarked' => 1, 'deleted' => 0)); + if (empty($setunmarked)) { + mtrace("No unmarked status configured for session id: ".$session->id); + continue; + } + if (empty($cacheatt[$session->attendanceid])) { + $cacheatt[$session->attendanceid] = $DB->get_record('attendance', array('id' => $session->attendanceid)); + } + if (empty($cachecm[$session->attendanceid])) { + $cachecm[$session->attendanceid] = get_coursemodule_from_instance('attendance', + $session->attendanceid, $cacheatt[$session->attendanceid]->course); + } + $courseid = $cacheatt[$session->attendanceid]->course; + if (empty($cachecourse[$courseid])) { + $cachecourse[$courseid] = $DB->get_record('course', array('id' => $courseid)); + } + $context = \context_module::instance($cachecm[$session->attendanceid]->id); + + $pageparams = new \mod_attendance_take_page_params(); + $pageparams->group = $session->groupid; + if (empty($session->groupid)) { + $pageparams->grouptype = 0; + } else { + $pageparams->grouptype = 1; + } + $pageparams->sessionid = $session->id; + + if ($session->automark == 1) { + $userfirstacess = array(); + // If set to do full automarking, get all users that have accessed course during session open. + $id = $DB->sql_concat('userid', 'ip'); // Users may access from multiple ip, make the first field unique. + $sql = "SELECT $id, userid, ip, min(timecreated) as timecreated + FROM {logstore_standard_log} + WHERE courseid = ? AND timecreated > ? AND timecreated < ? + GROUP BY userid, ip"; + + $timestart = $session->sessdate; + if (empty($session->lasttakenby) && $session->lasttaken > $timestart) { + // If the last time session was taken it was done automatically, use the last time taken + // as the start time for the logs we are interested in to help with performance. + $timestart = $session->lasttaken; + } + $duration = $session->duration; + if (empty($duration)) { + $duration = get_config('attendance', 'studentscanmarksessiontimeend') * 60; + } + $timeend = $timestart + $duration; + $logusers = $DB->get_recordset_sql($sql, array($courseid, $timestart, $timeend)); + // Check if user access is in allowed subnet. + foreach ($logusers as $loguser) { + if (!empty($session->subnet) && !address_in_subnet($loguser->ip, $session->subnet)) { + // This record isn't in the right subnet. + continue; + } + if (empty($userfirstaccess[$loguser->userid]) || + $userfirstaccess[$loguser->userid] > $loguser->timecreated) { + // Users may have accessed from mulitple ip addresses, find the earliest access. + $userfirstaccess[$loguser->userid] = $loguser->timecreated; + } + } + $logusers->close(); + } + + // Get all unmarked students. + $att = new \mod_attendance_structure($cacheatt[$session->attendanceid], + $cachecm[$session->attendanceid], $cachecourse[$courseid], $context, $pageparams); + + $users = $att->get_users($session->groupid, 0); + + $existinglog = $DB->get_recordset('attendance_log', array('sessionid' => $session->id)); + $updated = 0; + + foreach ($existinglog as $log) { + if (empty($log->statusid)) { + if ($sessionover || !empty($userfirstaccess[$log->studentid])) { + // Status needs updating. + if ($sessionover) { + $log->statusid = $setunmarked; + } else if (!empty($userfirstaccess[$log->studentid])) { + $log->statusid = $att->get_automark_status($userfirstaccess[$log->studentid], $session->id); + } + if (!empty($log->statusid)) { + $log->timetaken = $now; + $log->takenby = 0; + $log->remarks = get_string('autorecorded', 'attendance'); + + $DB->update_record('attendance_log', $log); + $updated++; + $donesomething = true; + } + } + } + unset($users[$log->studentid]); + } + $existinglog->close(); + mtrace($updated . " session status updated"); + + $newlog = new \stdClass(); + $newlog->timetaken = $now; + $newlog->takenby = 0; + $newlog->sessionid = $session->id; + $newlog->remarks = get_string('autorecorded', 'attendance'); + $newlog->statusset = implode(',', array_keys( (array)$att->get_statuses())); + + $added = 0; + foreach ($users as $user) { + if ($sessionover || !empty($userfirstaccess[$user->id])) { + if ($sessionover) { + $newlog->statusid = $setunmarked; + } else if (!empty($userfirstaccess[$user->id])) { + $newlog->statusid = $att->get_automark_status($userfirstaccess[$user->id], $session->id); + } + if (!empty($newlog->statusid)) { + $newlog->studentid = $user->id; + $DB->insert_record('attendance_log', $newlog); + $added++; + $donesomething = true; + } + } + } + mtrace($added . " session status inserted"); + + // Update lasttaken time and automarkcompleted for this session. + $session->lasttaken = $now; + $session->lasttakenby = 0; + if ($sessionover) { + $session->automarkcompleted = 2; + } else { + $session->automarkcompleted = 1; + } + + $DB->update_record('attendance_sessions', $session); + + if ($donesomething) { + if ($att->grade != 0) { + $att->update_users_grade(array_keys($users)); + } + + $params = array( + 'sessionid' => $att->pageparams->sessionid, + 'grouptype' => $att->pageparams->grouptype); + $event = \mod_attendance\event\attendance_taken::create(array( + 'objectid' => $att->id, + 'context' => $att->context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $att->cm); + $event->add_record_snapshot('attendance_sessions', $session); + $event->trigger(); + } + } + } + } +} \ No newline at end of file diff --git a/mod/attendance/classes/task/clear_temporary_passwords.php b/mod/attendance/classes/task/clear_temporary_passwords.php new file mode 100644 index 0000000..0f5d727 --- /dev/null +++ b/mod/attendance/classes/task/clear_temporary_passwords.php @@ -0,0 +1,54 @@ +<?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/>. + +/** + * Attendance task - clear temporary passwords. + * + * @package mod_attendance + * @copyright 2019 Maksud R + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\task; +defined('MOODLE_INTERNAL') || die(); + +/** + * clear_temporary_passwords class, used to clean up the temporary passwords. + * + * @package mod_attendance + * @copyright 2019 Maksud R + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class clear_temporary_passwords extends \core\task\scheduled_task { + /** + * Return the task's name as shown in admin screens. + * + * @return string + */ + public function get_name() { + return get_string('rotateqrcode_cleartemppass_task', 'mod_attendance'); + } + + /** + * Execute the task. + */ + public function execute() { + global $DB; + + $params = array('currenttime' => time()); + $DB->delete_records_select('attendance_rotate_passwords', 'expirytime < :currenttime', $params); + } +} \ No newline at end of file diff --git a/mod/attendance/classes/task/notify.php b/mod/attendance/classes/task/notify.php new file mode 100644 index 0000000..e857410 --- /dev/null +++ b/mod/attendance/classes/task/notify.php @@ -0,0 +1,173 @@ +<?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/>. + +/** + * Attendance task - Send warnings. + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_attendance\task; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/attendance/lib.php'); +require_once($CFG->dirroot . '/mod/attendance/locallib.php'); +/** + * Task class + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class notify extends \core\task\scheduled_task { + /** + * Returns localised general event name. + * + * @return string + */ + public function get_name() { + // Shown in admin screens. + return get_string('notifytask', 'mod_attendance'); + } + + /** + * Execute the task. + */ + public function execute() { + global $DB; + if (empty(get_config('attendance', 'enablewarnings'))) { + return; // Warnings not enabled. + } + $now = time(); // Store current time to use in queries so they all match nicely. + + $orderby = 'ORDER BY cm.id, atl.studentid, n.warningpercent ASC'; + + // Get records for attendance sessions that have been updated since last time this task ran. + // Note: this returns all users for these sessions - even if the users attendance wasn't changed + // since last time we ran, before sending a notification we check to see if the users have + // updated attendance logs since last time they were notified. + $records = attendance_get_users_to_notify(array(), $orderby, true); + $sentnotifications = array(); + $thirdpartynotifications = array(); + $numsentusers = 0; + $numsentthird = 0; + foreach ($records as $record) { + if (empty($sentnotifications[$record->userid])) { + $sentnotifications[$record->userid] = array(); + } + + if (!empty($record->emailuser)) { + // Only send one warning to this user from each attendance in this run. + // Flag any higher percent notifications as sent. + if (empty($sentnotifications[$record->userid]) || !in_array($record->aid, $sentnotifications[$record->userid])) { + + // If has previously been sent a warning, check to see if this user has + // attendance updated since the last time the notification was sent. + if (!empty($record->timesent)) { + $sql = "SELECT * + FROM {attendance_log} l + JOIN {attendance_sessions} s ON s.id = l.sessionid + WHERE s.attendanceid = ? AND studentid = ? AND timetaken > ?"; + if (!$DB->record_exists_sql($sql, array($record->aid, $record->userid, $record->timesent))) { + continue; // Skip this record and move to the next user. + } + } + + // Convert variables in emailcontent. + $record = attendance_template_variables($record); + $user = $DB->get_record('user', array('id' => $record->userid)); + $from = \core_user::get_noreply_user(); + $oldforcelang = force_current_language($user->lang); + + $emailcontent = format_text($record->emailcontent, $record->emailcontentformat); + $emailsubject = format_text($record->emailsubject, FORMAT_HTML); + email_to_user($user, $from, $emailsubject, $emailcontent, $emailcontent); + + force_current_language($oldforcelang); + $sentnotifications[$record->userid][] = $record->aid; + $numsentusers++; + } + } + // Only send one warning to this user from each attendance in this run. - flag any higher percent notifications as sent. + $thirdpartyusers = array(); + if (!empty($record->thirdpartyemails)) { + $sendto = explode(',', $record->thirdpartyemails); + $record->percent = round($record->percent * 100)."%"; + $context = \context_module::instance($record->cmid); + foreach ($sendto as $senduser) { + if (empty($senduser)) { + // Probably an extra comma in the thirdpartyusers field. + continue; + } + // Create array of the warnings this user will recieve in case we need to clean up. + $thirdpartyusers[$senduser][] = $record->notifyid; + + // Check user is allowed to receive warningemails. + if (has_capability('mod/attendance:warningemails', $context, $senduser)) { + if (empty($thirdpartynotifications[$senduser])) { + $thirdpartynotifications[$senduser] = array(); + } + if (!isset($thirdpartynotifications[$senduser][$record->aid . '_' . $record->userid])) { + $thirdpartynotifications[$senduser][$record->aid . '_' . $record->userid] + = get_string('thirdpartyemailtext', 'attendance', $record); + } + } else { + mtrace("user".$senduser. "does not have capablity in cm".$record->cmid); + } + } + } + $notify = new \stdClass(); + $notify->userid = $record->userid; + $notify->notifyid = $record->notifyid; + $notify->timesent = $now; + $DB->insert_record('attendance_warning_done', $notify); + } + if (!empty($numsentusers)) { + mtrace($numsentusers ." user emails sent"); + } + if (!empty($thirdpartynotifications)) { + foreach ($thirdpartynotifications as $sendid => $notifications) { + $user = $DB->get_record('user', array('id' => $sendid)); + if (empty($user) || !empty($user->deleted)) { + // Clean this user up and remove from the notification list. + $warnings = $DB->get_records_list('attendance_warning', 'id', $thirdpartyusers[$sendid]); + if (!empty($warnings)) { + attendance_remove_user_from_thirdpartyemails($warnings, $sendid); + } + // Don't send and skip to next notification. + continue; + } + + $from = \core_user::get_noreply_user(); + $oldforcelang = force_current_language($user->lang); + + $emailcontent = implode("\n", $notifications); + $emailcontent .= "\n\n".get_string('thirdpartyemailtextfooter', 'attendance'); + $emailcontent = format_text($emailcontent); + $emailsubject = get_string('thirdpartyemailsubject', 'attendance'); + + email_to_user($user, $from, $emailsubject, $emailcontent, $emailcontent); + force_current_language($oldforcelang); + $numsentthird++; + } + if (!empty($numsentthird)) { + mtrace($numsentthird ." thirdparty emails sent"); + } + } + } +} \ No newline at end of file diff --git a/mod/attendance/classes/view_page_params.php b/mod/attendance/classes/view_page_params.php new file mode 100644 index 0000000..fd5d696 --- /dev/null +++ b/mod/attendance/classes/view_page_params.php @@ -0,0 +1,85 @@ +<?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 definition for mod_attendance_view_page_params + * + * @package mod_attendance + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * contains functions/constants used by attendance view page. + * + * @copyright 2016 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_view_page_params extends mod_attendance_page_with_filter_controls { + /** Only This course */ + const MODE_THIS_COURSE = 0; + + /** All courses */ + const MODE_ALL_COURSES = 1; + + /** All sessions */ + const MODE_ALL_SESSIONS = 2; + + /** @var int */ + public $studentid; + + /** @var string */ + public $mode; + + /** @var string */ + public $groupby; + + /** @var string */ + public $sesscourses; + + /** + * mod_attendance_view_page_params constructor. + */ + public function __construct() { + $this->defaultview = ATT_VIEW_MONTHS; + } + + /** + * Get params for url. + * + * @return array + */ + public function get_significant_params() { + $params = array(); + + if (isset($this->studentid)) { + $params['studentid'] = $this->studentid; + } + if ($this->mode != self::MODE_THIS_COURSE) { + $params['mode'] = $this->mode; + } + if ($this->groupby != 'course') { + $params['groupby'] = $this->groupby; + } + if ($this->sesscourses != 'current') { + $params['sesscourses'] = $this->sesscourses; + } + + return $params; + } +} \ No newline at end of file diff --git a/mod/attendance/composer.json b/mod/attendance/composer.json new file mode 100644 index 0000000..43ab0b7 --- /dev/null +++ b/mod/attendance/composer.json @@ -0,0 +1,10 @@ +{ + "name": "danmarsden/moodle-mod_attendance", + "type": "moodle-mod", + "require": { + "composer/installers": "~1.0" + }, + "extra": { + "installer-name": "attendance" + } +} diff --git a/mod/attendance/coursesummary.php b/mod/attendance/coursesummary.php new file mode 100644 index 0000000..227f048 --- /dev/null +++ b/mod/attendance/coursesummary.php @@ -0,0 +1,129 @@ +<?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/>. + +/** + * Attendance course summary report. + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot.'/mod/attendance/lib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$category = optional_param('category', 0, PARAM_INT); +$download = optional_param('download', '', PARAM_ALPHA); +$sort = optional_param('tsort', '', PARAM_ALPHA); +$fromcourse = optional_param('fromcourse', 0, PARAM_INT); + +$admin = false; +if (empty($fromcourse)) { + $admin = true; + admin_externalpage_setup('managemodules'); +} else { + require_login($fromcourse); +} + +if (empty($category)) { + $context = context_system::instance(); + $courses = array(); // Show all courses. +} else { + $context = context_coursecat::instance($category); + $coursecat = core_course_category::get($category); + $courses = $coursecat->get_courses(array('recursive' => true, 'idonly' => true)); +} +// Check permissions. +require_capability('mod/attendance:viewsummaryreports', $context); + +$exportfilename = 'attendancecoursesummary.csv'; + +$PAGE->set_url('/mod/attendance/coursesummary.php', array('category' => $category)); + +$PAGE->set_heading($SITE->fullname); + +$table = new flexible_table('attendancecoursesummary'); +$table->define_baseurl($PAGE->url); + +if (!$table->is_downloading($download, $exportfilename)) { + echo $OUTPUT->header(); + $heading = get_string('coursesummary', 'mod_attendance'); + if (!empty($category)) { + $heading .= " (".$coursecat->name.")"; + } + echo $OUTPUT->heading($heading); + if ($admin) { + // Only show tabs if displaying via the admin page. + $tabmenu = attendance_print_settings_tabs('coursesummary'); + echo $tabmenu; + } + $url = new moodle_url('/mod/attendance/coursesummary.php', array('category' => $category, 'fromcourse' => $fromcourse)); + + if ($admin) { + $options = core_course_category::make_categories_list('mod/attendance:viewsummaryreports'); + echo $OUTPUT->single_select($url, 'category', $options, $category); + } + +} + +$table->define_columns(array('course', 'percentage')); +$table->define_headers(array(get_string('course'), + get_string('averageattendance', 'attendance'))); +$table->sortable(true); +$table->no_sorting('course'); +$table->set_attribute('cellspacing', '0'); +$table->set_attribute('class', 'generaltable generalbox'); +$table->show_download_buttons_at(array(TABLE_P_BOTTOM)); +$table->setup(); + +// Work out direction of sort required. +$sortcolumns = $table->get_sort_columns(); + +// Sanity check $sort var before including in sql. Make sure it matches a known column. +$allowedsort = array_diff(array_keys($table->columns), $table->column_nosort); +if (!in_array($sort, $allowedsort)) { + $sort = ''; +} + +// Now do sorting if specified. +$orderby = ' ORDER BY percentage ASC'; +if (!empty($sort)) { + $direction = ' DESC'; + if (!empty($sortcolumns[$sort]) && $sortcolumns[$sort] == SORT_ASC) { + $direction = ' ASC'; + } + $orderby = " ORDER BY $sort $direction"; + +} + +$records = attendance_course_users_points($courses, $orderby); +foreach ($records as $record) { + if (!$table->is_downloading($download, $exportfilename)) { + $url = new moodle_url('/mod/attendance/index.php', array('id' => $record->courseid)); + $name = html_writer::link($url, $record->coursename); + } else { + $name = $record->coursename; + } + $table->add_data(array($name, round($record->percentage * 100)."%")); +} +$table->finish_output(); + +if (!$table->is_downloading()) { + echo $OUTPUT->footer(); +} \ No newline at end of file diff --git a/mod/attendance/db/access.php b/mod/attendance/db/access.php new file mode 100644 index 0000000..22841ed --- /dev/null +++ b/mod/attendance/db/access.php @@ -0,0 +1,156 @@ +<?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/>. + +/** + * Capability definitions for this module. + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + 'mod/attendance:view' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/attendance:addinstance' => array( + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ), + + 'mod/attendance:viewreports' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/attendance:takeattendances' => array( + 'riskbitmask' => RISK_DATALOSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/attendance:changeattendances' => array( + 'riskbitmask' => RISK_DATALOSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/attendance:manageattendances' => array( + 'riskbitmask' => RISK_CONFIG, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/attendance:changepreferences' => array( + 'riskbitmask' => RISK_CONFIG, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/attendance:export' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + + 'mod/attendance:canbelisted' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW + ) + ), + + // Allow teachers to manage temporary users. + 'mod/attendance:managetemporaryusers' => array( + 'riskbitmask' => RISK_DATALOSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + // Allow access to site level reports. + 'mod/attendance:viewsummaryreports' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSECAT, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ) + ), + // Users that can receive extra warning e-mails. + 'mod/attendance:warningemails' => array( + 'riskbitmask' => RISK_DATALOSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ) +); diff --git a/mod/attendance/db/events.php b/mod/attendance/db/events.php new file mode 100644 index 0000000..6b877b3 --- /dev/null +++ b/mod/attendance/db/events.php @@ -0,0 +1,36 @@ +<?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/>. + +/** + * Attendance event handler definition. + * + * @package mod_attendance + * @category event + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// List of observers. +$observers = array( + + array( + 'eventname' => '\core\event\course_content_deleted', + 'callback' => 'mod_attendance_observer::course_content_deleted', + ), + +); diff --git a/mod/attendance/db/install.php b/mod/attendance/db/install.php new file mode 100644 index 0000000..43b5565 --- /dev/null +++ b/mod/attendance/db/install.php @@ -0,0 +1,53 @@ +<?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/>. + +/** + * post installation hook for adding data. + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Post installation procedure + */ +function xmldb_attendance_install() { + global $DB; + + $result = true; + $arr = array('P' => 2, 'A' => 0, 'L' => 1, 'E' => 1); + foreach ($arr as $k => $v) { + $rec = new stdClass; + $rec->attendanceid = 0; + $rec->acronym = get_string($k.'acronym', 'attendance'); + // Sanity check - if language translation uses more than the allowed 2 chars. + if (mb_strlen($rec->acronym) > 2) { + $rec->acronym = $k; + } + $rec->description = get_string($k.'full', 'attendance'); + $rec->grade = $v; + $rec->visible = 1; + $rec->deleted = 0; + if (!$DB->record_exists('attendance_statuses', array('attendanceid' => 0, 'acronym' => $rec->acronym))) { + $result = $result && $DB->insert_record('attendance_statuses', $rec); + } + } + + return $result; +} diff --git a/mod/attendance/db/install.xml b/mod/attendance/db/install.xml new file mode 100644 index 0000000..ae0d375 --- /dev/null +++ b/mod/attendance/db/install.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="mod/attendance/db" VERSION="20190622" COMMENT="XMLDB file for Moodle mod/attendance" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="attendance" COMMENT="Attendance module table"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="id of the table, please edit me"/> + <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="grade" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="100" SEQUENCE="false" COMMENT="This is maximum grade for instance"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time the settings for this attendance instance were last modified."/> + <FIELD NAME="intro" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="This field is a requirement for activity modules."/> + <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="This field is a requirement for activity modules."/> + <FIELD NAME="subnet" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Default subnet used when creating sessions."/> + <FIELD NAME="sessiondetailspos" TYPE="char" LENGTH="5" NOTNULL="true" DEFAULT="left" SEQUENCE="false" COMMENT="Position for the session detail columns related to summary columns."/> + <FIELD NAME="showsessiondetails" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Define if session details should be shown in reports."/> + <FIELD NAME="showextrauserdetails" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Define if extra user details should be shown in reports."/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for attendance"/> + </KEYS> + <INDEXES> + <INDEX NAME="course" UNIQUE="false" FIELDS="course"/> + </INDEXES> + </TABLE> + <TABLE NAME="attendance_sessions" COMMENT="attendance_sessions table"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="attendanceid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="sessdate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="duration" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="lasttaken" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="lasttakenby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="studentscanmark" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="autoassignstatus" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="studentpassword" TYPE="char" LENGTH="50" NOTNULL="false" DEFAULT="" SEQUENCE="false"/> + <FIELD NAME="subnet" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Restrict ability for students to mark by subnet."/> + <FIELD NAME="automark" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="automarkcompleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="statusset" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Which set of statuses to use"/> + <FIELD NAME="absenteereport" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="preventsharedip" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="preventsharediptime" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="caleventid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="calendarevent" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="includeqrcode" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Include a QR code image when displaying the password"/> + <FIELD NAME="rotateqrcode" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="rotateqrcodesecret" TYPE="char" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for attendance_sessions"/> + </KEYS> + <INDEXES> + <INDEX NAME="attendanceid" UNIQUE="false" FIELDS="attendanceid"/> + <INDEX NAME="groupid" UNIQUE="false" FIELDS="groupid"/> + <INDEX NAME="sessdate" UNIQUE="false" FIELDS="sessdate"/> + <INDEX NAME="caleventid" UNIQUE="false" FIELDS="caleventid"/> + </INDEXES> + </TABLE> + <TABLE NAME="attendance_log" COMMENT="attendance_log table retrofitted from MySQL"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="sessionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="studentid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="statusid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="link with attendance_status table"/> + <FIELD NAME="statusset" TYPE="char" LENGTH="1333" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="timetaken" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="When attendance of this student was taken"/> + <FIELD NAME="takenby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="remarks" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="ipaddress" TYPE="char" LENGTH="45" NOTNULL="false" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for attendance_log"/> + </KEYS> + <INDEXES> + <INDEX NAME="sessionid" UNIQUE="false" FIELDS="sessionid"/> + <INDEX NAME="studentid" UNIQUE="false" FIELDS="studentid"/> + <INDEX NAME="statusid" UNIQUE="false" FIELDS="statusid"/> + </INDEXES> + </TABLE> + <TABLE NAME="attendance_statuses" COMMENT="attendance_statuses table retrofitted from MySQL"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="attendanceid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="acronym" TYPE="char" LENGTH="2" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="description" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="grade" TYPE="number" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="2"/> + <FIELD NAME="studentavailability" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="How many minutes this status is available when self marking is enabled."/> + <FIELD NAME="setunmarked" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="Set this status if unmarked at end of session."/> + <FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="deleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="setnumber" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Allows different sets of statuses to be allocated to different sessions"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for attendance_settings"/> + </KEYS> + <INDEXES> + <INDEX NAME="attendanceid" UNIQUE="false" FIELDS="attendanceid"/> + <INDEX NAME="visible" UNIQUE="false" FIELDS="visible"/> + <INDEX NAME="deleted" UNIQUE="false" FIELDS="deleted"/> + </INDEXES> + </TABLE> + <TABLE NAME="attendance_tempusers" COMMENT="Stores temporary users details"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="studentid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="student id"/> + <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="course id"/> + <FIELD NAME="fullname" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="temp user fullname"/> + <FIELD NAME="email" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="temporary user email"/> + <FIELD NAME="created" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="unix timestamp for temp user creation"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/> + <INDEX NAME="studentid" UNIQUE="true" FIELDS="studentid"/> + </INDEXES> + </TABLE> + <TABLE NAME="attendance_warning" COMMENT="Warning configuration"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="idnumber" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="attendance id or other id relating to this warning."/> + <FIELD NAME="warningpercent" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Percentage that triggers this warning."/> + <FIELD NAME="warnafter" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Start warning after this number of taken sessions."/> + <FIELD NAME="maxwarn" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Maximum number of warnings to send."/> + <FIELD NAME="emailuser" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="Should the user be notified at this level."/> + <FIELD NAME="emailsubject" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Email subject line for emails going to user"/> + <FIELD NAME="emailcontent" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The html-formatted text that should be sent to the user"/> + <FIELD NAME="emailcontentformat" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="Format of the emailcontent field"/> + <FIELD NAME="thirdpartyemails" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="list of extra users to receive warnings"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + <KEY NAME="level_id" TYPE="unique" FIELDS="idnumber, warningpercent, warnafter"/> + </KEYS> + </TABLE> + <TABLE NAME="attendance_warning_done" COMMENT="Warnings processed"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="notifyid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="id of warning"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="user id of user"/> + <FIELD NAME="timesent" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time warning sent to user."/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="notifyid_userid" UNIQUE="false" FIELDS="notifyid, userid"/> + </INDEXES> + </TABLE> + <TABLE NAME="attendance_rotate_passwords" COMMENT="Table to hold temporary passwords for rotate QR code feature."> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="attendanceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="password" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="expirytime" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="id" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> \ No newline at end of file diff --git a/mod/attendance/db/mobile.php b/mod/attendance/db/mobile.php new file mode 100644 index 0000000..720e920 --- /dev/null +++ b/mod/attendance/db/mobile.php @@ -0,0 +1,70 @@ +<?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/>. + +/** + * Defines mobile handlers. + * + * @package mod_attendance + * @copyright 2018 Dan Marsdenb + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$addons = [ + 'mod_attendance' => [ + 'handlers' => [ + 'view' => [ + 'displaydata' => [ + 'icon' => $CFG->wwwroot . '/mod/attendance/pix/icon.png', + 'class' => '', + ], + 'delegate' => 'CoreCourseModuleDelegate', + 'method' => 'mobile_view_activity', + 'styles' => [ + 'url' => '/mod/attendance/mobilestyles.css', + 'version' => 22 + ] + ] + ], + 'lang' => [ // Language strings that are used in all the handlers. + ['pluginname', 'attendance'], + ['sessionscompleted', 'attendance'], + ['pointssessionscompleted', 'attendance'], + ['percentagesessionscompleted', 'attendance'], + ['sessionstotal', 'attendance'], + ['pointsallsessions', 'attendance'], + ['percentageallsessions', 'attendance'], + ['maxpossiblepoints', 'attendance'], + ['maxpossiblepercentage', 'attendance'], + ['submitattendance', 'attendance'], + ['strftimeh', 'attendance'], + ['strftimehm', 'attendance'], + ['attendancesuccess', 'attendance'], + ['attendance_no_status', 'attendance'], + ['attendance_already_submitted', 'attendance'], + ['somedisabledstatus', 'attendance'], + ['invalidstatus', 'attendance'], + ['preventsharederror', 'attendance'], + ['closed', 'attendance'], + ['subnetwrong', 'attendance'], + ['enterpassword', 'attendance'], + ['incorrectpasswordshort', 'attendance'], + ['attendancesuccess', 'attendance'], + ['setallstatuses', 'attendance'] + ], + ] +]; \ No newline at end of file diff --git a/mod/attendance/db/services.php b/mod/attendance/db/services.php new file mode 100644 index 0000000..6cc2584 --- /dev/null +++ b/mod/attendance/db/services.php @@ -0,0 +1,97 @@ +<?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/>. + +/** + * Web service local plugin attendance external functions and service definitions. + * + * @package mod_attendance + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = array( + 'mod_attendance_add_attendance' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'add_attendance', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Add attendance instance to course.', + 'type' => 'write', + ), + 'mod_attendance_remove_attendance' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'remove_attendance', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Delete attendance instance.', + 'type' => 'write', + ), + 'mod_attendance_add_session' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'add_session', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Add a new session.', + 'type' => 'write', + ), + 'mod_attendance_remove_session' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'remove_session', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Delete a session.', + 'type' => 'write', + ), + 'mod_attendance_get_courses_with_today_sessions' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'get_courses_with_today_sessions', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Method that retrieves courses with today sessions of a teacher.', + 'type' => 'read', + ), + 'mod_attendance_get_session' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'get_session', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Method that retrieves the session data', + 'type' => 'read', + ), + + 'mod_attendance_update_user_status' => array( + 'classname' => 'mod_attendance_external', + 'methodname' => 'update_user_status', + 'classpath' => 'mod/attendance/externallib.php', + 'description' => 'Method that updates the user status in a session.', + 'type' => 'write', + ) +); + + +// We define the services to install as pre-build services. A pre-build service is not editable by administrator. +$services = array( + 'Attendance' => array( + 'functions' => array( + 'mod_attendance_add_attendance', + 'mod_attendance_remove_attendance', + 'mod_attendance_add_session', + 'mod_attendance_remove_session', + 'mod_attendance_get_courses_with_today_sessions', + 'mod_attendance_get_session', + 'mod_attendance_update_user_status' + ), + 'restrictedusers' => 0, + 'enabled' => 1, + 'shortname' => 'mod_attendance' + ) +); diff --git a/mod/attendance/db/tasks.php b/mod/attendance/db/tasks.php new file mode 100644 index 0000000..4cf8e17 --- /dev/null +++ b/mod/attendance/db/tasks.php @@ -0,0 +1,52 @@ +<?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/>. + +/** + * Attendance module tasks. + * + * @package mod_attendance + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = array( + array( + 'classname' => 'mod_attendance\task\auto_mark', + 'blocking' => 0, + 'minute' => '8', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*'), + array( + 'classname' => 'mod_attendance\task\notify', + 'blocking' => 0, + 'minute' => '30', + 'hour' => '1', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*'), + array( + 'classname' => 'mod_attendance\task\clear_temporary_passwords', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '1', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*') +); \ No newline at end of file diff --git a/mod/attendance/db/upgrade.php b/mod/attendance/db/upgrade.php new file mode 100644 index 0000000..f86843c --- /dev/null +++ b/mod/attendance/db/upgrade.php @@ -0,0 +1,644 @@ +<?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/>. + +/** + * upgrade processes for this module. + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once(dirname(__FILE__) . '/upgradelib.php'); + +/** + * upgrade this attendance instance - this function could be skipped but it will be needed later + * @param int $oldversion The old version of the attendance module + * @return bool + */ +function xmldb_attendance_upgrade($oldversion=0) { + + global $DB; + $dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes. + + $result = true; + + if ($oldversion < 2014112000) { + $table = new xmldb_table('attendance_sessions'); + + $field = new xmldb_field('studentscanmark'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2014112000, 'attendance'); + } + + if ($oldversion < 2014112001) { + // Replace values that reference old module "attforblock" to "attendance". + $sql = "UPDATE {grade_items} + SET itemmodule = 'attendance' + WHERE itemmodule = 'attforblock'"; + + $DB->execute($sql); + + $sql = "UPDATE {grade_items_history} + SET itemmodule = 'attendance' + WHERE itemmodule = 'attforblock'"; + + $DB->execute($sql); + + /* + * The user's custom capabilities need to be preserved due to the module renaming. + * Capabilities with a modifierid = 0 value are installed by default. + * Only update the user's custom capabilities where modifierid is not zero. + */ + $sql = $DB->sql_like('capability', '?').' AND modifierid <> 0'; + $rs = $DB->get_recordset_select('role_capabilities', $sql, array('%mod/attforblock%')); + foreach ($rs as $cap) { + $renamedcapability = str_replace('mod/attforblock', 'mod/attendance', $cap->capability); + $exists = $DB->record_exists('role_capabilities', array('roleid' => $cap->roleid, 'capability' => $renamedcapability)); + if (!$exists) { + $DB->update_record('role_capabilities', array('id' => $cap->id, 'capability' => $renamedcapability)); + } + } + + // Delete old role capabilities. + $sql = $DB->sql_like('capability', '?'); + $DB->delete_records_select('role_capabilities', $sql, array('%mod/attforblock%')); + + // Delete old capabilities. + $DB->delete_records_select('capabilities', 'component = ?', array('mod_attforblock')); + + upgrade_mod_savepoint(true, 2014112001, 'attendance'); + } + + if ($oldversion < 2015040501) { + // Define table attendance_tempusers to be created. + $table = new xmldb_table('attendance_tempusers'); + + // Adding fields to table attendance_tempusers. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('studentid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('fullname', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('email', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table attendance_tempusers. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for attendance_tempusers. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Conditionally launch add index courseid. + $index = new xmldb_index('courseid', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Conditionally launch add index studentid. + $index = new xmldb_index('studentid', XMLDB_INDEX_UNIQUE, array('studentid')); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2015040501, 'attendance'); + } + + if ($oldversion < 2015040502) { + + // Define field setnumber to be added to attendance_statuses. + $table = new xmldb_table('attendance_statuses'); + $field = new xmldb_field('setnumber', XMLDB_TYPE_INTEGER, '5', null, XMLDB_NOTNULL, null, '0', 'deleted'); + + // Conditionally launch add field setnumber. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field statusset to be added to attendance_sessions. + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('statusset', XMLDB_TYPE_INTEGER, '5', null, XMLDB_NOTNULL, null, '0', 'descriptionformat'); + + // Conditionally launch add field statusset. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2015040502, 'attendance'); + } + + if ($oldversion < 2015040503) { + + // Changing type of field grade on table attendance_statuses to number. + $table = new xmldb_table('attendance_statuses'); + $field = new xmldb_field('grade', XMLDB_TYPE_NUMBER, '5, 2', null, XMLDB_NOTNULL, null, '0', 'description'); + + // Launch change of type for field grade. + $dbman->change_field_type($table, $field); + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2015040503, 'attendance'); + } + + if ($oldversion < 2016052202) { + // Adding field to store calendar event ids. + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('caleventid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', null); + + // Conditionally launch add field statusset. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Creating events for all existing sessions. + attendance_upgrade_create_calendar_events(); + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2016052202, 'attendance'); + } + + if ($oldversion < 2016082900) { + + // Define field timemodified to be added to attendance. + $table = new xmldb_table('attendance'); + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'grade'); + + // Conditionally launch add field timemodified. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2016082900, 'attendance'); + } + if ($oldversion < 2016112100) { + $table = new xmldb_table('attendance'); + $newfield = $table->add_field('subnet', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timemodified'); + if (!$dbman->field_exists($table, $newfield)) { + $dbman->add_field($table, $newfield); + } + upgrade_mod_savepoint(true, 2016112100, 'attendance'); + } + + if ($oldversion < 2016121300) { + $table = new xmldb_table('attendance'); + $field = new xmldb_field('sessiondetailspos', XMLDB_TYPE_CHAR, '5', null, null, null, 'left', 'subnet'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('showsessiondetails', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'subnet'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2016121300, 'attendance'); + } + + if ($oldversion < 2017020700) { + // Define field timemodified to be added to attendance. + $table = new xmldb_table('attendance'); + + $fields = []; + $fields[] = new xmldb_field('intro', XMLDB_TYPE_TEXT, null, null, null, null, null, 'timemodified'); + $fields[] = new xmldb_field('introformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, 0, 'intro'); + + // Conditionally launch add field. + foreach ($fields as $field) { + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017020700, 'attendance'); + } + + if ($oldversion < 2017042800) { + $table = new xmldb_table('attendance_sessions'); + + $field = new xmldb_field('studentpassword'); + $field->set_attributes(XMLDB_TYPE_CHAR, '50', null, false, null, '', 'studentscanmark'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2017042800, 'attendance'); + } + + if ($oldversion < 2017051101) { + + // Define field studentavailability to be added to attendance_statuses. + $table = new xmldb_table('attendance_statuses'); + $field = new xmldb_field('studentavailability', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'grade'); + + // Conditionally launch add field studentavailability. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017051101, 'attendance'); + } + + if ($oldversion < 2017051103) { + $table = new xmldb_table('attendance_sessions'); + $newfield = $table->add_field('subnet', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'studentpassword'); + if (!$dbman->field_exists($table, $newfield)) { + $dbman->add_field($table, $newfield); + } + upgrade_mod_savepoint(true, 2017051103, 'attendance'); + } + + if ($oldversion < 2017051104) { + // The meaning of the subnet in the attendance table has changed - it is now the "default" value - find all existing + // Attendance with subnet set and set the session subnet for these. + $attendances = $DB->get_recordset_select('attendance', 'subnet IS NOT NULL'); + foreach ($attendances as $attendance) { + if (!empty($attendance->subnet)) { + // Get all sessions for this attendance. + $sessions = $DB->get_recordset('attendance_sessions', array('attendanceid' => $attendance->id)); + foreach ($sessions as $session) { + $session->subnet = $attendance->subnet; + $DB->update_record('attendance_sessions', $session); + } + $sessions->close(); + } + } + $attendances->close(); + + upgrade_mod_savepoint(true, 2017051104, 'attendance'); + } + + if ($oldversion < 2017051900) { + // Define field setunmarked to be added to attendance_statuses. + $table = new xmldb_table('attendance_statuses'); + $field = new xmldb_field('setunmarked', XMLDB_TYPE_INTEGER, '2', null, null, null, null, 'studentavailability'); + + // Conditionally launch add field studentavailability. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017051900, 'attendance'); + } + + if ($oldversion < 2017052201) { + // Define field setunmarked to be added to attendance_statuses. + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('automark', XMLDB_TYPE_INTEGER, '1', null, true, null, '0', 'subnet'); + + // Conditionally launch add field automark. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('automarkcompleted', XMLDB_TYPE_INTEGER, '1', null, true, null, '0', 'automark'); + + // Conditionally launch add field automarkcompleted. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017052201, 'attendance'); + } + + if ($oldversion < 2017060900) { + // Automark values changed. + $default = get_config('attendance', 'automark_default'); + if (!empty($default)) { // Change default if set. + set_config('automark_default', 2, 'attendance'); + } + // Update any sessions set to use automark = 1. + $sql = "UPDATE {attendance_sessions} SET automark = 2 WHERE automark = 1"; + $DB->execute($sql); + + // Update automarkcompleted to 2 if already complete. + $sql = "UPDATE {attendance_sessions} SET automarkcompleted = 2 WHERE automarkcompleted = 1"; + $DB->execute($sql); + + upgrade_mod_savepoint(true, 2017060900, 'attendance'); + } + + if ($oldversion < 2017062000) { + + // Define table attendance_warning_done to be created. + $table = new xmldb_table('attendance_warning_done'); + + // Adding fields to table attendance_warning_done. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('notifyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timesent', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table attendance_warning_done. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table attendance_warning_done. + $table->add_index('notifyid_userid', XMLDB_INDEX_UNIQUE, array('notifyid', 'userid')); + + // Conditionally launch create table for attendance_warning_done. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017062000, 'attendance'); + } + + if ($oldversion < 2017071305) { + + // Define table attendance_warning to be created. + $table = new xmldb_table('attendance_warning'); + + if (!$dbman->table_exists($table)) { + // Adding fields to table attendance_warning. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('idnumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('warningpercent', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('warnafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('emailuser', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null); + $table->add_field('emailsubject', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('emailcontent', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('emailcontentformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null); + $table->add_field('thirdpartyemails', XMLDB_TYPE_TEXT, null, null, null, null, null); + + // Adding keys to table attendance_warning. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('level_id', XMLDB_KEY_UNIQUE, array('idnumber', 'warningpercent', 'warnafter')); + + // Conditionally launch create table for attendance_warning. + $dbman->create_table($table); + + } else { + // Key definition is probably incorrect so fix it - drop_key dml function doesn't seem to work. + $indexes = $DB->get_indexes('attendance_warning'); + foreach ($indexes as $name => $index) { + if ($DB->get_dbfamily() === 'mysql') { + $DB->execute("ALTER TABLE {attendance_warning} DROP INDEX ". $name); + } else { + $DB->execute("DROP INDEX ". $name); + } + } + $index = new xmldb_key('level_id', XMLDB_KEY_UNIQUE, array('idnumber', 'warningpercent', 'warnafter')); + $dbman->add_key($table, $index); + } + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017071305, 'attendance'); + } + + if ($oldversion < 2017071800) { + // Define field setunmarked to be added to attendance_statuses. + $table = new xmldb_table('attendance_warning'); + $field = new xmldb_field('maxwarn', XMLDB_TYPE_INTEGER, '10', null, true, null, '1', 'warnafter'); + + // Conditionally launch add field automark. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017071800, 'attendance'); + } + + if ($oldversion < 2017071802) { + // Define field setunmarked to be added to attendance_statuses. + $table = new xmldb_table('attendance_warning_done'); + + $index = new xmldb_index('notifyid_userid', XMLDB_INDEX_UNIQUE, array('notifyid', 'userid')); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + + $index = new xmldb_index('notifyid', XMLDB_INDEX_NOTUNIQUE, array('notifyid', 'userid')); + $dbman->add_index($table, $index); + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017071802, 'attendance'); + } + + if ($oldversion < 2017082200) { + // Warnings idnumber field should use attendanceid instead of cmid. + $sql = "SELECT cm.id, cm.instance + FROM {course_modules} cm + JOIN {modules} md ON md.id = cm.module AND md.name = 'attendance'"; + $idnumbers = $DB->get_records_sql_menu($sql); + $warnings = $DB->get_recordset('attendance_warning'); + foreach ($warnings as $warning) { + if (!empty($warning->idnumber) && !empty($idnumbers[$warning->idnumber])) { + $warning->idnumber = $idnumbers[$warning->idnumber]; + $DB->update_record("attendance_warning", $warning); + } + } + $warnings->close(); + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2017082200, 'attendance'); + } + + if ($oldversion < 2017120700) { + $table = new xmldb_table('attendance_sessions'); + + $field = new xmldb_field('absenteereport'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'statusset'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2017120700, 'attendance'); + } + + if ($oldversion < 2017120801) { + $table = new xmldb_table('attendance_sessions'); + + $field = new xmldb_field('autoassignstatus'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'studentscanmark'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2017120801, 'attendance'); + } + + if ($oldversion < 2018022204) { + $table = new xmldb_table('attendance'); + $field = new xmldb_field('showextrauserdetails', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '1', 'showsessiondetails'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2018022204, 'attendance'); + } + + if ($oldversion < 2018050100) { + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('preventsharedip', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'absenteereport'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('preventsharediptime', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, + null, null, null, 'preventsharedip'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $table = new xmldb_table('attendance_log'); + $field = new xmldb_field('ipaddress', XMLDB_TYPE_CHAR, '45', null, + null, null, '', 'remarks'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2018050100, 'attendance'); + } + + if ($oldversion < 2018072700) { + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('calendarevent', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '1', 'caleventid'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + if (empty(get_config('attendance', 'enablecalendar'))) { + // Calendar disabled on this site, set calendarevent for existing records to 0. + $DB->execute("UPDATE {attendance_sessions} set calendarevent = 0"); + } + } + upgrade_mod_savepoint(true, 2018072700, 'attendance'); + } + + if ($oldversion < 2018082605) { + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('includeqrcode', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'calendarevent'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2018082605, 'attendance'); + } + + if ($oldversion < 2019012500) { + + // Changing precision of field statusset on table attendance_log to (1333). + $table = new xmldb_table('attendance_log'); + $field = new xmldb_field('statusset', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'statusid'); + + // Launch change of precision for field statusset. + $dbman->change_field_precision($table, $field); + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2019012500, 'attendance'); + } + + if ($oldversion < 2019061800) { + + // Make sure default value to '0'. + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('preventsharedip', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'absenteereport'); + + if ($dbman->field_exists($table, $field)) { + $dbman->change_field_default($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2019061800, 'attendance'); + } + + if ($oldversion < 2019062000) { + // Make sure sessiondetailspos is not null. + $table = new xmldb_table('attendance'); + $field = new xmldb_field('sessiondetailspos', XMLDB_TYPE_CHAR, '5', null, XMLDB_NOTNULL, null, 'left', 'subnet'); + + if ($dbman->field_exists($table, $field)) { + $dbman->change_field_notnull($table, $field); + } + + // Make sure maxwarn has default value of '1'. + $table = new xmldb_table('attendance_warning'); + $field = new xmldb_field('maxwarn', XMLDB_TYPE_INTEGER, '10', null, true, null, '1', 'warnafter'); + + if ($dbman->field_exists($table, $field)) { + $dbman->change_field_default($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2019062000, 'attendance'); + } + + if ($oldversion < 2019062200) { + + // Define table attendance_rotate_passwords to be created. + $table = new xmldb_table('attendance_rotate_passwords'); + + // Adding fields to table attendance_rotate_passwords. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('attendanceid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('password', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, null); + $table->add_field('expirytime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table attendance_rotate_passwords. + $table->add_key('id', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for attendance_rotate_passwords. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define field rotateqrcode to be added to attendance_sessions. + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('rotateqrcode', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'includeqrcode'); + + // Conditionally launch add field rotateqrcode. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field rotateqrcodesecret to be added to attendance_sessions. + $table = new xmldb_table('attendance_sessions'); + $field = new xmldb_field('rotateqrcodesecret', XMLDB_TYPE_CHAR, '10', null, null, null, null, 'rotateqrcode'); + + // Conditionally launch add field rotateqrcodesecret. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2019062200, 'attendance'); + + } + + if ($oldversion < 2020072900) { + $table = new xmldb_table('attendance_sessions'); + + // Conditionally launch add index caleventid. + $index = new xmldb_index('caleventid', XMLDB_INDEX_NOTUNIQUE, array('caleventid')); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2020072900, 'attendance'); + } + + return $result; +} diff --git a/mod/attendance/db/upgradelib.php b/mod/attendance/db/upgradelib.php new file mode 100644 index 0000000..e2ba9ca --- /dev/null +++ b/mod/attendance/db/upgradelib.php @@ -0,0 +1,55 @@ +<?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/>. + +/** + * Helper functions to keep upgrade.php clean. + * + * @package mod_attendance + * @copyright 2016 Vyacheslav Strelkov <strelkov.vo@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Function to help upgrade old attendance records and create calendar events. + */ +function attendance_upgrade_create_calendar_events() { + global $DB; + + $attendances = $DB->get_records('attendance', null, null, 'id, name, course'); + foreach ($attendances as $att) { + $sessionsdata = $DB->get_records('attendance_sessions', array('attendanceid' => $att->id), null, + 'id, groupid, sessdate, duration, description, descriptionformat'); + foreach ($sessionsdata as $session) { + $calevent = new stdClass(); + $calevent->name = $att->name; + $calevent->courseid = $att->course; + $calevent->groupid = $session->groupid; + $calevent->instance = $att->id; + $calevent->timestart = $session->sessdate; + $calevent->timeduration = $session->duration; + $calevent->eventtype = 'attendance'; + $calevent->timemodified = time(); + $calevent->modulename = 'attendance'; + $calevent->description = $session->description; + $calevent->format = $session->descriptionformat; + + $caleventid = $DB->insert_record('event', $calevent); + $DB->set_field('attendance_sessions', 'caleventid', $caleventid, array('id' => $session->id)); + } + } +} diff --git a/mod/attendance/defaultstatus.php b/mod/attendance/defaultstatus.php new file mode 100644 index 0000000..425c9fc --- /dev/null +++ b/mod/attendance/defaultstatus.php @@ -0,0 +1,131 @@ +<?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/>. + +/** + * Allows default status set to be modified. + * + * @package mod_attendance + * @copyright 2017 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(__DIR__.'/../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot.'/mod/attendance/lib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + +$action = optional_param('action', null, PARAM_INT); +$statusid = optional_param('statusid', null, PARAM_INT); +admin_externalpage_setup('managemodules'); +$url = new moodle_url('/mod/attendance/defaultstatus.php', array('statusid' => $statusid, 'action' => $action)); + +// Check sesskey if we are performing an action. +if (!empty($action)) { + require_sesskey(); +} + +$output = $PAGE->get_renderer('mod_attendance'); +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('defaultstatus', 'mod_attendance')); +$tabmenu = attendance_print_settings_tabs('defaultstatus'); +echo $tabmenu; + +// TODO: Would be good to combine this code block with the one in preferences to avoid duplication. +$errors = array(); +switch ($action) { + case mod_attendance_preferences_page_params::ACTION_ADD: + $newacronym = optional_param('newacronym', null, PARAM_TEXT); + $newdescription = optional_param('newdescription', null, PARAM_TEXT); + $newgrade = optional_param('newgrade', 0, PARAM_RAW); + $newstudentavailability = optional_param('newstudentavailability', null, PARAM_INT); + $newgrade = unformat_float($newgrade); + + // Default value uses setnumber/attendanceid = 0. + $status = new stdClass(); + $status->attendanceid = 0; + $status->acronym = $newacronym; + $status->description = $newdescription; + $status->grade = $newgrade; + $status->studentavailability = $newstudentavailability; + $status->setnumber = 0; + attendance_add_status($status); + + break; + case mod_attendance_preferences_page_params::ACTION_DELETE: + $confirm = optional_param('confirm', null, PARAM_INT); + $statuses = attendance_get_statuses(0, false); + $status = $statuses[$statusid]; + + if (isset($confirm)) { + attendance_remove_status($status); + echo $OUTPUT->notification(get_string('statusdeleted', 'attendance'), 'success'); + break; + } + + $message = get_string('deletecheckfull', 'attendance', get_string('variable', 'attendance')); + $message .= str_repeat(html_writer::empty_tag('br'), 2); + $message .= $status->acronym.': '. + ($status->description ? $status->description : get_string('nodescription', 'attendance')); + $confirmurl = $url; + $confirmurl->param('confirm', 1); + + echo $OUTPUT->confirm($message, $confirmurl, $url); + echo $OUTPUT->footer(); + exit; + case mod_attendance_preferences_page_params::ACTION_HIDE: + $statuses = attendance_get_statuses(0, false); + $status = $statuses[$statusid]; + attendance_update_status($status, null, null, null, 0); + break; + case mod_attendance_preferences_page_params::ACTION_SHOW: + $statuses = attendance_get_statuses(0, false); + $status = $statuses[$statusid]; + attendance_update_status($status, null, null, null, 1); + break; + case mod_attendance_preferences_page_params::ACTION_SAVE: + $acronym = required_param_array('acronym', PARAM_TEXT); + $description = required_param_array('description', PARAM_TEXT); + $grade = required_param_array('grade', PARAM_RAW); + $studentavailability = optional_param_array('studentavailability', '0', PARAM_RAW); + $unmarkedstatus = optional_param('setunmarked', null, PARAM_INT); + foreach ($grade as &$val) { + $val = unformat_float($val); + } + $statuses = attendance_get_statuses(0, false); + + foreach ($acronym as $id => $v) { + $status = $statuses[$id]; + $setunmarked = false; + if ($unmarkedstatus == $id) { + $setunmarked = true; + } + if (!isset($studentavailability[$id]) || !is_numeric($studentavailability[$id])) { + $studentavailability[$id] = 0; + } + $errors[$id] = attendance_update_status($status, $acronym[$id], $description[$id], $grade[$id], + null, null, null, $studentavailability[$id], $setunmarked); + } + echo $OUTPUT->notification(get_string('eventstatusupdated', 'attendance'), 'success'); + + break; +} + +$statuses = attendance_get_statuses(0, false); +$prefdata = new attendance_default_statusset($statuses, $errors); +echo $output->render($prefdata); + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/attendance/export.php b/mod/attendance/export.php new file mode 100644 index 0000000..24b7a7c --- /dev/null +++ b/mod/attendance/export.php @@ -0,0 +1,225 @@ +<?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/>. + +/** + * Export attendance sessions + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); +require_once(dirname(__FILE__).'/renderables.php'); +require_once(dirname(__FILE__).'/renderhelpers.php'); +require_once($CFG->libdir.'/formslib.php'); + +$id = required_param('id', PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); +require_capability('mod/attendance:export', $context); + +$att = new mod_attendance_structure($att, $cm, $course, $context); + +$PAGE->set_url($att->url_export()); +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->force_settings_menu(true); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('export', 'attendance')); + +$formparams = array('course' => $course, 'cm' => $cm, 'modcontext' => $context); +$mform = new mod_attendance\form\export($att->url_export(), $formparams); + +if ($formdata = $mform->get_data()) { + + $pageparams = new mod_attendance_page_with_filter_controls(); + $pageparams->init($cm); + $pageparams->page = 0; + $pageparams->group = $formdata->group; + $pageparams->set_current_sesstype($formdata->group ? $formdata->group : mod_attendance_page_with_filter_controls::SESSTYPE_ALL); + if (isset($formdata->includeallsessions)) { + if (isset($formdata->includenottaken)) { + $pageparams->view = ATT_VIEW_ALL; + } else { + $pageparams->view = ATT_VIEW_ALLPAST; + $pageparams->curdate = time(); + } + $pageparams->init_start_end_date(); + } else { + $pageparams->startdate = $formdata->sessionstartdate; + $pageparams->enddate = $formdata->sessionenddate; + } + if ($formdata->selectedusers) { + $pageparams->userids = $formdata->users; + } + $att->pageparams = $pageparams; + + $reportdata = new attendance_report_data($att); + if ($reportdata->users) { + $filename = clean_filename($course->shortname.'_'. + get_string('modulenameplural', 'attendance'). + '_'.userdate(time(), '%Y%m%d-%H%M')); + + $group = $formdata->group ? $reportdata->groups[$formdata->group] : 0; + $data = new stdClass; + $data->tabhead = array(); + $data->course = $att->course->fullname; + $data->group = $group ? $group->name : get_string('allparticipants'); + + $data->tabhead[] = get_string('lastname'); + $data->tabhead[] = get_string('firstname'); + $groupmode = groups_get_activity_groupmode($cm, $course); + if (!empty($groupmode)) { + $data->tabhead[] = get_string('groups'); + } + require_once($CFG->dirroot . '/user/profile/lib.php'); + $customfields = profile_get_custom_fields(false); + + if (isset($formdata->ident)) { + foreach (array_keys($formdata->ident) as $opt) { + if ($opt == 'id') { + $data->tabhead[] = get_string('studentid', 'attendance'); + } else if (in_array($opt, array_column($customfields, 'shortname'))) { + foreach ($customfields as $customfield) { + if ($opt == $customfield->shortname) { + $data->tabhead[] = format_string($customfield->name, true, array('context' => $context)); + } + } + } else { + $data->tabhead[] = get_string($opt); + } + } + } + + if (count($reportdata->sessions) > 0) { + foreach ($reportdata->sessions as $sess) { + $text = userdate($sess->sessdate, get_string('strftimedmyhm', 'attendance')); + $text .= ' '; + if (!empty($sess->groupid) && empty($reportdata->groups[$sess->groupid])) { + $text .= get_string('deletedgroup', 'attendance'); + } else { + $text .= $sess->groupid ? $reportdata->groups[$sess->groupid]->name : get_string('commonsession', 'attendance'); + } + if (isset($formdata->includedescription) && !empty($sess->description)) { + $text .= " ". strip_tags($sess->description); + } + $data->tabhead[] = $text; + if (isset($formdata->includeremarks)) { + $data->tabhead[] = ''; // Space for the remarks. + } + } + } else { + print_error('sessionsnotfound', 'attendance', $att->url_manage()); + } + + $setnumber = -1; + foreach ($reportdata->statuses as $sts) { + if ($sts->setnumber != $setnumber) { + $setnumber = $sts->setnumber; + } + + $data->tabhead[] = $sts->acronym; + } + + $data->tabhead[] = get_string('takensessions', 'attendance'); + $data->tabhead[] = get_string('points', 'attendance'); + $data->tabhead[] = get_string('percentage', 'attendance'); + + $i = 0; + $data->table = array(); + foreach ($reportdata->users as $user) { + profile_load_custom_fields($user); + + $data->table[$i][] = $user->lastname; + $data->table[$i][] = $user->firstname; + if (!empty($groupmode)) { + $grouptext = ''; + $groupsraw = groups_get_all_groups($course->id, $user->id, 0, 'g.name'); + $groups = array(); + foreach ($groupsraw as $group) { + $groups[] = $group->name;; + } + $data->table[$i][] = implode(', ', $groups); + } + + if (isset($formdata->ident)) { + foreach (array_keys($formdata->ident) as $opt) { + if (in_array($opt, array_column($customfields, 'shortname'))) { + if (isset($user->profile[$opt])) { + $data->table[$i][] = format_string($user->profile[$opt], true, array('context' => $context)); + } else { + $data->table[$i][] = ''; + } + continue; + } + + $data->table[$i][] = $user->$opt; + } + } + + $cellsgenerator = new user_sessions_cells_text_generator($reportdata, $user); + $data->table[$i] = array_merge($data->table[$i], $cellsgenerator->get_cells(isset($formdata->includeremarks))); + + $usersummary = $reportdata->summary->get_taken_sessions_summary_for($user->id); + + foreach ($reportdata->statuses as $sts) { + if (isset($usersummary->userstakensessionsbyacronym[$sts->setnumber][$sts->acronym])) { + $data->table[$i][] = $usersummary->userstakensessionsbyacronym[$sts->setnumber][$sts->acronym]; + } else { + $data->table[$i][] = 0; + } + } + + $data->table[$i][] = $usersummary->numtakensessions; + $data->table[$i][] = $usersummary->pointssessionscompleted; + $data->table[$i][] = format_float($usersummary->takensessionspercentage * 100); + + $i++; + } + + if ($formdata->format === 'text') { + attendance_exporttocsv($data, $filename); + } else { + attendance_exporttotableed($data, $filename, $formdata->format); + } + exit; + } else { + print_error('studentsnotfound', 'attendance', $att->url_manage()); + } +} + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_EXPORT); +echo $output->header(); +echo $output->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); +echo $output->render($tabs); + +$mform->display(); + +echo $OUTPUT->footer(); + + + diff --git a/mod/attendance/externallib.php b/mod/attendance/externallib.php new file mode 100644 index 0000000..50f65ef --- /dev/null +++ b/mod/attendance/externallib.php @@ -0,0 +1,545 @@ +<?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/>. +/** + * Externallib.php file for attendance plugin. + * + * @package mod_attendance + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->libdir . '/filelib.php'); +require_once(dirname(__FILE__).'/classes/attendance_webservices_handler.php'); + +/** + * Class mod_attendance_external + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_external extends external_api { + + /** + * Describes the parameters for add_attendance. + * + * @return external_function_parameters + */ + public static function add_attendance_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'course id'), + 'name' => new external_value(PARAM_TEXT, 'attendance name'), + 'intro' => new external_value(PARAM_RAW, 'attendance description', VALUE_DEFAULT, ''), + 'groupmode' => new external_value(PARAM_INT, + 'group mode (0 - no groups, 1 - separate groups, 2 - visible groups)', VALUE_DEFAULT, 0), + ) + ); + } + + /** + * Adds attendance instance to course. + * + * @param int $courseid + * @param string $name + * @param string $intro + * @param int $groupmode + * @return array + */ + public static function add_attendance(int $courseid, $name, $intro, int $groupmode) { + global $CFG, $DB; + require_once($CFG->dirroot.'/course/modlib.php'); + + $params = self::validate_parameters(self::add_attendance_parameters(), array( + 'courseid' => $courseid, + 'name' => $name, + 'intro' => $intro, + 'groupmode' => $groupmode, + )); + + // Get course. + $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST); + + // Verify permissions. + list($module, $context) = can_add_moduleinfo($course, 'attendance', 0); + self::validate_context($context); + require_capability('mod/attendance:addinstance', $context); + + // Verify group mode. + if (!in_array($params['groupmode'], array(NOGROUPS, SEPARATEGROUPS, VISIBLEGROUPS))) { + throw new invalid_parameter_exception('Group mode is invalid.'); + } + + // Populate modinfo object. + $moduleinfo = new stdClass(); + $moduleinfo->modulename = 'attendance'; + $moduleinfo->module = $module->id; + + $moduleinfo->name = $params['name']; + $moduleinfo->intro = $params['intro']; + $moduleinfo->introformat = FORMAT_HTML; + + $moduleinfo->section = 0; + $moduleinfo->visible = 1; + $moduleinfo->visibleoncoursepage = 1; + $moduleinfo->cmidnumber = ''; + $moduleinfo->groupmode = $params['groupmode']; + $moduleinfo->groupingid = 0; + + // Add the module to the course. + $moduleinfo = add_moduleinfo($moduleinfo, $course); + + return array('attendanceid' => $moduleinfo->instance); + } + + /** + * Describes add_attendance return values. + * + * @return external_multiple_structure + */ + public static function add_attendance_returns() { + return new external_single_structure(array( + 'attendanceid' => new external_value(PARAM_INT, 'instance id of the created attendance'), + )); + } + + /** + * Describes the parameters for remove_attendance. + * + * @return external_function_parameters + */ + public static function remove_attendance_parameters() { + return new external_function_parameters( + array( + 'attendanceid' => new external_value(PARAM_INT, 'attendance instance id'), + ) + ); + } + + /** + * Remove attendance instance. + * + * @param int $attendanceid + */ + public static function remove_attendance(int $attendanceid) { + $params = self::validate_parameters(self::remove_attendance_parameters(), array( + 'attendanceid' => $attendanceid, + )); + + $cm = get_coursemodule_from_instance('attendance', $params['attendanceid'], 0, false, MUST_EXIST); + + // Check permissions. + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/attendance:manageattendances', $context); + + // Delete attendance instance. + $result = attendance_delete_instance($params['attendanceid']); + rebuild_course_cache($cm->course, true); + return $result; + } + + /** + * Describes remove_attendance return values. + * + * @return external_value + */ + public static function remove_attendance_returns() { + return new external_value(PARAM_BOOL, 'attendance deletion result'); + } + + /** + * Describes the parameters for add_session. + * + * @return external_function_parameters + */ + public static function add_session_parameters() { + return new external_function_parameters( + array( + 'attendanceid' => new external_value(PARAM_INT, 'attendance instance id'), + 'description' => new external_value(PARAM_RAW, 'description', VALUE_DEFAULT, ''), + 'sessiontime' => new external_value(PARAM_INT, 'session start timestamp'), + 'duration' => new external_value(PARAM_INT, 'session duration (seconds)', VALUE_DEFAULT, 0), + 'groupid' => new external_value(PARAM_INT, 'group id', VALUE_DEFAULT, 0), + 'addcalendarevent' => new external_value(PARAM_BOOL, 'add calendar event', VALUE_DEFAULT, true), + ) + ); + } + + /** + * Adds session to attendance instance. + * + * @param int $attendanceid + * @param string $description + * @param int $sessiontime + * @param int $duration + * @param int $groupid + * @param bool $addcalendarevent + * @return array + */ + public static function add_session(int $attendanceid, $description, int $sessiontime, int $duration, int $groupid, + bool $addcalendarevent) { + global $USER, $DB; + + $params = self::validate_parameters(self::add_session_parameters(), array( + 'attendanceid' => $attendanceid, + 'description' => $description, + 'sessiontime' => $sessiontime, + 'duration' => $duration, + 'groupid' => $groupid, + 'addcalendarevent' => $addcalendarevent, + )); + + $cm = get_coursemodule_from_instance('attendance', $params['attendanceid'], 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + + // Check permissions. + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/attendance:manageattendances', $context); + + // Validate group. + $groupid = $params['groupid']; + $groupmode = (int)groups_get_activity_groupmode($cm); + if ($groupmode === NOGROUPS && $groupid > 0) { + throw new invalid_parameter_exception('Group id is specified, but group mode is disabled for activity'); + } else if ($groupmode === SEPARATEGROUPS && $groupid === 0) { + throw new invalid_parameter_exception('Group id is not specified (or 0) in separate groups mode.'); + } + if ($groupmode === SEPARATEGROUPS || ($groupmode === VISIBLEGROUPS && $groupid > 0)) { + // Determine valid groups. + $userid = has_capability('moodle/site:accessallgroups', $context) ? 0 : $USER->id; + $validgroupids = array_map(function($group) { + return $group->id; + }, groups_get_all_groups($course->id, $userid, $cm->groupingid)); + if (!in_array($groupid, $validgroupids)) { + throw new invalid_parameter_exception('Invalid group id'); + } + } + + // Get attendance. + $attendance = new mod_attendance_structure($attendance, $cm, $course, $context); + + // Create session. + $sess = new stdClass(); + $sess->sessdate = $params['sessiontime']; + $sess->duration = $params['duration']; + $sess->descriptionitemid = 0; + $sess->description = $params['description']; + $sess->descriptionformat = FORMAT_HTML; + $sess->calendarevent = (int) $params['addcalendarevent']; + $sess->timemodified = time(); + $sess->studentscanmark = 0; + $sess->autoassignstatus = 0; + $sess->subnet = ''; + $sess->studentpassword = ''; + $sess->automark = 0; + $sess->automarkcompleted = 0; + $sess->absenteereport = get_config('attendance', 'absenteereport_default'); + $sess->includeqrcode = 0; + $sess->subnet = $attendance->subnet; + $sess->statusset = 0; + $sess->groupid = $groupid; + + $sessionid = $attendance->add_session($sess); + return array('sessionid' => $sessionid); + } + + /** + * Describes add_session return values. + * + * @return external_multiple_structure + */ + public static function add_session_returns() { + return new external_single_structure(array( + 'sessionid' => new external_value(PARAM_INT, 'id of the created session'), + )); + } + + /** + * Describes the parameters for remove_session. + * + * @return external_function_parameters + */ + public static function remove_session_parameters() { + return new external_function_parameters( + array( + 'sessionid' => new external_value(PARAM_INT, 'session id'), + ) + ); + } + + /** + * Delete session from attendance instance. + * + * @param int $sessionid + * @return bool + */ + public static function remove_session(int $sessionid) { + global $DB; + + $params = self::validate_parameters(self::remove_session_parameters(), + array('sessionid' => $sessionid)); + + $session = $DB->get_record('attendance_sessions', array('id' => $params['sessionid']), '*', MUST_EXIST); + $attendance = $DB->get_record('attendance', array('id' => $session->attendanceid), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('attendance', $attendance->id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + // Check permissions. + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/attendance:manageattendances', $context); + + // Get attendance. + $attendance = new mod_attendance_structure($attendance, $cm, $course, $context); + + // Delete session. + $attendance->delete_sessions(array($sessionid)); + attendance_update_users_grade($attendance); + + return true; + } + + /** + * Describes remove_session return values. + * + * @return external_value + */ + public static function remove_session_returns() { + return new external_value(PARAM_BOOL, 'attendance session deletion result'); + } + + /** + * Get parameter list. + * @return external_function_parameters + */ + public static function get_courses_with_today_sessions_parameters() { + return new external_function_parameters ( + array('userid' => new external_value(PARAM_INT, 'User id.', VALUE_DEFAULT, 0))); + } + + /** + * Get list of courses with active sessions for today. + * @param int $userid + * @return array + */ + public static function get_courses_with_today_sessions($userid) { + global $DB; + + $params = self::validate_parameters(self::get_courses_with_today_sessions_parameters(), array( + 'userid' => $userid, + )); + + // Check user id is valid. + $user = $DB->get_record('user', array('id' => $params['userid']), '*', MUST_EXIST); + + // Capability check is done in get_courses_with_today_sessions + // as it switches contexts in loop for each course. + return attendance_handler::get_courses_with_today_sessions($params['userid']); + } + + /** + * Get structure of an attendance session. + * + * @return array + */ + private static function get_session_structure() { + $session = array('id' => new external_value(PARAM_INT, 'Session id.'), + 'attendanceid' => new external_value(PARAM_INT, 'Attendance id.'), + 'groupid' => new external_value(PARAM_INT, 'Group id.'), + 'sessdate' => new external_value(PARAM_INT, 'Session date.'), + 'duration' => new external_value(PARAM_INT, 'Session duration.'), + 'lasttaken' => new external_value(PARAM_INT, 'Session last taken time.'), + 'lasttakenby' => new external_value(PARAM_INT, 'ID of the last user that took this session.'), + 'timemodified' => new external_value(PARAM_INT, 'Time modified.'), + 'description' => new external_value(PARAM_TEXT, 'Session description.'), + 'descriptionformat' => new external_value(PARAM_INT, 'Session description format.'), + 'studentscanmark' => new external_value(PARAM_INT, 'Students can mark their own presence.'), + 'absenteereport' => new external_value(PARAM_INT, 'Session included in absetee reports.'), + 'autoassignstatus' => new external_value(PARAM_INT, 'Automatically assign a status to students.'), + 'preventsharedip' => new external_value(PARAM_INT, 'Prevent students from sharing IP addresses.'), + 'preventsharediptime' => new external_value(PARAM_INT, 'Time delay before IP address is allowed again.'), + 'statusset' => new external_value(PARAM_INT, 'Session statusset.'), + 'includeqrcode' => new external_value(PARAM_INT, 'Include QR code when displaying password')); + + return $session; + } + + /** + * Show structure of return. + * @return external_multiple_structure + */ + public static function get_courses_with_today_sessions_returns() { + $todaysessions = self::get_session_structure(); + + $attendanceinstances = array('name' => new external_value(PARAM_TEXT, 'Attendance name.'), + 'today_sessions' => new external_multiple_structure( + new external_single_structure($todaysessions))); + + $courses = array('shortname' => new external_value(PARAM_TEXT, 'short name of a moodle course.'), + 'fullname' => new external_value(PARAM_TEXT, 'full name of a moodle course.'), + 'attendance_instances' => new external_multiple_structure( + new external_single_structure($attendanceinstances))); + + return new external_multiple_structure(new external_single_structure(($courses))); + } + + /** + * Get session params. + * + * @return external_function_parameters + */ + public static function get_session_parameters() { + return new external_function_parameters ( + array('sessionid' => new external_value(PARAM_INT, 'session id'))); + } + + /** + * Get session. + * + * @param int $sessionid + * @return mixed + */ + public static function get_session($sessionid) { + global $DB; + + $params = self::validate_parameters(self::get_session_parameters(), array( + 'sessionid' => $sessionid, + )); + + $session = $DB->get_record('attendance_sessions', array('id' => $params['sessionid']), '*', MUST_EXIST); + $attendance = $DB->get_record('attendance', array('id' => $session->attendanceid), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('attendance', $attendance->id, 0, false, MUST_EXIST); + + // Check permissions. + $context = context_module::instance($cm->id); + self::validate_context($context); + $capabilities = array( + 'mod/attendance:manageattendances', + 'mod/attendance:takeattendances', + 'mod/attendance:changeattendances' + ); + if (!has_any_capability($capabilities, $context)) { + throw new invalid_parameter_exception('Invalid session id or no permissions.'); + } + + return attendance_handler::get_session($sessionid); + } + + /** + * Show return values of get_session. + * + * @return external_single_structure + */ + public static function get_session_returns() { + $statuses = array('id' => new external_value(PARAM_INT, 'Status id.'), + 'attendanceid' => new external_value(PARAM_INT, 'Attendance id.'), + 'acronym' => new external_value(PARAM_TEXT, 'Status acronym.'), + 'description' => new external_value(PARAM_TEXT, 'Status description.'), + 'grade' => new external_value(PARAM_FLOAT, 'Status grade.'), + 'visible' => new external_value(PARAM_INT, 'Status visibility.'), + 'deleted' => new external_value(PARAM_INT, 'informs if this session was deleted.'), + 'setnumber' => new external_value(PARAM_INT, 'Set number.')); + + $users = array('id' => new external_value(PARAM_INT, 'User id.'), + 'firstname' => new external_value(PARAM_TEXT, 'User first name.'), + 'lastname' => new external_value(PARAM_TEXT, 'User last name.')); + + $attendancelog = array('studentid' => new external_value(PARAM_INT, 'Student id.'), + 'statusid' => new external_value(PARAM_TEXT, 'Status id (last time).'), + 'remarks' => new external_value(PARAM_TEXT, 'Last remark.'), + 'id' => new external_value(PARAM_TEXT, 'log id.')); + + $session = self::get_session_structure(); + $session['courseid'] = new external_value(PARAM_INT, 'Course moodle id.'); + $session['statuses'] = new external_multiple_structure(new external_single_structure($statuses)); + $session['attendance_log'] = new external_multiple_structure(new external_single_structure($attendancelog)); + $session['users'] = new external_multiple_structure(new external_single_structure($users)); + + return new external_single_structure($session); + } + + /** + * Update user status params. + * + * @return external_function_parameters + */ + public static function update_user_status_parameters() { + return new external_function_parameters( + array('sessionid' => new external_value(PARAM_INT, 'Session id'), + 'studentid' => new external_value(PARAM_INT, 'Student id'), + 'takenbyid' => new external_value(PARAM_INT, 'Id of the user who took this session'), + 'statusid' => new external_value(PARAM_INT, 'Status id'), + 'statusset' => new external_value(PARAM_TEXT, 'Status set of session'))); + } + + /** + * Update user status. + * + * @param int $sessionid + * @param int $studentid + * @param int $takenbyid + * @param int $statusid + * @param int $statusset + */ + public static function update_user_status($sessionid, $studentid, $takenbyid, $statusid, $statusset) { + global $DB; + + $params = self::validate_parameters(self::update_user_status_parameters(), array( + 'sessionid' => $sessionid, + 'studentid' => $studentid, + 'takenbyid' => $takenbyid, + 'statusid' => $statusid, + 'statusset' => $statusset, + )); + + $session = $DB->get_record('attendance_sessions', array('id' => $params['sessionid']), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('attendance', $session->attendanceid, 0, false, MUST_EXIST); + + // Check permissions. + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/attendance:view', $context); + + // If not a teacher, make sure session is open for self-marking. + if (!has_capability('mod/attendance:takeattendances', $context)) { + list($canmark, $reason) = attendance_can_student_mark($session); + if (!$canmark) { + throw new invalid_parameter_exception($reason); + } + } + + // Check user id is valid. + $student = $DB->get_record('user', array('id' => $params['studentid']), '*', MUST_EXIST); + $takenby = $DB->get_record('user', array('id' => $params['takenbyid']), '*', MUST_EXIST); + + // TODO: Verify statusset and statusid. + + return attendance_handler::update_user_status($params['sessionid'], $params['studentid'], $params['takenbyid'], + $params['statusid'], $params['statusset']); + } + + /** + * Show return values. + * @return external_value + */ + public static function update_user_status_returns() { + return new external_value(PARAM_TEXT, 'Http code'); + } +} diff --git a/mod/attendance/import/marksessions.php b/mod/attendance/import/marksessions.php new file mode 100644 index 0000000..378b17c --- /dev/null +++ b/mod/attendance/import/marksessions.php @@ -0,0 +1,122 @@ +<?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/>. + +/** + * Mark attendance sessions using a csv import. + * + * @package mod_attendance + * @author Dan Marsden + * @copyright 2020 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/mod/attendance/lib.php'); +require_once($CFG->dirroot . '/mod/attendance/locallib.php'); + +$pageparams = new mod_attendance_take_page_params(); + +$id = required_param('id', PARAM_INT); +$pageparams->sessionid = required_param('sessionid', PARAM_INT); +$pageparams->grouptype = optional_param('grouptype', null, PARAM_INT); +$pageparams->page = optional_param('page', 1, PARAM_INT); +$importid = optional_param('importid', null, PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +// Check this is a valid session for this attendance. +$session = $DB->get_record('attendance_sessions', array('id' => $pageparams->sessionid, 'attendanceid' => $att->id), + '*', MUST_EXIST); + +require_login($course, true, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/attendance:takeattendances', $context); + +$pageparams->init($course->id); + +$PAGE->set_context($context); +$url = new moodle_url('/mod/attendance/import/marksessions.php'); +$PAGE->set_url($url); +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add($att->name); + +$att = new mod_attendance_structure($att, $cm, $course, $PAGE->context, $pageparams); + +// Form processing and displaying is done here. +$output = $PAGE->get_renderer('mod_attendance'); + +$formparams = ['id' => $cm->id, + 'sessionid' => $pageparams->sessionid, + 'grouptype' => $pageparams->grouptype]; +$form = null; +if (optional_param('needsconfirm', 0, PARAM_BOOL)) { + $form = new \mod_attendance\form\import\marksessions($url->out(false), $formparams); +} else if (optional_param('confirm', 0, PARAM_BOOL)) { + $importer = new \mod_attendance\import\marksessions(null, $att, null, null, $importid); + $formparams['importer'] = $importer; + $form = new \mod_attendance\form\import\marksessions_confirm(null, $formparams); +} else { + $form = new \mod_attendance\form\import\marksessions($url->out(false), $formparams); +} + +if ($form->is_cancelled()) { + redirect(new moodle_url('/mod/attendance/take.php', + array('id' => $cm->id, + 'sessionid' => $pageparams->sessionid, + 'grouptype' => $pageparams->grouptype))); + return; +} else if ($data = $form->get_data()) { + if ($data->confirm) { + $importid = $data->importid; + $importer = new \mod_attendance\import\marksessions(null, $att, null, null, $importid, $data, true); + $error = $importer->get_error(); + if ($error) { + $form = new \mod_attendance\form\import\marksessions($url->out(false), $formparams); + $form->set_import_error($error); + } else { + echo $output->header(); + $sessions = $importer->import(); + mod_attendance_notifyqueue::show(); + $url = new moodle_url('/mod/attendance/manage.php', array('id' => $att->cmid)); + echo $output->continue_button($url); + echo $output->footer(); + die(); + } + } else { + $text = $form->get_file_content('attendancefile'); + $encoding = $data->encoding; + $delimiter = $data->separator; + $importer = new \mod_attendance\import\marksessions($text, $att, $encoding, $delimiter, 0, null, true); + $formparams['importer'] = $importer; + $confirmform = new \mod_attendance\form\import\marksessions_confirm(null, $formparams); + $form = $confirmform; + $pagetitle = get_string('confirmcolumnmappings', 'attendance'); + } +} + +// Output for the file upload form starts here. +echo $output->header(); +echo $output->heading(get_string('attendanceforthecourse', 'attendance') . ' :: ' . format_string($course->fullname)); +echo $output->box(get_string('marksessionimportcsvhelp', 'attendance')); +mod_attendance_notifyqueue::show(); +$form->display(); +echo $output->footer(); diff --git a/mod/attendance/import/sessions.php b/mod/attendance/import/sessions.php new file mode 100644 index 0000000..c231fd8 --- /dev/null +++ b/mod/attendance/import/sessions.php @@ -0,0 +1,94 @@ +<?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/>. + +/** + * Import attendance sessions. + * + * @package mod_attendance + * @author Chris Wharton <chriswharton@catalyst.net.nz> + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->dirroot . '/mod/attendance/lib.php'); +require_once($CFG->dirroot . '/mod/attendance/locallib.php'); + +admin_externalpage_setup('managemodules'); +$pagetitle = get_string('importsessions', 'attendance'); + +$context = context_system::instance(); + +$url = new moodle_url('/mod/attendance/import/sessions.php'); + +$PAGE->set_context($context); +$PAGE->set_url($url); +$PAGE->set_title($pagetitle); +$PAGE->set_pagelayout('admin'); +$PAGE->set_heading($pagetitle); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('importsessions', 'attendance')); +$tabmenu = attendance_print_settings_tabs('importsessions'); +echo $tabmenu; + +$form = null; +if (optional_param('needsconfirm', 0, PARAM_BOOL)) { + $form = new \mod_attendance\form\import\sessions($url->out(false)); +} else if (optional_param('confirm', 0, PARAM_BOOL)) { + $importer = new \mod_attendance\import\sessions(); + $form = new \mod_attendance\form\import\sessions_confirm(null, $importer); +} else { + $form = new \mod_attendance\form\import\sessions($url->out(false)); +} + +if ($form->is_cancelled()) { + $form = new \mod_attendance\form\import\sessions($url->out(false)); +} else if ($data = $form->get_data()) { + require_sesskey(); + if ($data->confirm) { + $importid = $data->importid; + $importer = new \mod_attendance\import\sessions(null, null, null, $importid, $data, true); + + $error = $importer->get_error(); + if ($error) { + $form = new \mod_attendance\form\import\sessions($url->out(false)); + $form->set_import_error($error); + } else { + $sessions = $importer->import(); + mod_attendance_notifyqueue::show(); + echo $OUTPUT->continue_button($url); + die(); + } + } else { + $text = $form->get_file_content('importfile'); + $encoding = $data->encoding; + $delimiter = $data->delimiter_name; + $importer = new \mod_attendance\import\sessions($text, $encoding, $delimiter, 0, null, true); + $confirmform = new \mod_attendance\form\import\sessions_confirm(null, $importer); + $form = $confirmform; + $pagetitle = get_string('confirmcolumnmappings', 'attendance'); + } +} + +echo $OUTPUT->heading($pagetitle); + +$form->display(); + +echo $OUTPUT->footer(); diff --git a/mod/attendance/index.php b/mod/attendance/index.php new file mode 100644 index 0000000..6abe561 --- /dev/null +++ b/mod/attendance/index.php @@ -0,0 +1,93 @@ +<?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/>. + +/** + * redjrects to the first Attendance in the course. + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); + +$id = required_param('id', PARAM_INT); + +$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); +require_login($course); + +$PAGE->set_url('/mod/attendance/index.php', array('id' => $id)); +$PAGE->set_pagelayout('incourse'); + +\mod_attendance\event\course_module_instance_list_viewed::create_from_course($course)->trigger(); + +// Print the header. +$strplural = get_string("modulename", "attendance"); +$PAGE->navbar->add($strplural); +$PAGE->set_title($strplural); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($strplural)); + +$context = context_course::instance($course->id); + +require_capability('mod/attendance:view', $context); + +if (! $atts = get_all_instances_in_course("attendance", $course)) { + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + notice(get_string('thereareno', 'moodle', $strplural), $url); + die; +} + +$usesections = course_format_uses_sections($course->format); + +// Print the list of instances. + +$timenow = time(); +$strname = get_string("name"); + +$table = new html_table(); + +if ($usesections) { + $strsectionname = get_string('sectionname', 'format_'.$course->format); + $table->head = array ($strsectionname, $strname); + $table->align = array ("center", "left"); +} else { + $table->head = array ($strname); + $table->align = array ("left"); +} + +foreach ($atts as $att) { + // Get the responses of each attendance. + $viewurl = new moodle_url('/mod/attendance/view.php', array('id' => $att->coursemodule)); + + $dimmedclass = $att->visible ? '' : 'class="dimmed"'; + $link = '<a '.$dimmedclass.' href="'.$viewurl->out().'">'.$att->name.'</a>'; + + if ($usesections) { + $tabledata = array (get_section_name($course, $att->section), $link); + } else { + $tabledata = array ($link); + } + + $table->data[] = $tabledata; +} + +echo "<br />"; + +echo html_writer::table($table); + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/attendance/js/password/attendance_QRCodeRotate.js b/mod/attendance/js/password/attendance_QRCodeRotate.js new file mode 100644 index 0000000..33020b3 --- /dev/null +++ b/mod/attendance/js/password/attendance_QRCodeRotate.js @@ -0,0 +1,90 @@ +/** + * + * @copyright 2019 Maksud R + * @package mod_attendance + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +class attendance_QRCodeRotate { + + constructor() { + this.sessionId = 0; + this.password = ""; + this.qrCodeInstance = ""; + this.qrCodeHTMLElement = ""; + } + + start(sessionId, qrCodeHTMLElement, textPasswordHTMLElement, timerHTMLElement) { + this.sessionId = sessionId; + this.qrCodeHTMLElement = qrCodeHTMLElement; + this.textPasswordHTMLElement = textPasswordHTMLElement; + this.timerHTMLElement = timerHTMLElement; + this.fetchAndRotate(); + } + + qrCodeSetUp() { + this.qrCodeInstance = new QRCode(this.qrCodeHTMLElement, { + text: '', + width: 328, + height: 328, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRCode.CorrectLevel.H + }); + } + + changeQRCode(password) { + var qrcodeurl = document.URL.substr(0,document.URL.lastIndexOf('/')) + '/attendance.php?qrpass=' + password + '&sessid=' + this.sessionId; + this.qrCodeInstance.clear(); + this.qrCodeInstance.makeCode(qrcodeurl); + // display new password + this.textPasswordHTMLElement.innerHTML = '<h2>'+password+'</h2>'; + } + + updateTimer(timeLeft) { + this.timerHTMLElement.innerHTML = '<h3>Time left: '+timeLeft+'</h3>'; + } + + startRotating() { + var parent = this; + + setInterval(function() { + var found = Object.values(parent.password).find(function(element) { + + if (element.expirytime > Math.round(new Date().getTime() / 1000)) { + return element; + } + }); + + if (found == undefined) { + location.reload(true); + } else { + parent.changeQRCode(found.password); + parent.updateTimer(found.expirytime - Math.round(new Date().getTime() / 1000)); + + } + + }, 1000); + + } + + fetchAndRotate() { + var parent = this; + + fetch('password.php?session='+this.sessionId+'&returnpasswords=1', { + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } + }) + .then((resp) => resp.json()) // Transform the data into json + .then(function(data) { + parent.password = data; + parent.qrCodeSetUp(); + // this.changeQRCode( password ); + parent.startRotating(); + }).catch(err => { + console.error("Error fetching QR passwords from API."); + }); + } +} \ No newline at end of file diff --git a/mod/attendance/js/qrcode/README.md b/mod/attendance/js/qrcode/README.md new file mode 100644 index 0000000..5e2d2dc --- /dev/null +++ b/mod/attendance/js/qrcode/README.md @@ -0,0 +1,46 @@ +# QRCode.js +QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. +QRCode.js has no dependencies. + +## Basic Usages +``` +<div id="qrcode"></div> +<script type="text/javascript"> +new QRCode(document.getElementById("qrcode"), "http://jindo.dev.naver.com/collie"); +</script> +``` + +or with some options + +``` +<div id="qrcode"></div> +<script type="text/javascript"> +var qrcode = new QRCode(document.getElementById("qrcode"), { + text: "http://jindo.dev.naver.com/collie", + width: 128, + height: 128, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRCode.CorrectLevel.H +}); +</script> +``` + +and you can use some methods + +``` +qrcode.clear(); // clear the code. +qrcode.makeCode("http://naver.com"); // make another code. +``` + +## Browser Compatibility +IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. + +## License +MIT License + +## Contact +twitter @davidshimjs + +[](https://bitdeli.com/free "Bitdeli Badge") + diff --git a/mod/attendance/js/qrcode/qrcode.js b/mod/attendance/js/qrcode/qrcode.js new file mode 100644 index 0000000..5507c15 --- /dev/null +++ b/mod/attendance/js/qrcode/qrcode.js @@ -0,0 +1,614 @@ +/** + * @fileoverview + * - Using the 'QRCode for Javascript library' + * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. + * - this library has no dependencies. + * + * @author davidshimjs + * @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a> + * @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a> + */ +var QRCode; + +(function () { + //--------------------------------------------------------------------- + // QRCode for JavaScript + // + // Copyright (c) 2009 Kazuhiko Arase + // + // URL: http://www.d-project.com/ + // + // Licensed under the MIT license: + // http://www.opensource.org/licenses/mit-license.php + // + // The word "QR Code" is registered trademark of + // DENSO WAVE INCORPORATED + // http://www.denso-wave.com/qrcode/faqpatent-e.html + // + //--------------------------------------------------------------------- + function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; + this.parsedData = []; + + // Added to support UTF-8 Characters + for (var i = 0, l = this.data.length; i < l; i++) { + var byteArray = []; + var code = this.data.charCodeAt(i); + + if (code > 0x10000) { + byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); + byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); + byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[3] = 0x80 | (code & 0x3F); + } else if (code > 0x800) { + byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); + byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[2] = 0x80 | (code & 0x3F); + } else if (code > 0x80) { + byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); + byteArray[1] = 0x80 | (code & 0x3F); + } else { + byteArray[0] = code; + } + + this.parsedData.push(byteArray); + } + + this.parsedData = Array.prototype.concat.apply([], this.parsedData); + + if (this.parsedData.length != this.data.length) { + this.parsedData.unshift(191); + this.parsedData.unshift(187); + this.parsedData.unshift(239); + } + } + + QR8bitByte.prototype = { + getLength: function (buffer) { + return this.parsedData.length; + }, + write: function (buffer) { + for (var i = 0, l = this.parsedData.length; i < l; i++) { + buffer.put(this.parsedData[i], 8); + } + } + }; + + function QRCodeModel(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = []; + } + + QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} + return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}} + this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);} + if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} + this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} + return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}} + return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;} + this.modules[r][6]=(r%2==0);} + for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;} + this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;} + for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} + for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} + for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} + this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);} + var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} + this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} + row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);} + var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;} + if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. (" + +buffer.getLengthInBits() + +">" + +totalDataCount*8 + +")");} + if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} + while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} + while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD1,8);} + return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];} + offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}} + var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;} + var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}} + for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}} + return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} + return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} + return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} + return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));} + return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;} + for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;} + if(r==0&&c==0){continue;} + if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}} + if(sameCount>5){lostPoint+=(3+sameCount-5);}}} + for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}} + for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}} + for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}} + var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}} + var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");} + return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;} + while(n>=256){n-=255;} + return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;} + for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];} + for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;} + function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);} + var offset=0;while(offset<num.length&&num[offset]==0){offset++;} + this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}} + QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}} + return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;} + var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);} + for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);} + return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;} + QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);} + var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}} + return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;} + QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} + if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} + this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; + + function _isSupportCanvas() { + return typeof CanvasRenderingContext2D != "undefined"; + } + + // android 2.x doesn't support Data-URI spec + function _getAndroid() { + var android = false; + var sAgent = navigator.userAgent; + + if (/android/i.test(sAgent)) { // android + android = true; + var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); + + if (aMat && aMat[1]) { + android = parseFloat(aMat[1]); + } + } + + return android; + } + + var svgDrawer = (function() { + + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + + this.clear(); + + function makeSVG(tag, attrs) { + var el = document.createElementNS('http://www.w3.org/2000/svg', tag); + for (var k in attrs) + if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); + return el; + } + + var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + _el.appendChild(svg); + + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + if (oQRCode.isDark(row, col)) { + var child = makeSVG("use", {"x": String(col), "y": String(row)}); + child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") + svg.appendChild(child); + } + } + } + }; + Drawing.prototype.clear = function () { + while (this._el.hasChildNodes()) + this._el.removeChild(this._el.lastChild); + }; + return Drawing; + })(); + + var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + + // Drawing in DOM by using Table tag + var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + var aHTML = ['<table style="border:0;border-collapse:collapse;">']; + + for (var row = 0; row < nCount; row++) { + aHTML.push('<tr>'); + + for (var col = 0; col < nCount; col++) { + aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>'); + } + + aHTML.push('</tr>'); + } + + aHTML.push('</table>'); + _el.innerHTML = aHTML.join(''); + + // Fix the margin values as real size. + var elTable = _el.childNodes[0]; + var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; + var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; + + if (nLeftMarginTable > 0 && nTopMarginTable > 0) { + elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; + } + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._el.innerHTML = ''; + }; + + return Drawing; + })() : (function () { // Drawing in Canvas + function _onMakeImage() { + this._elImage.src = this._elCanvas.toDataURL("image/png"); + this._elImage.style.display = "block"; + this._elCanvas.style.display = "none"; + } + + // Android 2.1 bug workaround + // http://code.google.com/p/android/issues/detail?id=5141 + if (this._android && this._android <= 2.1) { + var factor = 1 / window.devicePixelRatio; + var drawImage = CanvasRenderingContext2D.prototype.drawImage; + CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { + if (("nodeName" in image) && /img/i.test(image.nodeName)) { + for (var i = arguments.length - 1; i >= 1; i--) { + arguments[i] = arguments[i] * factor; + } + } else if (typeof dw == "undefined") { + arguments[1] *= factor; + arguments[2] *= factor; + arguments[3] *= factor; + arguments[4] *= factor; + } + + drawImage.apply(this, arguments); + }; + } + + /** + * Check whether the user's browser supports Data URI or not + * + * @private + * @param {Function} fSuccess Occurs if it supports Data URI + * @param {Function} fFail Occurs if it doesn't support Data URI + */ + function _safeSetDataURI(fSuccess, fFail) { + var self = this; + self._fFail = fFail; + self._fSuccess = fSuccess; + + // Check it just once + if (self._bSupportDataURI === null) { + var el = document.createElement("img"); + var fOnError = function() { + self._bSupportDataURI = false; + + if (self._fFail) { + self._fFail.call(self); + } + }; + var fOnSuccess = function() { + self._bSupportDataURI = true; + + if (self._fSuccess) { + self._fSuccess.call(self); + } + }; + + el.onabort = fOnError; + el.onerror = fOnError; + el.onload = fOnSuccess; + el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. + return; + } else if (self._bSupportDataURI === true && self._fSuccess) { + self._fSuccess.call(self); + } else if (self._bSupportDataURI === false && self._fFail) { + self._fFail.call(self); + } + }; + + /** + * Drawing QRCode by using canvas + * + * @constructor + * @param {HTMLElement} el + * @param {Object} htOption QRCode Options + */ + var Drawing = function (el, htOption) { + this._bIsPainted = false; + this._android = _getAndroid(); + + this._htOption = htOption; + this._elCanvas = document.createElement("canvas"); + this._elCanvas.width = htOption.width; + this._elCanvas.height = htOption.height; + el.appendChild(this._elCanvas); + this._el = el; + this._oContext = this._elCanvas.getContext("2d"); + this._bIsPainted = false; + this._elImage = document.createElement("img"); + this._elImage.alt = "Scan me!"; + this._elImage.style.display = "none"; + this._el.appendChild(this._elImage); + this._bSupportDataURI = null; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _elImage = this._elImage; + var _oContext = this._oContext; + var _htOption = this._htOption; + + var nCount = oQRCode.getModuleCount(); + var nWidth = _htOption.width / nCount; + var nHeight = _htOption.height / nCount; + var nRoundedWidth = Math.round(nWidth); + var nRoundedHeight = Math.round(nHeight); + + _elImage.style.display = "none"; + this.clear(); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + var bIsDark = oQRCode.isDark(row, col); + var nLeft = col * nWidth; + var nTop = row * nHeight; + _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.lineWidth = 1; + _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.fillRect(nLeft, nTop, nWidth, nHeight); + + // 안티 앨리어싱 방지 처리 + _oContext.strokeRect( + Math.floor(nLeft) + 0.5, + Math.floor(nTop) + 0.5, + nRoundedWidth, + nRoundedHeight + ); + + _oContext.strokeRect( + Math.ceil(nLeft) - 0.5, + Math.ceil(nTop) - 0.5, + nRoundedWidth, + nRoundedHeight + ); + } + } + + this._bIsPainted = true; + }; + + /** + * Make the image from Canvas if the browser supports Data URI. + */ + Drawing.prototype.makeImage = function () { + if (this._bIsPainted) { + _safeSetDataURI.call(this, _onMakeImage); + } + }; + + /** + * Return whether the QRCode is painted or not + * + * @return {Boolean} + */ + Drawing.prototype.isPainted = function () { + return this._bIsPainted; + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); + this._bIsPainted = false; + }; + + /** + * @private + * @param {Number} nNumber + */ + Drawing.prototype.round = function (nNumber) { + if (!nNumber) { + return nNumber; + } + + return Math.floor(nNumber * 1000) / 1000; + }; + + return Drawing; + })(); + + /** + * Get the type by string length + * + * @private + * @param {String} sText + * @param {Number} nCorrectLevel + * @return {Number} type + */ + function _getTypeNumber(sText, nCorrectLevel) { + var nType = 1; + var length = _getUTF8Length(sText); + + for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { + var nLimit = 0; + + switch (nCorrectLevel) { + case QRErrorCorrectLevel.L : + nLimit = QRCodeLimitLength[i][0]; + break; + case QRErrorCorrectLevel.M : + nLimit = QRCodeLimitLength[i][1]; + break; + case QRErrorCorrectLevel.Q : + nLimit = QRCodeLimitLength[i][2]; + break; + case QRErrorCorrectLevel.H : + nLimit = QRCodeLimitLength[i][3]; + break; + } + + if (length <= nLimit) { + break; + } else { + nType++; + } + } + + if (nType > QRCodeLimitLength.length) { + throw new Error("Too long data"); + } + + return nType; + } + + function _getUTF8Length(sText) { + var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); + return replacedText.length + (replacedText.length != sText ? 3 : 0); + } + + /** + * @class QRCode + * @constructor + * @example + * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); + * + * @example + * var oQRCode = new QRCode("test", { + * text : "http://naver.com", + * width : 128, + * height : 128 + * }); + * + * oQRCode.clear(); // Clear the QRCode. + * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. + * + * @param {HTMLElement|String} el target element or 'id' attribute of element. + * @param {Object|String} vOption + * @param {String} vOption.text QRCode link data + * @param {Number} [vOption.width=256] + * @param {Number} [vOption.height=256] + * @param {String} [vOption.colorDark="#000000"] + * @param {String} [vOption.colorLight="#ffffff"] + * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] + */ + QRCode = function (el, vOption) { + this._htOption = { + width : 256, + height : 256, + typeNumber : 4, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRErrorCorrectLevel.H + }; + + if (typeof vOption === 'string') { + vOption = { + text : vOption + }; + } + + // Overwrites options + if (vOption) { + for (var i in vOption) { + this._htOption[i] = vOption[i]; + } + } + + if (typeof el == "string") { + el = document.getElementById(el); + } + + if (this._htOption.useSVG) { + Drawing = svgDrawer; + } + + this._android = _getAndroid(); + this._el = el; + this._oQRCode = null; + this._oDrawing = new Drawing(this._el, this._htOption); + + if (this._htOption.text) { + this.makeCode(this._htOption.text); + } + }; + + /** + * Make the QRCode + * + * @param {String} sText link data + */ + QRCode.prototype.makeCode = function (sText) { + this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); + this._oQRCode.addData(sText); + this._oQRCode.make(); + this._el.title = sText; + this._oDrawing.draw(this._oQRCode); + this.makeImage(); + }; + + /** + * Make the Image from Canvas element + * - It occurs automatically + * - Android below 3 doesn't support Data-URI spec. + * + * @private + */ + QRCode.prototype.makeImage = function () { + if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { + this._oDrawing.makeImage(); + } + }; + + /** + * Clear the QRCode + */ + QRCode.prototype.clear = function () { + this._oDrawing.clear(); + }; + + /** + * @name QRCode.CorrectLevel + */ + QRCode.CorrectLevel = QRErrorCorrectLevel; +})(); diff --git a/mod/attendance/js/qrcode/qrcode.min.js b/mod/attendance/js/qrcode/qrcode.min.js new file mode 100644 index 0000000..993e88f --- /dev/null +++ b/mod/attendance/js/qrcode/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function j(a,b){this.totalCount=a,this.dataCount=b}function k(){this.buffer=[],this.length=0}function m(){return"undefined"!=typeof CanvasRenderingContext2D}function n(){var a=!1,b=navigator.userAgent;return/android/i.test(b)&&(a=!0,aMat=b.toString().match(/android ([0-9]\.[0-9])/i),aMat&&aMat[1]&&(a=parseFloat(aMat[1]))),a}function r(a,b){for(var c=1,e=s(a),f=0,g=l.length;g>=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=0==b%2)},setupPositionAdjustPattern:function(){for(var a=f.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var g=-2;2>=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g<a.length&&(j=1==(1&a[g]>>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h<d.length;h++){var i=d[h];g.put(i.mode,4),g.put(i.getLength(),f.getLengthInBits(i.mode,a)),i.write(g)}for(var l=0,h=0;h<e.length;h++)l+=e[h].dataCount;if(g.getLengthInBits()>8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j<b.length;j++){var k=b[j].dataCount,l=b[j].totalCount-k;d=Math.max(d,k),e=Math.max(e,l),g[j]=new Array(k);for(var m=0;m<g[j].length;m++)g[j][m]=255&a.buffer[m+c];c+=k;var n=f.getErrorCorrectPolynomial(l),o=new i(g[j],n.getLength()-1),p=o.mod(n);h[j]=new Array(n.getLength()-1);for(var m=0;m<h[j].length;m++){var q=m+p.getLength()-h[j].length;h[j][m]=q>=0?p.get(q):0}}for(var r=0,m=0;m<b.length;m++)r+=b[m].totalCount;for(var s=new Array(r),t=0,m=0;d>m;m++)for(var j=0;j<b.length;j++)m<g[j].length&&(s[t++]=g[j][m]);for(var m=0;e>m;m++)for(var j=0;j<b.length;j++)m<h[j].length&&(s[t++]=h[j][m]);return s};for(var c={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},d={L:1,M:0,Q:3,H:2},e={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},f={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;f.getBCHDigit(b)-f.getBCHDigit(f.G15)>=0;)b^=f.G15<<f.getBCHDigit(b)-f.getBCHDigit(f.G15);return(a<<10|b)^f.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;f.getBCHDigit(b)-f.getBCHDigit(f.G18)>=0;)b^=f.G18<<f.getBCHDigit(b)-f.getBCHDigit(f.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<<h;for(var h=8;256>h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;c<this.getLength();c++)for(var d=0;d<a.getLength();d++)b[c+d]^=g.gexp(g.glog(this.get(c))+g.glog(a.get(d)));return new i(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=g.glog(this.get(0))-g.glog(a.get(0)),c=new Array(this.getLength()),d=0;d<this.getLength();d++)c[d]=this.get(d);for(var d=0;d<a.getLength();d++)c[d]^=g.gexp(g.glog(a.get(d))+b);return new i(c,0).mod(a)}},j.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],j.getRSBlocks=function(a,b){var c=j.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var d=c.length/3,e=[],f=0;d>f;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=['<table style="border:0;border-collapse:collapse;">'],h=0;d>h;h++){g.push("<tr>");for(var i=0;d>i;i++)g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:'+e+"px;height:"+f+"px;background-color:"+(a.isDark(h,i)?b.colorDark:b.colorLight)+';"></td>');g.push("</tr>")}g.push("</table>"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/mod/attendance/lang/en/attendance.php b/mod/attendance/lang/en/attendance.php new file mode 100644 index 0000000..7a5b5ef --- /dev/null +++ b/mod/attendance/lang/en/attendance.php @@ -0,0 +1,632 @@ +<?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/>. + +/** + * Strings for component 'attendance', language 'en' + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['Aacronym'] = 'A'; +$string['Afull'] = 'Absent'; +$string['Eacronym'] = 'E'; +$string['Efull'] = 'Excused'; +$string['Lacronym'] = 'L'; +$string['Lfull'] = 'Late'; +$string['Pacronym'] = 'P'; +$string['Pfull'] = 'Present'; +$string['absenteereport'] = 'Absentee report'; +$string['acronym'] = 'Acronym'; +$string['add'] = 'Add'; +$string['addedrecip'] = 'Added {$a} new recipient'; +$string['addedrecips'] = 'Added {$a} new recipients'; +$string['addmultiplesessions'] = 'Multiple sessions'; +$string['addsession'] = 'Add session'; +$string['adduser'] = 'Add user'; +$string['addwarning'] = 'Add warning'; +$string['all'] = 'All'; +$string['allcourses'] = 'All courses'; +$string['allpast'] = 'All past'; +$string['allsessions'] = 'All sessions'; +$string['allsessionstotals'] = 'Totals for selected sessions'; +$string['attendance:addinstance'] = 'Add a new attendance activity'; +$string['attendance:canbelisted'] = 'Appears in the roster'; +$string['attendance:changeattendances'] = 'Changing Attendances'; +$string['attendance:changepreferences'] = 'Changing Preferences'; +$string['attendance:export'] = 'Export Reports'; +$string['attendance:manageattendances'] = 'Manage Attendances'; +$string['attendance:managetemporaryusers'] = 'Manage temporary users'; +$string['attendance:takeattendances'] = 'Taking Attendances'; +$string['attendance:view'] = 'Viewing Attendances'; +$string['attendance:viewreports'] = 'Viewing Reports'; +$string['attendance:viewsummaryreports'] = 'View course summary reports'; +$string['attendance:warningemails'] = 'Can be subscribed to emails with absentee users'; +$string['attendance_already_submitted'] = 'Your attendance has already been set.'; +$string['attendance_no_status'] = 'No valid status was available - you may be too late to record attendance.'; +$string['attendancedata'] = 'Attendance data'; +$string['attendancefile'] = 'Attendance file (csv format)'; +$string['attendancefile_help'] = 'The file must be a CSV file with a header row and fields for identifying the user and the time attendance was recorded eg (email,scantime) or (username,time)'; +$string['attendanceforthecourse'] = 'Attendance for the course'; +$string['attendancegrade'] = 'Attendance grade'; +$string['attendancenotset'] = 'You must set your attendance'; +$string['attendancenotstarted'] = 'Attendance has not started yet for this course'; +$string['attendancepercent'] = 'Attendance percent'; +$string['attendancereport'] = 'Attendance report'; +$string['attendanceslogged'] = 'Attendances logged'; +$string['attendancestaken'] = 'Attendances taken'; +$string['attendancesuccess'] = 'Attendance has been successfully taken'; +$string['attendanceupdated'] = 'Attendance successfully updated'; +$string['attforblockdirstillexists'] = 'old mod/attforblock directory still exists - you must delete this directory on your server before running this upgrade.'; +$string['attrecords'] = 'Attendances records'; +$string['autoassignstatus'] = 'Automatically select highest status available'; +$string['autoassignstatus_help'] = 'If this is selected, students will automatically be assigned the highest available grade.'; +$string['automark'] = 'Automatic marking'; +$string['automark_help'] = 'Allows marking to be completed automatically. +If "Yes" students will be automatically marked depending on their first access to the course. +If "Set unmarked at end of session" any students who have not marked their attendance will be set to the unmarked status selected.'; +$string['automarkall'] = 'Yes'; +$string['automarkclose'] = 'Set unmarked at end of session'; +$string['automarktask'] = 'Check for attendance sessions that require auto marking'; +$string['autorecorded'] = 'system auto recorded'; +$string['averageattendance'] = 'Average attendance'; +$string['averageattendancegraded'] = 'Average attendance'; +$string['backtoparticipants'] = 'Back to participants list'; +$string['below'] = 'Below {$a}%'; +$string['calclose'] = 'Close'; +$string['calendarevent'] = 'Create calendar event for session'; +$string['calendarevent_help'] = 'If enabled, a calendar event will be created for this session. +If disabled, any existing calendar event for this session will be deleted.'; +$string['caleventcreated'] = 'Calendar event for session successfully created'; +$string['caleventdeleted'] = 'Calendar event for session successfully deleted'; +$string['calmonths'] = 'January,February,March,April,May,June,July,August,September,October,November,December'; +$string['calshow'] = 'Choose date'; +$string['caltoday'] = 'Today'; +$string['calweekdays'] = 'Su,Mo,Tu,We,Th,Fr,Sa'; +$string['cannottakeforgroup'] = 'You can\'t take attendance for group "{$a}"'; +$string['cantaddstatus'] = 'You must set an acronym and description when adding a new status.'; +$string['categoryreport'] = 'Course category report'; +$string['changeattendance'] = 'Change attendance'; +$string['changeduration'] = 'Change duration'; +$string['changesession'] = 'Change session'; +$string['checkweekdays'] = 'Select weekdays that fall within your selected session date range.'; +$string['closed'] = 'This session is not currently available for self-marking'; +$string['column'] = 'column'; +$string['columnmap'] = 'Column mapping'; +$string['columnmap_help'] = 'For each of the fields presented, select the corresponding column in the csv file.'; +$string['columns'] = 'columns'; +$string['commonsession'] = 'All students'; +$string['commonsessions'] = 'All students'; +$string['confirm'] = 'Confirm'; +$string['confirmcolumnmappings'] = 'Confirm column mappings'; +$string['confirmdeletehiddensessions'] = 'Are you sure you want to delete {$a->count} sessions scheduled before the course start date ({$a->date})?'; +$string['confirmdeleteuser'] = 'Are you sure you want to delete user \'{$a->fullname}\' ({$a->email})?<br/>All of their attendance records will be permanently deleted.'; +$string['copyfrom'] = 'Copy attendance data from'; +$string['countofselected'] = 'Count of selected'; +$string['course'] = 'Course'; +$string['coursemessage'] = 'Message course users'; +$string['courseshortname'] = 'Course shortname'; +$string['coursesummary'] = 'Course summary report'; +$string['createmultiplesessions'] = 'Create multiple sessions'; +$string['createmultiplesessions_help'] = 'This function allows you to create multiple sessions in one simple step. +The sessions begin on the date of the base session and continue until the \'repeat until\' date. + + * <strong>Repeat on</strong>: Select the days of the week when your class will meet (for example, Monday/Wednesday/Friday). + * <strong>Repeat every</strong>: This allows for a frequency setting. If your class will meet every week, select 1; if it will meet every other week, select 2; every 3rd week, select 3, etc. + * <strong>Repeat until</strong>: Select the last day of class (the last day you want to take attendance). +'; +$string['createonesession'] = 'Create one session for the course'; +$string['csvdelimiter'] = 'CSV delimiter'; +$string['currentlyselectedusers'] = 'Currently selected users'; +$string['customexportfields'] = 'Export custom user profile fields'; +$string['customexportfields_help'] = 'Extra custom user profile fields to expose in the export report.'; +$string['date'] = 'Date'; +$string['days'] = 'Days'; +$string['defaultdisplaymode'] = 'Default display mode'; +$string['defaults'] = 'Defaults'; +$string['defaultsessionsettings'] = 'Default session settings'; +$string['defaultsessionsettings_help'] = 'These settings define the defaults for all new sessions'; +$string['defaultsettings'] = 'Default attendance settings'; +$string['defaultsettings_help'] = 'These settings define the defaults for all new attendances'; +$string['defaultstatus'] = 'Default status set'; +$string['defaultsubnet'] = 'Default network address'; +$string['defaultsubnet_help'] = 'Attendance recording may be restricted to particular subnets by specifying a comma-separated list of partial or full IP addresses. This is the default value used when creating new sessions.'; +$string['defaultview'] = 'Default view on login'; +$string['defaultview_desc'] = 'This is the default view shown to teachers on first login.'; +$string['defaultwarnings'] = 'Default warning set'; +$string['defaultwarningsettings'] = 'Default warning settings'; +$string['defaultwarningsettings_help'] = 'These settings define the defaults for all new warnings'; +$string['delete'] = 'Delete'; +$string['deletecheckfull'] = 'Are you absolutely sure you want to completely delete the {$a}, including all user data?'; +$string['deletedgroup'] = 'The group associated with this session has been deleted'; +$string['deletehiddensessions'] = 'Delete all hidden sessions'; +$string['deletelogs'] = 'Delete attendance data'; +$string['deleteselected'] = 'Delete selected'; +$string['deletesession'] = 'Delete session'; +$string['deletesessions'] = 'Delete all sessions'; +$string['deleteuser'] = 'Delete user'; +$string['deletewarningconfirm'] = 'Are you sure you want to delete this warning?'; +$string['deletingsession'] = 'Deleting session for the course'; +$string['deletingstatus'] = 'Deleting status for the course'; +$string['description'] = 'Description'; +$string['display'] = 'Display'; +$string['displaymode'] = 'Display mode'; +$string['donotusepaging'] = 'Do not use paging'; +$string['downloadexcel'] = 'Download in Excel format'; +$string['downloadooo'] = 'Download in OpenOffice format'; +$string['downloadtext'] = 'Download in text format'; +$string['duration'] = 'Duration'; +$string['editsession'] = 'Edit Session'; +$string['edituser'] = 'Edit user'; +$string['emailcontent'] = 'Email content'; +$string['emailcontent_default'] = 'Hi %userfirstname%, +Your attendance in %coursename% %attendancename% has dropped below %warningpercent% and is currently %percent% - we hope you are ok! + +To get the most out of this course you should improve your attendance, please get in touch if you require any further support.'; +$string['emailcontent_help'] = 'When a warning is sent to a student, it takes the email content from this field. The following wildcards can be used: +<ul> +<li>%coursename%</li> +<li>%userfirstname%</li> +<li>%userlastname%</li> +<li>%userid%</li> +<li>%warningpercent%</li> +<li>%attendancename%</li> +<li>%cmid%</li> +<li>%numtakensessions%</li> +<li>%points%</li> +<li>%maxpoints%</li> +<li>%percent%</li> +</ul>'; +$string['emailsubject'] = 'Email subject'; +$string['emailsubject_default'] = 'Attendance warning'; +$string['emailsubject_help'] = 'When a warning is sent to a student, it takes the email subject from this field.'; +$string['emailuser'] = 'Email user'; +$string['emailuser_help'] = 'If checked, a warning will be sent to the student.'; +$string['emptyacronym'] = 'Empty acronyms are not allowed. Status record not updated.'; +$string['emptydescription'] = 'Empty descriptions are not allowed. Status record not updated.'; +$string['enablecalendar'] = 'Create calendar events'; +$string['enablecalendar_desc'] = 'If enabled, a calendar event will be created for each attendance session. After changing this setting you should run the reset calendar report.'; +$string['enablewarnings'] = 'Enable warnings'; +$string['enablewarnings_desc'] = 'This allows a warning set to be defined for an attendance and email notifications to users when attendance drops below the configured threshold. <br/><strong>WARNING: This is a new feature and has not been tested extensively. Please use at your own-risk and provide feeback in the moodle forums if you find it works well.</strong>'; +$string['encoding'] = 'Encoding'; +$string['encoding_help'] = 'This refers to the type of barcode encoding used on the students\' id card. Typical types of barcode encoding schemes include Code-39, Code-128 and UPC-A.'; +$string['endofperiod'] = 'End of period'; +$string['endtime'] = 'Session end time'; +$string['enrolmentend'] = 'User enrolment ends {$a}'; +$string['enrolmentstart'] = 'User enrolment starts {$a}'; +$string['enrolmentsuspended'] = 'Enrolment suspended'; +$string['enterpassword'] = 'Enter password'; +$string['error:coursehasnoattendance'] = 'The course with the short name {$a} has no attendance activities.'; +$string['error:coursenotfound'] = 'A course with the short name {$a} can not be found.'; +$string['error:qrcode'] = 'Allow students to record own attendance must be enabled to use QR code! Skipping.'; +$string['error:sessioncourseinvalid'] = 'A session course is invalid! Skipping.'; +$string['error:sessiondateinvalid'] = 'A session date is invalid! Skipping.'; +$string['error:sessionendinvalid'] = 'A session end time is invalid! Skipping.'; +$string['error:sessionstartinvalid'] = 'A session start time is invalid! Skipping.'; +$string['error:statusnotfound'] = 'User: {$a->extuser} has a status value that could not be found: {$a->status}'; +$string['error:timenotreadable'] = 'User: {$a->extuser} has a scantime that could not be converted by strtotime: {$a->scantime}'; +$string['error:userduplicate'] = 'User {$a} was found twice in the import. please only include one record per user.'; +$string['error:usernotfound'] = 'A user with the {$a->userfield} set to {$a->extuser} could not be found'; +$string['errorgroupsnotselected'] = 'Select one or more groups'; +$string['errorinaddingsession'] = 'Error in adding session'; +$string['erroringeneratingsessions'] = 'Error in generating sessions '; +$string['eventdurationupdated'] = 'Session duration updated'; +$string['eventreportviewed'] = 'Attendance report viewed'; +$string['eventscreated'] = 'Calendar events created'; +$string['eventsdeleted'] = 'Calendar events deleted'; +$string['eventsessionadded'] = 'Session added'; +$string['eventsessiondeleted'] = 'Session deleted'; +$string['eventsessionipshared'] = 'Attendance self-marking IP conflict'; +$string['eventsessionsimported'] = 'Sessions imported'; +$string['eventsessionupdated'] = 'Session updated'; +$string['eventstatusadded'] = 'Status added'; +$string['eventstatusupdated'] = 'Status updated'; +$string['eventstudentattendancesessionsviewed'] = 'Session report viewed'; +$string['eventstudentattendancesessionsupdated'] = 'Session report updated'; +$string['eventtaken'] = 'Attendance taken'; +$string['eventtakenbystudent'] = 'Attendance taken by student'; +$string['export'] = 'Export'; +$string['extrarestrictions'] = 'Extra restrictions'; +$string['formattexttype'] = 'Formatting'; +$string['from'] = 'from:'; +$string['gradebookexplanation'] = 'Grade in gradebook'; +$string['gradebookexplanation_help'] = 'The Attendance module displays your current attendance grade based on the number of points you have earned to date and the number of points that could have been earned to date; it does not include class periods in the future. In the gradebook, your attendance grade is based on your current attendance percentage and the number of points that can be earned over the entire duration of the course, including future class periods. As such, your attendance grades displayed in the Attendance module and in the gradebook may not be the same number of points but they are the same percentage. + +For example, if you have earned 8 of 10 points to date (80% attendance) and attendance for the entire course is worth 50 points, the Attendance module will display 8/10 and the gradebook will display 40/50. You have not yet earned 40 points but 40 is the equivalent point value to your current attendance percentage of 80%. The point value you have earned in the Attendance module can never decrease, as it is based only on attendance to date; however, the attendance point value shown in the gradebook may increase or decrease depending on your future attendance, as it is based on attendance for the entire course.'; +$string['graded'] = 'Graded sessions'; +$string['gridcolumns'] = 'Grid columns'; +$string['group'] = 'Group'; +$string['groups'] = 'Groups'; +$string['groupsession'] = 'Group of students'; +$string['groupsessionsby'] = 'Group sessions by'; +$string['hiddensessions'] = 'Hidden sessions'; +$string['hiddensessions_help'] = 'Sessions are hidden if they are scheduled before the course start date. + +You can use this feature to hide older sessions instead of deleting them. Only visible sessions will appear in the Gradebook.'; +$string['hiddensessionsdeleted'] = 'All hidden sessions were delete'; +$string['hideextrauserdetails'] = 'Hide extra user details'; +$string['hidensessiondetails'] = 'Hide session details'; +$string['identifyby'] = 'Identify student by'; +$string['import'] = 'Import'; +$string['importfile'] = 'Import file'; +$string['importfile_help'] = 'Import file'; +$string['importsessions'] = 'Import Sessions'; +$string['importstatus'] = 'Status field'; +$string['importstatus_help'] = 'This allows a status value to be included in the import - eg values like P, L, or A'; +$string['includeabsentee'] = 'Include session when calculating absentee report'; +$string['includeabsentee_help'] = 'If checked this session will be included in the absentee report calculations.'; +$string['includeall'] = 'Select all sessions'; +$string['includedescription'] = 'Include session description'; +$string['includenottaken'] = 'Include not taken sessions'; +$string['includeqrcode'] = 'Include QR code'; +$string['includeremarks'] = 'Include remarks'; +$string['incorrectpassword'] = 'You have entered an incorrect password and your attendance has not been recorded, please enter the correct password.'; +$string['incorrectpasswordshort'] = 'Incorrect password, attendance not recorded.'; +$string['indetail'] = 'In detail...'; +$string['indicator:cognitivedepth'] = 'Attendance cognitive'; +$string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in an Attendance activity.'; +$string['indicator:cognitivedepthdef'] = 'Attendance cognitive'; +$string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the Attendance during this analysis interval (Levels = No view, View)'; +$string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; +$string['indicator:socialbreadth'] = 'Attendance social'; +$string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in an Attendance activity.'; +$string['indicator:socialbreadthdef'] = 'Attendance social'; +$string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Attendance during this analysis interval (Levels = No participation, Participant alone)'; +$string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; +$string['invalidaction'] = 'You must select an action'; +$string['invalidemails'] = 'You must specify addresses of existing user accounts, could not find: {$a}'; +$string['invalidimportfile'] = 'File format is invalid.'; +$string['invalidsessionenddate'] = 'This date can not be earlier than the session date'; +$string['invalidsessionendtime'] = 'The end time must be greater than start time'; +$string['invalidstatus'] = 'You have selected an invalid status, please try again'; +$string['iptimemissing'] = 'Invalid minutes to release'; +$string['jumpto'] = 'Jump to'; +$string['keepsearching'] = 'Keep searching'; +$string['marksessionimportcsvhelp'] = 'This form allows you to upload a csv file containing a user identifier and a status - the status field can be the status acronym or the time that attendance was recorded for that user. If a time value is passed then it will try to assign the status value with the highest grade available at that time.'; +$string['maxpossible'] = 'Maximum possible'; +$string['maxpossible_help'] = 'Shows the score each user can reach if they receive the maximum points in each session not yet taken (past and future): + <ul> + <li><strong>Points</strong>: maximum points each user can reach over all sessions.</li> + <li><strong>Percentage</strong>: maximum percentage each user can reach over all sessions.</li> + </ul>'; +$string['maxpossiblepercentage'] = 'Maximum possible percentage'; +$string['maxpossiblepoints'] = 'Maximum possible points'; +$string['maxwarn'] = 'Maximum number of e-mail warnings'; +$string['maxwarn_help'] = 'The maximum number of times a warning should be sent (only one warning per session is sent)'; +$string['mergeuser'] = 'Merge user'; +$string['mobilesessionfrom'] = 'Show sessions older than the last'; +$string['mobilesessionfrom_help'] = 'Allows the list of sessions to be restricted when marking in the app - only shows sessions that started since this value'; +$string['mobilesessionto'] = 'Show future sessions'; +$string['mobilesessionto_help'] = 'Allows the list of sessions to be restricted to only show a small number of future sessions.'; +$string['mobilesettings'] = 'Mobile app settings'; +$string['mobilesettings_help'] = 'These settings control Moodle mobile app behaviour'; +$string['modulename'] = 'Attendance'; +$string['modulename_help'] = 'The attendance activity module enables a teacher to take attendance during class and students to view their own attendance record. + +The teacher can create multiple sessions and can mark the attendance status as "Present", "Absent", "Late", or "Excused" or modify the statuses to suit their needs. + +Reports are available for the entire class or individual students.'; +$string['modulenameplural'] = 'Attendances'; +$string['months'] = 'Months'; +$string['moreattendance'] = 'Attendance has been successfully taken for this page'; +$string['moveleft'] = 'Move left'; +$string['moveright'] = 'Move right'; +$string['multisessionexpanded'] = 'Multiple sessions expanded'; +$string['multisessionexpanded_desc'] = 'Show the "Multiple sessions" settings as expanded by default when creating new sessions.'; +$string['mustselectusers'] = 'Must select users to export'; +$string['newdate'] = 'New date'; +$string['newduration'] = 'New duration'; +$string['newstatusset'] = 'New set of statuses'; +$string['noabsentstatusset'] = 'The status set in use does not have a status to use when not marked.'; +$string['noattendanceusers'] = 'It is not possible to export any data as there are no students enrolled in the course.'; +$string['noattforuser'] = 'No attendance records exist for the user'; +$string['noautomark'] = 'Disabled'; +$string['nocapabilitytotakethisattendance'] = 'You tried to change the attendance of a session with the cmid: {$a} that you do not have permission to modify.'; +$string['nodescription'] = 'Regular class session'; +$string['noeventstoreset'] = 'There are no calendar events that require an update.'; +$string['nogroups'] = 'You can\'t add group sessions. No groups exists in course.'; +$string['noguest'] = 'Guest can\'t see attendance'; +$string['noofdaysabsent'] = 'No of days absent'; +$string['noofdaysexcused'] = 'No of days excused'; +$string['noofdayslate'] = 'No of days late'; +$string['noofdayspresent'] = 'No of days present'; +$string['nosessiondayselected'] = 'No Session day selected'; +$string['nosessionexists'] = 'No Session exists for this course'; +$string['nosessionsselected'] = 'No sessions selected'; +$string['notfound'] = 'Attendance activity not found in this course!'; +$string['notifytask'] = 'Send warnings to users'; +$string['notmember'] = 'not member'; +$string['notset'] = 'not set'; +$string['noupgradefromthisversion'] = 'The Attendance module cannot upgrade from the version of attforblock you have installed. - please delete attforblock or upgrade it to the latest version before isntalling the new attendance module'; +$string['numsessions'] = 'Number of sessions'; +$string['olddate'] = 'Old date'; +$string['onlyselectedusers'] = 'Export specific users'; +$string['overallsessions'] = 'Over all sessions'; +$string['overallsessions_help'] = 'Shows statistics for all sessions including those not yet taken (past and future): + <ul> + <li><strong>Sessions</strong>: total number of sessions.</li> + <li><strong>Points</strong>: points awarded based on the taken sessions.</li> + <li><strong>Percentage</strong>: percentage of points awarded over the maxium possible points for all sessions.</li> + </ul>'; +$string['oversessionstaken'] = 'Over taken sessions'; +$string['oversessionstaken_help'] = 'Shows statistics for sessions where attendance has been taken: + <ul> + <li><strong>Sessions</strong>: number of already taken sessions.</li> + <li><strong>Points</strong>: points awarded based on the taken sessions.</li> + <li><strong>Percentage</strong>: percentage of points awarded over the maxium possible points of the taken sessions.</li> + </ul>'; +$string['pageof'] = 'Page {$a->page} of {$a->numpages}'; +$string['participant'] = 'Participant'; +$string['password'] = 'Password'; +$string['passwordgrp'] = 'Student password'; +$string['passwordgrp_help'] = 'If set students will be required to enter this password before they can set their own attendance status for the session. If empty, no password is required.'; +$string['passwordrequired'] = 'You must enter the session password before you can submit your attendance'; +$string['percentage'] = 'Percentage'; +$string['percentageallsessions'] = 'Percentage over all sessions'; +$string['percentagesessionscompleted'] = 'Percentage over taken sessions'; +$string['pluginadministration'] = 'Attendance administration'; +$string['pluginname'] = 'Attendance'; +$string['points'] = 'Points'; +$string['pointsallsessions'] = 'Points over all sessions'; +$string['pointssessionscompleted'] = 'Points over taken sessions'; +$string['preferences_desc'] = 'Changes to status sets will affect existing attendance sessions and may affect grading.'; +$string['preventsharederror'] = 'Self-marking has been disabled for a session because this device appears to have been used to record attendance for another student.'; +$string['preventsharedip'] = 'Prevent students sharing IP address'; +$string['preventsharedip_help'] = 'Prevent students from using the same device (identified using IP address) to take attendance for other students.'; +$string['preventsharediptime'] = 'Time to allow re-use of IP address (minutes)'; +$string['preventsharediptime_help'] = 'Allow an IP address to be re-used for taking attendance in this session after this time has elapsed.'; +$string['preview'] = 'File preview'; +$string['previewhtml'] = 'HTML format preview'; +$string['priorto'] = 'The session date is prior to the course start date ({$a}) so that the new sessions scheduled before this date will be hidden (not accessible). You can change the course start date at any time (see course settings) in order to have access to earlier sessions.<br><br>Please change the session date or just click the "Add session" button again to confirm?'; +$string['privacy:metadata:attendancelog'] = 'Log of user attendances recorded.'; +$string['privacy:metadata:attendancesessions'] = 'Sessions to which attendance will be recorded.'; +$string['privacy:metadata:attendancewarningdone'] = 'Log of warnings sent to users over their attendance record.'; +$string['privacy:metadata:duration'] = 'Session duration in seconds'; +$string['privacy:metadata:groupid'] = 'Group ID associated with session.'; +$string['privacy:metadata:ipaddress'] = 'IP address attendance was marked from.'; +$string['privacy:metadata:lasttaken'] = 'Timestamp of when session attendance was last taken.'; +$string['privacy:metadata:lasttakenby'] = 'User ID of the last user to take attendance in this session'; +$string['privacy:metadata:notifyid'] = 'ID of attendance session warning is associated with.'; +$string['privacy:metadata:remarks'] = 'Comments about the user\'s attendance.'; +$string['privacy:metadata:sessdate'] = 'Timestamp of when session starts.'; +$string['privacy:metadata:sessionid'] = 'Attendance session ID.'; +$string['privacy:metadata:statusid'] = 'ID of student\'s attendance status.'; +$string['privacy:metadata:statusset'] = 'Status set to which status ID belongs.'; +$string['privacy:metadata:studentid'] = 'ID of student having attendance recorded.'; +$string['privacy:metadata:takenby'] = 'User ID of the user who took attendance for the student.'; +$string['privacy:metadata:timemodified'] = 'Timestamp of when session was last modified'; +$string['privacy:metadata:timesent'] = 'Timestamp when warning was sent.'; +$string['privacy:metadata:timetaken'] = 'Timestamp of when attendance was taken for the student.'; +$string['privacy:metadata:userid'] = 'ID of user to send warning to.'; +$string['processingfile'] = 'Processing file'; +$string['qr_cookie_error'] = 'QR session has expired.'; +$string['qr_pass_wrong'] = 'QR password is wrong or has expired.'; +$string['qrcode'] = 'QR Code'; +$string['randompassword'] = 'Random password'; +$string['remark'] = 'Remark for: {$a}'; +$string['remarks'] = 'Remarks'; +$string['repeatasfollows'] = 'Repeat the session above as follows'; +$string['repeatevery'] = 'Repeat every'; +$string['repeaton'] = 'Repeat on'; +$string['repeatuntil'] = 'Repeat until'; +$string['report'] = 'Report'; +$string['required'] = 'Required*'; +$string['requiredentries'] = ' Temporary records overwrite participant attendance records'; +$string['requiredentry'] = ' Temporary user merge help guide'; +$string['requiredentry_help'] = '<p align="center"><b>Attendance</b></p> +<p align="left"><strong>Merge Accounts</strong></p> +<p align="left"> +<table border="2" cellpadding="4"> +<tr> +<th>Moodle User</th> +<th>Temporary User</th> +<th>Action</th> +</tr> +<tr> +<td>Attendance data</td> +<td>Attendance data</td> +<td>Temporary user will override Moodle user</td> +</tr> +<tr> +<td>No attendance data</td> +<td>Attendance data</td> +<td>Temporary user attendance will be transfered to Moodle user</td> +</tr> +<tr> +<td>Attendance data</td> +<td>No attendance data</td> +<td>Temporary user will be deleted</td> +</tr> +<tr> +<td>No attendance data</td> +<td>No attendance data</td> +<td>Temporary user will be deleted</td> +</tr> +</table> + +</p> +<p align="left"><strong>Temporay user will be deleted in all cases after merge action</strong></p>'; +$string['requiresubnet'] = 'Require network address'; +$string['requiresubnet_help'] = 'Attendance recording may be restricted to particular subnets by specifying a comma-separated list of partial or full IP addresses.'; +$string['resetcaledarcreate'] = 'Calendar events have been enabled but a number of existing sessions do not have events. Do you want to create calendar events for all existing sessions?'; +$string['resetcaledardelete'] = 'Calendar events have been disabled but a number of existing sessions have events that should be deleted. Do you want to delete all existing events?'; +$string['resetcalendar'] = 'Reset calendar'; +$string['resetdescription'] = 'Remember that deleting attendance data will erase information from database. You can just hide older sessions having changed start date of course!'; +$string['resetstatuses'] = 'Reset statuses to default'; +$string['restoredefaults'] = 'Restore defaults'; +$string['resultsperpage'] = 'Results per page'; +$string['resultsperpage_desc'] = 'Number of students displayed on a page'; +$string['rotateqrcode'] = 'Rotate QR code'; +$string['rotateqrcode_cleartemppass_task'] = 'Task to clear temporary passwords generated by rotate QR code functionality.'; +$string['rotateqrcodeexpirymargin'] = 'Rotate QR code/password expiry margin (seconds)'; +$string['rotateqrcodeexpirymargin_desc'] = 'Time interval (seconds) to allow expired QR code/password by.'; +$string['rotateqrcodeinterval'] = 'Rotate QR code/password interval (seconds)'; +$string['rotateqrcodeinterval_desc'] = 'Time interval (seconds) to rotate QR code/password by.'; +$string['save'] = 'Save attendance'; +$string['scantime'] = 'Scan time'; +$string['scantime_help'] = 'This allows a timestamp to be included in the import file - it will attempt to convert the timestamp passed using the PHP strtotime function and then use attendance status settings to decide which status to set for the user'; +$string['search:activity'] = 'Attendance - activity information'; +$string['session'] = 'Session'; +$string['session_help'] = 'Session'; +$string['sessionadded'] = 'Session successfully added'; +$string['sessionalreadyexists'] = 'Session already exists for this date'; +$string['sessiondate'] = 'Date'; +$string['sessiondays'] = 'Session Days'; +$string['sessiondeleted'] = 'Session successfully deleted'; +$string['sessionduplicate'] = 'A duplicate session exists for course: {$a->course} in attendance: {$a->activity}'; +$string['sessionexist'] = 'Session not added (already exists)!'; +$string['sessiongenerated'] = 'One session was successfully generated'; +$string['sessions'] = 'Sessions'; +$string['sessionsallcourses'] = 'All courses'; +$string['sessionsbyactivity'] = 'Attendance instance'; +$string['sessionsbycourse'] = 'Course'; +$string['sessionsbydate'] = 'Week'; +$string['sessionscompleted'] = 'Taken sessions'; +$string['sessionscurrentcourses'] = 'Current courses'; +$string['sessionsgenerated'] = '{$a} sessions were successfully generated'; +$string['sessionsids'] = 'IDs of sessions: '; +$string['sessionsnotfound'] = 'There is no sessions in the selected timespan'; +$string['sessionstartdate'] = 'Session start date'; +$string['sessionstotal'] = 'Total number of sessions'; +$string['sessionsupdated'] = 'Sessions updated'; +$string['sessiontype'] = 'Type'; +$string['sessiontype_help'] = 'You can add sessions for all students or for a group of students. Ability to add different types depends on activity group mode. + +* In group mode "No groups" you can add only sessions for all students. +* In group mode "Separate groups" you can add only sessions for a group of students. +* In group mode "Visible groups" you can add both types of sessions. +'; +$string['sessiontypeshort'] = 'Type'; +$string['sessionunknowngroup'] = 'A session specifies unknown group(s): {$a}'; +$string['sessionupdated'] = 'Session successfully updated'; +$string['set_by_student'] = 'Self-recorded'; +$string['setallstatuses'] = 'Set status for'; +$string['setallstatusesto'] = 'Set status to «{$a}»'; +$string['setperiod'] = 'Specified time in minutes to release IP'; +$string['settings'] = 'Settings'; +$string['setunmarked'] = 'Automatically set when not marked'; +$string['setunmarked_help'] = 'If enabled in the session, set this status if a student has not marked their own attendance.'; +$string['showdefaults'] = 'Show defaults'; +$string['showduration'] = 'Show duration'; +$string['showextrauserdetails'] = 'Show extra user details'; +$string['showqrcode'] = 'Show QR Code'; +$string['showsessiondescriptiononreport'] = 'Show session description in report'; +$string['showsessiondescriptiononreport_desc'] = 'Show the session description in the attendance report listing.'; +$string['showsessiondetails'] = 'Show session details'; +$string['somedisabledstatus'] = '(Some options have been removed as the session has started.)'; +$string['sortedgrid'] = 'Sorted grid'; +$string['sortedlist'] = 'Sorted list'; +$string['startofperiod'] = 'Start of period'; +$string['starttime'] = 'Start time'; +$string['status'] = 'Status'; +$string['statusall'] = 'all'; +$string['statusdeleted'] = 'Status deleted'; +$string['statuses'] = 'Statuses'; +$string['statusset'] = 'Status set {$a}'; +$string['statussetsettings'] = 'Status set'; +$string['statusunselected'] = 'unselected'; +$string['strftimedm'] = '%b %d'; +$string['strftimedmw'] = '<nobr>%a %b %d</nobr>'; +$string['strftimedmy'] = '%d %b %Y'; +$string['strftimedmyhm'] = '%d %b %Y %I.%M%p'; // Line added to allow multiple sessions in the same day. +$string['strftimedmyw'] = '<nobr>%a %d %b %Y</nobr>'; +$string['strftimeh'] = '%I%p'; +$string['strftimehm'] = '%I:%M%p'; +$string['strftimeshortdate'] = '%d.%m.%Y'; +$string['studentavailability'] = 'Available for students (minutes)'; +$string['studentavailability_help'] = 'When students are marking their own attendance, the number of minutes after session starts that this status is available. + <br/>If empty, this status will always be available, If set to 0 it will always be hidden to students.'; +$string['studentid'] = 'Student ID'; +$string['studentmarked'] = 'Your attendance in this session has been recorded.'; +$string['studentmarking'] = 'Student recording'; +$string['studentpassword'] = 'Student password'; +$string['studentrecordingexpanded'] = 'Student recording expanded'; +$string['studentrecordingexpanded_desc'] = 'Show the "Student recording" settings as expanded by default when creating new sessions.'; +$string['studentscanmark'] = 'Allow students to record own attendance'; +$string['studentscanmark_desc'] = 'If checked, teachers will be able to allow students to mark their own attendance.'; +$string['studentscanmark_help'] = 'If checked students will be able to change their own attendance status for the session.'; +$string['studentscanmarksessiontime'] = 'Students record attendance during session time'; +$string['studentscanmarksessiontime_desc'] = 'If checked students can only record their attendance during the session.'; +$string['studentscanmarksessiontimeend'] = 'Session end (minutes)'; +$string['studentscanmarksessiontimeend_desc'] = 'If the session does not have an end time, how many minutes should the session be available for students to record their attendance.'; +$string['submit'] = 'Submit'; +$string['submitattendance'] = 'Submit attendance'; +$string['submitpassword'] = 'Submit password'; +$string['subnet'] = 'Subnet'; +$string['subnetactivitylevel'] = 'Allow subnet config at activity level'; +$string['subnetactivitylevel_desc'] = 'If enabled, teachers can override the default subnet at the activity level when creating an attendance. Otherwise the site default will be used when creating a session.'; +$string['subnetwrong'] = 'Attendance can only be recorded from certain locations, and this computer is not on the allowed list.'; +$string['summary'] = 'Summary'; +$string['tablerenamefailed'] = 'Rename of old attforblock table to attendance failed'; +$string['tactions'] = 'Action'; +$string['takeattendance'] = 'Take attendance'; +$string['takensessions'] = 'Taken sessions'; +$string['tcreated'] = 'Created'; +$string['tempaddform'] = 'Add temporary user'; +$string['tempexists'] = 'There is already a temporary user with this email address'; +$string['temptable'] = 'List of temporary users'; +$string['tempuser'] = 'Temporary user'; +$string['tempusermerge'] = 'Merge temporary user'; +$string['tempusers'] = 'Temporary users'; +$string['tempusersedit'] = 'Edit temporary user'; +$string['tempuserslist'] = 'Temporary users'; +$string['thirdpartyemails'] = 'Notify other users'; +$string['thirdpartyemails_help'] = 'List of other users who will be notified. (requires the capability mod/attendance:viewreports)'; +$string['thirdpartyemailsubject'] = 'Attendance warning'; +$string['thirdpartyemailtext'] = '{$a->firstname} {$a->lastname} attendance within {$a->coursename} {$a->aname} is lower than {$a->warningpercent} ({$a->percent})'; +$string['thirdpartyemailtextfooter'] = 'You are receiving this because the teacher of this course has added your email to the recipient’s list'; +$string['thiscourse'] = 'This course'; +$string['time'] = 'Time'; +$string['timeahead'] = 'Multiple sessions that exceed one year cannot be created, please adjust the start and end dates.'; +$string['to'] = 'to:'; +$string['todate'] = 'to date'; +$string['triggered'] = 'First notified'; +$string['tuseremail'] = 'Email'; +$string['tusername'] = 'Full name'; +$string['ungraded'] = 'Ungraded sessions'; +$string['unknowngroup'] = 'Unknown group'; +$string['update'] = 'Update'; +$string['uploadattendance'] = 'Upload attendance by CSV'; +$string['usedefaultsubnet'] = 'Use default'; +$string['usemessageform'] = 'or use the form below to send a message to the selected students'; +$string['userexists'] = 'There is already a real user with this email address'; +$string['userid'] = 'User ID'; +$string['userimportfield'] = 'External user field'; +$string['userimportfield_help'] = 'Field from uploaded CSV that contains user identifier'; +$string['userimportto'] = 'Moodle user field'; +$string['userimportto_help'] = 'Moodle field that matches the data from the CSV export'; +$string['users'] = 'Users to export'; +$string['usestatusset'] = 'Status set'; +$string['variable'] = 'variable'; +$string['variablesupdated'] = 'Variables successfully updated'; +$string['versionforprinting'] = 'version for printing'; +$string['viewmode'] = 'View mode'; +$string['warnafter'] = 'Number of sessions taken before warning'; +$string['warnafter_help'] = 'Warnings will only be triggered when the user has had their attendance taken for at least this number of sessions.'; +$string['warningdeleted'] = 'Warning deleted'; +$string['warningdesc'] = 'These warnings will be automatically added to any new attendance activities. If more than one warning is triggered at exactly the same time, only the warning with the lower warning threshold will be sent.'; +$string['warningdesc_course'] = 'Warnings thresholds set here affect the absentee report and allow students and third parties to be notified. If more than one warning is triggered at exactly the same time, only the warning with the lower warning threshold will be sent.'; +$string['warningfailed'] = 'You cannot create a warning that uses the same percentage and number of sessions.'; +$string['warningpercent'] = 'Warn if percentage falls under'; +$string['warningpercent_help'] = 'A warning will be triggered when the overall percentage falls below this number.'; +$string['warnings'] = 'Warnings set'; +$string['warningthreshold'] = 'Warning threshold'; +$string['warningupdated'] = 'Updated warnings'; +$string['week'] = 'week(s)'; +$string['weekcommencing'] = 'Week commencing'; +$string['weeks'] = 'Weeks'; +$string['youcantdo'] = 'You can\'t do anything'; \ No newline at end of file diff --git a/mod/attendance/lib.php b/mod/attendance/lib.php new file mode 100644 index 0000000..ee880f7 --- /dev/null +++ b/mod/attendance/lib.php @@ -0,0 +1,538 @@ +<?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/>. + +/** + * Library of functions and constants for module attendance + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once(dirname(__FILE__) . '/classes/calendar_helpers.php'); + +/** + * Returns the information if the module supports a feature + * + * @see plugin_supports() in lib/moodlelib.php + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed true if the feature is supported, null if unknown + */ +function attendance_supports($feature) { + switch($feature) { + case FEATURE_GRADE_HAS_GRADE: + return true; + case FEATURE_GROUPS: + return true; + case FEATURE_GROUPINGS: + return true; + case FEATURE_SHOW_DESCRIPTION: + return true; + case FEATURE_MOD_INTRO: + return true; + case FEATURE_BACKUP_MOODLE2: + return true; + // Artem Andreev: AFAIK it's not tested. + case FEATURE_COMPLETION_TRACKS_VIEWS: + return false; + default: + return null; + } +} + +/** + * Add default set of statuses to the new attendance. + * + * @param int $attid - id of attendance instance. + */ +function att_add_default_statuses($attid) { + global $DB; + + $statuses = $DB->get_recordset('attendance_statuses', array('attendanceid' => 0), 'id'); + foreach ($statuses as $st) { + $rec = $st; + $rec->attendanceid = $attid; + $DB->insert_record('attendance_statuses', $rec); + } + $statuses->close(); +} + +/** + * Add default set of warnings to the new attendance. + * + * @param int $id - id of attendance instance. + */ +function attendance_add_default_warnings($id) { + global $DB, $CFG; + require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + + $warnings = $DB->get_recordset('attendance_warning', + array('idnumber' => 0), 'id'); + foreach ($warnings as $n) { + $rec = $n; + $rec->idnumber = $id; + $DB->insert_record('attendance_warning', $rec); + } + $warnings->close(); +} + +/** + * Add new attendance instance. + * + * @param stdClass $attendance + * @return bool|int + */ +function attendance_add_instance($attendance) { + global $DB; + + $attendance->timemodified = time(); + + // Default grade (similar to what db fields defaults if no grade attribute is passed), + // but we need it in object for grading update. + if (!isset($attendance->grade)) { + $attendance->grade = 100; + } + + $attendance->id = $DB->insert_record('attendance', $attendance); + + att_add_default_statuses($attendance->id); + + attendance_add_default_warnings($attendance->id); + + attendance_grade_item_update($attendance); + + return $attendance->id; +} + +/** + * Update existing attendance instance. + * + * @param stdClass $attendance + * @return bool + */ +function attendance_update_instance($attendance) { + global $DB; + + $attendance->timemodified = time(); + $attendance->id = $attendance->instance; + + if (! $DB->update_record('attendance', $attendance)) { + return false; + } + + attendance_grade_item_update($attendance); + + return true; +} + +/** + * Delete existing attendance + * + * @param int $id + * @return bool + */ +function attendance_delete_instance($id) { + global $DB, $CFG; + require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + + if (! $attendance = $DB->get_record('attendance', array('id' => $id))) { + return false; + } + + if ($sessids = array_keys($DB->get_records('attendance_sessions', array('attendanceid' => $id), '', 'id'))) { + if (attendance_existing_calendar_events_ids($sessids)) { + attendance_delete_calendar_events($sessids); + } + $DB->delete_records_list('attendance_log', 'sessionid', $sessids); + $DB->delete_records('attendance_sessions', array('attendanceid' => $id)); + } + $DB->delete_records('attendance_statuses', array('attendanceid' => $id)); + + $DB->delete_records('attendance_warning', array('idnumber' => $id)); + + $DB->delete_records('attendance', array('id' => $id)); + + attendance_grade_item_delete($attendance); + + return true; +} + +/** + * Called by course/reset.php + * @param moodleform $mform form passed by reference + */ +function attendance_reset_course_form_definition(&$mform) { + $mform->addElement('header', 'attendanceheader', get_string('modulename', 'attendance')); + + $mform->addElement('static', 'description', get_string('description', 'attendance'), + get_string('resetdescription', 'attendance')); + $mform->addElement('checkbox', 'reset_attendance_log', get_string('deletelogs', 'attendance')); + + $mform->addElement('checkbox', 'reset_attendance_sessions', get_string('deletesessions', 'attendance')); + $mform->disabledIf('reset_attendance_sessions', 'reset_attendance_log', 'notchecked'); + + $mform->addElement('checkbox', 'reset_attendance_statuses', get_string('resetstatuses', 'attendance')); + $mform->setAdvanced('reset_attendance_statuses'); + $mform->disabledIf('reset_attendance_statuses', 'reset_attendance_log', 'notchecked'); +} + +/** + * Course reset form defaults. + * + * @param stdClass $course + * @return array + */ +function attendance_reset_course_form_defaults($course) { + return array('reset_attendance_log' => 0, 'reset_attendance_statuses' => 0, 'reset_attendance_sessions' => 0); +} + +/** + * Reset user data within attendance. + * + * @param stdClass $data + * @return array + */ +function attendance_reset_userdata($data) { + global $DB; + + $status = array(); + + $attids = array_keys($DB->get_records('attendance', array('course' => $data->courseid), '', 'id')); + + if (!empty($data->reset_attendance_log)) { + $sess = $DB->get_records_list('attendance_sessions', 'attendanceid', $attids, '', 'id'); + if (!empty($sess)) { + list($sql, $params) = $DB->get_in_or_equal(array_keys($sess)); + $DB->delete_records_select('attendance_log', "sessionid $sql", $params); + list($sql, $params) = $DB->get_in_or_equal($attids); + $DB->set_field_select('attendance_sessions', 'lasttaken', 0, "attendanceid $sql", $params); + if (empty($data->reset_attendance_sessions)) { + // If sessions are being retained, clear automarkcompleted value. + $DB->set_field_select('attendance_sessions', 'automarkcompleted', 0, "attendanceid $sql", $params); + } + + $status[] = array( + 'component' => get_string('modulenameplural', 'attendance'), + 'item' => get_string('attendancedata', 'attendance'), + 'error' => false + ); + } + } + + if (!empty($data->reset_attendance_statuses)) { + $DB->delete_records_list('attendance_statuses', 'attendanceid', $attids); + foreach ($attids as $attid) { + att_add_default_statuses($attid); + } + + $status[] = array( + 'component' => get_string('modulenameplural', 'attendance'), + 'item' => get_string('sessions', 'attendance'), + 'error' => false + ); + } + + if (!empty($data->reset_attendance_sessions)) { + $sessionsids = array_keys($DB->get_records_list('attendance_sessions', 'attendanceid', $attids, '', 'id')); + if (attendance_existing_calendar_events_ids($sessionsids)) { + attendance_delete_calendar_events($sessionsids); + } + $DB->delete_records_list('attendance_sessions', 'attendanceid', $attids); + + $status[] = array( + 'component' => get_string('modulenameplural', 'attendance'), + 'item' => get_string('statuses', 'attendance'), + 'error' => false + ); + } + + return $status; +} +/** + * Return a small object with summary information about what a + * user has done with a given particular instance of this module + * Used for user activity reports. + * $return->time = the time they did it + * $return->info = a short text description + * + * @param stdClass $course - full course record. + * @param stdClass $user - full user record + * @param stdClass $mod + * @param stdClass $attendance + * @return stdClass. + */ +function attendance_user_outline($course, $user, $mod, $attendance) { + global $CFG; + require_once(dirname(__FILE__).'/locallib.php'); + require_once($CFG->libdir.'/gradelib.php'); + + $grades = grade_get_grades($course->id, 'mod', 'attendance', $attendance->id, $user->id); + + $result = new stdClass(); + if (!empty($grades->items[0]->grades)) { + $grade = reset($grades->items[0]->grades); + $result->time = $grade->dategraded; + } else { + $result->time = 0; + } + if (has_capability('mod/attendance:canbelisted', $mod->context, $user->id)) { + $summary = new mod_attendance_summary($attendance->id, $user->id); + $usersummary = $summary->get_all_sessions_summary_for($user->id); + + $result->info = $usersummary->pointsallsessions; + } + + return $result; +} +/** + * Print a detailed representation of what a user has done with + * a given particular instance of this module, for user activity reports. + * + * @param stdClass $course + * @param stdClass $user + * @param stdClass $mod + * @param stdClass $attendance + */ +function attendance_user_complete($course, $user, $mod, $attendance) { + global $CFG; + + require_once(dirname(__FILE__).'/renderhelpers.php'); + require_once($CFG->libdir.'/gradelib.php'); + + if (has_capability('mod/attendance:canbelisted', $mod->context, $user->id)) { + echo construct_full_user_stat_html_table($attendance, $user); + } +} + +/** + * Dummy function - must exist to allow quick editing of module name. + * + * @param stdClass $attendance + * @param int $userid + * @param bool $nullifnone + */ +function attendance_update_grades($attendance, $userid=0, $nullifnone=true) { + // We need this function to exist so that quick editing of module name is passed to gradebook. +} +/** + * Create grade item for given attendance + * + * @param stdClass $attendance object with extra cmidnumber + * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook + * @return int 0 if ok, error code otherwise + */ +function attendance_grade_item_update($attendance, $grades=null) { + global $CFG, $DB; + + require_once('locallib.php'); + + if (!function_exists('grade_update')) { // Workaround for buggy PHP versions. + require_once($CFG->libdir.'/gradelib.php'); + } + + if (!isset($attendance->courseid)) { + $attendance->courseid = $attendance->course; + } + if (!$DB->get_record('course', array('id' => $attendance->course))) { + error("Course is misconfigured"); + } + + if (!empty($attendance->cmidnumber)) { + $params = array('itemname' => $attendance->name, 'idnumber' => $attendance->cmidnumber); + } else { + // MDL-14303. + $params = array('itemname' => $attendance->name); + } + + if ($attendance->grade > 0) { + $params['gradetype'] = GRADE_TYPE_VALUE; + $params['grademax'] = $attendance->grade; + $params['grademin'] = 0; + } else if ($attendance->grade < 0) { + $params['gradetype'] = GRADE_TYPE_SCALE; + $params['scaleid'] = -$attendance->grade; + + } else { + $params['gradetype'] = GRADE_TYPE_NONE; + } + + if ($grades === 'reset') { + $params['reset'] = true; + $grades = null; + } + + return grade_update('mod/attendance', $attendance->courseid, 'mod', 'attendance', $attendance->id, 0, $grades, $params); +} + +/** + * Delete grade item for given attendance + * + * @param object $attendance object + * @return object attendance + */ +function attendance_grade_item_delete($attendance) { + global $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + if (!isset($attendance->courseid)) { + $attendance->courseid = $attendance->course; + } + + return grade_update('mod/attendance', $attendance->courseid, 'mod', 'attendance', + $attendance->id, 0, null, array('deleted' => 1)); +} + +/** + * This function returns if a scale is being used by one attendance + * it it has support for grading and scales. Commented code should be + * modified if necessary. See book, glossary or journal modules + * as reference. + * + * @param int $attendanceid + * @param int $scaleid + * @return boolean True if the scale is used by any attendance + */ +function attendance_scale_used ($attendanceid, $scaleid) { + return false; +} + +/** + * Checks if scale is being used by any instance of attendance + * + * This is used to find out if scale used anywhere + * + * @param int $scaleid + * @return bool true if the scale is used by any book + */ +function attendance_scale_used_anywhere($scaleid) { + return false; +} + +/** + * Serves the attendance sessions descriptions files. + * + * @param object $course + * @param object $cm + * @param object $context + * @param string $filearea + * @param array $args + * @param bool $forcedownload + * @return bool false if file not found, does not return if found - justsend the file + */ +function attendance_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return false; + } + + require_login($course, false, $cm); + + if (!$DB->record_exists('attendance', array('id' => $cm->instance))) { + return false; + } + + // Session area is served by pluginfile.php. + $fileareas = array('session'); + if (!in_array($filearea, $fileareas)) { + return false; + } + + $sessid = (int)array_shift($args); + if (!$DB->record_exists('attendance_sessions', array('id' => $sessid))) { + return false; + } + + $fs = get_file_storage(); + $relativepath = implode('/', $args); + $fullpath = "/$context->id/mod_attendance/$filearea/$sessid/$relativepath"; + if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { + return false; + } + send_stored_file($file, 0, 0, true); +} + +/** + * Print tabs on attendance settings page. + * + * @param string $selected - current selected tab. + */ +function attendance_print_settings_tabs($selected = 'settings') { + global $CFG; + // Print tabs for different settings pages. + $tabs = array(); + $tabs[] = new tabobject('settings', "{$CFG->wwwroot}/{$CFG->admin}/settings.php?section=modsettingattendance", + get_string('settings', 'attendance'), get_string('settings'), false); + + $tabs[] = new tabobject('defaultstatus', $CFG->wwwroot.'/mod/attendance/defaultstatus.php', + get_string('defaultstatus', 'attendance'), get_string('defaultstatus', 'attendance'), false); + + if (get_config('attendance', 'enablewarnings')) { + $tabs[] = new tabobject('defaultwarnings', $CFG->wwwroot . '/mod/attendance/warnings.php', + get_string('defaultwarnings', 'attendance'), get_string('defaultwarnings', 'attendance'), false); + } + + $tabs[] = new tabobject('coursesummary', $CFG->wwwroot.'/mod/attendance/coursesummary.php', + get_string('coursesummary', 'attendance'), get_string('coursesummary', 'attendance'), false); + + if (get_config('attendance', 'enablewarnings')) { + $tabs[] = new tabobject('absentee', $CFG->wwwroot . '/mod/attendance/absentee.php', + get_string('absenteereport', 'attendance'), get_string('absenteereport', 'attendance'), false); + } + + $tabs[] = new tabobject('resetcalendar', $CFG->wwwroot.'/mod/attendance/resetcalendar.php', + get_string('resetcalendar', 'attendance'), get_string('resetcalendar', 'attendance'), false); + + $tabs[] = new tabobject('importsessions', $CFG->wwwroot . '/mod/attendance/import/sessions.php', + get_string('importsessions', 'attendance'), get_string('importsessions', 'attendance'), false); + + ob_start(); + print_tabs(array($tabs), $selected); + $tabmenu = ob_get_contents(); + ob_end_clean(); + + return $tabmenu; +} + +/** + * Helper function to remove a user from the thirdpartyemails record of the attendance_warning table. + * + * @param array $warnings - list of warnings to parse. + * @param int $userid - User id of user to remove. + */ +function attendance_remove_user_from_thirdpartyemails($warnings, $userid) { + global $DB; + + // Update the third party emails list for all the relevant warnings. + $updatedwarnings = array_map( + function(stdClass $warning) use ($userid) : stdClass { + $warning->thirdpartyemails = implode(',', array_diff(explode(',', $warning->thirdpartyemails), [$userid])); + return $warning; + }, + array_filter( + $warnings, + function (stdClass $warning) use ($userid) : bool { + return in_array($userid, explode(',', $warning->thirdpartyemails)); + } + ) + ); + + // Sadly need to update each individually, no way to bulk update as all the thirdpartyemails field can be different. + foreach ($updatedwarnings as $updatedwarning) { + $DB->update_record('attendance_warning', $updatedwarning); + } +} \ No newline at end of file diff --git a/mod/attendance/locallib.php b/mod/attendance/locallib.php new file mode 100644 index 0000000..7764bef --- /dev/null +++ b/mod/attendance/locallib.php @@ -0,0 +1,1328 @@ +<?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/>. + +/** + * local functions and constants for module attendance + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/gradelib.php'); +require_once(dirname(__FILE__).'/renderhelpers.php'); + +define('ATT_VIEW_DAYS', 1); +define('ATT_VIEW_WEEKS', 2); +define('ATT_VIEW_MONTHS', 3); +define('ATT_VIEW_ALLPAST', 4); +define('ATT_VIEW_ALL', 5); +define('ATT_VIEW_NOTPRESENT', 6); +define('ATT_VIEW_SUMMARY', 7); + +define('ATT_SORT_DEFAULT', 0); +define('ATT_SORT_LASTNAME', 1); +define('ATT_SORT_FIRSTNAME', 2); + +define('ATTENDANCE_AUTOMARK_DISABLED', 0); +define('ATTENDANCE_AUTOMARK_ALL', 1); +define('ATTENDANCE_AUTOMARK_CLOSE', 2); + +define('ATTENDANCE_SHAREDIP_DISABLED', 0); +define('ATTENDANCE_SHAREDIP_MINUTES', 1); +define('ATTENDANCE_SHAREDIP_FORCE', 2); + +// Max number of sessions available in the warnings set form to trigger warnings. +define('ATTENDANCE_MAXWARNAFTER', 100); + +/** + * Get statuses, + * + * @param int $attid + * @param bool $onlyvisible + * @param int $statusset + * @return array + */ +function attendance_get_statuses($attid, $onlyvisible=true, $statusset = -1) { + global $DB; + + // Set selector. + $params = array('aid' => $attid); + $setsql = ''; + if ($statusset >= 0) { + $params['statusset'] = $statusset; + $setsql = ' AND setnumber = :statusset '; + } + + if ($onlyvisible) { + $statuses = $DB->get_records_select('attendance_statuses', "attendanceid = :aid AND visible = 1 AND deleted = 0 $setsql", + $params, 'setnumber ASC, grade DESC'); + } else { + $statuses = $DB->get_records_select('attendance_statuses', "attendanceid = :aid AND deleted = 0 $setsql", + $params, 'setnumber ASC, grade DESC'); + } + + return $statuses; +} + +/** + * Get the name of the status set. + * + * @param int $attid + * @param int $statusset + * @param bool $includevalues + * @return string + */ +function attendance_get_setname($attid, $statusset, $includevalues = true) { + $statusname = get_string('statusset', 'mod_attendance', $statusset + 1); + if ($includevalues) { + $statuses = attendance_get_statuses($attid, true, $statusset); + $statusesout = array(); + foreach ($statuses as $status) { + $statusesout[] = $status->acronym; + } + if ($statusesout) { + if (count($statusesout) > 6) { + $statusesout = array_slice($statusesout, 0, 6); + $statusesout[] = '...'; + } + $statusesout = implode(' ', $statusesout); + $statusname .= ' ('.$statusesout.')'; + } + } + + return $statusname; +} + +/** + * Get full filtered log. + * @param int $userid + * @param stdClass $pageparams + * @return array + */ +function attendance_get_user_sessions_log_full($userid, $pageparams) { + global $DB; + // All taken sessions (including previous groups). + + $usercourses = enrol_get_users_courses($userid); + list($usql, $uparams) = $DB->get_in_or_equal(array_keys($usercourses), SQL_PARAMS_NAMED, 'cid0'); + + $coursesql = "(1 = 1)"; + $courseparams = array(); + $now = time(); + if ($pageparams->sesscourses === 'current') { + $coursesql = "(c.startdate = 0 OR c.startdate <= :now1) AND (c.enddate = 0 OR c.enddate >= :now2)"; + $courseparams = array( + 'now1' => $now, + 'now2' => $now, + ); + } + + $datesql = "(1 = 1)"; + $dateparams = array(); + if ($pageparams->startdate && $pageparams->enddate) { + $datesql = "ats.sessdate >= :sdate AND ats.sessdate < :edate"; + $dateparams = array( + 'sdate' => $pageparams->startdate, + 'edate' => $pageparams->enddate, + ); + } + + if ($pageparams->groupby === 'date') { + $ordersql = "ats.sessdate ASC, c.fullname ASC, att.name ASC, att.id ASC"; + } else { + $ordersql = "c.fullname ASC, att.name ASC, att.id ASC, ats.sessdate ASC"; + } + + // WHERE clause is important: + // gm.userid not null => get unmarked attendances for user's current groups + // ats.groupid 0 => get all sessions that are for all students enrolled in course + // al.id not null => get all marked sessions whether or not user currently still in group. + $sql = "SELECT ats.id, ats.groupid, ats.sessdate, ats.duration, ats.description, ats.statusset, + al.statusid, al.remarks, ats.studentscanmark, ats.autoassignstatus, + ats.preventsharedip, ats.preventsharediptime, + ats.attendanceid, att.name AS attname, att.course AS courseid, c.fullname AS cname + FROM {attendance_sessions} ats + JOIN {attendance} att + ON att.id = ats.attendanceid + JOIN {course} c + ON att.course = c.id + LEFT JOIN {attendance_log} al + ON ats.id = al.sessionid AND al.studentid = :uid + LEFT JOIN {groups_members} gm + ON (ats.groupid = gm.groupid AND gm.userid = :uid1) + WHERE (gm.userid IS NOT NULL OR ats.groupid = 0 OR al.id IS NOT NULL) + AND att.course $usql + AND $datesql + AND $coursesql + ORDER BY $ordersql"; + + $params = array( + 'uid' => $userid, + 'uid1' => $userid, + ); + $params = array_merge($params, $uparams); + $params = array_merge($params, $dateparams); + $params = array_merge($params, $courseparams); + $sessions = $DB->get_records_sql($sql, $params); + + foreach ($sessions as $sess) { + if (empty($sess->description)) { + $sess->description = get_string('nodescription', 'attendance'); + } else { + $modinfo = get_fast_modinfo($sess->courseid); + $cmid = $modinfo->instances['attendance'][$sess->attendanceid]->get_course_module_record()->id; + $ctx = context_module::instance($cmid); + $sess->description = file_rewrite_pluginfile_urls($sess->description, + 'pluginfile.php', $ctx->id, 'mod_attendance', 'session', $sess->id); + } + } + + return $sessions; +} + +/** + * Get users courses and the relevant attendances. + * + * @param int $userid + * @return array + */ +function attendance_get_user_courses_attendances($userid) { + global $DB; + + $usercourses = enrol_get_users_courses($userid); + + list($usql, $uparams) = $DB->get_in_or_equal(array_keys($usercourses), SQL_PARAMS_NAMED, 'cid0'); + + $sql = "SELECT att.id as attid, att.course as courseid, course.fullname as coursefullname, + course.startdate as coursestartdate, att.name as attname, att.grade as attgrade + FROM {attendance} att + JOIN {course} course + ON att.course = course.id + WHERE att.course $usql + ORDER BY coursefullname ASC, attname ASC"; + + $params = array_merge($uparams, array('uid' => $userid)); + + return $DB->get_records_sql($sql, $params); +} + +/** + * Used to calculate a fraction based on the part and total values + * + * @param float $part - part of the total value + * @param float $total - total value. + * @return float the calculated fraction. + */ +function attendance_calc_fraction($part, $total) { + if ($total == 0) { + return 0; + } else { + return $part / $total; + } +} + +/** + * Check to see if statusid in use to help prevent deletion etc. + * + * @param integer $statusid + */ +function attendance_has_logs_for_status($statusid) { + global $DB; + return $DB->record_exists('attendance_log', array('statusid' => $statusid)); +} + +/** + * Helper function to add sessiondate_selector to add/update forms. + * + * @param MoodleQuickForm $mform + */ +function attendance_form_sessiondate_selector (MoodleQuickForm $mform) { + + $mform->addElement('date_selector', 'sessiondate', get_string('sessiondate', 'attendance')); + + for ($i = 0; $i <= 23; $i++) { + $hours[$i] = sprintf("%02d", $i); + } + for ($i = 0; $i < 60; $i += 5) { + $minutes[$i] = sprintf("%02d", $i); + } + + $sesendtime = array(); + if (!right_to_left()) { + $sesendtime[] =& $mform->createElement('static', 'from', '', get_string('from', 'attendance')); + $sesendtime[] =& $mform->createElement('select', 'starthour', get_string('hour', 'form'), $hours, false, true); + $sesendtime[] =& $mform->createElement('select', 'startminute', get_string('minute', 'form'), $minutes, false, true); + $sesendtime[] =& $mform->createElement('static', 'to', '', get_string('to', 'attendance')); + $sesendtime[] =& $mform->createElement('select', 'endhour', get_string('hour', 'form'), $hours, false, true); + $sesendtime[] =& $mform->createElement('select', 'endminute', get_string('minute', 'form'), $minutes, false, true); + } else { + $sesendtime[] =& $mform->createElement('static', 'from', '', get_string('from', 'attendance')); + $sesendtime[] =& $mform->createElement('select', 'startminute', get_string('minute', 'form'), $minutes, false, true); + $sesendtime[] =& $mform->createElement('select', 'starthour', get_string('hour', 'form'), $hours, false, true); + $sesendtime[] =& $mform->createElement('static', 'to', '', get_string('to', 'attendance')); + $sesendtime[] =& $mform->createElement('select', 'endminute', get_string('minute', 'form'), $minutes, false, true); + $sesendtime[] =& $mform->createElement('select', 'endhour', get_string('hour', 'form'), $hours, false, true); + } + $mform->addGroup($sesendtime, 'sestime', get_string('time', 'attendance'), array(' '), true); +} + +/** + * Count the number of status sets that exist for this instance. + * + * @param int $attendanceid + * @return int + */ +function attendance_get_max_statusset($attendanceid) { + global $DB; + + $max = $DB->get_field_sql('SELECT MAX(setnumber) FROM {attendance_statuses} WHERE attendanceid = ? AND deleted = 0', + array($attendanceid)); + if ($max) { + return $max; + } + return 0; +} + +/** + * Returns the maxpoints for each statusset + * + * @param array $statuses + * @return array + */ +function attendance_get_statusset_maxpoints($statuses) { + $statussetmaxpoints = array(); + foreach ($statuses as $st) { + if (!isset($statussetmaxpoints[$st->setnumber])) { + $statussetmaxpoints[$st->setnumber] = $st->grade; + } + } + return $statussetmaxpoints; +} + +/** + * Update user grades + * + * @param mod_attendance_structure|stdClass $attendance + * @param array $userids + */ +function attendance_update_users_grade($attendance, $userids=array()) { + global $DB; + + if (empty($attendance->grade)) { + return false; + } + + list($course, $cm) = get_course_and_cm_from_instance($attendance->id, 'attendance'); + + $summary = new mod_attendance_summary($attendance->id, $userids); + + if (empty($userids)) { + $context = context_module::instance($cm->id); + $userids = array_keys(get_enrolled_users($context, 'mod/attendance:canbelisted', 0, 'u.id')); + } + + if ($attendance->grade < 0) { + $dbparams = array('id' => -($attendance->grade)); + $scale = $DB->get_record('scale', $dbparams); + $scalearray = explode(',', $scale->scale); + $attendancegrade = count($scalearray); + } else { + $attendancegrade = $attendance->grade; + } + + $grades = array(); + foreach ($userids as $userid) { + $grades[$userid] = new stdClass(); + $grades[$userid]->userid = $userid; + + if ($summary->has_taken_sessions($userid)) { + $usersummary = $summary->get_taken_sessions_summary_for($userid); + $grades[$userid]->rawgrade = $usersummary->takensessionspercentage * $attendancegrade; + } else { + $grades[$userid]->rawgrade = null; + } + } + + return grade_update('mod/attendance', $course->id, 'mod', 'attendance', $attendance->id, 0, $grades); +} + +/** + * Update grades for specified users for specified attendance + * + * @param integer $attendanceid - the id of the attendance to update + * @param integer $grade - the value of the 'grade' property of the specified attendance + * @param array $userids - the userids of the users to be updated + */ +function attendance_update_users_grades_by_id($attendanceid, $grade, $userids) { + global $DB; + + if (empty($grade)) { + return false; + } + + list($course, $cm) = get_course_and_cm_from_instance($attendanceid, 'attendance'); + + $summary = new mod_attendance_summary($attendanceid, $userids); + + if (empty($userids)) { + $context = context_module::instance($cm->id); + $userids = array_keys(get_enrolled_users($context, 'mod/attendance:canbelisted', 0, 'u.id')); + } + + if ($grade < 0) { + $dbparams = array('id' => -($grade)); + $scale = $DB->get_record('scale', $dbparams); + $scalearray = explode(',', $scale->scale); + $attendancegrade = count($scalearray); + } else { + $attendancegrade = $grade; + } + + $grades = array(); + foreach ($userids as $userid) { + $grades[$userid] = new stdClass(); + $grades[$userid]->userid = $userid; + + if ($summary->has_taken_sessions($userid)) { + $usersummary = $summary->get_taken_sessions_summary_for($userid); + $grades[$userid]->rawgrade = $usersummary->takensessionspercentage * $attendancegrade; + } else { + $grades[$userid]->rawgrade = null; + } + } + + return grade_update('mod/attendance', $course->id, 'mod', 'attendance', $attendanceid, 0, $grades); +} + +/** + * Add an attendance status variable + * + * @param stdClass $status + * @return bool + */ +function attendance_add_status($status) { + global $DB; + if (empty($status->context)) { + $status->context = context_system::instance(); + } + + if (!empty($status->acronym) && !empty($status->description)) { + $status->deleted = 0; + $status->visible = 1; + $status->setunmarked = 0; + + $id = $DB->insert_record('attendance_statuses', $status); + $status->id = $id; + + $event = \mod_attendance\event\status_added::create(array( + 'objectid' => $status->attendanceid, + 'context' => $status->context, + 'other' => array('acronym' => $status->acronym, + 'description' => $status->description, + 'grade' => $status->grade))); + if (!empty($status->cm)) { + $event->add_record_snapshot('course_modules', $status->cm); + } + $event->add_record_snapshot('attendance_statuses', $status); + $event->trigger(); + return true; + } else { + return false; + } +} + +/** + * Remove a status variable from an attendance instance + * + * @param stdClass $status + * @param stdClass $context + * @param stdClass $cm + */ +function attendance_remove_status($status, $context = null, $cm = null) { + global $DB; + if (empty($context)) { + $context = context_system::instance(); + } + $DB->set_field('attendance_statuses', 'deleted', 1, array('id' => $status->id)); + $event = \mod_attendance\event\status_removed::create(array( + 'objectid' => $status->id, + 'context' => $context, + 'other' => array( + 'acronym' => $status->acronym, + 'description' => $status->description + ))); + if (!empty($cm)) { + $event->add_record_snapshot('course_modules', $cm); + } + $event->add_record_snapshot('attendance_statuses', $status); + $event->trigger(); +} + +/** + * Update status variable for a particular Attendance module instance + * + * @param stdClass $status + * @param string $acronym + * @param string $description + * @param int $grade + * @param bool $visible + * @param stdClass $context + * @param stdClass $cm + * @param int $studentavailability + * @param bool $setunmarked + * @return array + */ +function attendance_update_status($status, $acronym, $description, $grade, $visible, + $context = null, $cm = null, $studentavailability = null, $setunmarked = false) { + global $DB; + + if (empty($context)) { + $context = context_system::instance(); + } + + if (isset($visible)) { + $status->visible = $visible; + $updated[] = $visible ? get_string('show') : get_string('hide'); + } else if (empty($acronym) || empty($description)) { + return array('acronym' => $acronym, 'description' => $description); + } + + $updated = array(); + + if ($acronym) { + $status->acronym = $acronym; + $updated[] = $acronym; + } + if ($description) { + $status->description = $description; + $updated[] = $description; + } + if (isset($grade)) { + $status->grade = $grade; + $updated[] = $grade; + } + if (isset($studentavailability)) { + if (empty($studentavailability)) { + if ($studentavailability !== '0') { + $studentavailability = null; + } + } + + $status->studentavailability = $studentavailability; + $updated[] = $studentavailability; + } + if ($setunmarked) { + $status->setunmarked = 1; + } else { + $status->setunmarked = 0; + } + $DB->update_record('attendance_statuses', $status); + + $event = \mod_attendance\event\status_updated::create(array( + 'objectid' => $status->attendanceid, + 'context' => $context, + 'other' => array('acronym' => $acronym, 'description' => $description, 'grade' => $grade, + 'updated' => implode(' ', $updated)))); + if (!empty($cm)) { + $event->add_record_snapshot('course_modules', $cm); + } + $event->add_record_snapshot('attendance_statuses', $status); + $event->trigger(); +} + +/** + * Similar to core random_string function but only lowercase letters. + * designed to make it relatively easy to provide a simple password in class. + * + * @param int $length The length of the string to be created. + * @return string + */ +function attendance_random_string($length=6) { + $randombytes = random_bytes_emulate($length); + $pool = 'abcdefghijklmnopqrstuvwxyz'; + $pool .= '0123456789'; + $poollen = strlen($pool); + $string = ''; + for ($i = 0; $i < $length; $i++) { + $rand = ord($randombytes[$i]); + $string .= substr($pool, ($rand % ($poollen)), 1); + } + return $string; +} + +/** + * Check to see if this session is open for student marking. + * + * @param stdclass $sess the session record from attendance_sessions. + * @param boolean $log - if student cannot mark, generate log event. + * @return array (boolean, string reason for failure) + */ +function attendance_can_student_mark($sess, $log = true) { + global $DB, $USER, $OUTPUT; + $canmark = false; + $reason = 'closed'; + $attconfig = get_config('attendance'); + if (!empty($attconfig->studentscanmark) && !empty($sess->studentscanmark)) { + if (empty($attconfig->studentscanmarksessiontime)) { + $canmark = true; + $reason = ''; + } else { + $duration = $sess->duration; + if (empty($duration)) { + $duration = $attconfig->studentscanmarksessiontimeend * 60; + } + if ($sess->sessdate < time() && time() < ($sess->sessdate + $duration)) { + $canmark = true; + $reason = ''; + } + } + } + // Check if another student has marked attendance from this IP address recently. + if ($canmark && !empty($sess->preventsharedip)) { + if ($sess->preventsharedip == ATTENDANCE_SHAREDIP_MINUTES) { + $time = time() - ($sess->preventsharediptime * 60); + $sql = 'sessionid = ? AND studentid <> ? AND timetaken > ? AND ipaddress = ?'; + $params = array($sess->id, $USER->id, $time, getremoteaddr()); + $record = $DB->get_record_select('attendance_log', $sql, $params); + } else { + // Assume ATTENDANCE_SHAREDIP_FORCED. + $sql = 'sessionid = ? AND studentid <> ? AND ipaddress = ?'; + $params = array($sess->id, $USER->id, getremoteaddr()); + $record = $DB->get_record_select('attendance_log', $sql, $params); + } + + if (!empty($record)) { + $canmark = false; + $reason = 'preventsharederror'; + if ($log) { + // Trigger an ip_shared event. + $attendanceid = $DB->get_field('attendance_sessions', 'attendanceid', array('id' => $record->sessionid)); + $cm = get_coursemodule_from_instance('attendance', $attendanceid); + $event = \mod_attendance\event\session_ip_shared::create(array( + 'objectid' => 0, + 'context' => \context_module::instance($cm->id), + 'other' => array( + 'sessionid' => $record->sessionid, + 'otheruser' => $record->studentid + ) + )); + + $event->trigger(); + } + } + } + return array($canmark, $reason); +} + +/** + * Generate worksheet for Attendance export + * + * @param stdclass $data The data for the report + * @param string $filename The name of the file + * @param string $format excel|ods + * + */ +function attendance_exporttotableed($data, $filename, $format) { + global $CFG; + + if ($format === 'excel') { + require_once("$CFG->libdir/excellib.class.php"); + $filename .= ".xls"; + $workbook = new MoodleExcelWorkbook("-"); + } else { + require_once("$CFG->libdir/odslib.class.php"); + $filename .= ".ods"; + $workbook = new MoodleODSWorkbook("-"); + } + // Sending HTTP headers. + $workbook->send($filename); + // Creating the first worksheet. + $myxls = $workbook->add_worksheet(get_string('modulenameplural', 'attendance')); + // Format types. + $formatbc = $workbook->add_format(); + $formatbc->set_bold(1); + + $myxls->write(0, 0, get_string('course'), $formatbc); + $myxls->write(0, 1, $data->course); + $myxls->write(1, 0, get_string('group'), $formatbc); + $myxls->write(1, 1, $data->group); + + $i = 3; + $j = 0; + foreach ($data->tabhead as $cell) { + // Merge cells if the heading would be empty (remarks column). + if (empty($cell)) { + $myxls->merge_cells($i, $j - 1, $i, $j); + } else { + $myxls->write($i, $j, $cell, $formatbc); + } + $j++; + } + $i++; + $j = 0; + foreach ($data->table as $row) { + foreach ($row as $cell) { + $myxls->write($i, $j++, $cell); + } + $i++; + $j = 0; + } + $workbook->close(); +} + +/** + * Generate csv for Attendance export + * + * @param stdclass $data The data for the report + * @param string $filename The name of the file + * + */ +function attendance_exporttocsv($data, $filename) { + $filename .= ".txt"; + + header("Content-Type: application/download\n"); + header("Content-Disposition: attachment; filename=\"$filename\""); + header("Expires: 0"); + header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); + header("Pragma: public"); + + echo get_string('course')."\t".$data->course."\n"; + echo get_string('group')."\t".$data->group."\n\n"; + + echo implode("\t", $data->tabhead)."\n"; + foreach ($data->table as $row) { + echo implode("\t", $row)."\n"; + } +} + +/** + * Get session data for form. + * @param stdClass $formdata moodleform - attendance form. + * @param mod_attendance_structure $att - used to get attendance level subnet. + * @return array. + */ +function attendance_construct_sessions_data_for_add($formdata, mod_attendance_structure $att) { + global $CFG; + + $sesstarttime = $formdata->sestime['starthour'] * HOURSECS + $formdata->sestime['startminute'] * MINSECS; + $sesendtime = $formdata->sestime['endhour'] * HOURSECS + $formdata->sestime['endminute'] * MINSECS; + $sessiondate = $formdata->sessiondate + $sesstarttime; + $duration = $sesendtime - $sesstarttime; + if (empty(get_config('attendance', 'enablewarnings'))) { + $absenteereport = get_config('attendance', 'absenteereport_default'); + } else { + $absenteereport = empty($formdata->absenteereport) ? 0 : 1; + } + + $now = time(); + + if (empty(get_config('attendance', 'studentscanmark'))) { + $formdata->studentscanmark = 0; + } + + $calendarevent = 0; + if (isset($formdata->calendarevent)) { // Calendar event should be created. + $calendarevent = 1; + } + + $sessions = array(); + if (isset($formdata->addmultiply)) { + $startdate = $sessiondate; + $enddate = $formdata->sessionenddate + DAYSECS; // Because enddate in 0:0am. + + if ($enddate < $startdate) { + return null; + } + + // Getting first day of week. + $sdate = $startdate; + $dinfo = usergetdate($sdate); + if ($CFG->calendar_startwday === '0') { // Week start from sunday. + $startweek = $startdate - $dinfo['wday'] * DAYSECS; // Call new variable. + } else { + $wday = $dinfo['wday'] === 0 ? 7 : $dinfo['wday']; + $startweek = $startdate - ($wday - 1) * DAYSECS; + } + + $wdaydesc = array(0 => 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'); + + while ($sdate < $enddate) { + if ($sdate < $startweek + WEEKSECS) { + $dinfo = usergetdate($sdate); + if (isset($formdata->sdays) && array_key_exists($wdaydesc[$dinfo['wday']], $formdata->sdays)) { + $sess = new stdClass(); + $sess->sessdate = make_timestamp($dinfo['year'], $dinfo['mon'], $dinfo['mday'], + $formdata->sestime['starthour'], $formdata->sestime['startminute']); + $sess->duration = $duration; + $sess->descriptionitemid = $formdata->sdescription['itemid']; + $sess->description = $formdata->sdescription['text']; + $sess->descriptionformat = $formdata->sdescription['format']; + $sess->calendarevent = $calendarevent; + $sess->timemodified = $now; + $sess->absenteereport = $absenteereport; + $sess->studentpassword = ''; + $sess->includeqrcode = 0; + $sess->rotateqrcode = 0; + $sess->rotateqrcodesecret = ''; + + if (!empty($formdata->usedefaultsubnet)) { + $sess->subnet = $att->subnet; + } else { + $sess->subnet = $formdata->subnet; + } + $sess->automark = $formdata->automark; + $sess->automarkcompleted = 0; + if (!empty($formdata->preventsharedip)) { + $sess->preventsharedip = $formdata->preventsharedip; + } + if (!empty($formdata->preventsharediptime)) { + $sess->preventsharediptime = $formdata->preventsharediptime; + } + + if (isset($formdata->studentscanmark)) { // Students will be able to mark their own attendance. + $sess->studentscanmark = 1; + if (isset($formdata->autoassignstatus)) { + $sess->autoassignstatus = 1; + } + + if (!empty($formdata->randompassword)) { + $sess->studentpassword = attendance_random_string(); + } else if (!empty($formdata->studentpassword)) { + $sess->studentpassword = $formdata->studentpassword; + } + if (!empty($formdata->includeqrcode)) { + $sess->includeqrcode = $formdata->includeqrcode; + } + if (!empty($formdata->rotateqrcode)) { + $sess->rotateqrcode = $formdata->rotateqrcode; + $sess->studentpassword = attendance_random_string(); + $sess->rotateqrcodesecret = attendance_random_string(); + } + if (!empty($formdata->preventsharedip)) { + $sess->preventsharedip = $formdata->preventsharedip; + } + if (!empty($formdata->preventsharediptime)) { + $sess->preventsharediptime = $formdata->preventsharediptime; + } + } else { + $sess->subnet = ''; + $sess->automark = 0; + $sess->automarkcompleted = 0; + $sess->preventsharedip = 0; + $sess->preventsharediptime = ''; + } + $sess->statusset = $formdata->statusset; + + attendance_fill_groupid($formdata, $sessions, $sess); + } + $sdate += DAYSECS; + } else { + $startweek += WEEKSECS * $formdata->period; + $sdate = $startweek; + } + } + } else { + $sess = new stdClass(); + $sess->sessdate = $sessiondate; + $sess->duration = $duration; + $sess->descriptionitemid = $formdata->sdescription['itemid']; + $sess->description = $formdata->sdescription['text']; + $sess->descriptionformat = $formdata->sdescription['format']; + $sess->calendarevent = $calendarevent; + $sess->timemodified = $now; + $sess->studentscanmark = 0; + $sess->autoassignstatus = 0; + $sess->subnet = ''; + $sess->studentpassword = ''; + $sess->automark = 0; + $sess->automarkcompleted = 0; + $sess->absenteereport = $absenteereport; + $sess->includeqrcode = 0; + $sess->rotateqrcode = 0; + $sess->rotateqrcodesecret = ''; + + if (!empty($formdata->usedefaultsubnet)) { + $sess->subnet = $att->subnet; + } else { + $sess->subnet = $formdata->subnet; + } + + if (!empty($formdata->automark)) { + $sess->automark = $formdata->automark; + } + if (!empty($formdata->preventsharedip)) { + $sess->preventsharedip = $formdata->preventsharedip; + } + if (!empty($formdata->preventsharediptime)) { + $sess->preventsharediptime = $formdata->preventsharediptime; + } + + if (isset($formdata->studentscanmark) && !empty($formdata->studentscanmark)) { + // Students will be able to mark their own attendance. + $sess->studentscanmark = 1; + if (isset($formdata->autoassignstatus) && !empty($formdata->autoassignstatus)) { + $sess->autoassignstatus = 1; + } + if (!empty($formdata->randompassword)) { + $sess->studentpassword = attendance_random_string(); + } else if (!empty($formdata->studentpassword)) { + $sess->studentpassword = $formdata->studentpassword; + } + if (!empty($formdata->includeqrcode)) { + $sess->includeqrcode = $formdata->includeqrcode; + } + if (!empty($formdata->rotateqrcode)) { + $sess->rotateqrcode = $formdata->rotateqrcode; + $sess->studentpassword = attendance_random_string(); + $sess->rotateqrcodesecret = attendance_random_string(); + } + if (!empty($formdata->usedefaultsubnet)) { + $sess->subnet = $att->subnet; + } else { + $sess->subnet = $formdata->subnet; + } + + if (!empty($formdata->automark)) { + $sess->automark = $formdata->automark; + } + if (!empty($formdata->preventsharedip)) { + $sess->preventsharedip = $formdata->preventsharedip; + } + if (!empty($formdata->preventsharediptime)) { + $sess->preventsharediptime = $formdata->preventsharediptime; + } + } + $sess->statusset = $formdata->statusset; + + attendance_fill_groupid($formdata, $sessions, $sess); + } + + return $sessions; +} + +/** + * Helper function for attendance_construct_sessions_data_for_add(). + * + * @param stdClass $formdata + * @param stdClass $sessions + * @param stdClass $sess + */ +function attendance_fill_groupid($formdata, &$sessions, $sess) { + if ($formdata->sessiontype == mod_attendance_structure::SESSION_COMMON) { + $sess = clone $sess; + $sess->groupid = 0; + $sessions[] = $sess; + } else { + foreach ($formdata->groups as $groupid) { + $sess = clone $sess; + $sess->groupid = $groupid; + $sessions[] = $sess; + } + } +} + +/** + * Generates a summary of points for the courses selected. + * + * @param array $courseids optional list of courses to return + * @param string $orderby - optional order by param + * @return stdClass + */ +function attendance_course_users_points($courseids = array(), $orderby = '') { + global $DB; + + $where = ''; + $params = array(); + $where .= ' AND ats.sessdate < :enddate '; + $params['enddate'] = time(); + + $joingroup = 'LEFT JOIN {groups_members} gm ON (gm.userid = atl.studentid AND gm.groupid = ats.groupid)'; + $where .= ' AND (ats.groupid = 0 or gm.id is NOT NULL)'; + + if (!empty($courseids)) { + list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); + $where .= ' AND c.id ' . $insql; + $params = array_merge($params, $inparams); + } + + $sql = "SELECT courseid, coursename, sum(points) / sum(maxpoints) as percentage FROM ( +SELECT a.id, a.course as courseid, c.fullname as coursename, atl.studentid AS userid, COUNT(DISTINCT ats.id) AS numtakensessions, + SUM(stg.grade) AS points, SUM(stm.maxgrade) AS maxpoints + FROM {attendance_sessions} ats + JOIN {attendance} a ON a.id = ats.attendanceid + JOIN {course} c ON c.id = a.course + JOIN {attendance_log} atl ON (atl.sessionid = ats.id) + JOIN {attendance_statuses} stg ON (stg.id = atl.statusid AND stg.deleted = 0 AND stg.visible = 1) + JOIN (SELECT attendanceid, setnumber, MAX(grade) AS maxgrade + FROM {attendance_statuses} + WHERE deleted = 0 + AND visible = 1 + GROUP BY attendanceid, setnumber) stm + ON (stm.setnumber = ats.statusset AND stm.attendanceid = ats.attendanceid) + {$joingroup} + WHERE ats.sessdate >= c.startdate + AND ats.lasttaken != 0 + {$where} + GROUP BY a.id, a.course, c.fullname, atl.studentid + ) p GROUP by courseid, coursename {$orderby}"; + + return $DB->get_records_sql($sql, $params); +} + +/** + * Generates a list of users flagged absent. + * + * @param array $courseids optional list of courses to return + * @param string $orderby how to order results. + * @param bool $allfornotify get notification list for scheduled task. + * @return stdClass + */ +function attendance_get_users_to_notify($courseids = array(), $orderby = '', $allfornotify = false) { + global $DB; + + $joingroup = 'LEFT JOIN {groups_members} gm ON (gm.userid = atl.studentid AND gm.groupid = ats.groupid)'; + $where = ' AND (ats.groupid = 0 or gm.id is NOT NULL)'; + $having = ''; + $params = array(); + + if (!empty($courseids)) { + list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); + $where .= ' AND c.id ' . $insql; + $params = array_merge($params, $inparams); + } + if ($allfornotify) { + // Exclude warnings that have already sent the max num. + $having .= ' AND n.maxwarn > COUNT(DISTINCT ns.id) '; + } + + $unames = get_all_user_name_fields(true); + $unames2 = get_all_user_name_fields(true, 'u'); + + $idfield = $DB->sql_concat('cm.id', 'atl.studentid', 'n.id'); + $sql = "SELECT {$idfield} as uniqueid, a.id as aid, {$unames2}, a.name as aname, cm.id as cmid, c.id as courseid, + c.fullname as coursename, atl.studentid AS userid, n.id as notifyid, n.warningpercent, n.emailsubject, + n.emailcontent, n.emailcontentformat, n.emailuser, n.thirdpartyemails, n.warnafter, n.maxwarn, + COUNT(DISTINCT ats.id) AS numtakensessions, SUM(stg.grade) AS points, SUM(stm.maxgrade) AS maxpoints, + COUNT(DISTINCT ns.id) as nscount, MAX(ns.timesent) as timesent, + SUM(stg.grade) / SUM(stm.maxgrade) AS percent + FROM {attendance_sessions} ats + JOIN {attendance} a ON a.id = ats.attendanceid + JOIN {course_modules} cm ON cm.instance = a.id + JOIN {course} c on c.id = cm.course + JOIN {modules} md ON md.id = cm.module AND md.name = 'attendance' + JOIN {attendance_log} atl ON (atl.sessionid = ats.id) + JOIN {user} u ON (u.id = atl.studentid) + JOIN {attendance_statuses} stg ON (stg.id = atl.statusid AND stg.deleted = 0 AND stg.visible = 1) + JOIN {attendance_warning} n ON n.idnumber = a.id + LEFT JOIN {attendance_warning_done} ns ON ns.notifyid = n.id AND ns.userid = atl.studentid + JOIN (SELECT attendanceid, setnumber, MAX(grade) AS maxgrade + FROM {attendance_statuses} + WHERE deleted = 0 + AND visible = 1 + GROUP BY attendanceid, setnumber) stm + ON (stm.setnumber = ats.statusset AND stm.attendanceid = ats.attendanceid) + {$joingroup} + WHERE ats.absenteereport = 1 {$where} + GROUP BY uniqueid, a.id, a.name, a.course, c.fullname, atl.studentid, n.id, n.warningpercent, + n.emailsubject, n.emailcontent, n.emailcontentformat, n.warnafter, n.maxwarn, + n.emailuser, n.thirdpartyemails, cm.id, c.id, {$unames2}, ns.userid + HAVING n.warnafter <= COUNT(DISTINCT ats.id) AND n.warningpercent > ((SUM(stg.grade) / SUM(stm.maxgrade)) * 100) + {$having} + {$orderby}"; + + if (!$allfornotify) { + $idfield = $DB->sql_concat('cmid', 'userid'); + // Only show one record per attendance for teacher reports. + $sql = "SELECT DISTINCT {$idfield} as id, {$unames}, aid, cmid, courseid, aname, coursename, userid, + numtakensessions, percent, MAX(timesent) as timesent + FROM ({$sql}) as m + GROUP BY id, aid, cmid, courseid, aname, userid, numtakensessions, + percent, coursename, {$unames} {$orderby}"; + } + + return $DB->get_records_sql($sql, $params); + +} + +/** + * Template variables into place in supplied email content. + * + * @param object $record db record of details + * @return array - the content of the fields after templating. + */ +function attendance_template_variables($record) { + $templatevars = array( + '/%coursename%/' => $record->coursename, + '/%courseid%/' => $record->courseid, + '/%userfirstname%/' => $record->firstname, + '/%userlastname%/' => $record->lastname, + '/%userid%/' => $record->userid, + '/%warningpercent%/' => $record->warningpercent, + '/%attendancename%/' => $record->aname, + '/%cmid%/' => $record->cmid, + '/%numtakensessions%/' => $record->numtakensessions, + '/%points%/' => $record->points, + '/%maxpoints%/' => $record->maxpoints, + '/%percent%/' => $record->percent, + ); + $extrauserfields = get_all_user_name_fields(); + foreach ($extrauserfields as $extra) { + $templatevars['/%'.$extra.'%/'] = $record->$extra; + } + $patterns = array_keys($templatevars); // The placeholders which are to be replaced. + $replacements = array_values($templatevars); // The values which are to be templated in for the placeholders. + // Array to describe which fields in reengagement object should have a template replacement. + $replacementfields = array('emailsubject', 'emailcontent'); + + // Replace %variable% with relevant value everywhere it occurs in reengagement->field. + foreach ($replacementfields as $field) { + $record->$field = preg_replace($patterns, $replacements, $record->$field); + } + return $record; +} + +/** + * Find highest available status for a user. + * + * @param mod_attendance_structure $att attendance structure + * @param stdclass $attforsession attendance_session record. + * @param int $scantime - time that session should be recorded against. + * @return bool/int + */ +function attendance_session_get_highest_status(mod_attendance_structure $att, $attforsession, $scantime = null) { + // Find the status to set here. + $statuses = $att->get_statuses(); + $highestavailablegrade = 0; + $highestavailablestatus = new stdClass(); + // Override time used in status recording. + $scantime = empty($scantime) ? time() : $scantime; + foreach ($statuses as $status) { + if ($status->studentavailability === '0') { + // This status is never available to students. + continue; + } + if (!empty($status->studentavailability)) { + $toolateforstatus = (($attforsession->sessdate + ($status->studentavailability * 60)) < $scantime); + if ($toolateforstatus) { + continue; + } + } + // This status is available to the student. + if ($status->grade >= $highestavailablegrade) { + // This is the most favourable grade so far; save it. + $highestavailablegrade = $status->grade; + $highestavailablestatus = $status; + } + } + if (empty($highestavailablestatus)) { + return false; + } + return $highestavailablestatus->id; +} + +/** + * Get available automark options. + * + * @return array + */ +function attendance_get_automarkoptions() { + $options = array(); + $options[ATTENDANCE_AUTOMARK_DISABLED] = get_string('noautomark', 'attendance'); + if (strpos(get_config('tool_log', 'enabled_stores'), 'logstore_standard') !== false) { + $options[ATTENDANCE_AUTOMARK_ALL] = get_string('automarkall', 'attendance'); + } + $options[ATTENDANCE_AUTOMARK_CLOSE] = get_string('automarkclose', 'attendance'); + return $options; +} + +/** + * Get available sharedip options. + * + * @return array + */ +function attendance_get_sharedipoptions() { + $options = array(); + $options[ATTENDANCE_SHAREDIP_DISABLED] = get_string('no'); + $options[ATTENDANCE_SHAREDIP_FORCE] = get_string('yes'); + $options[ATTENDANCE_SHAREDIP_MINUTES] = get_string('setperiod', 'attendance'); + + return $options; +} + +/** + * Used to print simple time - 1am instead of 1:00am. + * + * @param int $time - unix timestamp. + */ +function attendance_strftimehm($time) { + $mins = userdate($time, '%M'); + + if ($mins == '00') { + $format = get_string('strftimeh', 'attendance'); + } else { + $format = get_string('strftimehm', 'attendance'); + } + + $userdate = userdate($time, $format); + + // Some Lang packs use %p to suffix with AM/PM but not all strftime support this. + // Check if %p is in use and make sure it's being respected. + if (stripos($format, '%p')) { + // Check if $userdate did something with %p by checking userdate against the same format without %p. + $formatwithoutp = str_ireplace('%p', '', $format); + if (userdate($time, $formatwithoutp) == $userdate) { + // The date is the same with and without %p - we have a problem. + if (userdate($time, '%H') > 11) { + $userdate .= 'pm'; + } else { + $userdate .= 'am'; + } + } + // Some locales and O/S don't respect correct intended case of %p vs %P + // This can cause problems with behat which expects AM vs am. + if (strpos($format, '%p')) { // Should be upper case according to PHP spec. + $userdate = str_replace('am', 'AM', $userdate); + $userdate = str_replace('pm', 'PM', $userdate); + } + } + + return $userdate; +} + +/** + * Used to print simple time - 1am instead of 1:00am. + * + * @param int $datetime - unix timestamp. + * @param int $duration - number of seconds. + */ +function attendance_construct_session_time($datetime, $duration) { + $starttime = attendance_strftimehm($datetime); + $endtime = attendance_strftimehm($datetime + $duration); + + return $starttime . ($duration > 0 ? ' - ' . $endtime : ''); +} + +/** + * Used to print session time. + * + * @param int $datetime - unix timestamp. + * @param int $duration - number of seconds duration. + * @return string. + */ +function construct_session_full_date_time($datetime, $duration) { + $sessinfo = userdate($datetime, get_string('strftimedmyw', 'attendance')); + $sessinfo .= ' '.attendance_construct_session_time($datetime, $duration); + + return $sessinfo; +} + +/** + * Render the session password. + * + * @param stdClass $session + */ +function attendance_renderpassword($session) { + echo html_writer::tag('h2', get_string('passwordgrp', 'attendance')); + echo html_writer::span($session->studentpassword, 'student-password'); +} + +/** + * Render the session QR code. + * + * @param stdClass $session + */ +function attendance_renderqrcode($session) { + global $CFG; + + if (strlen($session->studentpassword) > 0) { + $qrcodeurl = $CFG->wwwroot . '/mod/attendance/attendance.php?qrpass=' . + $session->studentpassword . '&sessid=' . $session->id; + } else { + $qrcodeurl = $CFG->wwwroot . '/mod/attendance/attendance.php?sessid=' . $session->id; + } + + echo html_writer::tag('h3', get_string('qrcode', 'attendance')); + + $barcode = new TCPDF2DBarcode($qrcodeurl, 'QRCODE'); + $image = $barcode->getBarcodePngData(15, 15); + echo html_writer::img('data:image/png;base64,' . base64_encode($image), get_string('qrcode', 'attendance')); +} + +/** + * Generate QR code passwords. + * + * @param stdClass $session + */ +function attendance_generate_passwords($session) { + global $DB; + $attconfig = get_config('attendance'); + $password = array(); + + for ($i = 0; $i < 30; $i++) { + array_push($password, array("attendanceid" => $session->id, + "password" => mt_rand(1000, 10000), "expirytime" => time() + ($attconfig->rotateqrcodeinterval * $i))); + } + + $DB->insert_records('attendance_rotate_passwords', $password); +} + +/** + * Render JS for rotate QR code passwords. + * + * @param stdClass $session + */ +function attendance_renderqrcoderotate($session) { + // Load required js. + echo html_writer::tag('script', '', + [ + 'src' => 'js/qrcode/qrcode.min.js', + 'type' => 'text/javascript' + ] + ); + echo html_writer::tag('script', '', + [ + 'src' => 'js/password/attendance_QRCodeRotate.js', + 'type' => 'text/javascript' + ] + ); + echo html_writer::tag('div', '', ['id' => 'rotate-time']); // Div to display timer. + echo html_writer::tag('h3', get_string('passwordgrp', 'attendance')); + echo html_writer::tag('div', '', ['id' => 'text-password']); // Div to display password. + echo html_writer::tag('h3', get_string('qrcode', 'attendance')); + echo html_writer::tag('div', '', ['id' => 'qrcode']); // Div to display qr code. + // Js to start the password manager. + echo ' + <script type="text/javascript"> + let qrCodeRotate = new attendance_QRCodeRotate(); + qrCodeRotate.start(' . $session->id . ', document.getElementById("qrcode"), document.getElementById("text-password"), + document.getElementById("rotate-time")); + </script>'; +} + +/** + * Return QR code passwords. + * + * @param stdClass $session + */ +function attendance_return_passwords($session) { + global $DB; + + $sql = 'SELECT * FROM {attendance_rotate_passwords} WHERE attendanceid = ? AND expirytime > ? ORDER BY expirytime ASC'; + return json_encode($DB->get_records_sql($sql, ['attendanceid' => $session->id, time()], $strictness = IGNORE_MISSING)); +} diff --git a/mod/attendance/manage.php b/mod/attendance/manage.php new file mode 100644 index 0000000..468cdfe --- /dev/null +++ b/mod/attendance/manage.php @@ -0,0 +1,100 @@ +<?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/>. + +/** + * Manage attendance sessions + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); + +$pageparams = new mod_attendance_manage_page_params(); + +$id = required_param('id', PARAM_INT); +$from = optional_param('from', null, PARAM_ALPHANUMEXT); +$pageparams->view = optional_param('view', null, PARAM_INT); +$pageparams->curdate = optional_param('curdate', null, PARAM_INT); +$pageparams->perpage = get_config('attendance', 'resultsperpage'); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); +$capabilities = array( + 'mod/attendance:manageattendances', + 'mod/attendance:takeattendances', + 'mod/attendance:changeattendances' +); +if (!has_any_capability($capabilities, $context)) { + $url = new moodle_url('/mod/attendance/view.php', array('id' => $cm->id)); + redirect($url); +} + +$pageparams->init($cm); +$att = new mod_attendance_structure($att, $cm, $course, $context, $pageparams); + +// If teacher is coming from block, then check for a session exists for today. +if ($from === 'block') { + $sessions = $att->get_today_sessions(); + $size = count($sessions); + if ($size == 1) { + $sess = reset($sessions); + $nottaken = !$sess->lasttaken && has_capability('mod/attendance:takeattendances', $context); + $canchange = $sess->lasttaken && has_capability('mod/attendance:changeattendances', $context); + if ($nottaken || $canchange) { + redirect($att->url_take(array('sessionid' => $sess->id, 'grouptype' => $sess->groupid))); + } + } else if ($size > 1) { + $att->curdate = $today; + // Temporarily set $view for single access to page from block. + $att->view = ATT_VIEW_DAYS; + } +} + +$PAGE->set_url($att->url_manage()); +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->force_settings_menu(true); +$PAGE->navbar->add($att->name); + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_SESSIONS); +$filtercontrols = new attendance_filter_controls($att); +$sesstable = new attendance_manage_data($att); + + +$title = get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname); +$header = new mod_attendance_header($att, $title); + +// Output starts here. + +echo $output->header(); +echo $output->render($header); +mod_attendance_notifyqueue::show(); +echo $output->render($tabs); +echo $output->render($filtercontrols); +echo $output->render($sesstable); + +echo $output->footer(); + diff --git a/mod/attendance/message.html b/mod/attendance/message.html new file mode 100644 index 0000000..86fc3ab --- /dev/null +++ b/mod/attendance/message.html @@ -0,0 +1,66 @@ +<form id="theform" method="post" action="messageselect.php"> + <input type="hidden" name="id" value="<?php p($id) ?>" /> + <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" /> + <input type="hidden" name="returnto" value="<?php p($returnto) ?>" /> + <input type="hidden" name="deluser" value="" /> + <?php echo $OUTPUT->box_start(); ?> + <table border="0" cellpadding="5"> + <tr valign="top"> + <td align="right"><b> + <?php print_string("messagebody"); ?>: + </b></td> + <td align="left"> + <?php echo $OUTPUT->print_textarea("messagebody", "edit-messagebody", $messagebody, 15, 65); ?> + </td> + </tr> + + <tr valign="top"> + <td align="right"><label for="menuformat"> + <b><?php print_string("formattexttype", 'mod_attendance'); ?>:</b></label> + </td> + <td> + <?php + print_string('formathtml'); + echo '<input type="hidden" name="format" value="'.FORMAT_HTML.'" />'; + ?> + </td> + </tr> + + <tr><td align="center" colspan="2"> + <input type="submit" name="send" value="<?php print_string('sendmessage', 'message'); ?>" /> + <input type="submit" name="preview" value="<?php print_string('preview'); ?>" /> + </td></tr> + </table> + <?php echo $OUTPUT->box_end(); ?> + <table align="center"> + <tr> + <th colspan="4" scope="row"><?php print_string('currentlyselectedusers', 'mod_attendance'); ?></th> + </tr> + <?php + if (count($SESSION->emailto[$id])) { + foreach ($SESSION->emailto[$id] as $user) { + echo '<tr><td>'.fullname($user,true).'</td>'; + // Check to see if we should be showing the email address. + if ($user->maildisplay == 0) { // 0 = don't display my email to anyone. + echo '<td>' . get_string('emaildisplayhidden') . '</td><td>'; + } else { + echo '<td>'.$user->email.'</td><td>'; + } + if (empty($user->email)) { + $error = get_string('emailempty'); + } + if (!empty($error)) { + echo $OUTPUT->pix_icon('t/emailno', $error); + unset($error); + } + echo '</td><td> + <input type="submit" onClick="this.form.deluser.value='.$user->id.';" value="' . get_string('remove') . '" /> + </td></tr>'; + } + } + else { + echo '<tr><td colspan="3" align="center">'.get_string('nousersyet').'</td></tr>'; + } + ?> + </table> +</form> diff --git a/mod/attendance/messageselect.php b/mod/attendance/messageselect.php new file mode 100644 index 0000000..99d712a --- /dev/null +++ b/mod/attendance/messageselect.php @@ -0,0 +1,183 @@ +<?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/>. + +/** + * Copied from Moodle 3.3 messageselect.php - allows sending messages to multiple users. + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_attendance + */ +require_once('../../config.php'); +require_once($CFG->dirroot.'/message/lib.php'); +$id = required_param('id', PARAM_INT); +$messagebody = optional_param_array('messagebody', '', PARAM_CLEANHTML); + +$send = optional_param('send', '', PARAM_BOOL); +$preview = optional_param('preview', '', PARAM_BOOL); +$edit = optional_param('edit', '', PARAM_BOOL); +$returnto = optional_param('returnto', '', PARAM_LOCALURL); +$format = optional_param('format', FORMAT_MOODLE, PARAM_INT); +$deluser = optional_param('deluser', 0, PARAM_INT); +$url = new moodle_url('/user/messageselect.php', array('id' => $id)); + +if ($send !== '') { + $url->param('send', $send); +} +if ($preview !== '') { + $url->param('preview', $preview); +} +if ($edit !== '') { + $url->param('edit', $edit); +} +if ($returnto !== '') { + $url->param('returnto', $returnto); +} +if ($format !== FORMAT_MOODLE) { + $url->param('format', $format); +} +if ($deluser !== 0) { + $url->param('deluser', $deluser); +} +if (!empty($messagebody['text'])) { + $messagebody = $messagebody['text']; +} +$PAGE->set_url($url); +if (!$course = $DB->get_record('course', array('id' => $id))) { + print_error('invalidcourseid'); +} +require_login($course); +$coursecontext = context_course::instance($id); // Course context. +$systemcontext = context_system::instance(); // SYSTEM context. +require_capability('moodle/course:bulkmessaging', $coursecontext); +if (empty($SESSION->emailto)) { + $SESSION->emailto = array(); +} +if (!array_key_exists($id, $SESSION->emailto)) { + $SESSION->emailto[$id] = array(); +} +if ($deluser) { + if (array_key_exists($id, $SESSION->emailto) && array_key_exists($deluser, $SESSION->emailto[$id])) { + unset($SESSION->emailto[$id][$deluser]); + } +} +if (empty($SESSION->emailselect[$id]) || $messagebody) { + $SESSION->emailselect[$id] = array('messagebody' => $messagebody); +} +$messagebody = $SESSION->emailselect[$id]['messagebody']; +$count = 0; +if ($data = data_submitted()) { + require_sesskey(); + $namefields = get_all_user_name_fields(true); + foreach ($data as $k => $v) { + if (preg_match('/^(user|teacher)(\d+)$/', $k, $m)) { + if (!array_key_exists($m[2], $SESSION->emailto[$id])) { + if ($user = $DB->get_record_select('user', "id = ?", array($m[2]), 'id, '. + $namefields . ', idnumber, email, mailformat, lastaccess, lang, '. + 'maildisplay, auth, suspended, deleted, emailstop, username')) { + $SESSION->emailto[$id][$m[2]] = $user; + $count++; + } + } + } + } +} +if ($course->id == SITEID) { + $strtitle = get_string('sitemessage'); + $PAGE->set_pagelayout('admin'); +} else { + $strtitle = get_string('coursemessage', 'mod_attendance'); + $PAGE->set_pagelayout('incourse'); +} +$link = null; +if (has_capability('moodle/course:viewparticipants', $coursecontext) || + has_capability('moodle/site:viewparticipants', $systemcontext)) { + $link = new moodle_url("/user/index.php", array('id' => $course->id)); +} +$PAGE->navbar->add(get_string('participants'), $link); +$PAGE->navbar->add($strtitle); +$PAGE->set_title($strtitle); +$PAGE->set_heading($strtitle); +echo $OUTPUT->header(); + +if ($count) { + if ($count == 1) { + $heading = get_string('addedrecip', 'mod_attendance', $count); + } else { + $heading = get_string('addedrecips', 'mod_attendance', $count); + } + echo $OUTPUT->heading($heading); +} +if (!empty($messagebody) && !$edit && !$deluser && ($preview || $send)) { + require_sesskey(); + if (count($SESSION->emailto[$id])) { + if (!empty($preview)) { + echo '<form method="post" action="messageselect.php" style="margin: 0 20px;"> +<input type="hidden" name="returnto" value="'.s($returnto).'" /> +<input type="hidden" name="id" value="'.$id.'" /> +<input type="hidden" name="format" value="'.$format.'" /> +<input type="hidden" name="sesskey" value="' . sesskey() . '" /> +'; + echo "<h3>".get_string('previewhtml', 'mod_attendance')."</h3>"; + echo "<div class=\"messagepreview\">\n".format_text($messagebody, $format)."\n</div>\n"; + echo '<p align="center"><input type="submit" name="send" value="'.get_string('sendmessage', 'message').'" />'."\n"; + echo '<input type="submit" name="edit" value="'.get_string('update').'" /></p>'; + echo "\n</form>"; + } else if (!empty($send)) { + $fails = array(); + foreach ($SESSION->emailto[$id] as $user) { + if (!message_post_message($USER, $user, $messagebody, $format)) { + $user->fullname = fullname($user); + $fails[] = get_string('messagedselecteduserfailed', 'moodle', $user); + }; + } + if (empty($fails)) { + echo $OUTPUT->heading(get_string('messagedselectedusers')); + unset($SESSION->emailto[$id]); + unset($SESSION->emailselect[$id]); + } else { + echo $OUTPUT->heading(get_string('messagedselectedcountusersfailed', 'moodle', count($fails))); + echo '<ul>'; + foreach ($fails as $f) { + echo '<li>', $f, '</li>'; + } + echo '</ul>'; + } + echo '<p align="center"><a href="index.php?id='.$id.'">'.get_string('backtoparticipants', 'mod_attendance').'</a></p>'; + } + echo $OUTPUT->footer(); + exit; + } else { + echo $OUTPUT->notification(get_string('nousersyet')); + } +} +echo '<p align="center"><a href="'.$returnto.'">'.get_string("keepsearching", 'mod_attendance').'</a>'. + ((count($SESSION->emailto[$id])) ? ', '.get_string('usemessageform', 'mod_attendance') : '').'</p>'; +if ((!empty($send) || !empty($preview) || !empty($edit)) && (empty($messagebody))) { + echo $OUTPUT->notification(get_string('allfieldsrequired')); +} +if (count($SESSION->emailto[$id])) { + require_sesskey(); + require("message.html"); +} +$PAGE->requires->yui_module('moodle-core-formchangechecker', + 'M.core_formchangechecker.init', + array(array( + 'formid' => 'theform' + )) +); +$PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle'); +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/attendance/mobilestyles.css b/mod/attendance/mobilestyles.css new file mode 100644 index 0000000..93c150f --- /dev/null +++ b/mod/attendance/mobilestyles.css @@ -0,0 +1,30 @@ +.attendance_mobile_teacher_form .item-radio { + display: inline-block; + margin-top: 10px; + margin-left: 5px; + padding: 0; + width: 70px; +} +.attendance_mobile_teacher_form .item-inner { + padding: 0; +} +.attendance_mobile_teacher_form .radiolabel .item-inner { + text-align: center; +} +.attendance_mobile_teacher_form .item-inner .input-wrapper label, +.attendance_mobile_teacher_form .radio { + margin: 0; +} +.attendance_mobile_teacher_form .radio .radio-icon { + display: none; +} + +.attendance_mobile_teacher_form .messages .label, +.attendance_mobile_user_form .messages .label, +.attendance_mobile_view_page .messages .label { + white-space: normal; +} + +.attendance_mobile_teacher_form .attendance_user_row { + padding-bottom: 5px; +} \ No newline at end of file diff --git a/mod/attendance/mod_form.php b/mod/attendance/mod_form.php new file mode 100644 index 0000000..6d6b78d --- /dev/null +++ b/mod/attendance/mod_form.php @@ -0,0 +1,76 @@ +<?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/>. + +/** + * Forms for updating/adding attendance + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot.'/course/moodleform_mod.php'); + +/** + * class for displaying add/update form. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_mod_form extends moodleform_mod { + + /** + * Called to define this moodle form + * + * @return void + */ + public function definition() { + $attendanceconfig = get_config('attendance'); + if (!isset($attendanceconfig->subnet)) { + $attendanceconfig->subnet = ''; + } + $mform =& $this->_form; + + $mform->addElement('header', 'general', get_string('general', 'form')); + + $mform->addElement('text', 'name', get_string('name'), array('size' => '64')); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->setDefault('name', get_string('modulename', 'attendance')); + + $this->standard_intro_elements(); + + // Grade settings. + $this->standard_grading_coursemodule_elements(); + + $this->standard_coursemodule_elements(true); + + // IP address. + if (get_config('attendance', 'subnetactivitylevel')) { + $mform->addElement('header', 'security', get_string('extrarestrictions', 'attendance')); + $mform->addElement('text', 'subnet', get_string('defaultsubnet', 'attendance'), array('size' => '164')); + $mform->setType('subnet', PARAM_TEXT); + $mform->addHelpButton('subnet', 'defaultsubnet', 'attendance'); + $mform->setDefault('subnet', $attendanceconfig->subnet); + } else { + $mform->addElement('hidden', 'subnet', ''); + $mform->setType('subnet', PARAM_TEXT); + } + + $this->add_action_buttons(); + } +} \ No newline at end of file diff --git a/mod/attendance/module.js b/mod/attendance/module.js new file mode 100644 index 0000000..a2d51e8 --- /dev/null +++ b/mod/attendance/module.js @@ -0,0 +1,30 @@ +M.mod_attendance = {}; // eslint-disable-line camelcase + +M.mod_attendance.init_manage = function(Y) { // eslint-disable-line camelcase + + Y.on('click', function(e) { + if (e.target.get('checked')) { + Y.all('input.attendancesesscheckbox').each(function() { + this.set('checked', 'checked'); + }); + } else { + Y.all('input.attendancesesscheckbox').each(function() { + this.set('checked', ''); + }); + } + }, '#cb_selector'); +}; + +M.mod_attendance.set_preferences_action = function(action) { // eslint-disable-line camelcase + var item = document.getElementById('preferencesaction'); + if (item) { + item.setAttribute('value', action); + } else { + item = document.getElementById('preferencesform'); + var input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", "action"); + input.setAttribute("value", action); + item.appendChild(input); + } +}; \ No newline at end of file diff --git a/mod/attendance/password.php b/mod/attendance/password.php new file mode 100644 index 0000000..45edda0 --- /dev/null +++ b/mod/attendance/password.php @@ -0,0 +1,76 @@ +<?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/>. + +/** + * Displays help via AJAX call or in a new page + * + * Use {@see core_renderer::help_icon()} or {@see addHelpButton()} to display + * the help icon. + * + * @copyright 2017 Dan Marsden + * @package mod_attendance + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); +require_once($CFG->libdir.'/tcpdf/tcpdf_barcodes_2d.php'); // Used for generating qrcode. + +$session = required_param('session', PARAM_INT); +$session = $DB->get_record('attendance_sessions', array('id' => $session), '*', MUST_EXIST); + +$cm = get_coursemodule_from_instance('attendance', $session->attendanceid); +require_login($cm->course, $cm); + +$context = context_module::instance($cm->id); +$capabilities = array('mod/attendance:manageattendances', 'mod/attendance:takeattendances', 'mod/attendance:changeattendances'); +if (!has_any_capability($capabilities, $context)) { + exit; +} + +if (optional_param('returnpasswords', 0, PARAM_INT) == 1) { + header('Content-Type: application/json'); + echo attendance_return_passwords($session); + exit; +} + +$PAGE->set_url('/mod/attendance/password.php'); +$PAGE->set_pagelayout('popup'); + +$PAGE->set_context(context_system::instance()); + +$PAGE->set_title(get_string('password', 'attendance')); + +echo $OUTPUT->header(); + +$showpassword = (isset($session->studentpassword) && strlen($session->studentpassword) > 0); +$showqr = (isset($session->includeqrcode) && $session->includeqrcode == 1); +$rotateqr = (isset($session->rotateqrcode) && $session->rotateqrcode == 1); + +if ($showpassword && !$rotateqr) { + attendance_renderpassword($session); +} + +if ($showqr) { + attendance_renderqrcode($session); +} + +if ($rotateqr) { + attendance_generate_passwords($session); + attendance_renderqrcoderotate($session); +} + +echo $OUTPUT->footer(); diff --git a/mod/attendance/password_ajax.php b/mod/attendance/password_ajax.php new file mode 100644 index 0000000..de6c9a3 --- /dev/null +++ b/mod/attendance/password_ajax.php @@ -0,0 +1,57 @@ +<?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/>. + +/** + * Displays help via AJAX call or in a new page + * + * Use {@see core_renderer::help_icon()} or {@see addHelpButton()} to display + * the help icon. + * + * @copyright 2017 Dan Marsden + * @package mod_attendance + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define('AJAX_SCRIPT', true); +require_once(dirname(__FILE__).'/../../config.php'); + +$session = required_param('session', PARAM_INT); +$session = $DB->get_record('attendance_sessions', array('id' => $session), '*', MUST_EXIST); + +$cm = get_coursemodule_from_instance('attendance', $session->attendanceid); +require_login($cm->course, $cm); + +$context = context_module::instance($cm->id); +$capabilities = array('mod/attendance:manageattendances', 'mod/attendance:takeattendances', 'mod/attendance:changeattendances'); +if (!has_any_capability($capabilities, $context)) { + exit; +} + +$PAGE->set_url('/mod/attendance/password.php'); +$PAGE->set_pagelayout('popup'); + +$PAGE->set_context(context_system::instance()); + +$data->heading = get_string('passwordgrp', 'attendance'); +if (isset($session->includeqrcode) && $session->includeqrcode == 1) { + $studentattendancepage = '/mod/attendance/password.php?session=' . $session->id; + $data->text = html_writer::tag('p', html_writer::span($session->studentpassword, 'student-password') . + html_writer::empty_tag('br') . + html_writer::link($CFG->wwwroot . $studentattendancepage, get_string('showqrcode', 'attendance'))); +} else { + $data->text = html_writer::span($session->studentpassword, 'student-password'); +} + +echo json_encode($data); diff --git a/mod/attendance/pix/ghost.png b/mod/attendance/pix/ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..b199efa48aee951d390b92d90730c748d6d893f3 GIT binary patch literal 48062 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%0DF_wxhdGMy;l=p;7lhY(Z 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 Roq>UY!PC{xWt~$(69Bb2-0}bb literal 0 HcmV?d00001 diff --git a/mod/attendance/pix/icon.gif b/mod/attendance/pix/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..5157566d1f8d9b5145bc8d0bb5ed8486fccacdd0 GIT binary patch literal 125 zcmZ?wbhEHb6krfwSj52a|Nnmm28JCwcAS}+R#a5P00W9YSr|cV9S{Lh%fRf!A+YA3 zUXSF0satNIc(x*1f#b$<*T*NOEKpjtYWuE&U8lT#g;;j4(UQymR>9h$A5>`Svr^AD Tz58&1mbs<ox9v$P3=Gx)l0_=N literal 0 HcmV?d00001 diff --git a/mod/attendance/pix/icon.png b/mod/attendance/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b622f89202d6b87130b0e330d3bb8ca9dea10bd5 GIT binary patch literal 1475 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo*pj^6T^Rm@;DWu&Co?cG za29w(7BevLUI$@DCym(^3=9nHC7!;n?2kBk8T6F4xjyn`U|<dKba4#vIKFhMcTUVz z*<<zJ4fD^(oIjl0KY!!gBRedny<MTS$mYr_tsIT1n-VYT1qz6SI%H1uQqYeI2n=)) zSDCFBwdzvXqM)T*TSY~iTn>8l1~q!N%xw{<eskn;<=u~WcCIh>6IaVyW0tpYe#y4~ z_kZ@C`~Kg=?y`3Jro%xer(djK_|RaU)b+Uk{_QoN>pgZ`+Dy51Xx}~k>BoNWZ(8UY zdDG_n@zweIzfSC~uQ;Ffx|^wl!QLS5?P<TM$JqM{4o4=GzSgZOJ}mQQcYf9j;lEEl zP2S5NTeW8Uk5-vIU$#H8n6<6u{g?7BZW}tJuQ7C7Px#VrW@vq&L;b_sRr*Z@b&pb> z9zFk5Val^w!_CpgK6AHi?-UTarP#bbZlU$0<rbFC>*{Abx-xOaOppBqA0y6Bp3k1J zD&TeZ(n~uU_u9(5u)M91{KRAFq>lc3&Yq4l&Tp*opK?0v!>P{vX#GVix0f8;e1G@- zxO<ly4@JEGp=ZVB#ML5F5}PquPh{d#?GpwnS48sVZY?mH_i)|L_gVKX{={UZy_wj) zb;Z^XpI#n1xR-HZ<+iJN+UL0qw(7Ykbc8zdIJ=k4(onUm54iO1bz;?@*(Qg$V{-p> zR$NL+`|@y($ecUEAMQAZPfK0#|Ldt&LOY*GczKxUGUc(ixM-eqSC(45RQbvr*Qj?| zbuS7Qm+~EMsWADqRe1T@wZiFZPIVezTN&;jeY5`eGL5${r@qc^eB7dv<QnDK$iqBk zS@2sX^`uF@(=$reDYm$9bUv@16J6CD^iLpuZl&7SuQMOFb{=H!uZ((gcm6!hCFP9I z_LshF61deqHA>XE`;yIsWA|1pR5vbJ#5GqUgUP<6oMEch)R^ma-|kPEo0QkWy!7|- zFAdMj8SekTTJq}0`*#+ihL4Jm)`;D{o05BONz`^hw<#{{O#wn(0!vL=L@M_6KmO74 z{r=Ia*R?YB!sZ(^Z>LLY_#{cabbo&T7|Vs;6@T3ArdyZ&xWJgR{!G|%KWniKW#w(Z z+I+XmtKVwujheY6(uJe*@edc@?&!_REh-$b+nRXSgzOjn&i0Pwf^u5Q{|f<girzY# z&Au++v>>K2Dpw&!{x1KFJ&%ejQ#*ZX1eW+c;7)qBe^c@6k502ImT)g!9mf#NG1p~F zw@V$9uSe4RSoNRJ)R)vcZZq#H**;_UiX8?M7R5xUPnKMiS>N-z%KGNby|%nodF>9f zQW-9nKAc+~Cb{!YO3_9yJ08sx-$|LRML83iGz|7!d*~L=yLvajaZ<qQZ98Ym>Th8* z_`+ehcedB#nAlq{QyCtopFMW8lYiCbWa;Rg2lYJrSp}Iw1R8>N2u^P8l<7{6Jm%j1 z(PgUNjUy8zZ_PFP+gf|$oK%qKr<I0USJ|$cU#qjLFL|lYux!eoV_u%hqSHC#XZ_oD z^}?0NJNM7BPqs9wy?dtbi{L7;(A_;jlPa{9?Ahm+ztF|#QnT=t&X<<c*6Xbiz8Zfc zvdn3P{gs>*H*e3J+_o%s-s!$NtGT~}mQC^W;eBqXY&Z3qknH(;mD!EFQ9WL3z09PK zUP!tU%^MT{=cl^6{>|M|Z<o!h&8_lVv_CI3iQU~@XN}Jv`*-5!xp#!EyLhX}vgQTf zs&hvB3M5XQE2=B>+<I&7_0}u(e=aF^i<dLb`}Oo;_JIp;e}^ud-Q?k76miPfC}6?M zxaIpo*%an#1bDn(9F$vFn(AB@^M3oS|3Olp_wV0)n)8dgN$mf?mDf-8@T7ip?XFtg zBVJY}-Xo_tTW@0L_kwTl!?!=)GksoN?zfE7&qMs}0wrW$stKAix*oc5?%Z1yL5-!G rmp9A*N!n&_@YVm}{`e<lztj(y&U0HbRh@}}fq}u()z4*}Q$iB}=C#dG literal 0 HcmV?d00001 diff --git a/mod/attendance/pix/icon.svg b/mod/attendance/pix/icon.svg new file mode 100644 index 0000000..8c58a21 --- /dev/null +++ b/mod/attendance/pix/icon.svg @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" + id="svg2" sodipodi:docname="Group_Choice.svg" inkscape:version="0.48.4 r9939" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px" + viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"> +<sodipodi:namedview bordercolor="#666666" borderopacity="1" id="namedview73" inkscape:window-width="1440" objecttolerance="10" gridtolerance="10" guidetolerance="10" pagecolor="#ffffff" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="878" showgrid="false" inkscape:zoom="9.8333333" inkscape:cx="-4.1694915" inkscape:cy="12" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg2"> + </sodipodi:namedview> +<g> + + <linearGradient id="path32_3_" gradientUnits="userSpaceOnUse" x1="-19.9995" y1="-817" x2="-19.9995" y2="-833.0005" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#90C50E"/> + <stop offset="1" style="stop-color:#70A034"/> + </linearGradient> + <path id="path32_1_" fill="url(#path32_3_)" d="M11,16v-3.4l-3-1.5c-0.5-0.3-0.6-0.8-0.301-1.199c0,0,1.6-2,1.6-4.2 + C9.299,2.5,7.4,0,4.9,0C2.5,0,0.5,2.6,0.5,5.7c0,2.1,1.6,4.2,1.6,4.2c0.301,0.399,0.199,1-0.301,1.3L0,12.444c0,0,0,2.955,0,3.556 + H11z"/> + + <linearGradient id="path39_3_" gradientUnits="userSpaceOnUse" x1="-19.9995" y1="-818" x2="-19.9995" y2="-832.0147" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#D9F991"/> + <stop offset="1" style="stop-color:#B1DD4B"/> + </linearGradient> + <path id="path39_1_" fill="url(#path39_3_)" d="M0.9,13l1.6-1c0.5-0.3,0.799-0.7,1-1.2c0.1-0.5,0-1.1-0.4-1.5c0,0-1.4-1.8-1.4-3.6 + C1.7,3.1,3.2,1,5.1,1s3.4,2.1,3.4,4.7c0,1.8-1.4,3.6-1.4,3.6c-0.301,0.4-0.5,1-0.4,1.5c0.1,0.5,0.5,1,1,1.2l2.4,1.201V15H0.9V13z" + /> + + <linearGradient id="path46_3_" gradientUnits="userSpaceOnUse" x1="-20.5005" y1="-818.9863" x2="-20.5005" y2="-831.0005" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#B3E73A"/> + <stop offset="1" style="stop-color:#90C61D"/> + </linearGradient> + <path id="path46_1_" fill="url(#path46_3_)" d="M0.9,14L3,12.9C3.799,12.5,4.299,11.8,4.5,11c0.199-0.8,0-1.7-0.6-2.3 + c-0.301-0.4-1.201-1.8-1.201-3c0-2,1.1-3.7,2.4-3.7c1.3,0,2.4,1.7,2.4,3.7c0,1.2-0.9,2.5-1.201,3c-0.5,0.7-0.699,1.5-0.6,2.3 + c0.201,0.8,0.701,1.5,1.5,1.9l1.9,1V14H0.9z"/> +</g> +<g> + + <linearGradient id="path11_1_" gradientUnits="userSpaceOnUse" x1="-1157.0952" y1="-819.666" x2="-1157.0952" y2="-835.6655" gradientTransform="matrix(-1 0 0 -1 -1149.5 -817)"> + <stop offset="0" style="stop-color:#DB6D17"/> + <stop offset="1" style="stop-color:#BF3B08"/> + </linearGradient> + <path id="path11" fill="url(#path11_1_)" d="M12.05,12.566c0,0,1.601-2.101,1.601-4.2c0-3.1-2-5.7-4.398-5.7s-4.4,2.5-4.3,5.6 + c0,2.2,1.6,4.2,1.6,4.2c0.3,0.401,0.2,0.901-0.3,1.2l-6,3.1c-0.5,0.301-0.252,1.9-0.252,1.9h15.252v-3.199l-3-1.6 + C11.75,13.566,11.65,12.967,12.05,12.566z"/> + + <linearGradient id="path18_1_" gradientUnits="userSpaceOnUse" x1="-1157.0005" y1="-820.667" x2="-1157.0005" y2="-834.6655" gradientTransform="matrix(-1 0 0 -1 -1149.5 -817)"> + <stop offset="0" style="stop-color:#F6A55E"/> + <stop offset="1" style="stop-color:#EA5B03"/> + </linearGradient> + <path id="path18" fill="url(#path18_1_)" d="M0.75,17.666l5.9-3c0.5-0.199,0.9-0.699,1-1.199s-0.1-1.1-0.4-1.5 + c0-0.1-1.4-1.899-1.4-3.6c0-2.6,1.6-4.7,3.4-4.7s3.4,2.1,3.3,4.7c0,1.8-1.4,3.6-1.4,3.6c-0.4,0.4-0.5,1-0.4,1.5 + c0.2,0.5,0.5,0.9,1,1.199l2.5,1.301v1.699H0.75z"/> + + <linearGradient id="path25_1_" gradientUnits="userSpaceOnUse" x1="-1158.5503" y1="-821.6523" x2="-1158.5503" y2="-833.6642" gradientTransform="matrix(-1 0 0 -1 -1149.5 -817)"> + <stop offset="0" style="stop-color:#F17219"/> + <stop offset="1" style="stop-color:#EA5B03"/> + </linearGradient> + <path id="path25" fill="url(#path25_1_)" d="M4.85,16.666l2.2-1.1c0.8-0.4,1.3-1.102,1.5-1.9c0.2-0.799,0-1.6-0.5-2.299 + c-0.3-0.5-1.2-1.9-1.2-3c0-2,1.1-3.7,2.4-3.7s2.4,1.7,2.4,3.7c0,1.2-0.9,2.6-1.2,3c-0.6,0.6-0.8,1.5-0.6,2.299 + c0.2,0.801,0.7,1.5,1.5,1.9l1.9,1.1l0,0H4.85z"/> +</g> +<g> + + <linearGradient id="path32_4_" gradientUnits="userSpaceOnUse" x1="-14.3999" y1="-822" x2="-14.3999" y2="-838.0005" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#90C50E"/> + <stop offset="1" style="stop-color:#70A034"/> + </linearGradient> + <path id="path32_2_" fill="url(#path32_4_)" d="M19.199,21v-3.4l-3-1.5c-0.5-0.299-0.6-0.799-0.301-1.199c0,0,1.601-2,1.601-4.198 + C17.499,7.5,15.6,5,13.1,5C10.7,5,8.7,7.6,8.7,10.7c0,2.1,1.6,4.2,1.6,4.2c0.301,0.398,0.2,1-0.3,1.301L4,19.4 + c-0.5,0.301-1,0.898-1,1.5V21H19.199z"/> + + <linearGradient id="path39_4_" gradientUnits="userSpaceOnUse" x1="-13.8511" y1="-823.001" x2="-13.8511" y2="-837.0157" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#D9F991"/> + <stop offset="1" style="stop-color:#B1DD4B"/> + </linearGradient> + <path id="path39_2_" fill="url(#path39_4_)" d="M4.999,20l5.7-3c0.5-0.299,0.8-0.699,1-1.199c0.1-0.5,0-1.1-0.4-1.5 + c0,0-1.399-1.801-1.399-3.6c0-2.6,1.5-4.7,3.399-4.7c1.9,0,3.4,2.101,3.4,4.7c0,1.799-1.4,3.6-1.4,3.6c-0.3,0.4-0.5,1-0.398,1.5 + c0.101,0.5,0.5,1,1,1.199l2.398,1.201V20H4.999z"/> + + <linearGradient id="path46_4_" gradientUnits="userSpaceOnUse" x1="-12.3013" y1="-823.9863" x2="-12.3013" y2="-836.001" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#B3E73A"/> + <stop offset="1" style="stop-color:#90C61D"/> + </linearGradient> + <path id="path46_2_" fill="url(#path46_4_)" d="M9.1,19l2.1-1.1c0.8-0.4,1.3-1.102,1.5-1.9s0-1.699-0.6-2.299 + c-0.301-0.4-1.2-1.801-1.2-3c0-2,1.1-3.7,2.399-3.7c1.301,0,2.4,1.7,2.4,3.7c0,1.199-0.9,2.5-1.2,3c-0.5,0.699-0.7,1.5-0.601,2.299 + c0.201,0.801,0.701,1.5,1.5,1.9l1.898,1V19H9.1z"/> +</g> +<g> + + <linearGradient id="path32_5_" gradientUnits="userSpaceOnUse" x1="-9.6001" y1="-825" x2="-9.6001" y2="-841.0005" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#90C50E"/> + <stop offset="1" style="stop-color:#70A034"/> + </linearGradient> + <path id="path32" fill="url(#path32_5_)" d="M24,24v-3.4l-3-1.5c-0.5-0.3-0.6-0.8-0.3-1.199c0,0,1.6-2,1.6-4.198 + C22.3,10.5,20.4,8,17.9,8c-2.4,0-4.4,2.6-4.4,5.7c0,2.1,1.6,4.2,1.6,4.2c0.301,0.397,0.2,1-0.3,1.3l-6,3.2c-0.5,0.3-1,0.897-1,1.5 + V24H24z"/> + + <linearGradient id="path39_5_" gradientUnits="userSpaceOnUse" x1="-9.0503" y1="-826" x2="-9.0503" y2="-840.0152" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#D9F991"/> + <stop offset="1" style="stop-color:#B1DD4B"/> + </linearGradient> + <path id="path39" fill="url(#path39_5_)" d="M9.8,23l5.7-3c0.5-0.3,0.8-0.7,1-1.2c0.1-0.5,0-1.1-0.4-1.5c0,0-1.397-1.8-1.397-3.6 + c0-2.6,1.5-4.7,3.397-4.7c1.9,0,3.4,2.1,3.4,4.7c0,1.8-1.4,3.6-1.4,3.6c-0.3,0.4-0.5,1-0.397,1.5c0.1,0.5,0.5,1,1,1.2l2.397,1.2V23 + H9.8z"/> + + <linearGradient id="path46_5_" gradientUnits="userSpaceOnUse" x1="-7.502" y1="-826.9863" x2="-7.502" y2="-839.0015" gradientTransform="matrix(1 0 0 -1 25.5 -817)"> + <stop offset="0" style="stop-color:#B3E73A"/> + <stop offset="1" style="stop-color:#90C61D"/> + </linearGradient> + <path id="path46" fill="url(#path46_5_)" d="M13.9,22l2.1-1.1c0.8-0.4,1.3-1.103,1.5-1.9s0-1.7-0.6-2.3c-0.301-0.4-1.2-1.8-1.2-3 + c0-2,1.1-3.7,2.399-3.7c1.301,0,2.398,1.7,2.398,3.7c0,1.2-0.898,2.5-1.2,3c-0.5,0.7-0.698,1.5-0.6,2.3 + c0.198,0.8,0.698,1.5,1.5,1.9l1.897,1V22H13.9z"/> +</g> +</svg> diff --git a/mod/attendance/pix/key.svg b/mod/attendance/pix/key.svg new file mode 100644 index 0000000..0d3f3d9 --- /dev/null +++ b/mod/attendance/pix/key.svg @@ -0,0 +1 @@ +<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M832 512q0-80-56-136t-136-56-136 56-56 136q0 42 19 83-41-19-83-19-80 0-136 56t-56 136 56 136 136 56 136-56 56-136q0-42-19-83 41 19 83 19 80 0 136-56t56-136zm851 704q0 17-49 66t-66 49q-9 0-28.5-16t-36.5-33-38.5-40-24.5-26l-96 96 220 220q28 28 28 68 0 42-39 81t-81 39q-40 0-68-28l-671-671q-176 131-365 131-163 0-265.5-102.5t-102.5-265.5q0-160 95-313t248-248 313-95q163 0 265.5 102.5t102.5 265.5q0 189-131 365l355 355 96-96q-3-3-26-24.5t-40-38.5-33-36.5-16-28.5q0-17 49-66t66-49q13 0 23 10 6 6 46 44.5t82 79.5 86.5 86 73 78 28.5 41z"/></svg> \ No newline at end of file diff --git a/mod/attendance/pix/qrcode.svg b/mod/attendance/pix/qrcode.svg new file mode 100644 index 0000000..18dca2a --- /dev/null +++ b/mod/attendance/pix/qrcode.svg @@ -0,0 +1 @@ +<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="qrcode" class="svg-inline--fa fa-qrcode fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 224h192V32H0v192zM64 96h64v64H64V96zm192-64v192h192V32H256zm128 128h-64V96h64v64zM0 480h192V288H0v192zm64-128h64v64H64v-64zm352-64h32v128h-96v-32h-32v96h-64V288h96v32h64v-32zm0 160h32v32h-32v-32zm-64 0h32v32h-32v-32z"></path></svg> \ No newline at end of file diff --git a/mod/attendance/pix/redo.png b/mod/attendance/pix/redo.png new file mode 100644 index 0000000000000000000000000000000000000000..480a3291a53c258f994a37fd6a8c424a0aa48de5 GIT binary patch literal 524 zcmeAS@N?(olHy`uVBq!ia0y~yVBiK}4mJh`hLvl|)fgBUBuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFs}1-aSW-rRWrfcLpo8U_57dD{*yXSs%(_(kh(49 zBCX6Kt+itVx9f{V3Q48G=8uxqudLM;f4Agz5R2f#4((Paha7X)4H20O6`F!vvuYls zT$ph(Wm09S(#u17zjXP;-|d`U&ClKL(bfE6g6s1w;$Fv^v?4R44=;bsQx~aaw8H7# z6}D4dyuMc_&M}hkH_nK?a^G~P%zqxn&g!(;j6bRtAAS8u{8dx`x3*`C)Zd;|`($(T zt!(A@ipf6&R{xmMW%6ahy6O4zYI6EZTA1?$Yk1jx_qVQk`asC^jm*0{H4^(P9OurP z@Jd{Ka<Bj4I<tnRO2(gN-Az+nZ=N~4_C<xj_2zd9NmD*A-mEFtw>p*c`5E&kiU$)u zFId*v;Gtk_7RG*d`zQW2f#UrcGba~>SR6f=_wq>v^Xr|)kH252KA-bMl2hla*z@z- z4Ijs3D_n7Eci&&|?X~GX4VN0`tL8hxGyVK}Hfa0#{$J*3+WF_qT)|+Dk8J75+yMs! zHoP>fT~yJzzFd0shPJd*N43RoTRnI^xo^wkH;ylNPu`uXal<|JQ@q55D8q;IKDl0O g|Np_gfsx^F(X`hSOFph<U|?YIboFyt=akR{0CZmNhX4Qo literal 0 HcmV?d00001 diff --git a/mod/attendance/preferences.php b/mod/attendance/preferences.php new file mode 100644 index 0000000..fe3e21a --- /dev/null +++ b/mod/attendance/preferences.php @@ -0,0 +1,170 @@ +<?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/>. + +/** + * Manage attendance settings + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); + +$pageparams = new mod_attendance_preferences_page_params(); + +$id = required_param('id', PARAM_INT); +$pageparams->action = optional_param('action', null, PARAM_INT); +$pageparams->statusid = optional_param('statusid', null, PARAM_INT); +$pageparams->statusset = optional_param('statusset', 0, PARAM_INT); // Set of statuses to view. + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); +require_capability('mod/attendance:changepreferences', $context); + +// Make sure the statusset is valid. +$maxstatusset = attendance_get_max_statusset($att->id); +if ($pageparams->statusset > $maxstatusset + 1) { + $pageparams->statusset = $maxstatusset + 1; +} + +$att = new mod_attendance_structure($att, $cm, $course, $context, $pageparams); + +$PAGE->set_url($att->url_preferences()); +$PAGE->set_title($course->shortname. ": ".$att->name.' - '.get_string('settings', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->force_settings_menu(true); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('settings', 'attendance')); + +$errors = array(); + +// Check sesskey if we are performing an action. +if (!empty($att->pageparams->action)) { + require_sesskey(); +} +$notification = ''; +// TODO: combine this with the stuff in defaultstatus.php to avoid code duplication. +switch ($att->pageparams->action) { + case mod_attendance_preferences_page_params::ACTION_ADD: + $newacronym = optional_param('newacronym', null, PARAM_TEXT); + $newdescription = optional_param('newdescription', null, PARAM_TEXT); + $newgrade = optional_param('newgrade', 0, PARAM_RAW); + $newstudentavailability = optional_param('newstudentavailability', null, PARAM_INT); + $newgrade = empty($newgrade) ? 0 : unformat_float($newgrade); + + $newstatus = new stdClass(); + $newstatus->attendanceid = $att->id; + $newstatus->acronym = $newacronym; + $newstatus->description = $newdescription; + $newstatus->grade = $newgrade; + $newstatus->studentavailability = $newstudentavailability; + $newstatus->setnumber = $att->pageparams->statusset; + $newstatus->cm = $att->cm; + $newstatus->context = $att->context; + + $status = attendance_add_status($newstatus); + if (!$status) { + $notification = $OUTPUT->notification(get_string('cantaddstatus', 'attendance'), 'error'); + } + + if ($pageparams->statusset > $maxstatusset) { + $maxstatusset = $pageparams->statusset; // Make sure the new maximum is shown without a page refresh. + } + break; + case mod_attendance_preferences_page_params::ACTION_DELETE: + if (attendance_has_logs_for_status($att->pageparams->statusid)) { + print_error('cantdeletestatus', 'attendance', "attsettings.php?id=$id"); + } + + $confirm = optional_param('confirm', null, PARAM_INT); + $statuses = $att->get_statuses(false); + $status = $statuses[$att->pageparams->statusid]; + + if (isset($confirm)) { + attendance_remove_status($status); + redirect($att->url_preferences(), get_string('statusdeleted', 'attendance')); + } + + $message = get_string('deletecheckfull', 'attendance', get_string('variable', 'attendance')); + $message .= str_repeat(html_writer::empty_tag('br'), 2); + $message .= $status->acronym.': '. + ($status->description ? $status->description : get_string('nodescription', 'attendance')); + $params = array_merge($att->pageparams->get_significant_params(), array('confirm' => 1)); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); + echo $OUTPUT->confirm($message, $att->url_preferences($params), $att->url_preferences()); + echo $OUTPUT->footer(); + exit; + case mod_attendance_preferences_page_params::ACTION_HIDE: + $statuses = $att->get_statuses(false); + $status = $statuses[$att->pageparams->statusid]; + attendance_update_status($status, null, null, null, 0, $att->context, $att->cm); + break; + case mod_attendance_preferences_page_params::ACTION_SHOW: + $statuses = $att->get_statuses(false); + $status = $statuses[$att->pageparams->statusid]; + attendance_update_status($status, null, null, null, 1, $att->context, $att->cm); + break; + case mod_attendance_preferences_page_params::ACTION_SAVE: + $acronym = required_param_array('acronym', PARAM_TEXT); + $description = required_param_array('description', PARAM_TEXT); + $grade = required_param_array('grade', PARAM_RAW); + $studentavailability = optional_param_array('studentavailability', null, PARAM_RAW); + $unmarkedstatus = optional_param('setunmarked', null, PARAM_INT); + + foreach ($grade as &$val) { + $val = unformat_float($val); + } + $statuses = $att->get_statuses(false); + + foreach ($acronym as $id => $v) { + $status = $statuses[$id]; + $setunmarked = false; + if ($unmarkedstatus == $id) { + $setunmarked = true; + } + $errors[$id] = attendance_update_status($status, $acronym[$id], $description[$id], $grade[$id], + null, $att->context, $att->cm, $studentavailability[$id], $setunmarked); + } + attendance_update_users_grade($att); + break; +} + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_PREFERENCES); +$prefdata = new attendance_preferences_data($att, array_filter($errors)); +$setselector = new attendance_set_selector($att, $maxstatusset); + +// Output starts here. + +echo $output->header(); +if (!empty($notification)) { + echo $notification; +} +echo $output->heading(get_string('attendanceforthecourse', 'attendance').' :: '. format_string($course->fullname)); +echo $output->render($tabs); +echo $OUTPUT->box(get_string('preferences_desc', 'attendance'), 'generalbox attendancedesc', 'notice'); +echo $output->render($setselector); +echo $output->render($prefdata); + +echo $output->footer(); diff --git a/mod/attendance/renderables.php b/mod/attendance/renderables.php new file mode 100644 index 0000000..3a5b9ca --- /dev/null +++ b/mod/attendance/renderables.php @@ -0,0 +1,1029 @@ +<?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/>. + +/** + * Attendance module renderable components are defined here + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/locallib.php'); + + +/** + * Represents info about attendance tabs. + * + * Proxy class for security reasons (renderers must not have access to all attendance methods) + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +class attendance_tabs implements renderable { + /** Sessions tab */ + const TAB_SESSIONS = 1; + /** Add tab */ + const TAB_ADD = 2; + /** Rerort tab */ + const TAB_REPORT = 3; + /** Export tab */ + const TAB_EXPORT = 4; + /** Preferences tab */ + const TAB_PREFERENCES = 5; + /** Temp users tab */ + const TAB_TEMPORARYUSERS = 6; // Tab for managing temporary users. + /** Update tab */ + const TAB_UPDATE = 7; + /** Warnings tab */ + const TAB_WARNINGS = 8; + /** Absentee tab */ + const TAB_ABSENTEE = 9; + /** @var int current tab */ + public $currenttab; + + /** @var stdClass attendance */ + private $att; + + /** + * Prepare info about sessions for attendance taking into account view parameters. + * + * @param mod_attendance_structure $att + * @param int $currenttab - one of attendance_tabs constants + */ + public function __construct(mod_attendance_structure $att, $currenttab=null) { + $this->att = $att; + $this->currenttab = $currenttab; + } + + /** + * Return array of rows where each row is an array of tab objects + * taking into account permissions of current user + */ + public function get_tabs() { + $toprow = array(); + $context = $this->att->context; + $capabilities = array( + 'mod/attendance:manageattendances', + 'mod/attendance:takeattendances', + 'mod/attendance:changeattendances' + ); + if (has_any_capability($capabilities, $context)) { + $toprow[] = new tabobject(self::TAB_SESSIONS, $this->att->url_manage()->out(), + get_string('sessions', 'attendance')); + } + + if (has_capability('mod/attendance:manageattendances', $context)) { + $toprow[] = new tabobject(self::TAB_ADD, + $this->att->url_sessions()->out(true, + array('action' => mod_attendance_sessions_page_params::ACTION_ADD)), + get_string('addsession', 'attendance')); + } + if (has_capability('mod/attendance:viewreports', $context)) { + $toprow[] = new tabobject(self::TAB_REPORT, $this->att->url_report()->out(), + get_string('report', 'attendance')); + } + + if (has_capability('mod/attendance:viewreports', $context) && + get_config('attendance', 'enablewarnings')) { + $toprow[] = new tabobject(self::TAB_ABSENTEE, $this->att->url_absentee()->out(), + get_string('absenteereport', 'attendance')); + } + + if (has_capability('mod/attendance:export', $context)) { + $toprow[] = new tabobject(self::TAB_EXPORT, $this->att->url_export()->out(), + get_string('export', 'attendance')); + } + + if (has_capability('mod/attendance:changepreferences', $context)) { + $toprow[] = new tabobject(self::TAB_PREFERENCES, $this->att->url_preferences()->out(), + get_string('statussetsettings', 'attendance')); + + if (get_config('attendance', 'enablewarnings')) { + $toprow[] = new tabobject(self::TAB_WARNINGS, $this->att->url_warnings()->out(), + get_string('warnings', 'attendance')); + } + } + if (has_capability('mod/attendance:managetemporaryusers', $context)) { + $toprow[] = new tabobject(self::TAB_TEMPORARYUSERS, $this->att->url_managetemp()->out(), + get_string('tempusers', 'attendance')); + } + if ($this->currenttab == self::TAB_UPDATE && has_capability('mod/attendance:manageattendances', $context)) { + $toprow[] = new tabobject(self::TAB_UPDATE, + $this->att->url_sessions()->out(true, + array('action' => mod_attendance_sessions_page_params::ACTION_UPDATE)), + get_string('changesession', 'attendance')); + } + + return array($toprow); + } +} + +/** + * Class attendance_filter_controls + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_filter_controls implements renderable { + /** @var int current view mode */ + public $pageparams; + /** @var stdclass */ + public $cm; + /** @var int */ + public $curdate; + /** @var int */ + public $prevcur; + /** @var int */ + public $nextcur; + /** @var string */ + public $curdatetxt; + /** @var boolean */ + public $reportcontrol; + /** @var string */ + private $urlpath; + /** @var array */ + private $urlparams; + /** @var mod_attendance_structure */ + public $att; + + /** + * attendance_filter_controls constructor. + * @param mod_attendance_structure $att + * @param bool $report + */ + public function __construct(mod_attendance_structure $att, $report = false) { + global $PAGE; + + $this->pageparams = $att->pageparams; + + $this->cm = $att->cm; + + // This is a report control only if $reports is true and the attendance block can be graded. + $this->reportcontrol = $report; + + $this->curdate = $att->pageparams->curdate; + + $date = usergetdate($att->pageparams->curdate); + $mday = $date['mday']; + $mon = $date['mon']; + $year = $date['year']; + + switch ($this->pageparams->view) { + case ATT_VIEW_DAYS: + $format = get_string('strftimedm', 'attendance'); + $this->prevcur = make_timestamp($year, $mon, $mday - 1); + $this->nextcur = make_timestamp($year, $mon, $mday + 1); + $this->curdatetxt = userdate($att->pageparams->startdate, $format); + break; + case ATT_VIEW_WEEKS: + $format = get_string('strftimedm', 'attendance'); + $this->prevcur = $att->pageparams->startdate - WEEKSECS; + $this->nextcur = $att->pageparams->startdate + WEEKSECS; + $this->curdatetxt = userdate($att->pageparams->startdate, $format). + " - ".userdate($att->pageparams->enddate, $format); + break; + case ATT_VIEW_MONTHS: + $format = '%B'; + $this->prevcur = make_timestamp($year, $mon - 1); + $this->nextcur = make_timestamp($year, $mon + 1); + $this->curdatetxt = userdate($att->pageparams->startdate, $format); + break; + } + + $this->urlpath = $PAGE->url->out_omit_querystring(); + $params = $att->pageparams->get_significant_params(); + $params['id'] = $att->cm->id; + $this->urlparams = $params; + + $this->att = $att; + } + + /** + * Helper function for url. + * + * @param array $params + * @return moodle_url + */ + public function url($params=array()) { + $params = array_merge($this->urlparams, $params); + + return new moodle_url($this->urlpath, $params); + } + + /** + * Helper function for url path. + * @return string + */ + public function url_path() { + return $this->urlpath; + } + + /** + * Helper function for url_params. + * @param array $params + * @return array + */ + public function url_params($params=array()) { + $params = array_merge($this->urlparams, $params); + + return $params; + } + + /** + * Return groupmode. + * @return int + */ + public function get_group_mode() { + return $this->att->get_group_mode(); + } + + /** + * Return groupslist. + * @return mixed + */ + public function get_sess_groups_list() { + return $this->att->pageparams->get_sess_groups_list(); + } + + /** + * Get current session type. + * @return mixed + */ + public function get_current_sesstype() { + return $this->att->pageparams->get_current_sesstype(); + } +} + +/** + * Represents info about attendance sessions taking into account view parameters. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_manage_data implements renderable { + /** @var array of sessions*/ + public $sessions; + + /** @var int number of hidden sessions (sessions before $course->startdate)*/ + public $hiddensessionscount; + /** @var array */ + public $groups; + /** @var int */ + public $hiddensesscount; + + /** @var mod_attendance_structure */ + public $att; + /** + * Prepare info about attendance sessions taking into account view parameters. + * + * @param mod_attendance_structure $att instance + */ + public function __construct(mod_attendance_structure $att) { + + $this->sessions = $att->get_filtered_sessions(); + + $this->groups = groups_get_all_groups($att->course->id); + + $this->hiddensessionscount = $att->get_hidden_sessions_count(); + + $this->att = $att; + } + + /** + * Helper function to return urls. + * @param int $sessionid + * @param int $grouptype + * @return mixed + */ + public function url_take($sessionid, $grouptype) { + return url_helpers::url_take($this->att, $sessionid, $grouptype); + } + + /** + * Must be called without or with both parameters + * + * @param int $sessionid + * @param null $action + * @return mixed + */ + public function url_sessions($sessionid=null, $action=null) { + return url_helpers::url_sessions($this->att, $sessionid, $action); + } +} + +/** + * class take data. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_take_data implements renderable { + /** @var array */ + public $users; + /** @var array|null|stdClass */ + public $pageparams; + /** @var int */ + public $groupmode; + /** @var stdclass */ + public $cm; + /** @var array */ + public $statuses; + /** @var mixed */ + public $sessioninfo; + /** @var array */ + public $sessionlog; + /** @var array */ + public $sessions4copy; + /** @var bool */ + public $updatemode; + /** @var string */ + private $urlpath; + /** @var array */ + private $urlparams; + /** @var mod_attendance_structure */ + public $att; + + /** + * attendance_take_data constructor. + * @param mod_attendance_structure $att + */ + public function __construct(mod_attendance_structure $att) { + if ($att->pageparams->grouptype) { + $this->users = $att->get_users($att->pageparams->grouptype, $att->pageparams->page); + } else { + $this->users = $att->get_users($att->pageparams->group, $att->pageparams->page); + } + + $this->pageparams = $att->pageparams; + + $this->groupmode = $att->get_group_mode(); + $this->cm = $att->cm; + + $this->statuses = $att->get_statuses(); + + $this->sessioninfo = $att->get_session_info($att->pageparams->sessionid); + $this->updatemode = $this->sessioninfo->lasttaken > 0; + + if (isset($att->pageparams->copyfrom)) { + $this->sessionlog = $att->get_session_log($att->pageparams->copyfrom); + } else if ($this->updatemode) { + $this->sessionlog = $att->get_session_log($att->pageparams->sessionid); + } else { + $this->sessionlog = array(); + } + + if (!$this->updatemode) { + $this->sessions4copy = $att->get_today_sessions_for_copy($this->sessioninfo); + } + + $this->urlpath = $att->url_take()->out_omit_querystring(); + $params = $att->pageparams->get_significant_params(); + $params['id'] = $att->cm->id; + $this->urlparams = $params; + + $this->att = $att; + } + + /** + * Url function + * @param array $params + * @param array $excludeparams + * @return moodle_url + */ + public function url($params=array(), $excludeparams=array()) { + $params = array_merge($this->urlparams, $params); + + foreach ($excludeparams as $paramkey) { + unset($params[$paramkey]); + } + + return new moodle_url($this->urlpath, $params); + } + + /** + * Url view helper. + * @param array $params + * @return mixed + */ + public function url_view($params=array()) { + return url_helpers::url_view($this->att, $params); + } + + /** + * Url path helper. + * @return string + */ + public function url_path() { + return $this->urlpath; + } +} + +/** + * Class user data. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_user_data implements renderable { + /** @var mixed|object */ + public $user; + /** @var array|null|stdClass */ + public $pageparams; + /** @var array */ + public $statuses; + /** @var array */ + public $summary; + /** @var attendance_filter_controls */ + public $filtercontrols; + /** @var array */ + public $sessionslog; + /** @var array */ + public $groups; + /** @var array */ + public $coursesatts; + /** @var string */ + private $urlpath; + /** @var array */ + private $urlparams; + + /** + * attendance_user_data constructor. + * @param mod_attendance_structure $att + * @param int $userid + * @param boolean $mobile - this is called by the mobile code, don't generate everything. + */ + public function __construct(mod_attendance_structure $att, $userid, $mobile = false) { + $this->user = $att->get_user($userid); + + $this->pageparams = $att->pageparams; + + if ($this->pageparams->mode == mod_attendance_view_page_params::MODE_THIS_COURSE) { + $this->statuses = $att->get_statuses(true, true); + + if (!$mobile) { + $this->summary = new mod_attendance_summary($att->id, array($userid), $att->pageparams->startdate, + $att->pageparams->enddate); + + $this->filtercontrols = new attendance_filter_controls($att); + } + + $this->sessionslog = $att->get_user_filtered_sessions_log_extended($userid); + + $this->groups = groups_get_all_groups($att->course->id); + } else if ($this->pageparams->mode == mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $this->coursesatts = attendance_get_user_courses_attendances($userid); + $this->statuses = array(); + $this->summaries = array(); + $this->groups = array(); + + foreach ($this->coursesatts as $atid => $ca) { + // Check to make sure the user can view this cm. + $modinfo = get_fast_modinfo($ca->courseid); + if (!$modinfo->instances['attendance'][$ca->attid]->uservisible) { + unset($this->coursesatts[$atid]); + continue; + } else { + $this->coursesatts[$atid]->cmid = $modinfo->instances['attendance'][$ca->attid]->get_course_module_record()->id; + } + $this->statuses[$ca->attid] = attendance_get_statuses($ca->attid); + $this->summaries[$ca->attid] = new mod_attendance_summary($ca->attid, array($userid)); + + if (!array_key_exists($ca->courseid, $this->groups)) { + $this->groups[$ca->courseid] = groups_get_all_groups($ca->courseid); + } + } + + if (!$mobile) { + $this->summary = new mod_attendance_summary($att->id, array($userid), $att->pageparams->startdate, + $att->pageparams->enddate); + + $this->filtercontrols = new attendance_filter_controls($att); + } + + $this->sessionslog = attendance_get_user_sessions_log_full($userid, $this->pageparams); + + foreach ($this->sessionslog as $sessid => $sess) { + $this->sessionslog[$sessid]->cmid = $this->coursesatts[$sess->attendanceid]->cmid; + } + + } else { + $this->coursesatts = attendance_get_user_courses_attendances($userid); + $this->statuses = array(); + $this->summary = array(); + foreach ($this->coursesatts as $atid => $ca) { + // Check to make sure the user can view this cm. + $modinfo = get_fast_modinfo($ca->courseid); + if (!$modinfo->instances['attendance'][$ca->attid]->uservisible) { + unset($this->coursesatts[$atid]); + continue; + } else { + $this->coursesatts[$atid]->cmid = $modinfo->instances['attendance'][$ca->attid]->get_course_module_record()->id; + } + $this->statuses[$ca->attid] = attendance_get_statuses($ca->attid); + $this->summary[$ca->attid] = new mod_attendance_summary($ca->attid, array($userid)); + } + } + $this->urlpath = $att->url_view()->out_omit_querystring(); + $params = $att->pageparams->get_significant_params(); + $params['id'] = $att->cm->id; + $this->urlparams = $params; + } + + /** + * Url function + * @param array $params + * @param array $excludeparams + * @return moodle_url + */ + public function url($params=array(), $excludeparams=array()) { + $params = array_merge($this->urlparams, $params); + + foreach ($excludeparams as $paramkey) { + unset($params[$paramkey]); + } + + return new moodle_url($this->urlpath, $params); + } + + /** + * Take multiple sessions attendance from form data. + * + * @param stdClass $formdata + */ + public function take_sessions_from_form_data($formdata) { + global $DB, $USER; + // TODO: WARNING - $formdata is unclean - comes from direct $_POST - ideally needs a rewrite but we do some cleaning below. + // This whole function could do with a nice clean up. + + $now = time(); + $sesslog = array(); + $formdata = (array)$formdata; + $updatedsessions = array(); + $sessionatt = array(); + + foreach ($formdata as $key => $value) { + // Look at Remarks field because the user options may not be passed if empty. + if (substr($key, 0, 7) == 'remarks') { + $parts = explode('sess', substr($key, 7)); + $stid = $parts[0]; + if (!(is_numeric($stid))) { // Sanity check on $stid. + print_error('nonnumericid', 'attendance'); + } + $sessid = $parts[1]; + if (!(is_numeric($sessid))) { // Sanity check on $sessid. + print_error('nonnumericid', 'attendance'); + } + $dbsession = $this->sessionslog[$sessid]; + + $context = context_module::instance($dbsession->cmid); + if (!has_capability('mod/attendance:takeattendances', $context)) { + // How do we tell user about this? + \core\notification::warning(get_string("nocapabilitytotakethisattendance", "attendance", $dbsession->cmid)); + continue; + } + + $formkey = 'user'.$stid.'sess'.$sessid; + $attid = $dbsession->attendanceid; + $statusset = array_filter($this->statuses[$attid], + function($x) use($dbsession) { + return $x->setnumber === $dbsession->statusset; + }); + $sessionatt[$sessid] = $attid; + $formlog = new stdClass(); + if (array_key_exists($formkey, $formdata) && is_numeric($formdata[$formkey])) { + $formlog->statusid = $formdata[$formkey]; + } + $formlog->studentid = $stid; // We check is_numeric on this above. + $formlog->statusset = implode(',', array_keys($statusset)); + $formlog->remarks = $value; + $formlog->sessionid = $sessid; + $formlog->timetaken = $now; + $formlog->takenby = $USER->id; + + if (!array_key_exists($stid, $sesslog)) { + $sesslog[$stid] = array(); + } + $sesslog[$stid][$sessid] = $formlog; + } + } + + $updateatts = array(); + foreach ($sesslog as $stid => $userlog) { + $dbstudlog = $DB->get_records('attendance_log', array('studentid' => $stid), '', + 'sessionid,statusid,remarks,id,statusset'); + foreach ($userlog as $log) { + if (array_key_exists($log->sessionid, $dbstudlog)) { + $attid = $sessionatt[$log->sessionid]; + // Check if anything important has changed before updating record. + // Don't update timetaken/takenby records if nothing has changed. + if ($dbstudlog[$log->sessionid]->remarks != $log->remarks || + $dbstudlog[$log->sessionid]->statusid != $log->statusid || + $dbstudlog[$log->sessionid]->statusset != $log->statusset) { + + $log->id = $dbstudlog[$log->sessionid]->id; + $DB->update_record('attendance_log', $log); + + $updatedsessions[$log->sessionid] = $log->sessionid; + if (!array_key_exists($attid, $updateatts)) { + $updateatts[$attid] = array(); + } + array_push($updateatts[$attid], $log->studentid); + } + } else { + $DB->insert_record('attendance_log', $log, false); + $updatedsessions[$log->sessionid] = $log->sessionid; + if (!array_key_exists($attid, $updateatts)) { + $updateatts[$attid] = array(); + } + array_push($updateatts[$attid], $log->studentid); + } + } + } + + foreach ($updatedsessions as $sessionid) { + $session = $this->sessionslog[$sessionid]; + $session->lasttaken = $now; + $session->lasttakenby = $USER->id; + $DB->update_record('attendance_sessions', $session); + } + + if (!empty($updateatts)) { + $attendancegrade = $DB->get_records_list('attendance', 'id', array_keys($updateatts), '', 'id, grade'); + foreach ($updateatts as $attid => $updateusers) { + if ($attendancegrade[$attid] != 0) { + attendance_update_users_grades_by_id($attid, $grade, $updateusers); + } + } + } + } +} + +/** + * Class report data. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_report_data implements renderable { + /** @var array|null|stdClass */ + public $pageparams; + /** @var array */ + public $users; + /** @var array */ + public $groups; + /** @var array */ + public $sessions; + /** @var array */ + public $statuses; + /** @var array includes disablrd/deleted statuses. */ + public $allstatuses; + /** @var array */ + public $usersgroups = array(); + /** @var array */ + public $sessionslog = array(); + /** @var array|mod_attendance_summary */ + public $summary = array(); + /** @var mod_attendance_structure */ + public $att; + + /** + * attendance_report_data constructor. + * @param mod_attendance_structure $att + */ + public function __construct(mod_attendance_structure $att) { + $this->pageparams = $att->pageparams; + + $this->users = $att->get_users($att->pageparams->group, $att->pageparams->page); + + if (isset($att->pageparams->userids)) { + foreach ($this->users as $key => $user) { + if (!in_array($user->id, $att->pageparams->userids)) { + unset($this->users[$key]); + } + } + } + + $this->groups = groups_get_all_groups($att->course->id); + + $this->sessions = $att->get_filtered_sessions(); + + $this->statuses = $att->get_statuses(true, true); + $this->allstatuses = $att->get_statuses(false, true); + + if ($att->pageparams->view == ATT_VIEW_SUMMARY) { + $this->summary = new mod_attendance_summary($att->id); + } else { + $this->summary = new mod_attendance_summary($att->id, array_keys($this->users), + $att->pageparams->startdate, $att->pageparams->enddate); + } + + foreach ($this->users as $key => $user) { + $usersummary = $this->summary->get_taken_sessions_summary_for($user->id); + if ($att->pageparams->view != ATT_VIEW_NOTPRESENT || + attendance_calc_fraction($usersummary->takensessionspoints, $usersummary->takensessionsmaxpoints) < + $att->get_lowgrade_threshold()) { + + $this->usersgroups[$user->id] = groups_get_all_groups($att->course->id, $user->id); + + $this->sessionslog[$user->id] = $att->get_user_filtered_sessions_log($user->id); + } else { + unset($this->users[$key]); + } + } + + $this->att = $att; + } + + /** + * url take helper. + * @param int $sessionid + * @param int $grouptype + * @return mixed + */ + public function url_take($sessionid, $grouptype) { + return url_helpers::url_take($this->att, $sessionid, $grouptype); + } + + /** + * url view helper. + * @param array $params + * @return mixed + */ + public function url_view($params=array()) { + return url_helpers::url_view($this->att, $params); + } + + /** + * url helper. + * @param array $params + * @return moodle_url + */ + public function url($params=array()) { + $params = array_merge($params, $this->pageparams->get_significant_params()); + + return $this->att->url_report($params); + } + +} + +/** + * Class preferences data. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_preferences_data implements renderable { + /** @var array */ + public $statuses; + /** @var mod_attendance_structure */ + private $att; + /** @var array */ + public $errors; + + /** + * attendance_preferences_data constructor. + * @param mod_attendance_structure $att + * @param array $errors + */ + public function __construct(mod_attendance_structure $att, $errors) { + $this->statuses = $att->get_statuses(false); + $this->errors = $errors; + + foreach ($this->statuses as $st) { + $st->haslogs = attendance_has_logs_for_status($st->id); + } + + $this->att = $att; + } + + /** + * url helper function + * @param array $params + * @param bool $significantparams + * @return moodle_url + */ + public function url($params=array(), $significantparams=true) { + if ($significantparams) { + $params = array_merge($this->att->pageparams->get_significant_params(), $params); + } + + return $this->att->url_preferences($params); + } +} + +/** + * Default status set + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_default_statusset implements renderable { + /** @var array */ + public $statuses; + /** @var array */ + public $errors; + + /** + * attendance_default_statusset constructor. + * @param array $statuses + * @param array $errors + */ + public function __construct($statuses, $errors) { + $this->statuses = $statuses; + $this->errors = $errors; + } + + /** + * url helper. + * @param stdClass $params + * @return moodle_url + */ + public function url($params) { + return new moodle_url('/mod/attendance/defaultstatus.php', $params); + } +} + +/** + * Output a selector to change between status sets. + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_set_selector implements renderable { + /** @var int */ + public $maxstatusset; + /** @var mod_attendance_structure */ + private $att; + + /** + * attendance_set_selector constructor. + * @param mod_attendance_structure $att + * @param int $maxstatusset + */ + public function __construct(mod_attendance_structure $att, $maxstatusset) { + $this->att = $att; + $this->maxstatusset = $maxstatusset; + } + + /** + * url helper + * @param array $statusset + * @return moodle_url + */ + public function url($statusset) { + $params = array(); + $params['statusset'] = $statusset; + + return $this->att->url_preferences($params); + } + + /** + * get current statusset. + * @return int + */ + public function get_current_statusset() { + if (isset($this->att->pageparams->statusset)) { + return $this->att->pageparams->statusset; + } + return 0; + } + + /** + * get statusset name. + * @param int $statusset + * @return string + */ + public function get_status_name($statusset) { + return attendance_get_setname($this->att->id, $statusset, true); + } +} + +/** + * Url helpers + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class url_helpers { + /** + * Url take. + * @param stdClass $att + * @param int $sessionid + * @param int $grouptype + * @return mixed + */ + public static function url_take($att, $sessionid, $grouptype) { + $params = array('sessionid' => $sessionid); + if (isset($grouptype)) { + $params['grouptype'] = $grouptype; + } + + return $att->url_take($params); + } + + /** + * Must be called without or with both parameters + * @param stdClass $att + * @param null $sessionid + * @param null $action + * @return mixed + */ + public static function url_sessions($att, $sessionid=null, $action=null) { + if (isset($sessionid) && isset($action)) { + $params = array('sessionid' => $sessionid, 'action' => $action); + } else { + $params = array(); + } + + return $att->url_sessions($params); + } + + /** + * Url view helper. + * @param stdClass $att + * @param array $params + * @return mixed + */ + public static function url_view($att, $params=array()) { + return $att->url_view($params); + } +} + +/** + * Data structure representing an attendance password icon. + * + * @copyright 2017 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class attendance_password_icon implements renderable, templatable { + + /** + * @var string text to show + */ + public $text; + + /** + * @var string Extra descriptive text next to the icon + */ + public $linktext = null; + + /** + * Constructor + * + * @param string $text string for help page title, + * string with _help suffix is used for the actual help text. + * string with _link suffix is used to create a link to further info (if it exists) + * @param string $sessionid + */ + public function __construct($text, $sessionid) { + $this->text = $text; + $this->sessionid = $sessionid; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return array + */ + public function export_for_template(renderer_base $output) { + + $title = get_string('password', 'attendance'); + + $data = new stdClass(); + $data->heading = ''; + $data->text = $this->text; + + if ($this->includeqrcode == 1) { + $pix = 'qrcode'; + } else { + $pix = 'key'; + } + + $data->alt = $title; + $data->icon = (new pix_icon($pix, '', 'attendance'))->export_for_template($output); + $data->linktext = ''; + $data->title = $title; + $data->url = (new moodle_url('/mod/attendance/password.php', [ + 'session' => $this->sessionid]))->out(false); + + $data->ltr = !right_to_left(); + return $data; + } +} diff --git a/mod/attendance/renderer.php b/mod/attendance/renderer.php new file mode 100644 index 0000000..00036de --- /dev/null +++ b/mod/attendance/renderer.php @@ -0,0 +1,2828 @@ +<?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/>. + +/** + * Attendance module renderering methods + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/locallib.php'); +require_once(dirname(__FILE__).'/renderables.php'); +require_once(dirname(__FILE__).'/renderhelpers.php'); +require_once($CFG->libdir.'/tablelib.php'); +require_once($CFG->libdir.'/moodlelib.php'); + +/** + * Attendance module renderer class + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_renderer extends plugin_renderer_base { + // External API - methods to render attendance renderable components. + + /** + * Renders tabs for attendance + * + * @param attendance_tabs $atttabs - tabs to display + * @return string html code + */ + protected function render_attendance_tabs(attendance_tabs $atttabs) { + return print_tabs($atttabs->get_tabs(), $atttabs->currenttab, null, null, true); + } + + /** + * Renders filter controls for attendance + * + * @param attendance_filter_controls $fcontrols - filter controls data to display + * @return string html code + */ + protected function render_attendance_filter_controls(attendance_filter_controls $fcontrols) { + $classes = 'attfiltercontrols'; + $filtertable = new html_table(); + $filtertable->attributes['class'] = ' '; + $filtertable->width = '100%'; + $filtertable->align = array('left', 'center', 'right', 'right'); + + if (property_exists($fcontrols->pageparams, 'mode') && + $fcontrols->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $classes .= ' float-right'; + + $row = array(); + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = $this->render_grouping_controls($fcontrols); + $filtertable->data[] = $row; + + $row = array(); + $row[] = ''; + $row[] = ''; + $row[] = ''; + $row[] = $this->render_course_controls($fcontrols); + $filtertable->data[] = $row; + } + + $row = array(); + + $row[] = $this->render_sess_group_selector($fcontrols); + $row[] = $this->render_curdate_controls($fcontrols); + $row[] = $this->render_paging_controls($fcontrols); + $row[] = $this->render_view_controls($fcontrols); + + $filtertable->data[] = $row; + + $o = html_writer::table($filtertable); + $o = $this->output->container($o, $classes); + + return $o; + } + + /** + * Render group selector + * + * @param attendance_filter_controls $fcontrols + * @return mixed|string + */ + protected function render_sess_group_selector(attendance_filter_controls $fcontrols) { + switch ($fcontrols->pageparams->selectortype) { + case mod_attendance_page_with_filter_controls::SELECTOR_SESS_TYPE: + $sessgroups = $fcontrols->get_sess_groups_list(); + if ($sessgroups) { + $select = new single_select($fcontrols->url(), 'group', $sessgroups, + $fcontrols->get_current_sesstype(), null, 'selectgroup'); + $select->label = get_string('sessions', 'attendance'); + $output = $this->output->render($select); + + return html_writer::tag('div', $output, array('class' => 'groupselector')); + } + break; + case mod_attendance_page_with_filter_controls::SELECTOR_GROUP: + return groups_print_activity_menu($fcontrols->cm, $fcontrols->url(), true); + } + + return ''; + } + + /** + * Render paging controls. + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_paging_controls(attendance_filter_controls $fcontrols) { + $pagingcontrols = ''; + + $group = 0; + if (!empty($fcontrols->pageparams->group)) { + $group = $fcontrols->pageparams->group; + } + + $totalusers = count_enrolled_users(context_module::instance($fcontrols->cm->id), 'mod/attendance:canbelisted', $group); + + if (empty($fcontrols->pageparams->page) || !$fcontrols->pageparams->page || !$totalusers || + empty($fcontrols->pageparams->perpage)) { + + return $pagingcontrols; + } + + $numberofpages = ceil($totalusers / $fcontrols->pageparams->perpage); + + if ($fcontrols->pageparams->page > 1) { + $pagingcontrols .= html_writer::link($fcontrols->url(array('curdate' => $fcontrols->curdate, + 'page' => $fcontrols->pageparams->page - 1)), + $this->output->larrow()); + } + $a = new stdClass(); + $a->page = $fcontrols->pageparams->page; + $a->numpages = $numberofpages; + $text = get_string('pageof', 'attendance', $a); + $pagingcontrols .= html_writer::tag('span', $text, + array('class' => 'attbtn')); + if ($fcontrols->pageparams->page < $numberofpages) { + $pagingcontrols .= html_writer::link($fcontrols->url(array('curdate' => $fcontrols->curdate, + 'page' => $fcontrols->pageparams->page + 1)), + $this->output->rarrow()); + } + + return $pagingcontrols; + } + + /** + * Render date controls. + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_curdate_controls(attendance_filter_controls $fcontrols) { + global $CFG; + + $curdatecontrols = ''; + if ($fcontrols->curdatetxt) { + $this->page->requires->strings_for_js(array('calclose', 'caltoday'), 'attendance'); + $jsvals = array( + 'cal_months' => explode(',', get_string('calmonths', 'attendance')), + 'cal_week_days' => explode(',', get_string('calweekdays', 'attendance')), + 'cal_start_weekday' => $CFG->calendar_startwday, + 'cal_cur_date' => $fcontrols->curdate); + $curdatecontrols = html_writer::script(js_writer::set_variable('M.attendance', $jsvals)); + + $this->page->requires->js('/mod/attendance/calendar.js'); + + $curdatecontrols .= html_writer::link($fcontrols->url(array('curdate' => $fcontrols->prevcur)), + $this->output->larrow()); + $params = array( + 'title' => get_string('calshow', 'attendance'), + 'id' => 'show', + 'class' => 'btn btn-secondary', + 'type' => 'button'); + $buttonform = html_writer::tag('button', $fcontrols->curdatetxt, $params); + foreach ($fcontrols->url_params(array('curdate' => '')) as $name => $value) { + $params = array( + 'type' => 'hidden', + 'id' => $name, + 'name' => $name, + 'value' => $value); + $buttonform .= html_writer::empty_tag('input', $params); + } + $params = array( + 'id' => 'currentdate', + 'action' => $fcontrols->url_path(), + 'method' => 'post' + ); + + $buttonform = html_writer::tag('form', $buttonform, $params); + $curdatecontrols .= $buttonform; + + $curdatecontrols .= html_writer::link($fcontrols->url(array('curdate' => $fcontrols->nextcur)), + $this->output->rarrow()); + } + + return $curdatecontrols; + } + + /** + * Render grouping controls (for all sessions report). + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_grouping_controls(attendance_filter_controls $fcontrols) { + if ($fcontrols->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $groupoptions = array( + 'date' => get_string('sessionsbydate', 'attendance'), + 'activity' => get_string('sessionsbyactivity', 'attendance'), + 'course' => get_string('sessionsbycourse', 'attendance') + ); + $groupcontrols = get_string('groupsessionsby', 'attendance') . ":"; + foreach ($groupoptions as $key => $opttext) { + if ($key != $fcontrols->pageparams->groupby) { + $link = html_writer::link($fcontrols->url(array('groupby' => $key)), $opttext); + $groupcontrols .= html_writer::tag('span', $link, array('class' => 'attbtn')); + } else { + $groupcontrols .= html_writer::tag('span', $opttext, array('class' => 'attcurbtn')); + } + } + return html_writer::tag('nobr', $groupcontrols); + } + return ""; + } + + /** + * Render course controls (for all sessions report). + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_course_controls(attendance_filter_controls $fcontrols) { + if ($fcontrols->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $courseoptions = array( + 'all' => get_string('sessionsallcourses', 'attendance'), + 'current' => get_string('sessionscurrentcourses', 'attendance') + ); + $coursecontrols = ""; + foreach ($courseoptions as $key => $opttext) { + if ($key != $fcontrols->pageparams->sesscourses) { + $link = html_writer::link($fcontrols->url(array('sesscourses' => $key)), $opttext); + $coursecontrols .= html_writer::tag('span', $link, array('class' => 'attbtn')); + } else { + $coursecontrols .= html_writer::tag('span', $opttext, array('class' => 'attcurbtn')); + } + } + return html_writer::tag('nobr', $coursecontrols); + } + return ""; + } + + /** + * Render view controls. + * + * @param attendance_filter_controls $fcontrols + * @return string + */ + protected function render_view_controls(attendance_filter_controls $fcontrols) { + $views[ATT_VIEW_ALL] = get_string('all', 'attendance'); + $views[ATT_VIEW_ALLPAST] = get_string('allpast', 'attendance'); + $views[ATT_VIEW_MONTHS] = get_string('months', 'attendance'); + $views[ATT_VIEW_WEEKS] = get_string('weeks', 'attendance'); + $views[ATT_VIEW_DAYS] = get_string('days', 'attendance'); + if ($fcontrols->reportcontrol && $fcontrols->att->grade > 0) { + $a = $fcontrols->att->get_lowgrade_threshold() * 100; + $views[ATT_VIEW_NOTPRESENT] = get_string('below', 'attendance', $a); + } + if ($fcontrols->reportcontrol) { + $views[ATT_VIEW_SUMMARY] = get_string('summary', 'attendance'); + } + $viewcontrols = ''; + foreach ($views as $key => $sview) { + if ($key != $fcontrols->pageparams->view) { + $link = html_writer::link($fcontrols->url(array('view' => $key)), $sview); + $viewcontrols .= html_writer::tag('span', $link, array('class' => 'attbtn')); + } else { + $viewcontrols .= html_writer::tag('span', $sview, array('class' => 'attcurbtn')); + } + } + + return html_writer::tag('nobr', $viewcontrols); + } + + /** + * Renders attendance sessions managing table + * + * @param attendance_manage_data $sessdata to display + * @return string html code + */ + protected function render_attendance_manage_data(attendance_manage_data $sessdata) { + $o = $this->render_sess_manage_table($sessdata) . $this->render_sess_manage_control($sessdata); + $o = html_writer::tag('form', $o, array('method' => 'post', 'action' => $sessdata->url_sessions()->out())); + $o = $this->output->container($o, 'generalbox attwidth'); + $o = $this->output->container($o, 'attsessions_manage_table'); + + return $o; + } + + /** + * Render session manage table. + * + * @param attendance_manage_data $sessdata + * @return string + */ + protected function render_sess_manage_table(attendance_manage_data $sessdata) { + $this->page->requires->js_init_call('M.mod_attendance.init_manage'); + + $table = new html_table(); + $table->width = '100%'; + $table->head = array( + '#', + get_string('date', 'attendance'), + get_string('time', 'attendance'), + get_string('sessiontypeshort', 'attendance'), + get_string('description', 'attendance'), + get_string('actions'), + html_writer::checkbox('cb_selector', 0, false, '', array('id' => 'cb_selector')) + ); + $table->align = array('', 'right', '', '', 'left', 'right', 'center'); + $table->size = array('1px', '1px', '1px', '', '*', '120px', '1px'); + + $i = 0; + foreach ($sessdata->sessions as $key => $sess) { + $i++; + + $dta = $this->construct_date_time_actions($sessdata, $sess); + + $table->data[$sess->id][] = $i; + $table->data[$sess->id][] = $dta['date']; + $table->data[$sess->id][] = $dta['time']; + if ($sess->groupid) { + if (empty($sessdata->groups[$sess->groupid])) { + $table->data[$sess->id][] = get_string('deletedgroup', 'attendance'); + // Remove actions and links on date/time. + $dta['actions'] = ''; + $dta['date'] = userdate($sess->sessdate, get_string('strftimedmyw', 'attendance')); + $dta['time'] = $this->construct_time($sess->sessdate, $sess->duration); + } else { + $table->data[$sess->id][] = get_string('group') . ': ' . $sessdata->groups[$sess->groupid]->name; + } + } else { + $table->data[$sess->id][] = get_string('commonsession', 'attendance'); + } + $table->data[$sess->id][] = $sess->description; + $table->data[$sess->id][] = $dta['actions']; + $table->data[$sess->id][] = html_writer::checkbox('sessid[]', $sess->id, false, '', + array('class' => 'attendancesesscheckbox')); + } + + return html_writer::table($table); + } + + /** + * Implementation of user image rendering. + * + * @param attendance_password_icon $helpicon A help icon instance + * @return string HTML fragment + */ + protected function render_attendance_password_icon(attendance_password_icon $helpicon) { + return $this->render_from_template('attendance/attendance_password_icon', $helpicon->export_for_template($this)); + } + /** + * Construct date time actions. + * + * @param attendance_manage_data $sessdata + * @param stdClass $sess + * @return array + */ + private function construct_date_time_actions(attendance_manage_data $sessdata, $sess) { + $actions = ''; + if ((!empty($sess->studentpassword) || ($sess->includeqrcode == 1)) && + (has_capability('mod/attendance:manageattendances', $sessdata->att->context) || + has_capability('mod/attendance:takeattendances', $sessdata->att->context) || + has_capability('mod/attendance:changeattendances', $sessdata->att->context))) { + + $icon = new attendance_password_icon($sess->studentpassword, $sess->id); + + if ($sess->includeqrcode == 1||$sess->rotateqrcode == 1) { + $icon->includeqrcode = 1; + } else { + $icon->includeqrcode = 0; + } + + $actions .= $this->render($icon); + } + + $date = userdate($sess->sessdate, get_string('strftimedmyw', 'attendance')); + $time = $this->construct_time($sess->sessdate, $sess->duration); + if ($sess->lasttaken > 0) { + if (has_capability('mod/attendance:changeattendances', $sessdata->att->context)) { + $url = $sessdata->url_take($sess->id, $sess->groupid); + $title = get_string('changeattendance', 'attendance'); + + $date = html_writer::link($url, $date, array('title' => $title)); + $time = html_writer::link($url, $time, array('title' => $title)); + + $actions .= $this->output->action_icon($url, new pix_icon('redo', $title, 'attendance')); + } else { + $date = '<i>' . $date . '</i>'; + $time = '<i>' . $time . '</i>'; + } + } else { + if (has_capability('mod/attendance:takeattendances', $sessdata->att->context)) { + $url = $sessdata->url_take($sess->id, $sess->groupid); + $title = get_string('takeattendance', 'attendance'); + $actions .= $this->output->action_icon($url, new pix_icon('t/go', $title)); + } + } + + if (has_capability('mod/attendance:manageattendances', $sessdata->att->context)) { + $url = $sessdata->url_sessions($sess->id, mod_attendance_sessions_page_params::ACTION_UPDATE); + $title = get_string('editsession', 'attendance'); + $actions .= $this->output->action_icon($url, new pix_icon('t/edit', $title)); + + $url = $sessdata->url_sessions($sess->id, mod_attendance_sessions_page_params::ACTION_DELETE); + $title = get_string('deletesession', 'attendance'); + $actions .= $this->output->action_icon($url, new pix_icon('t/delete', $title)); + } + + return array('date' => $date, 'time' => $time, 'actions' => $actions); + } + + /** + * Render session manage control. + * + * @param attendance_manage_data $sessdata + * @return string + */ + protected function render_sess_manage_control(attendance_manage_data $sessdata) { + $table = new html_table(); + $table->attributes['class'] = ' '; + $table->width = '100%'; + $table->align = array('left', 'right'); + + $table->data[0][] = $this->output->help_icon('hiddensessions', 'attendance', + get_string('hiddensessions', 'attendance').': '.$sessdata->hiddensessionscount); + + if (has_capability('mod/attendance:manageattendances', $sessdata->att->context)) { + if ($sessdata->hiddensessionscount > 0) { + $attributes = array( + 'type' => 'submit', + 'name' => 'deletehiddensessions', + 'class' => 'btn btn-secondary', + 'value' => get_string('deletehiddensessions', 'attendance')); + $table->data[1][] = html_writer::empty_tag('input', $attributes); + } + + $options = array(mod_attendance_sessions_page_params::ACTION_DELETE_SELECTED => get_string('delete'), + mod_attendance_sessions_page_params::ACTION_CHANGE_DURATION => get_string('changeduration', 'attendance')); + + $controls = html_writer::select($options, 'action'); + $attributes = array( + 'type' => 'submit', + 'name' => 'ok', + 'value' => get_string('ok'), + 'class' => 'btn btn-secondary'); + $controls .= html_writer::empty_tag('input', $attributes); + } else { + $controls = get_string('youcantdo', 'attendance'); // You can't do anything. + } + $table->data[0][] = $controls; + + return html_writer::table($table); + } + + /** + * Render take data. + * + * @param attendance_take_data $takedata + * @return string + */ + protected function render_attendance_take_data(attendance_take_data $takedata) { + user_preference_allow_ajax_update('mod_attendance_statusdropdown', PARAM_TEXT); + + $controls = $this->render_attendance_take_controls($takedata); + $table = html_writer::start_div('no-overflow'); + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_LIST) { + $table .= $this->render_attendance_take_list($takedata); + } else { + $table .= $this->render_attendance_take_grid($takedata); + } + $table .= html_writer::input_hidden_params($takedata->url(array('sesskey' => sesskey(), + 'page' => $takedata->pageparams->page, + 'perpage' => $takedata->pageparams->perpage))); + $table .= html_writer::end_div(); + $params = array( + 'type' => 'submit', + 'class' => 'btn btn-primary', + 'value' => get_string('save', 'attendance')); + $table .= html_writer::tag('center', html_writer::empty_tag('input', $params)); + $table = html_writer::tag('form', $table, array('method' => 'post', 'action' => $takedata->url_path(), + 'id' => 'attendancetakeform')); + + foreach ($takedata->statuses as $status) { + $sessionstats[$status->id] = 0; + } + // Calculate the sum of statuses for each user. + $sessionstats[] = array(); + foreach ($takedata->sessionlog as $userlog) { + foreach ($takedata->statuses as $status) { + if ($userlog->statusid == $status->id && in_array($userlog->studentid, array_keys($takedata->users))) { + $sessionstats[$status->id]++; + } + } + } + + $statsoutput = '<br/>'; + foreach ($takedata->statuses as $status) { + $statsoutput .= "$status->description = ".$sessionstats[$status->id]." <br/>"; + } + + return $controls.$table.$statsoutput; + } + + /** + * Render take controls. + * + * @param attendance_take_data $takedata + * @return string + */ + protected function render_attendance_take_controls(attendance_take_data $takedata) { + + $urlparams = array('id' => $takedata->cm->id, + 'sessionid' => $takedata->pageparams->sessionid, + 'grouptype' => $takedata->pageparams->grouptype); + $url = new moodle_url('/mod/attendance/import/marksessions.php', $urlparams); + $return = $this->output->single_button($url, get_string('uploadattendance', 'attendance')); + + $table = new html_table(); + $table->attributes['class'] = ' '; + + $table->data[0][] = $this->construct_take_session_info($takedata); + $table->data[0][] = $this->construct_take_controls($takedata); + + $return .= $this->output->container(html_writer::table($table), 'generalbox takecontrols'); + return $return; + } + + /** + * Construct take session info. + * + * @param attendance_take_data $takedata + * @return string + */ + private function construct_take_session_info(attendance_take_data $takedata) { + $sess = $takedata->sessioninfo; + $date = userdate($sess->sessdate, get_string('strftimedate')); + $starttime = attendance_strftimehm($sess->sessdate); + $endtime = attendance_strftimehm($sess->sessdate + $sess->duration); + $time = html_writer::tag('nobr', $starttime . ($sess->duration > 0 ? ' - ' . $endtime : '')); + $sessinfo = $date.' '.$time; + $sessinfo .= html_writer::empty_tag('br'); + $sessinfo .= html_writer::empty_tag('br'); + $sessinfo .= $sess->description; + + return $sessinfo; + } + + /** + * Construct take controls. + * + * @param attendance_take_data $takedata + * @return string + */ + private function construct_take_controls(attendance_take_data $takedata) { + + $controls = ''; + $context = context_module::instance($takedata->cm->id); + $group = 0; + if ($takedata->pageparams->grouptype != mod_attendance_structure::SESSION_COMMON) { + $group = $takedata->pageparams->grouptype; + } else { + if ($takedata->pageparams->group) { + $group = $takedata->pageparams->group; + } + } + + if (!empty($takedata->cm->groupingid)) { + if ($group == 0) { + $groups = array_keys(groups_get_all_groups($takedata->cm->course, 0, $takedata->cm->groupingid, 'g.id')); + } else { + $groups = $group; + } + $users = get_users_by_capability($context, 'mod/attendance:canbelisted', + 'u.id, u.firstname, u.lastname, u.email', + '', '', '', $groups, + '', false, true); + $totalusers = count($users); + } else { + $totalusers = count_enrolled_users($context, 'mod/attendance:canbelisted', $group); + } + $usersperpage = $takedata->pageparams->perpage; + if (!empty($takedata->pageparams->page) && $takedata->pageparams->page && $totalusers && $usersperpage) { + $controls .= html_writer::empty_tag('br'); + $numberofpages = ceil($totalusers / $usersperpage); + + if ($takedata->pageparams->page > 1) { + $controls .= html_writer::link($takedata->url(array('page' => $takedata->pageparams->page - 1)), + $this->output->larrow()); + } + $a = new stdClass(); + $a->page = $takedata->pageparams->page; + $a->numpages = $numberofpages; + $text = get_string('pageof', 'attendance', $a); + $controls .= html_writer::tag('span', $text, + array('class' => 'attbtn')); + if ($takedata->pageparams->page < $numberofpages) { + $controls .= html_writer::link($takedata->url(array('page' => $takedata->pageparams->page + 1, + 'perpage' => $takedata->pageparams->perpage)), $this->output->rarrow()); + } + } + + if ($takedata->pageparams->grouptype == mod_attendance_structure::SESSION_COMMON and + ($takedata->groupmode == VISIBLEGROUPS or + ($takedata->groupmode and has_capability('moodle/site:accessallgroups', $context)))) { + $controls .= groups_print_activity_menu($takedata->cm, $takedata->url(), true); + } + + $controls .= html_writer::empty_tag('br'); + + $options = array( + mod_attendance_take_page_params::SORTED_LIST => get_string('sortedlist', 'attendance'), + mod_attendance_take_page_params::SORTED_GRID => get_string('sortedgrid', 'attendance')); + $select = new single_select($takedata->url(), 'viewmode', $options, $takedata->pageparams->viewmode, null); + $select->set_label(get_string('viewmode', 'attendance')); + $select->class = 'singleselect inline'; + $controls .= $this->output->render($select); + + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_LIST) { + $options = array( + 0 => get_string('donotusepaging', 'attendance'), + get_config('attendance', 'resultsperpage') => get_config('attendance', 'resultsperpage')); + $select = new single_select($takedata->url(), 'perpage', $options, $takedata->pageparams->perpage, null); + $select->class = 'singleselect inline'; + $controls .= $this->output->render($select); + } + + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID) { + $options = array (1 => '1 '.get_string('column', 'attendance'), '2 '.get_string('columns', 'attendance'), + '3 '.get_string('columns', 'attendance'), '4 '.get_string('columns', 'attendance'), + '5 '.get_string('columns', 'attendance'), '6 '.get_string('columns', 'attendance'), + '7 '.get_string('columns', 'attendance'), '8 '.get_string('columns', 'attendance'), + '9 '.get_string('columns', 'attendance'), '10 '.get_string('columns', 'attendance')); + $select = new single_select($takedata->url(), 'gridcols', $options, $takedata->pageparams->gridcols, null); + $select->class = 'singleselect inline'; + $controls .= $this->output->render($select); + } + + if (isset($takedata->sessions4copy) && count($takedata->sessions4copy) > 0) { + $controls .= html_writer::empty_tag('br'); + $controls .= html_writer::empty_tag('br'); + + $options = array(); + foreach ($takedata->sessions4copy as $sess) { + $start = attendance_strftimehm($sess->sessdate); + $end = $sess->duration ? ' - '.attendance_strftimehm($sess->sessdate + $sess->duration) : ''; + $options[$sess->id] = $start . $end; + } + $select = new single_select($takedata->url(array(), array('copyfrom')), 'copyfrom', $options); + $select->set_label(get_string('copyfrom', 'attendance')); + $select->class = 'singleselect inline'; + $controls .= $this->output->render($select); + } + + return $controls; + } + + /** + * get statusdropdown + * + * @return \single_select + */ + private function statusdropdown() { + $pref = get_user_preferences('mod_attendance_statusdropdown'); + if (empty($pref)) { + $pref = 'unselected'; + } + $options = array('all' => get_string('statusall', 'attendance'), + 'unselected' => get_string('statusunselected', 'attendance')); + + $select = new \single_select(new \moodle_url('/'), 'setallstatus-select', $options, + $pref, null, 'setallstatus-select'); + $select->label = get_string('setallstatuses', 'attendance'); + + return $select; + } + + /** + * Render take list. + * + * @param attendance_take_data $takedata + * @return string + */ + protected function render_attendance_take_list(attendance_take_data $takedata) { + global $CFG; + $table = new html_table(); + $table->width = '0%'; + $table->head = array( + '#', + $this->construct_fullname_head($takedata) + ); + $table->align = array('left', 'left'); + $table->size = array('20px', ''); + $table->wrap[1] = 'nowrap'; + // Check if extra useridentity fields need to be added. + $extrasearchfields = array(); + if (!empty($CFG->showuseridentity) && has_capability('moodle/site:viewuseridentity', $takedata->att->context)) { + $extrasearchfields = explode(',', $CFG->showuseridentity); + } + foreach ($extrasearchfields as $field) { + $table->head[] = get_string($field); + $table->align[] = 'left'; + } + foreach ($takedata->statuses as $st) { + $table->head[] = html_writer::link("#", $st->acronym, array('id' => 'checkstatus'.$st->id, + 'title' => get_string('setallstatusesto', 'attendance', $st->description))); + $table->align[] = 'center'; + $table->size[] = '20px'; + // JS to select all radios of this status and prevent default behaviour of # link. + $this->page->requires->js_amd_inline(" + require(['jquery'], function($) { + $('#checkstatus".$st->id."').click(function(e) { + if ($('select[name=\"setallstatus-select\"] option:selected').val() == 'all') { + $('#attendancetakeform').find('.st".$st->id."').prop('checked', true); + M.util.set_user_preference('mod_attendance_statusdropdown','all'); + } + else { + $('#attendancetakeform').find('input:indeterminate.st".$st->id."').prop('checked', true); + M.util.set_user_preference('mod_attendance_statusdropdown','unselected'); + } + e.preventDefault(); + }); + });"); + + } + + $table->head[] = get_string('remarks', 'attendance'); + $table->align[] = 'center'; + $table->size[] = '20px'; + $table->attributes['class'] = 'generaltable takelist'; + + // Show a 'select all' row of radio buttons. + $row = new html_table_row(); + $row->attributes['class'] = 'setallstatusesrow'; + foreach ($extrasearchfields as $field) { + $row->cells[] = ''; + } + + $cell = new html_table_cell(html_writer::div($this->output->render($this->statusdropdown()), 'setallstatuses')); + $cell->colspan = 2; + $row->cells[] = $cell; + foreach ($takedata->statuses as $st) { + $attribs = array( + 'id' => 'radiocheckstatus'.$st->id, + 'type' => 'radio', + 'title' => get_string('setallstatusesto', 'attendance', $st->description), + 'name' => 'setallstatuses', + 'class' => "st{$st->id}", + ); + $row->cells[] = html_writer::empty_tag('input', $attribs); + // Select all radio buttons of the same status. + $this->page->requires->js_amd_inline(" + require(['jquery'], function($) { + $('#radiocheckstatus".$st->id."').click(function(e) { + if ($('select[name=\"setallstatus-select\"] option:selected').val() == 'all') { + $('#attendancetakeform').find('.st".$st->id."').prop('checked', true); + M.util.set_user_preference('mod_attendance_statusdropdown','all'); + } + else { + $('#attendancetakeform').find('input:indeterminate.st".$st->id."').prop('checked', true); + M.util.set_user_preference('mod_attendance_statusdropdown','unselected'); + } + }); + });"); + } + $row->cells[] = ''; + $table->data[] = $row; + + $i = 0; + foreach ($takedata->users as $user) { + $i++; + $row = new html_table_row(); + $row->cells[] = $i; + $fullname = html_writer::link($takedata->url_view(array('studentid' => $user->id)), fullname($user)); + $fullname = $this->user_picture($user).$fullname; // Show different picture if it is a temporary user. + + $ucdata = $this->construct_take_user_controls($takedata, $user); + if (array_key_exists('warning', $ucdata)) { + $fullname .= html_writer::empty_tag('br'); + $fullname .= $ucdata['warning']; + } + $row->cells[] = $fullname; + foreach ($extrasearchfields as $field) { + $row->cells[] = $user->$field; + } + + if (array_key_exists('colspan', $ucdata)) { + $cell = new html_table_cell($ucdata['text']); + $cell->colspan = $ucdata['colspan']; + $row->cells[] = $cell; + } else { + $row->cells = array_merge($row->cells, $ucdata['text']); + } + + if (array_key_exists('class', $ucdata)) { + $row->attributes['class'] = $ucdata['class']; + } + + $table->data[] = $row; + } + + return html_writer::table($table); + } + + /** + * Render take grid. + * + * @param attendance_take_data $takedata + * @return string + */ + protected function render_attendance_take_grid(attendance_take_data $takedata) { + $table = new html_table(); + for ($i = 0; $i < $takedata->pageparams->gridcols; $i++) { + $table->align[] = 'center'; + $table->size[] = '110px'; + } + $table->attributes['class'] = 'generaltable takegrid'; + $table->headspan = $takedata->pageparams->gridcols; + + $head = array(); + $head[] = html_writer::div($this->output->render($this->statusdropdown()), 'setallstatuses'); + foreach ($takedata->statuses as $st) { + $head[] = html_writer::link("#", $st->acronym, array('id' => 'checkstatus'.$st->id, + 'title' => get_string('setallstatusesto', 'attendance', $st->description))); + // JS to select all radios of this status and prevent default behaviour of # link. + $this->page->requires->js_amd_inline(" + require(['jquery'], function($) { + $('#checkstatus".$st->id."').click(function(e) { + if ($('select[name=\"setallstatus-select\"] option:selected').val() == 'unselected') { + $('#attendancetakeform').find('input:indeterminate.st".$st->id."').prop('checked', true); + M.util.set_user_preference('mod_attendance_statusdropdown','unselected'); + } + else { + $('#attendancetakeform').find('.st".$st->id."').prop('checked', true); + M.util.set_user_preference('mod_attendance_statusdropdown','all'); + } + e.preventDefault(); + }); + });"); + } + $table->head[] = implode(' ', $head); + + $i = 0; + $row = new html_table_row(); + foreach ($takedata->users as $user) { + $celltext = $this->user_picture($user, array('size' => 100)); // Show different picture if it is a temporary user. + $celltext .= html_writer::empty_tag('br'); + $fullname = html_writer::link($takedata->url_view(array('studentid' => $user->id)), fullname($user)); + $celltext .= html_writer::tag('span', $fullname, array('class' => 'fullname')); + $celltext .= html_writer::empty_tag('br'); + $ucdata = $this->construct_take_user_controls($takedata, $user); + $celltext .= is_array($ucdata['text']) ? implode('', $ucdata['text']) : $ucdata['text']; + if (array_key_exists('warning', $ucdata)) { + $celltext .= html_writer::empty_tag('br'); + $celltext .= $ucdata['warning']; + } + + $cell = new html_table_cell($celltext); + if (array_key_exists('class', $ucdata)) { + $cell->attributes['class'] = $ucdata['class']; + } + $row->cells[] = $cell; + + $i++; + if ($i % $takedata->pageparams->gridcols == 0) { + $table->data[] = $row; + $row = new html_table_row(); + } + } + if ($i % $takedata->pageparams->gridcols > 0) { + $table->data[] = $row; + } + + return html_writer::table($table); + } + + /** + * Construct full name. + * + * @param stdClass $data + * @return string + */ + private function construct_fullname_head($data) { + global $CFG; + + $url = $data->url(); + if ($data->pageparams->sort == ATT_SORT_LASTNAME) { + $url->param('sort', ATT_SORT_FIRSTNAME); + $firstname = html_writer::link($url, get_string('firstname')); + $lastname = get_string('lastname'); + } else if ($data->pageparams->sort == ATT_SORT_FIRSTNAME) { + $firstname = get_string('firstname'); + $url->param('sort', ATT_SORT_LASTNAME); + $lastname = html_writer::link($url, get_string('lastname')); + } else { + $firstname = html_writer::link($data->url(array('sort' => ATT_SORT_FIRSTNAME)), get_string('firstname')); + $lastname = html_writer::link($data->url(array('sort' => ATT_SORT_LASTNAME)), get_string('lastname')); + } + + if ($CFG->fullnamedisplay == 'lastname firstname') { + $fullnamehead = "$lastname / $firstname"; + } else { + $fullnamehead = "$firstname / $lastname "; + } + + return $fullnamehead; + } + + /** + * Construct take user controls. + * + * @param attendance_take_data $takedata + * @param stdClass $user + * @return array + */ + private function construct_take_user_controls(attendance_take_data $takedata, $user) { + $celldata = array(); + if ($user->enrolmentend and $user->enrolmentend < $takedata->sessioninfo->sessdate) { + $celldata['text'] = get_string('enrolmentend', 'attendance', userdate($user->enrolmentend, '%d.%m.%Y')); + $celldata['colspan'] = count($takedata->statuses) + 1; + $celldata['class'] = 'userwithoutenrol'; + } else if (!$user->enrolmentend and $user->enrolmentstatus == ENROL_USER_SUSPENDED) { + // No enrolmentend and ENROL_USER_SUSPENDED. + $celldata['text'] = get_string('enrolmentsuspended', 'attendance'); + $celldata['colspan'] = count($takedata->statuses) + 1; + $celldata['class'] = 'userwithoutenrol'; + } else { + if ($takedata->updatemode and !array_key_exists($user->id, $takedata->sessionlog)) { + $celldata['class'] = 'userwithoutdata'; + } + + $celldata['text'] = array(); + foreach ($takedata->statuses as $st) { + $params = array( + 'type' => 'radio', + 'name' => 'user'.$user->id, + 'class' => 'st'.$st->id, + 'value' => $st->id); + if (array_key_exists($user->id, $takedata->sessionlog) and $st->id == $takedata->sessionlog[$user->id]->statusid) { + $params['checked'] = ''; + } + + $input = html_writer::empty_tag('input', $params); + + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID) { + $input = html_writer::tag('nobr', $input . $st->acronym); + } + + $celldata['text'][] = $input; + } + $params = array( + 'type' => 'text', + 'name' => 'remarks'.$user->id, + 'maxlength' => 255); + if (array_key_exists($user->id, $takedata->sessionlog)) { + $params['value'] = $takedata->sessionlog[$user->id]->remarks; + } + $celldata['text'][] = html_writer::empty_tag('input', $params); + + if ($user->enrolmentstart > $takedata->sessioninfo->sessdate + $takedata->sessioninfo->duration) { + $celldata['warning'] = get_string('enrolmentstart', 'attendance', + userdate($user->enrolmentstart, '%H:%M %d.%m.%Y')); + $celldata['class'] = 'userwithoutenrol'; + } + } + + return $celldata; + } + + /** + * Construct take session controls. + * + * @param attendance_take_data $takedata + * @param stdClass $user + * @return array + */ + private function construct_take_session_controls(attendance_take_data $takedata, $user) { + $celldata = array(); + $celldata['remarks'] = ''; + if ($user->enrolmentend and $user->enrolmentend < $takedata->sessioninfo->sessdate) { + $celldata['text'] = get_string('enrolmentend', 'attendance', userdate($user->enrolmentend, '%d.%m.%Y')); + $celldata['colspan'] = count($takedata->statuses) + 1; + $celldata['class'] = 'userwithoutenrol'; + } else if (!$user->enrolmentend and $user->enrolmentstatus == ENROL_USER_SUSPENDED) { + // No enrolmentend and ENROL_USER_SUSPENDED. + $celldata['text'] = get_string('enrolmentsuspended', 'attendance'); + $celldata['colspan'] = count($takedata->statuses) + 1; + $celldata['class'] = 'userwithoutenrol'; + } else { + if ($takedata->updatemode and !array_key_exists($user->id, $takedata->sessionlog)) { + $celldata['class'] = 'userwithoutdata'; + } + + $celldata['text'] = array(); + foreach ($takedata->statuses as $st) { + $params = array( + 'type' => 'radio', + 'name' => 'user'.$user->id.'sess'.$takedata->sessioninfo->id, + 'class' => 'st'.$st->id, + 'value' => $st->id); + if (array_key_exists($user->id, $takedata->sessionlog) and $st->id == $takedata->sessionlog[$user->id]->statusid) { + $params['checked'] = ''; + } + + $input = html_writer::empty_tag('input', $params); + + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID) { + $input = html_writer::tag('nobr', $input . $st->acronym); + } + + $celldata['text'][] = $input; + } + $params = array( + 'type' => 'text', + 'name' => 'remarks'.$user->id.'sess'.$takedata->sessioninfo->id, + 'maxlength' => 255); + if (array_key_exists($user->id, $takedata->sessionlog)) { + $params['value'] = $takedata->sessionlog[$user->id]->remarks; + } + $input = html_writer::empty_tag('input', $params); + if ($takedata->pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID) { + $input = html_writer::empty_tag('br').$input; + } + $celldata['remarks'] = $input; + + if ($user->enrolmentstart > $takedata->sessioninfo->sessdate + $takedata->sessioninfo->duration) { + $celldata['warning'] = get_string('enrolmentstart', 'attendance', + userdate($user->enrolmentstart, '%H:%M %d.%m.%Y')); + $celldata['class'] = 'userwithoutenrol'; + } + } + + return $celldata; + } + + /** + * Render header. + * + * @param mod_attendance_header $header + * @return string + */ + protected function render_mod_attendance_header(mod_attendance_header $header) { + if (!$header->should_render()) { + return ''; + } + + $attendance = $header->get_attendance(); + + $heading = format_string($header->get_title(), false, ['context' => $attendance->context]); + $o = $this->output->heading($heading); + + $o .= $this->output->box_start('generalbox boxaligncenter', 'intro'); + $o .= format_module_intro('attendance', $attendance, $attendance->cm->id); + $o .= $this->output->box_end(); + + return $o; + } + + /** + * Render user data. + * + * @param attendance_user_data $userdata + * @return string + */ + protected function render_attendance_user_data(attendance_user_data $userdata) { + global $USER; + + $o = $this->render_user_report_tabs($userdata); + + if ($USER->id == $userdata->user->id || + $userdata->pageparams->mode === mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + + $o .= $this->construct_user_data($userdata); + + } else { + + $table = new html_table(); + + $table->attributes['class'] = 'userinfobox'; + $table->colclasses = array('left side', ''); + // Show different picture if it is a temporary user. + $table->data[0][] = $this->user_picture($userdata->user, array('size' => 100)); + $table->data[0][] = $this->construct_user_data($userdata); + + $o .= html_writer::table($table); + } + + return $o; + } + + /** + * Render user report tabs. + * + * @param attendance_user_data $userdata + * @return string + */ + protected function render_user_report_tabs(attendance_user_data $userdata) { + $tabs = array(); + + $tabs[] = new tabobject(mod_attendance_view_page_params::MODE_THIS_COURSE, + $userdata->url()->out(true, array('mode' => mod_attendance_view_page_params::MODE_THIS_COURSE)), + get_string('thiscourse', 'attendance')); + + // Skip the 'all courses' and 'all sessions' tabs for 'temporary' users. + if ($userdata->user->type == 'standard') { + $tabs[] = new tabobject(mod_attendance_view_page_params::MODE_ALL_COURSES, + $userdata->url()->out(true, array('mode' => mod_attendance_view_page_params::MODE_ALL_COURSES)), + get_string('allcourses', 'attendance')); + $tabs[] = new tabobject(mod_attendance_view_page_params::MODE_ALL_SESSIONS, + $userdata->url()->out(true, array('mode' => mod_attendance_view_page_params::MODE_ALL_SESSIONS)), + get_string('allsessions', 'attendance')); + } + + return print_tabs(array($tabs), $userdata->pageparams->mode, null, null, true); + } + + /** + * Construct user data. + * + * @param attendance_user_data $userdata + * @return string + */ + private function construct_user_data(attendance_user_data $userdata) { + global $USER; + $o = ''; + if ($USER->id <> $userdata->user->id) { + $o = html_writer::tag('h2', fullname($userdata->user)); + } + + if ($userdata->pageparams->mode == mod_attendance_view_page_params::MODE_THIS_COURSE) { + $o .= $this->render_attendance_filter_controls($userdata->filtercontrols); + $o .= $this->construct_user_sessions_log($userdata); + $o .= html_writer::empty_tag('hr'); + $o .= construct_user_data_stat($userdata->summary->get_all_sessions_summary_for($userdata->user->id), + $userdata->pageparams->view); + } else if ($userdata->pageparams->mode == mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + $allsessions = $this->construct_user_allsessions_log($userdata); + $o .= html_writer::start_div('allsessionssummary'); + $o .= html_writer::start_div('float-left'); + $o .= html_writer::start_div('float-left'); + $o .= $this->user_picture($userdata->user, array('size' => 100, 'class' => 'userpicture float-left')); + $o .= html_writer::end_div(); + $o .= html_writer::start_div('float-right'); + $o .= $allsessions->summary; + $o .= html_writer::end_div(); + $o .= html_writer::end_div(); + $o .= html_writer::start_div('float-right'); + $o .= $this->render_attendance_filter_controls($userdata->filtercontrols); + $o .= html_writer::end_div(); + $o .= html_writer::end_div(); + $o .= $allsessions->detail; + } else { + $table = new html_table(); + $table->head = array(get_string('course'), + get_string('pluginname', 'mod_attendance'), + get_string('sessionscompleted', 'attendance'), + get_string('pointssessionscompleted', 'attendance'), + get_string('percentagesessionscompleted', 'attendance')); + $table->align = array('left', 'left', 'center', 'center', 'center'); + $table->colclasses = array('colcourse', 'colatt', 'colsessionscompleted', + 'colpointssessionscompleted', 'colpercentagesessionscompleted'); + + $table2 = clone($table); // Duplicate table for ungraded sessions. + $totalattendance = 0; + $totalpercentage = 0; + foreach ($userdata->coursesatts as $ca) { + $row = new html_table_row(); + $courseurl = new moodle_url('/course/view.php', array('id' => $ca->courseid)); + $row->cells[] = html_writer::link($courseurl, $ca->coursefullname); + $attendanceurl = new moodle_url('/mod/attendance/view.php', array('id' => $ca->cmid, + 'studentid' => $userdata->user->id, + 'view' => ATT_VIEW_ALL)); + $row->cells[] = html_writer::link($attendanceurl, $ca->attname); + $usersummary = new stdClass(); + if (isset($userdata->summary[$ca->attid])) { + $usersummary = $userdata->summary[$ca->attid]->get_all_sessions_summary_for($userdata->user->id); + + $row->cells[] = $usersummary->numtakensessions; + $row->cells[] = $usersummary->pointssessionscompleted; + if (empty($usersummary->numtakensessions)) { + $row->cells[] = '-'; + } else { + $row->cells[] = $usersummary->percentagesessionscompleted; + } + + } + if (empty($ca->attgrade)) { + $table2->data[] = $row; + } else { + $table->data[] = $row; + if ($usersummary->numtakensessions > 0) { + $totalattendance++; + $totalpercentage = $totalpercentage + format_float($usersummary->takensessionspercentage * 100); + } + } + } + $row = new html_table_row(); + if (empty($totalattendance)) { + $average = '-'; + } else { + $average = format_float($totalpercentage / $totalattendance).'%'; + } + + $col = new html_table_cell(get_string('averageattendancegraded', 'mod_attendance')); + $col->attributes['class'] = 'averageattendance'; + $col->colspan = 4; + + $col2 = new html_table_cell($average); + $col2->style = 'text-align: center'; + $row->cells = array($col, $col2); + $table->data[] = $row; + + if (!empty($table2->data) && !empty($table->data)) { + // Print graded header if both tables are being shown. + $o .= html_writer::div("<h3>".get_string('graded', 'mod_attendance')."</h3>"); + } + if (!empty($table->data)) { + // Don't bother printing the table if no sessions are being shown. + $o .= html_writer::table($table); + } + + if (!empty($table2->data)) { + // Don't print this if it doesn't contain any data. + $o .= html_writer::div("<h3>".get_string('ungraded', 'mod_attendance')."</h3>"); + $o .= html_writer::table($table2); + } + } + + return $o; + } + + /** + * Construct user sessions log. + * + * @param attendance_user_data $userdata + * @return string + */ + private function construct_user_sessions_log(attendance_user_data $userdata) { + global $USER; + $context = context_module::instance($userdata->filtercontrols->cm->id); + + $shortform = false; + if ($USER->id == $userdata->user->id) { + // This is a user viewing their own stuff - hide non-relevant columns. + $shortform = true; + } + + $table = new html_table(); + $table->attributes['class'] = 'generaltable attwidth boxaligncenter'; + $table->head = array(); + $table->align = array(); + $table->size = array(); + $table->colclasses = array(); + if (!$shortform) { + $table->head[] = get_string('sessiontypeshort', 'attendance'); + $table->align[] = ''; + $table->size[] = '1px'; + $table->colclasses[] = ''; + } + $table->head[] = get_string('date'); + $table->head[] = get_string('description', 'attendance'); + $table->head[] = get_string('status', 'attendance'); + $table->head[] = get_string('points', 'attendance'); + $table->head[] = get_string('remarks', 'attendance'); + + $table->align = array_merge($table->align, array('', 'left', 'center', 'center', 'center')); + $table->colclasses = array_merge($table->colclasses, array('datecol', 'desccol', 'statuscol', 'pointscol', 'remarkscol')); + $table->size = array_merge($table->size, array('1px', '*', '*', '1px', '*')); + + if (has_capability('mod/attendance:takeattendances', $context)) { + $table->head[] = get_string('action'); + $table->align[] = ''; + $table->size[] = ''; + } + + $statussetmaxpoints = attendance_get_statusset_maxpoints($userdata->statuses); + + $i = 0; + foreach ($userdata->sessionslog as $sess) { + $i++; + + $row = new html_table_row(); + if (!$shortform) { + if ($sess->groupid) { + $sessiontypeshort = get_string('group') . ': ' . $userdata->groups[$sess->groupid]->name; + } else { + $sessiontypeshort = get_string('commonsession', 'attendance'); + } + + $row->cells[] = html_writer::tag('nobr', $sessiontypeshort); + } + $row->cells[] = userdate($sess->sessdate, get_string('strftimedmyw', 'attendance')) . + " ". $this->construct_time($sess->sessdate, $sess->duration); + $row->cells[] = $sess->description; + if (!empty($sess->statusid)) { + $status = $userdata->statuses[$sess->statusid]; + $row->cells[] = $status->description; + $row->cells[] = format_float($status->grade, 1, true, true) . ' / ' . + format_float($statussetmaxpoints[$status->setnumber], 1, true, true); + $row->cells[] = $sess->remarks; + } else if (($sess->sessdate + $sess->duration) < $userdata->user->enrolmentstart) { + $cell = new html_table_cell(get_string('enrolmentstart', 'attendance', + userdate($userdata->user->enrolmentstart, '%d.%m.%Y'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else if ($userdata->user->enrolmentend and $sess->sessdate > $userdata->user->enrolmentend) { + $cell = new html_table_cell(get_string('enrolmentend', 'attendance', + userdate($userdata->user->enrolmentend, '%d.%m.%Y'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else { + list($canmark, $reason) = attendance_can_student_mark($sess, false); + if ($canmark) { + if ($sess->rotateqrcode == 1) { + $url = new moodle_url('/mod/attendance/attendance.php'); + $output = html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sessid', + 'value' => $sess->id)); + $output .= html_writer::empty_tag('input', array('type' => 'text', 'name' => 'qrpass', + 'placeholder' => "Enter password")); + $output .= html_writer::empty_tag('input', array('type' => 'submit', + 'value' => get_string('submit'), + 'class' => 'btn btn-secondary')); + $cell = new html_table_cell(html_writer::tag('form', $output, + array('action' => $url->out(), 'method' => 'get'))); + } else { + // Student can mark their own attendance. + // URL to the page that lets the student modify their attendance. + $url = new moodle_url('/mod/attendance/attendance.php', + array('sessid' => $sess->id, 'sesskey' => sesskey())); + $cell = new html_table_cell(html_writer::link($url, get_string('submitattendance', 'attendance'))); + } + $cell->colspan = 3; + $row->cells[] = $cell; + } else { // Student cannot mark their own attendace. + $row->cells[] = '?'; + $row->cells[] = '? / ' . format_float($statussetmaxpoints[$sess->statusset], 1, true, true); + $row->cells[] = ''; + } + } + + if (has_capability('mod/attendance:takeattendances', $context)) { + $params = array('id' => $userdata->filtercontrols->cm->id, + 'sessionid' => $sess->id, + 'grouptype' => $sess->groupid); + $url = new moodle_url('/mod/attendance/take.php', $params); + $icon = $this->output->pix_icon('redo', get_string('changeattendance', 'attendance'), 'attendance'); + $row->cells[] = html_writer::link($url, $icon); + } + + $table->data[] = $row; + } + + return html_writer::table($table); + } + + /** + * Construct table showing all sessions, not limited to current course. + * + * @param attendance_user_data $userdata + * @return string + */ + private function construct_user_allsessions_log(attendance_user_data $userdata) { + global $USER; + + $allsessions = new stdClass(); + + $shortform = false; + if ($USER->id == $userdata->user->id) { + // This is a user viewing their own stuff - hide non-relevant columns. + $shortform = true; + } + + $groupby = $userdata->pageparams->groupby; + + $table = new html_table(); + $table->attributes['class'] = 'generaltable attwidth boxaligncenter allsessions'; + $table->head = array(); + $table->align = array(); + $table->size = array(); + $table->colclasses = array(); + $colcount = 0; + $summarywidth = 0; + + // If grouping by date, we need some form of date up front. + // Only need course column if we are not using course to group + // (currently date is only option which does not use course). + if ($groupby === 'date') { + $table->head[] = ''; + $table->align[] = 'left'; + $table->colclasses[] = 'grouper'; + $table->size[] = '1px'; + + $table->head[] = get_string('date'); + $table->align[] = 'left'; + $table->colclasses[] = 'datecol'; + $table->size[] = '1px'; + $colcount++; + + $table->head[] = get_string('course'); + $table->align[] = 'left'; + $table->colclasses[] = 'colcourse'; + $colcount++; + } else { + $table->head[] = ''; + $table->align[] = 'left'; + $table->colclasses[] = 'grouper'; + $table->size[] = '1px'; + if ($groupby === 'activity') { + $table->head[] = ''; + $table->align[] = 'left'; + $table->colclasses[] = 'grouper'; + $table->size[] = '1px'; + } + } + + // Need activity column unless we are using activity to group. + if ($groupby !== 'activity') { + $table->head[] = get_string('pluginname', 'mod_attendance'); + $table->align[] = 'left'; + $table->colclasses[] = 'colcourse'; + $table->size[] = '*'; + $colcount++; + } + + // If grouping by date, it belongs up front rather than here. + if ($groupby !== 'date') { + $table->head[] = get_string('date'); + $table->align[] = 'left'; + $table->colclasses[] = 'datecol'; + $table->size[] = '1px'; + $colcount++; + } + + // Use "session" instead of "description". + $table->head[] = get_string('session', 'attendance'); + $table->align[] = 'left'; + $table->colclasses[] = 'desccol'; + $table->size[] = '*'; + $colcount++; + + if (!$shortform) { + $table->head[] = get_string('sessiontypeshort', 'attendance'); + $table->align[] = ''; + $table->size[] = '*'; + $table->colclasses[] = ''; + $colcount++; + } + + if (!empty($USER->attendanceediting)) { + $table->head[] = get_string('status', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'statuscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + + $table->head[] = get_string('remarks', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'remarkscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + } else { + $table->head[] = get_string('status', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'statuscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + + $table->head[] = get_string('points', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'pointscol'; + $table->size[] = '1px'; + $colcount++; + $summarywidth++; + + $table->head[] = get_string('remarks', 'attendance'); + $table->align[] = 'center'; + $table->colclasses[] = 'remarkscol'; + $table->size[] = '*'; + $colcount++; + $summarywidth++; + } + + $statusmaxpoints = array(); + foreach ($userdata->statuses as $attid => $attstatuses) { + $statusmaxpoints[$attid] = attendance_get_statusset_maxpoints($attstatuses); + } + + $lastgroup = array(null, null); + $groups = array(); + $stats = array( + 'course' => array(), + 'activity' => array(), + 'date' => array(), + 'overall' => array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ) + ); + $group = null; + if ($userdata->sessionslog) { + foreach ($userdata->sessionslog as $sess) { + if ($groupby === 'date') { + $weekformat = date("YW", $sess->sessdate); + if ($weekformat != $lastgroup[0]) { + if ($group !== null) { + array_push($groups, $group); + } + $group = array(); + $lastgroup[0] = $weekformat; + } + if (!array_key_exists($weekformat, $stats['date'])) { + $stats['date'][$weekformat] = array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ); + } + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + // Ensure all possible acronyms for current sess's statusset are available as + // keys in status array for period. + // + // A bit yucky because we can't tell whether we've seen statusset before, and + // we usually will have, so much wasted spinning. + foreach ($userdata->statuses[$sess->attendanceid] as $attstatus) { + if ($attstatus->setnumber === $sess->statusset) { + if (!array_key_exists($attstatus->acronym, $stats['date'][$weekformat]['statuses'])) { + $stats['date'][$weekformat]['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + if (!array_key_exists($attstatus->acronym, $stats['overall']['statuses'])) { + $stats['overall']['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + } + } + // The array_key_exists check is for hidden statuses. + if (isset($sess->statusid) && array_key_exists($sess->statusid, $userdata->statuses[$sess->attendanceid])) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $stats['date'][$weekformat]['statuses'][$status->acronym]['count']++; + $stats['date'][$weekformat]['points'] += $status->grade; + $stats['date'][$weekformat]['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['statuses'][$status->acronym]['count']++; + $stats['overall']['points'] += $status->grade; + $stats['overall']['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + } + $stats['date'][$weekformat]['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + } else { + // By course and perhaps activity. + if ( + ($sess->courseid != $lastgroup[0]) || + ($groupby === 'activity' && $sess->cmid != $lastgroup[1]) + ) { + if ($group !== null) { + array_push($groups, $group); + } + $group = array(); + $lastgroup[0] = $sess->courseid; + $lastgroup[1] = $sess->cmid; + } + if (!array_key_exists($sess->courseid, $stats['course'])) { + $stats['course'][$sess->courseid] = array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ); + } + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + // Ensure all possible acronyms for current sess's statusset are available as + // keys in status array for course + // + // A bit yucky because we can't tell whether we've seen statusset before, and + // we usually will have, so much wasted spinning. + foreach ($userdata->statuses[$sess->attendanceid] as $attstatus) { + if ($attstatus->setnumber === $sess->statusset) { + if (!array_key_exists($attstatus->acronym, $stats['course'][$sess->courseid]['statuses'])) { + $stats['course'][$sess->courseid]['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + if (!array_key_exists($attstatus->acronym, $stats['overall']['statuses'])) { + $stats['overall']['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + } + } + // The array_key_exists check is for hidden statuses. + if (isset($sess->statusid) && array_key_exists($sess->statusid, $userdata->statuses[$sess->attendanceid])) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $stats['course'][$sess->courseid]['statuses'][$status->acronym]['count']++; + $stats['course'][$sess->courseid]['points'] += $status->grade; + $stats['course'][$sess->courseid]['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['statuses'][$status->acronym]['count']++; + $stats['overall']['points'] += $status->grade; + $stats['overall']['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + } + $stats['course'][$sess->courseid]['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + + if (!array_key_exists($sess->cmid, $stats['activity'])) { + $stats['activity'][$sess->cmid] = array( + 'points' => 0, + 'maxpointstodate' => 0, + 'maxpoints' => 0, + 'pcpointstodate' => null, + 'pcpoints' => null, + 'statuses' => array() + ); + } + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + // Ensure all possible acronyms for current sess's statusset are available as + // keys in status array for period + // + // A bit yucky because we can't tell whether we've seen statusset before, and + // we usually will have, so much wasted spinning. + foreach ($userdata->statuses[$sess->attendanceid] as $attstatus) { + if ($attstatus->setnumber === $sess->statusset) { + if (!array_key_exists($attstatus->acronym, $stats['activity'][$sess->cmid]['statuses'])) { + $stats['activity'][$sess->cmid]['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + if (!array_key_exists($attstatus->acronym, $stats['overall']['statuses'])) { + $stats['overall']['statuses'][$attstatus->acronym] = + array('count' => 0, 'description' => $attstatus->description); + } + } + } + // The array_key_exists check is for hidden statuses. + if (isset($sess->statusid) && array_key_exists($sess->statusid, $userdata->statuses[$sess->attendanceid])) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $stats['activity'][$sess->cmid]['statuses'][$status->acronym]['count']++; + $stats['activity'][$sess->cmid]['points'] += $status->grade; + $stats['activity'][$sess->cmid]['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['statuses'][$status->acronym]['count']++; + $stats['overall']['points'] += $status->grade; + $stats['overall']['maxpointstodate'] += $statussetmaxpoints[$sess->statusset]; + } + $stats['activity'][$sess->cmid]['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + $stats['overall']['maxpoints'] += $statussetmaxpoints[$sess->statusset]; + } + array_push($group, $sess); + } + array_push($groups, $group); + } + + $points = $stats['overall']['points']; + $maxpoints = $stats['overall']['maxpointstodate']; + $summarytable = new html_table(); + $summarytable->attributes['class'] = 'generaltable table-bordered table-condensed'; + $row = new html_table_row(); + $cell = new html_table_cell(get_string('allsessionstotals', 'attendance')); + $cell->colspan = 2; + $cell->header = true; + $row->cells[] = $cell; + $summarytable->data[] = $row; + foreach ($stats['overall']['statuses'] as $acronym => $status) { + $row = new html_table_row(); + $row->cells[] = $status['description'] . ":"; + $row->cells[] = $status['count']; + $summarytable->data[] = $row; + } + + $row = new html_table_row(); + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $pointsinfo = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $pointsinfo .= " (" . $pctodate . "%)"; + } else { + $pointsinfo = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $pointsinfo .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($pointsinfo); + $cell->colspan = 2; + $row->cells[] = $cell; + $summarytable->data[] = $row; + $allsessions->summary = html_writer::table($summarytable); + + $lastgroup = array(null, null); + foreach ($groups as $group) { + + $statussetmaxpoints = $statusmaxpoints[$sess->attendanceid]; + + // For use in headings etc. + $sess = $group[0]; + + if ($groupby === 'date') { + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $cell = new html_table_cell(); + $cell->rowspan = count($group) + 2; + $row->cells[] = $cell; + $week = date("W", $sess->sessdate); + $year = date("Y", $sess->sessdate); + // ISO week starts on day 1, Monday. + $weekstart = date_timestamp_get(date_isodate_set(date_create(), $year, $week, 1)); + $dmywformat = get_string('strftimedmyw', 'attendance'); + $cell = new html_table_cell(get_string('weekcommencing', 'attendance') . ": " . userdate($weekstart, $dmywformat)); + $cell->colspan = $colcount - $summarywidth; + $cell->rowspan = 2; + $cell->attributes['class'] = 'groupheading'; + $row->cells[] = $cell; + $weekformat = date("YW", $sess->sessdate); + $points = $stats['date'][$weekformat]['points']; + $maxpoints = $stats['date'][$weekformat]['maxpointstodate']; + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $summary .= " (" . $pctodate . "%)"; + } else { + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $summary .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($summary); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $summary = array(); + foreach ($stats['date'][$weekformat]['statuses'] as $acronym => $status) { + array_push($summary, html_writer::tag('b', $acronym) . $status['count']); + } + $cell = new html_table_cell(implode(" ", $summary)); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $lastgroup[0] = date("YW", $weekstart); + } else { + if ($groupby === 'course' || $sess->courseid !== $lastgroup[0]) { + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $cell = new html_table_cell(); + $cell->rowspan = count($group) + 2; + if ($groupby === 'activity') { + $headcell = $cell; // Keep ref to be able to adjust rowspan later. + $cell->rowspan += 2; + $row->cells[] = $cell; + $cell = new html_table_cell(); + $cell->rowspan = 2; + } + $row->cells[] = $cell; + $courseurl = new moodle_url('/course/view.php', array('id' => $sess->courseid)); + $cell = new html_table_cell(get_string('course', 'attendance') . ": " . + html_writer::link($courseurl, $sess->cname)); + $cell->colspan = $colcount - $summarywidth; + $cell->rowspan = 2; + $cell->attributes['class'] = 'groupheading'; + $row->cells[] = $cell; + $points = $stats['course'][$sess->courseid]['points']; + $maxpoints = $stats['course'][$sess->courseid]['maxpointstodate']; + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $summary .= " (" . $pctodate . "%)"; + } else { + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $summary .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($summary); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $summary = array(); + foreach ($stats['course'][$sess->courseid]['statuses'] as $acronym => $status) { + array_push($summary, html_writer::tag('b', $acronym) . $status['count']); + } + $cell = new html_table_cell(implode(" ", $summary)); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + } + if ($groupby === 'activity') { + if ($sess->courseid === $lastgroup[0]) { + $headcell->rowspan += count($group) + 2; + } + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $cell = new html_table_cell(); + $cell->rowspan = count($group) + 2; + $row->cells[] = $cell; + $attendanceurl = new moodle_url('/mod/attendance/view.php', array('id' => $sess->cmid, + 'studentid' => $userdata->user->id, + 'view' => ATT_VIEW_ALL)); + $cell = new html_table_cell(get_string('pluginname', 'mod_attendance') . + ": " . html_writer::link($attendanceurl, $sess->attname)); + $cell->colspan = $colcount - $summarywidth; + $cell->rowspan = 2; + $cell->attributes['class'] = 'groupheading'; + $row->cells[] = $cell; + $points = $stats['activity'][$sess->cmid]['points']; + $maxpoints = $stats['activity'][$sess->cmid]['maxpointstodate']; + if ($maxpoints !== 0) { + $pctodate = format_float( $points * 100 / $maxpoints); + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + $summary .= " (" . $pctodate . "%)"; + } else { + $summary = get_string('points', 'attendance') . ": " . $points . "/" . $maxpoints; + } + $summary .= " " . get_string('todate', 'attendance'); + $cell = new html_table_cell($summary); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + $row = new html_table_row(); + $row->attributes['class'] = 'grouper'; + $summary = array(); + foreach ($stats['activity'][$sess->cmid]['statuses'] as $acronym => $status) { + array_push($summary, html_writer::tag('b', $acronym) . $status['count']); + } + $cell = new html_table_cell(implode(" ", $summary)); + $cell->colspan = $summarywidth; + $row->cells[] = $cell; + $table->data[] = $row; + } + $lastgroup[0] = $sess->courseid; + $lastgroup[1] = $sess->cmid; + } + + // Now iterate over sessions in group... + + foreach ($group as $sess) { + $row = new html_table_row(); + + // If grouping by date, we need some form of date up front. + // Only need course column if we are not using course to group + // (currently date is only option which does not use course). + if ($groupby === 'date') { + // What part of date do we want if grouped by it already? + $row->cells[] = userdate($sess->sessdate, get_string('strftimedmw', 'attendance')) . + " ". $this->construct_time($sess->sessdate, $sess->duration); + + $courseurl = new moodle_url('/course/view.php', array('id' => $sess->courseid)); + $row->cells[] = html_writer::link($courseurl, $sess->cname); + } + + // Need activity column unless we are using activity to group. + if ($groupby !== 'activity') { + $attendanceurl = new moodle_url('/mod/attendance/view.php', array('id' => $sess->cmid, + 'studentid' => $userdata->user->id, + 'view' => ATT_VIEW_ALL)); + $row->cells[] = html_writer::link($attendanceurl, $sess->attname); + } + + // If grouping by date, it belongs up front rather than here. + if ($groupby !== 'date') { + $row->cells[] = userdate($sess->sessdate, get_string('strftimedmyw', 'attendance')) . + " ". $this->construct_time($sess->sessdate, $sess->duration); + } + + $sesscontext = context_module::instance($sess->cmid); + if (has_capability('mod/attendance:takeattendances', $sesscontext)) { + $sessionurl = new moodle_url('/mod/attendance/take.php', array('id' => $sess->cmid, + 'sessionid' => $sess->id, + 'grouptype' => $sess->groupid)); + $description = html_writer::link($sessionurl, $sess->description); + } else { + $description = $sess->description; + } + $row->cells[] = $description; + + if (!$shortform) { + if ($sess->groupid) { + $sessiontypeshort = get_string('group') . ': ' . $userdata->groups[$sess->courseid][$sess->groupid]->name; + } else { + $sessiontypeshort = get_string('commonsession', 'attendance'); + } + $row->cells[] = html_writer::tag('nobr', $sessiontypeshort); + } + + if (!empty($USER->attendanceediting)) { + $context = context_module::instance($sess->cmid); + if (has_capability('mod/attendance:takeattendances', $context)) { + // Takedata needs: + // sessioninfo->sessdate + // sessioninfo->duration + // statuses + // updatemode + // sessionlog[userid]->statusid + // sessionlog[userid]->remarks + // pageparams->viewmode == mod_attendance_take_page_params::SORTED_GRID + // and urlparams to be able to use url method later. + // + // user needs: + // enrolmentstart + // enrolmentend + // enrolmentstatus + // id. + + $nastyhack = new ReflectionClass('attendance_take_data'); + $takedata = $nastyhack->newInstanceWithoutConstructor(); + $takedata->sessioninfo = $sess; + $takedata->statuses = array_filter($userdata->statuses[$sess->attendanceid], function($x) use ($sess) { + return ($x->setnumber == $sess->statusset); + }); + $takedata->updatemode = true; + $takedata->sessionlog = array($userdata->user->id => $sess); + $takedata->pageparams = new stdClass(); + $takedata->pageparams->viewmode = mod_attendance_take_page_params::SORTED_GRID; + $ucdata = $this->construct_take_session_controls($takedata, $userdata->user); + + $celltext = join($ucdata['text']); + + if (array_key_exists('warning', $ucdata)) { + $celltext .= html_writer::empty_tag('br'); + $celltext .= $ucdata['warning']; + } + if (array_key_exists('class', $ucdata)) { + $row->attributes['class'] = $ucdata['class']; + } + + $cell = new html_table_cell($celltext); + $row->cells[] = $cell; + + $celltext = empty($ucdata['remarks']) ? '' : $ucdata['remarks']; + $cell = new html_table_cell($celltext); + $row->cells[] = $cell; + + } else { + if (!empty($sess->statusid)) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $row->cells[] = $status->description; + $row->cells[] = $sess->remarks; + } + } + + } else { + if (!empty($sess->statusid)) { + $status = $userdata->statuses[$sess->attendanceid][$sess->statusid]; + $row->cells[] = $status->description; + $row->cells[] = format_float($status->grade, 1, true, true) . ' / ' . + format_float($statussetmaxpoints[$status->setnumber], 1, true, true); + $row->cells[] = $sess->remarks; + } else if (($sess->sessdate + $sess->duration) < $userdata->user->enrolmentstart) { + $cell = new html_table_cell(get_string('enrolmentstart', 'attendance', + userdate($userdata->user->enrolmentstart, '%d.%m.%Y'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else if ($userdata->user->enrolmentend and $sess->sessdate > $userdata->user->enrolmentend) { + $cell = new html_table_cell(get_string('enrolmentend', 'attendance', + userdate($userdata->user->enrolmentend, '%d.%m.%Y'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else { + list($canmark, $reason) = attendance_can_student_mark($sess, false); + if ($canmark) { + // Student can mark their own attendance. + // URL to the page that lets the student modify their attendance. + + $url = new moodle_url('/mod/attendance/attendance.php', + array('sessid' => $sess->id, 'sesskey' => sesskey())); + $cell = new html_table_cell(html_writer::link($url, get_string('submitattendance', 'attendance'))); + $cell->colspan = 3; + $row->cells[] = $cell; + } else { // Student cannot mark their own attendace. + $row->cells[] = '?'; + $row->cells[] = '? / ' . format_float($statussetmaxpoints[$sess->statusset], 1, true, true); + $row->cells[] = ''; + } + } + } + + $table->data[] = $row; + } + } + + if (!empty($USER->attendanceediting)) { + $row = new html_table_row(); + $params = array( + 'type' => 'submit', + 'class' => 'btn btn-primary', + 'value' => get_string('save', 'attendance')); + $cell = new html_table_cell(html_writer::tag('center', html_writer::empty_tag('input', $params))); + $cell->colspan = $colcount + (($groupby == 'activity') ? 2 : 1); + $row->cells[] = $cell; + $table->data[] = $row; + } + + $logtext = html_writer::table($table); + + if (!empty($USER->attendanceediting)) { + $formtext = html_writer::start_div('no-overflow'); + $formtext .= $logtext; + $formtext .= html_writer::input_hidden_params($userdata->url(array('sesskey' => sesskey()))); + $formtext .= html_writer::end_div(); + // Could use userdata->urlpath if not private or userdata->url_path() if existed, but '' turns + // out to DTRT. + $logtext = html_writer::tag('form', $formtext, array('method' => 'post', 'action' => '', + 'id' => 'attendancetakeform')); + } + $allsessions->detail = $logtext; + return $allsessions; + } + + /** + * Construct time for display. + * + * @param int $datetime + * @param int $duration + * @return string + */ + private function construct_time($datetime, $duration) { + $time = html_writer::tag('nobr', attendance_construct_session_time($datetime, $duration)); + + return $time; + } + + /** + * Render report data. + * + * @param attendance_report_data $reportdata + * @return string + */ + protected function render_attendance_report_data(attendance_report_data $reportdata) { + global $COURSE; + + // Initilise Javascript used to (un)check all checkboxes. + $this->page->requires->js_init_call('M.mod_attendance.init_manage'); + + $table = new html_table(); + $table->attributes['class'] = 'generaltable attwidth attreport'; + + $userrows = $this->get_user_rows($reportdata); + + if ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) { + $sessionrows = array(); + } else { + $sessionrows = $this->get_session_rows($reportdata); + } + + $setnumber = -1; + $statusetcount = 0; + foreach ($reportdata->statuses as $sts) { + if ($sts->setnumber != $setnumber) { + $statusetcount++; + $setnumber = $sts->setnumber; + } + } + + $acronymrows = $this->get_acronym_rows($reportdata, true); + $startwithcontrast = $statusetcount % 2 == 0; + $summaryrows = $this->get_summary_rows($reportdata, $startwithcontrast); + + // Check if the user should be able to bulk send messages to other users on the course. + $bulkmessagecapability = has_capability('moodle/course:bulkmessaging', $this->page->context); + + // Extract rows from each part and collate them into one row each. + $sessiondetailsleft = $reportdata->pageparams->sessiondetailspos == 'left'; + foreach ($userrows as $index => $row) { + $summaryrow = isset($summaryrows[$index]->cells) ? $summaryrows[$index]->cells : array(); + $sessionrow = isset($sessionrows[$index]->cells) ? $sessionrows[$index]->cells : array(); + if ($sessiondetailsleft) { + $row->cells = array_merge($row->cells, $sessionrow, $acronymrows[$index]->cells, $summaryrow); + } else { + $row->cells = array_merge($row->cells, $acronymrows[$index]->cells, $summaryrow, $sessionrow); + } + $table->data[] = $row; + } + + if ($bulkmessagecapability) { // Require that the user can bulk message users. + // Display check boxes that will allow the user to send a message to the students that have been checked. + $output = html_writer::empty_tag('input', array('name' => 'sesskey', 'type' => 'hidden', 'value' => sesskey())); + $output .= html_writer::empty_tag('input', array('name' => 'id', 'type' => 'hidden', 'value' => $COURSE->id)); + $output .= html_writer::empty_tag('input', array('name' => 'returnto', 'type' => 'hidden', 'value' => s(me()))); + $output .= html_writer::start_div('attendancereporttable'); + $output .= html_writer::table($table).html_writer::tag('div', get_string('users').': '.count($reportdata->users)); + $output .= html_writer::end_div(); + $output .= html_writer::tag('div', + html_writer::empty_tag('input', array('type' => 'submit', + 'value' => get_string('messageselectadd'), + 'class' => 'btn btn-secondary')), + array('class' => 'buttons')); + $url = new moodle_url('/mod/attendance/messageselect.php'); + return html_writer::tag('form', $output, array('action' => $url->out(), 'method' => 'post')); + } else { + return html_writer::table($table).html_writer::tag('div', get_string('users').': '.count($reportdata->users)); + } + } + + /** + * Build and return the rows that will make up the left part of the attendance report. + * This consists of student names, as well as header cells for these columns. + * + * @param attendance_report_data $reportdata the report data + * @return array Array of html_table_row objects + */ + protected function get_user_rows(attendance_report_data $reportdata) { + $rows = array(); + + $bulkmessagecapability = has_capability('moodle/course:bulkmessaging', $this->page->context); + $extrafields = get_extra_user_fields($reportdata->att->context); + $showextrauserdetails = $reportdata->pageparams->showextrauserdetails; + $params = $reportdata->pageparams->get_significant_params(); + $text = get_string('users'); + if ($extrafields) { + if ($showextrauserdetails) { + $params['showextrauserdetails'] = 0; + $url = $reportdata->att->url_report($params); + $text .= $this->output->action_icon($url, new pix_icon('t/switch_minus', + get_string('hideextrauserdetails', 'attendance')), null, null); + } else { + $params['showextrauserdetails'] = 1; + $url = $reportdata->att->url_report($params); + $text .= $this->output->action_icon($url, new pix_icon('t/switch_plus', + get_string('showextrauserdetails', 'attendance')), null, null); + $extrafields = array(); + } + } + $usercolspan = count($extrafields); + + $row = new html_table_row(); + $cell = $this->build_header_cell($text, false, false); + $cell->attributes['class'] = $cell->attributes['class'] . ' headcol'; + $row->cells[] = $cell; + if (!empty($usercolspan)) { + $row->cells[] = $this->build_header_cell('', false, false, $usercolspan); + } + $rows[] = $row; + + $row = new html_table_row(); + $text = ''; + if ($bulkmessagecapability) { + $text .= html_writer::checkbox('cb_selector', 0, false, '', array('id' => 'cb_selector')); + } + $text .= $this->construct_fullname_head($reportdata); + $cell = $this->build_header_cell($text, false, false); + $cell->attributes['class'] = $cell->attributes['class'] . ' headcol'; + $row->cells[] = $cell; + + foreach ($extrafields as $field) { + $row->cells[] = $this->build_header_cell(get_string($field), false, false); + } + + $rows[] = $row; + + foreach ($reportdata->users as $user) { + $row = new html_table_row(); + $text = ''; + if ($bulkmessagecapability) { + $text .= html_writer::checkbox('user'.$user->id, 'on', false, '', array('class' => 'attendancesesscheckbox')); + } + $text .= html_writer::link($reportdata->url_view(array('studentid' => $user->id)), fullname($user)); + $cell = $this->build_data_cell($text, false, false, null, null, false); + $cell->attributes['class'] = $cell->attributes['class'] . ' headcol'; + $row->cells[] = $cell; + + foreach ($extrafields as $field) { + $row->cells[] = $this->build_data_cell($user->$field, false, false); + } + $rows[] = $row; + } + + $row = new html_table_row(); + $text = ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) ? '' : get_string('summary'); + $cell = $this->build_data_cell($text, false, true, $usercolspan); + $cell->attributes['class'] = $cell->attributes['class'] . ' headcol'; + $row->cells[] = $cell; + if (!empty($usercolspan)) { + $row->cells[] = $this->build_header_cell('', false, false, $usercolspan); + } + $rows[] = $row; + + return $rows; + } + + /** + * Build and return the rows that will make up the summary part of the attendance report. + * This consists of countings for each status set acronyms, as well as header cells for these columns. + * + * @param attendance_report_data $reportdata the report data + * @param boolean $startwithcontrast true if the first column must start with contrast (bgcolor) + * @return array Array of html_table_row objects + */ + protected function get_acronym_rows(attendance_report_data $reportdata, $startwithcontrast=false) { + $rows = array(); + + $summarycells = array(); + + $row1 = new html_table_row(); + $row2 = new html_table_row(); + + $setnumber = -1; + $contrast = !$startwithcontrast; + foreach ($reportdata->statuses as $sts) { + if ($sts->setnumber != $setnumber) { + $contrast = !$contrast; + $setnumber = $sts->setnumber; + $text = attendance_get_setname($reportdata->att->id, $setnumber, false); + $cell = $this->build_header_cell($text, $contrast); + $row1->cells[] = $cell; + } + $cell->colspan++; + $sts->contrast = $contrast; + $row2->cells[] = $this->build_header_cell($sts->acronym, $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + } + + $rows[] = $row1; + $rows[] = $row2; + + foreach ($reportdata->users as $user) { + if ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) { + $usersummary = $reportdata->summary->get_all_sessions_summary_for($user->id); + } else { + $usersummary = $reportdata->summary->get_taken_sessions_summary_for($user->id); + } + + $row = new html_table_row(); + foreach ($reportdata->statuses as $sts) { + if (isset($usersummary->userstakensessionsbyacronym[$sts->setnumber][$sts->acronym])) { + $text = $usersummary->userstakensessionsbyacronym[$sts->setnumber][$sts->acronym]; + } else { + $text = 0; + } + $row->cells[] = $this->build_data_cell($text, $sts->contrast); + } + + $rows[] = $row; + } + + $rows[] = new html_table_row($summarycells); + + return $rows; + } + + /** + * Build and return the rows that will make up the summary part of the attendance report. + * This consists of counts and percentages for taken sessions (all sessions for summary report), + * as well as header cells for these columns. + * + * @param attendance_report_data $reportdata the report data + * @param boolean $startwithcontrast true if the first column must start with contrast (bgcolor) + * @return array Array of html_table_row objects + */ + protected function get_summary_rows(attendance_report_data $reportdata, $startwithcontrast=false) { + $rows = array(); + + $contrast = $startwithcontrast; + $summarycells = array(); + + $row1 = new html_table_row(); + $helpicon = $this->output->help_icon('oversessionstaken', 'attendance'); + $row1->cells[] = $this->build_header_cell(get_string('oversessionstaken', 'attendance') . $helpicon, $contrast, true, 3); + + $row2 = new html_table_row(); + $row2->cells[] = $this->build_header_cell(get_string('sessions', 'attendance'), $contrast); + $row2->cells[] = $this->build_header_cell(get_string('points', 'attendance'), $contrast); + $row2->cells[] = $this->build_header_cell(get_string('percentage', 'attendance'), $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + + if ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) { + $contrast = !$contrast; + + $helpicon = $this->output->help_icon('overallsessions', 'attendance'); + $row1->cells[] = $this->build_header_cell(get_string('overallsessions', 'attendance') . $helpicon, $contrast, true, 3); + + $row2->cells[] = $this->build_header_cell(get_string('sessions', 'attendance'), $contrast); + $row2->cells[] = $this->build_header_cell(get_string('points', 'attendance'), $contrast); + $row2->cells[] = $this->build_header_cell(get_string('percentage', 'attendance'), $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + + $contrast = !$contrast; + $helpicon = $this->output->help_icon('maxpossible', 'attendance'); + $row1->cells[] = $this->build_header_cell(get_string('maxpossible', 'attendance') . $helpicon, $contrast, true, 2); + + $row2->cells[] = $this->build_header_cell(get_string('points', 'attendance'), $contrast); + $row2->cells[] = $this->build_header_cell(get_string('percentage', 'attendance'), $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + $summarycells[] = $this->build_data_cell('', $contrast); + } + + $rows[] = $row1; + $rows[] = $row2; + + foreach ($reportdata->users as $user) { + if ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) { + $usersummary = $reportdata->summary->get_all_sessions_summary_for($user->id); + } else { + $usersummary = $reportdata->summary->get_taken_sessions_summary_for($user->id); + } + + $contrast = $startwithcontrast; + $row = new html_table_row(); + $row->cells[] = $this->build_data_cell($usersummary->numtakensessions, $contrast); + $row->cells[] = $this->build_data_cell($usersummary->pointssessionscompleted, $contrast); + $row->cells[] = $this->build_data_cell(format_float($usersummary->takensessionspercentage * 100) . '%', $contrast); + + if ($reportdata->pageparams->view == ATT_VIEW_SUMMARY) { + $contrast = !$contrast; + $row->cells[] = $this->build_data_cell($usersummary->numallsessions, $contrast); + $text = $usersummary->pointsallsessions; + $row->cells[] = $this->build_data_cell($text, $contrast); + $row->cells[] = $this->build_data_cell($usersummary->allsessionspercentage, $contrast); + + $contrast = !$contrast; + $text = $usersummary->maxpossiblepoints; + $row->cells[] = $this->build_data_cell($text, $contrast); + $row->cells[] = $this->build_data_cell($usersummary->maxpossiblepercentage, $contrast); + } + + $rows[] = $row; + } + + $rows[] = new html_table_row($summarycells); + + return $rows; + } + + /** + * Build and return the rows that will make up the attendance report. + * This consists of details for each selected session, as well as header and summary cells for these columns. + * + * @param attendance_report_data $reportdata the report data + * @param boolean $startwithcontrast true if the first column must start with contrast (bgcolor) + * @return array Array of html_table_row objects + */ + protected function get_session_rows(attendance_report_data $reportdata, $startwithcontrast=false) { + + $rows = array(); + + $row = new html_table_row(); + + $showsessiondetails = $reportdata->pageparams->showsessiondetails; + $text = get_string('sessions', 'attendance'); + $params = $reportdata->pageparams->get_significant_params(); + if (count($reportdata->sessions) > 1) { + if ($showsessiondetails) { + $params['showsessiondetails'] = 0; + $url = $reportdata->att->url_report($params); + $text .= $this->output->action_icon($url, new pix_icon('t/switch_minus', + get_string('hidensessiondetails', 'attendance')), null, null); + $colspan = count($reportdata->sessions); + } else { + $params['showsessiondetails'] = 1; + $url = $reportdata->att->url_report($params); + $text .= $this->output->action_icon($url, new pix_icon('t/switch_plus', + get_string('showsessiondetails', 'attendance')), null, null); + $colspan = 1; + } + } else { + $colspan = 1; + } + + $params = $reportdata->pageparams->get_significant_params(); + if ($reportdata->pageparams->sessiondetailspos == 'left') { + $params['sessiondetailspos'] = 'right'; + $url = $reportdata->att->url_report($params); + $text .= $this->output->action_icon($url, new pix_icon('t/right', get_string('moveright', 'attendance')), + null, null); + } else { + $params['sessiondetailspos'] = 'left'; + $url = $reportdata->att->url_report($params); + $text = $this->output->action_icon($url, new pix_icon('t/left', get_string('moveleft', 'attendance')), + null, null) . $text; + } + + $row->cells[] = $this->build_header_cell($text, '', true, $colspan); + $rows[] = $row; + + $row = new html_table_row(); + if ($showsessiondetails && !empty($reportdata->sessions)) { + foreach ($reportdata->sessions as $sess) { + $sesstext = userdate($sess->sessdate, get_string('strftimedm', 'attendance')); + $sesstext .= html_writer::empty_tag('br'); + $sesstext .= attendance_strftimehm($sess->sessdate); + $capabilities = array( + 'mod/attendance:takeattendances', + 'mod/attendance:changeattendances' + ); + if (is_null($sess->lasttaken) and has_any_capability($capabilities, $reportdata->att->context)) { + $sesstext = html_writer::link($reportdata->url_take($sess->id, $sess->groupid), $sesstext, + array('class' => 'attendancereporttakelink')); + } + $sesstext .= html_writer::empty_tag('br', array('class' => 'attendancereportseparator')); + if (!empty($sess->description) && + !empty(get_config('attendance', 'showsessiondescriptiononreport'))) { + $sesstext .= html_writer::tag('small', format_text($sess->description), + array('class' => 'attendancereportcommon')); + } + if ($sess->groupid) { + if (empty($reportdata->groups[$sess->groupid])) { + $sesstext .= html_writer::tag('small', get_string('deletedgroup', 'attendance'), + array('class' => 'attendancereportgroup')); + } else { + $sesstext .= html_writer::tag('small', $reportdata->groups[$sess->groupid]->name, + array('class' => 'attendancereportgroup')); + } + + } else { + $sesstext .= html_writer::tag('small', get_string('commonsession', 'attendance'), + array('class' => 'attendancereportcommon')); + } + + $row->cells[] = $this->build_header_cell($sesstext, false, true, null, null, false); + } + } else { + $row->cells[] = $this->build_header_cell(''); + } + $rows[] = $row; + + foreach ($reportdata->users as $user) { + $row = new html_table_row(); + if ($showsessiondetails && !empty($reportdata->sessions)) { + $cellsgenerator = new user_sessions_cells_html_generator($reportdata, $user); + foreach ($cellsgenerator->get_cells(true) as $cell) { + if ($cell instanceof html_table_cell) { + $cell->attributes['class'] .= ' center'; + $row->cells[] = $cell; + } else { + $row->cells[] = $this->build_data_cell($cell); + } + } + } else { + $row->cells[] = $this->build_data_cell(''); + } + $rows[] = $row; + } + + $row = new html_table_row(); + if ($showsessiondetails && !empty($reportdata->sessions)) { + foreach ($reportdata->sessions as $sess) { + $sessionstats = array(); + foreach ($reportdata->statuses as $status) { + if ($status->setnumber == $sess->statusset) { + $status->count = 0; + $sessionstats[$status->id] = $status; + } + } + + foreach ($reportdata->users as $user) { + if (!empty($reportdata->sessionslog[$user->id][$sess->id])) { + $statusid = $reportdata->sessionslog[$user->id][$sess->id]->statusid; + if (isset($sessionstats[$statusid]->count)) { + $sessionstats[$statusid]->count++; + } + } + } + + $statsoutput = ''; + foreach ($sessionstats as $status) { + $statsoutput .= "$status->description: {$status->count}<br/>"; + } + $row->cells[] = $this->build_data_cell($statsoutput); + } + } else { + $row->cells[] = $this->build_header_cell(''); + } + $rows[] = $row; + + return $rows; + } + + /** + * Build and return a html_table_cell for header rows + * + * @param html_table_cell|string $cell the cell or a label for a cell + * @param boolean $contrast true menans the cell must be shown with bgcolor contrast + * @param boolean $center true means the cell text should be centered. Othersiwe it should be left-aligned. + * @param int $colspan how many columns should cell spans + * @param int $rowspan how many rows should cell spans + * @param boolean $nowrap true means the cell text must be shown with nowrap option + * @return html_table_cell a html table cell + */ + protected function build_header_cell($cell, $contrast=false, $center=true, $colspan=null, $rowspan=null, $nowrap=true) { + $classes = array('header', 'bottom'); + if ($center) { + $classes[] = 'center'; + $classes[] = 'narrow'; + } else { + $classes[] = 'left'; + } + if ($contrast) { + $classes[] = 'contrast'; + } + if ($nowrap) { + $classes[] = 'nowrap'; + } + return $this->build_cell($cell, $classes, $colspan, $rowspan, true); + } + + /** + * Build and return a html_table_cell for data rows + * + * @param html_table_cell|string $cell the cell or a label for a cell + * @param boolean $contrast true menans the cell must be shown with bgcolor contrast + * @param boolean $center true means the cell text should be centered. Othersiwe it should be left-aligned. + * @param int $colspan how many columns should cell spans + * @param int $rowspan how many rows should cell spans + * @param boolean $nowrap true means the cell text must be shown with nowrap option + * @return html_table_cell a html table cell + */ + protected function build_data_cell($cell, $contrast=false, $center=true, $colspan=null, $rowspan=null, $nowrap=true) { + $classes = array(); + if ($center) { + $classes[] = 'center'; + $classes[] = 'narrow'; + } else { + $classes[] = 'left'; + } + if ($nowrap) { + $classes[] = 'nowrap'; + } + if ($contrast) { + $classes[] = 'contrast'; + } + return $this->build_cell($cell, $classes, $colspan, $rowspan, false); + } + + /** + * Build and return a html_table_cell for header or data rows + * + * @param html_table_cell|string $cell the cell or a label for a cell + * @param Array $classes a list of css classes + * @param int $colspan how many columns should cell spans + * @param int $rowspan how many rows should cell spans + * @param boolean $header true if this should be a header cell + * @return html_table_cell a html table cell + */ + protected function build_cell($cell, $classes, $colspan=null, $rowspan=null, $header=false) { + if (!($cell instanceof html_table_cell)) { + $cell = new html_table_cell($cell); + } + $cell->header = $header; + $cell->scope = 'col'; + + if (!empty($colspan) && $colspan > 1) { + $cell->colspan = $colspan; + } + + if (!empty($rowspan) && $rowspan > 1) { + $cell->rowspan = $rowspan; + } + + if (!empty($classes)) { + $classes = implode(' ', $classes); + if (empty($cell->attributes['class'])) { + $cell->attributes['class'] = $classes; + } else { + $cell->attributes['class'] .= ' ' . $classes; + } + } + + return $cell; + } + + /** + * Output the status set selector. + * + * @param attendance_set_selector $sel + * @return string + */ + protected function render_attendance_set_selector(attendance_set_selector $sel) { + $current = $sel->get_current_statusset(); + $selected = null; + $opts = array(); + for ($i = 0; $i <= $sel->maxstatusset; $i++) { + $url = $sel->url($i); + $display = $sel->get_status_name($i); + $opts[$url->out(false)] = $display; + if ($i == $current) { + $selected = $url->out(false); + } + } + $newurl = $sel->url($sel->maxstatusset + 1); + $opts[$newurl->out(false)] = get_string('newstatusset', 'mod_attendance'); + if ($current == $sel->maxstatusset + 1) { + $selected = $newurl->out(false); + } + + return $this->output->url_select($opts, $selected, null); + } + + /** + * Render preferences data. + * + * @param stdClass $prefdata + * @return string + */ + protected function render_attendance_preferences_data($prefdata) { + $this->page->requires->js('/mod/attendance/module.js'); + + $table = new html_table(); + $table->width = '100%'; + $table->head = array('#', + get_string('acronym', 'attendance'), + get_string('description'), + get_string('points', 'attendance')); + $table->align = array('center', 'center', 'center', 'center', 'center', 'center'); + + $table->head[] = get_string('studentavailability', 'attendance'). + $this->output->help_icon('studentavailability', 'attendance'); + $table->align[] = 'center'; + + $table->head[] = get_string('setunmarked', 'attendance'). + $this->output->help_icon('setunmarked', 'attendance'); + $table->align[] = 'center'; + + $table->head[] = get_string('action'); + + $i = 1; + foreach ($prefdata->statuses as $st) { + $emptyacronym = ''; + $emptydescription = ''; + if (isset($prefdata->errors[$st->id]) && !empty(($prefdata->errors[$st->id]))) { + if (empty($prefdata->errors[$st->id]['acronym'])) { + $emptyacronym = $this->construct_notice(get_string('emptyacronym', 'mod_attendance'), 'notifyproblem'); + } + if (empty($prefdata->errors[$st->id]['description'])) { + $emptydescription = $this->construct_notice(get_string('emptydescription', 'mod_attendance') , 'notifyproblem'); + } + } + $cells = array(); + $cells[] = $i; + $cells[] = $this->construct_text_input('acronym['.$st->id.']', 2, 2, $st->acronym) . $emptyacronym; + $cells[] = $this->construct_text_input('description['.$st->id.']', 30, 30, $st->description) . + $emptydescription; + $cells[] = $this->construct_text_input('grade['.$st->id.']', 4, 4, $st->grade); + $checked = ''; + if ($st->setunmarked) { + $checked = ' checked '; + } + $cells[] = $this->construct_text_input('studentavailability['.$st->id.']', 4, 5, $st->studentavailability); + $cells[] = '<input type="radio" name="setunmarked" value="'.$st->id.'"'.$checked.'>'; + + $cells[] = $this->construct_preferences_actions_icons($st, $prefdata); + + $table->data[$i] = new html_table_row($cells); + $table->data[$i]->id = "statusrow".$i; + $i++; + } + + $table->data[$i][] = '*'; + $table->data[$i][] = $this->construct_text_input('newacronym', 2, 2); + $table->data[$i][] = $this->construct_text_input('newdescription', 30, 30); + $table->data[$i][] = $this->construct_text_input('newgrade', 4, 4); + $table->data[$i][] = $this->construct_text_input('newstudentavailability', 4, 5); + + $table->data[$i][] = $this->construct_preferences_button(get_string('add', 'attendance'), + mod_attendance_preferences_page_params::ACTION_ADD); + + $o = html_writer::table($table); + $o .= html_writer::input_hidden_params($prefdata->url(array(), false)); + // We should probably rewrite this to use mforms but for now add sesskey. + $o .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()))."\n"; + + $o .= $this->construct_preferences_button(get_string('update', 'attendance'), + mod_attendance_preferences_page_params::ACTION_SAVE); + $o = html_writer::tag('form', $o, array('id' => 'preferencesform', 'method' => 'post', + 'action' => $prefdata->url(array(), false)->out_omit_querystring())); + $o = $this->output->container($o, 'generalbox attwidth'); + + return $o; + } + + /** + * Render default statusset. + * + * @param attendance_default_statusset $prefdata + * @return string + */ + protected function render_attendance_default_statusset(attendance_default_statusset $prefdata) { + return $this->render_attendance_preferences_data($prefdata); + } + + /** + * Render preferences data. + * + * @param stdClass $prefdata + * @return string + */ + protected function render_attendance_pref($prefdata) { + + } + + /** + * Construct text input. + * + * @param string $name + * @param integer $size + * @param integer $maxlength + * @param string $value + * @return string + */ + private function construct_text_input($name, $size, $maxlength, $value='') { + $attributes = array( + 'type' => 'text', + 'name' => $name, + 'size' => $size, + 'maxlength' => $maxlength, + 'value' => $value, + 'class' => 'form-control'); + return html_writer::empty_tag('input', $attributes); + } + + /** + * Construct action icons. + * + * @param stdClass $st + * @param stdClass $prefdata + * @return string + */ + private function construct_preferences_actions_icons($st, $prefdata) { + $params = array('sesskey' => sesskey(), + 'statusid' => $st->id); + if ($st->visible) { + $params['action'] = mod_attendance_preferences_page_params::ACTION_HIDE; + $showhideicon = $this->output->action_icon( + $prefdata->url($params), + new pix_icon("t/hide", get_string('hide'))); + } else { + $params['action'] = mod_attendance_preferences_page_params::ACTION_SHOW; + $showhideicon = $this->output->action_icon( + $prefdata->url($params), + new pix_icon("t/show", get_string('show'))); + } + if (empty($st->haslogs)) { + $params['action'] = mod_attendance_preferences_page_params::ACTION_DELETE; + $deleteicon = $this->output->action_icon( + $prefdata->url($params), + new pix_icon("t/delete", get_string('delete'))); + } else { + $deleteicon = ''; + } + + return $showhideicon . $deleteicon; + } + + /** + * Construct preferences button. + * + * @param string $text + * @param string $action + * @return string + */ + private function construct_preferences_button($text, $action) { + $attributes = array( + 'type' => 'submit', + 'value' => $text, + 'class' => 'btn btn-secondary', + 'onclick' => 'M.mod_attendance.set_preferences_action('.$action.')'); + return html_writer::empty_tag('input', $attributes); + } + + /** + * Construct a notice message + * + * @param string $text + * @param string $class + * @return string + */ + private function construct_notice($text, $class = 'notifymessage') { + $attributes = array('class' => $class); + return html_writer::tag('p', $text, $attributes); + } + + /** + * Show different picture if it is a temporary user. + * + * @param stdClass $user + * @param array $opts + * @return string + */ + protected function user_picture($user, array $opts = null) { + if ($user->type == 'temporary') { + $attrib = array( + 'width' => '35', + 'height' => '35', + 'class' => 'userpicture defaultuserpic', + ); + if (isset($opts['size'])) { + $attrib['width'] = $attrib['height'] = $opts['size']; + } + return $this->output->pix_icon('ghost', '', 'mod_attendance', $attrib); + } + + return $this->output->user_picture($user, $opts); + } +} diff --git a/mod/attendance/renderhelpers.php b/mod/attendance/renderhelpers.php new file mode 100644 index 0000000..b1478b1 --- /dev/null +++ b/mod/attendance/renderhelpers.php @@ -0,0 +1,386 @@ +<?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/>. + +/** + * Attendance module renderering helpers + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/renderables.php'); + +/** + * class Template method for generating user's session's cells + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_sessions_cells_generator { + /** @var array $cells - list of table cells. */ + protected $cells = array(); + + /** @var stdClass $reportdata - data for report. */ + protected $reportdata; + + /** @var stdClass $user - user record. */ + protected $user; + + /** + * Set up params. + * @param attendance_report_data $reportdata - reportdata. + * @param stdClass $user - user record. + */ + public function __construct(attendance_report_data $reportdata, $user) { + $this->reportdata = $reportdata; + $this->user = $user; + } + + /** + * Get cells for the table. + * + * @param boolean $remarks - include remarks cell. + */ + public function get_cells($remarks = false) { + foreach ($this->reportdata->sessions as $sess) { + if (array_key_exists($sess->id, $this->reportdata->sessionslog[$this->user->id]) && + !empty($this->reportdata->sessionslog[$this->user->id][$sess->id]->statusid)) { + $statusid = $this->reportdata->sessionslog[$this->user->id][$sess->id]->statusid; + if (array_key_exists($statusid, $this->reportdata->statuses)) { + $points = format_float($this->reportdata->statuses[$statusid]->grade, 1, true, true); + $maxpoints = format_float($sess->maxpoints, 1, true, true); + $this->construct_existing_status_cell($this->reportdata->statuses[$statusid]->acronym . + " ({$points}/{$maxpoints})"); + } else { + $this->construct_hidden_status_cell($this->reportdata->allstatuses[$statusid]->acronym); + } + if ($remarks) { + $this->construct_remarks_cell($this->reportdata->sessionslog[$this->user->id][$sess->id]->remarks); + } + } else { + if ($this->user->enrolmentstart > ($sess->sessdate + $sess->duration)) { + $starttext = get_string('enrolmentstart', 'attendance', userdate($this->user->enrolmentstart, '%d.%m.%Y')); + $this->construct_enrolments_info_cell($starttext); + } else if ($this->user->enrolmentend and $this->user->enrolmentend < $sess->sessdate) { + $endtext = get_string('enrolmentend', 'attendance', userdate($this->user->enrolmentend, '%d.%m.%Y')); + $this->construct_enrolments_info_cell($endtext); + } else if (!$this->user->enrolmentend and $this->user->enrolmentstatus == ENROL_USER_SUSPENDED) { + // No enrolmentend and ENROL_USER_SUSPENDED. + $suspendext = get_string('enrolmentsuspended', 'attendance', userdate($this->user->enrolmentend, '%d.%m.%Y')); + $this->construct_enrolments_info_cell($suspendext); + } else { + if ($sess->groupid == 0 or array_key_exists($sess->groupid, $this->reportdata->usersgroups[$this->user->id])) { + $this->construct_not_taken_cell('?'); + } else { + $this->construct_not_existing_for_user_session_cell(''); + } + } + if ($remarks) { + $this->construct_remarks_cell(''); + } + } + } + $this->finalize_cells(); + + return $this->cells; + } + + /** + * Construct status cell. + * + * @param string $text - text for the cell. + */ + protected function construct_existing_status_cell($text) { + $this->cells[] = $text; + } + + /** + * Construct hidden status cell. + * + * @param string $text - text for the cell. + */ + protected function construct_hidden_status_cell($text) { + $this->cells[] = $text; + } + + /** + * Construct enrolments info cell. + * + * @param string $text - text for the cell. + */ + protected function construct_enrolments_info_cell($text) { + $this->cells[] = $text; + } + + /** + * Construct not taken cell. + * + * @param string $text - text for the cell. + */ + protected function construct_not_taken_cell($text) { + $this->cells[] = $text; + } + + /** + * Construct remarks cell. + * + * @param string $text - text for the cell. + */ + protected function construct_remarks_cell($text) { + $this->cells[] = $text; + } + + /** + * Construct not existing user session cell. + * + * @param string $text - text for the cell. + */ + protected function construct_not_existing_for_user_session_cell($text) { + $this->cells[] = $text; + } + + /** + * Dummy stub method, called at the end. - override if you need/ + */ + protected function finalize_cells() { + } +} + +/** + * class Template method for generating user's session's cells in html + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_sessions_cells_html_generator extends user_sessions_cells_generator { + /** @var html_table_cell $cell */ + private $cell; + + /** + * Construct status cell. + * + * @param string $text - text for the cell. + */ + protected function construct_existing_status_cell($text) { + $this->close_open_cell_if_needed(); + $this->cells[] = html_writer::span($text, 'attendancestatus-'.$text); + } + + /** + * Construct hidden status cell. + * + * @param string $text - text for the cell. + */ + protected function construct_hidden_status_cell($text) { + $this->cells[] = html_writer::tag('s', $text); + } + + /** + * Construct enrolments info cell. + * + * @param string $text - text for the cell. + */ + protected function construct_enrolments_info_cell($text) { + if (is_null($this->cell)) { + $this->cell = new html_table_cell($text); + $this->cell->colspan = 1; + } else { + if ($this->cell->text != $text) { + $this->cells[] = $this->cell; + $this->cell = new html_table_cell($text); + $this->cell->colspan = 1; + } else { + $this->cell->colspan++; + } + } + } + + /** + * Close cell if needed. + */ + private function close_open_cell_if_needed() { + if ($this->cell) { + $this->cells[] = $this->cell; + $this->cell = null; + } + } + + /** + * Construct not taken cell. + * + * @param string $text - text for the cell. + */ + protected function construct_not_taken_cell($text) { + $this->close_open_cell_if_needed(); + $this->cells[] = $text; + } + + /** + * Construct remarks cell. + * + * @param string $text - text for the cell. + */ + protected function construct_remarks_cell($text) { + global $OUTPUT; + + if (!trim($text)) { + return; + } + + // Format the remark. + $icon = $OUTPUT->pix_icon('i/info', ''); + $remark = html_writer::span($text, 'remarkcontent'); + $remark = html_writer::span($icon.$remark, 'remarkholder'); + + // Add it into the previous cell. + $markcell = array_pop($this->cells); + $markcell .= ' '.$remark; + $this->cells[] = $markcell; + } + + /** + * Construct not existing for user session cell. + * + * @param string $text - text for the cell. + */ + protected function construct_not_existing_for_user_session_cell($text) { + $this->close_open_cell_if_needed(); + $this->cells[] = $text; + } + + /** + * Finalize cells. + * + */ + protected function finalize_cells() { + if ($this->cell) { + $this->cells[] = $this->cell; + } + } +} + +/** + * class Template method for generating user's session's cells in text + * + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_sessions_cells_text_generator extends user_sessions_cells_generator { + /** @var string $enrolmentsinfocelltext. */ + private $enrolmentsinfocelltext; + + /** + * Construct hidden status cell. + * + * @param string $text - text for the cell. + */ + protected function construct_hidden_status_cell($text) { + $this->cells[] = '-'.$text; + } + + /** + * Construct enrolments info cell. + * + * @param string $text - text for the cell. + */ + protected function construct_enrolments_info_cell($text) { + if ($this->enrolmentsinfocelltext != $text) { + $this->enrolmentsinfocelltext = $text; + $this->cells[] = $text; + } else { + $this->cells[] = '←'; + } + } +} + +/** + * Used to construct user summary. + * + * @param stdclass $usersummary - data for summary. + * @param int $view - ATT_VIEW_ALL|ATT_VIEW_ + * @return string. + */ +function construct_user_data_stat($usersummary, $view) { + $stattable = new html_table(); + $stattable->attributes['class'] = 'attlist'; + $row = new html_table_row(); + $row->attributes['class'] = 'normal'; + $row->cells[] = get_string('sessionscompleted', 'attendance') . ':'; + $row->cells[] = $usersummary->numtakensessions; + $stattable->data[] = $row; + + $row = new html_table_row(); + $row->attributes['class'] = 'normal'; + $row->cells[] = get_string('pointssessionscompleted', 'attendance') . ':'; + $row->cells[] = $usersummary->pointssessionscompleted; + $stattable->data[] = $row; + + $row = new html_table_row(); + $row->attributes['class'] = 'normal'; + $row->cells[] = get_string('percentagesessionscompleted', 'attendance') . ':'; + $row->cells[] = $usersummary->percentagesessionscompleted; + $stattable->data[] = $row; + + if ($view == ATT_VIEW_ALL) { + $row = new html_table_row(); + $row->attributes['class'] = 'highlight'; + $row->cells[] = get_string('sessionstotal', 'attendance') . ':'; + $row->cells[] = $usersummary->numallsessions; + $stattable->data[] = $row; + + $row = new html_table_row(); + $row->attributes['class'] = 'highlight'; + $row->cells[] = get_string('pointsallsessions', 'attendance') . ':'; + $row->cells[] = $usersummary->pointsallsessions; + $stattable->data[] = $row; + + $row = new html_table_row(); + $row->attributes['class'] = 'highlight'; + $row->cells[] = get_string('percentageallsessions', 'attendance') . ':'; + $row->cells[] = $usersummary->allsessionspercentage; + $stattable->data[] = $row; + + $row = new html_table_row(); + $row->attributes['class'] = 'normal'; + $row->cells[] = get_string('maxpossiblepoints', 'attendance') . ':'; + $row->cells[] = $usersummary->maxpossiblepoints; + $stattable->data[] = $row; + + $row = new html_table_row(); + $row->attributes['class'] = 'normal'; + $row->cells[] = get_string('maxpossiblepercentage', 'attendance') . ':'; + $row->cells[] = $usersummary->maxpossiblepercentage; + $stattable->data[] = $row; + } + + return html_writer::table($stattable); +} + +/** + * Returns html user summary + * + * @param stdclass $attendance - attendance record. + * @param stdclass $user - user record + * @return string. + * + */ +function construct_full_user_stat_html_table($attendance, $user) { + $summary = new mod_attendance_summary($attendance->id, $user->id); + return construct_user_data_stat($summary->get_all_sessions_summary_for($user->id), ATT_VIEW_ALL); +} diff --git a/mod/attendance/report.php b/mod/attendance/report.php new file mode 100644 index 0000000..96e552d --- /dev/null +++ b/mod/attendance/report.php @@ -0,0 +1,88 @@ +<?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/>. + +/** + * Attendance report + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); + +$pageparams = new mod_attendance_report_page_params(); + +$id = required_param('id', PARAM_INT); +$from = optional_param('from', null, PARAM_ACTION); +$pageparams->view = optional_param('view', null, PARAM_INT); +$pageparams->curdate = optional_param('curdate', null, PARAM_INT); +$pageparams->group = optional_param('group', null, PARAM_INT); +$pageparams->sort = optional_param('sort', ATT_SORT_DEFAULT, PARAM_INT); +$pageparams->page = optional_param('page', 1, PARAM_INT); +$pageparams->perpage = get_config('attendance', 'resultsperpage'); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$attrecord = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); +require_capability('mod/attendance:viewreports', $context); + +$pageparams->init($cm); +$pageparams->showextrauserdetails = optional_param('showextrauserdetails', $attrecord->showextrauserdetails, PARAM_INT); +$pageparams->showsessiondetails = optional_param('showsessiondetails', $attrecord->showsessiondetails, PARAM_INT); +$pageparams->sessiondetailspos = optional_param('sessiondetailspos', $attrecord->sessiondetailspos, PARAM_TEXT); + +$att = new mod_attendance_structure($attrecord, $cm, $course, $context, $pageparams); + +$PAGE->set_url($att->url_report()); +$PAGE->set_pagelayout('report'); +$PAGE->set_title($course->shortname. ": ".$att->name.' - '.get_string('report', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->force_settings_menu(true); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('report', 'attendance')); + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_REPORT); +$filtercontrols = new attendance_filter_controls($att, true); +$reportdata = new attendance_report_data($att); + +// Trigger a report viewed event. +$event = \mod_attendance\event\report_viewed::create(array( + 'objectid' => $att->id, + 'context' => $PAGE->context, + 'other' => array() +)); +$event->add_record_snapshot('course_modules', $cm); +$event->add_record_snapshot('attendance', $attrecord); +$event->trigger(); + +$title = get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname); +$header = new mod_attendance_header($att, $title); + +// Output starts here. +echo $output->header(); +echo $output->render($header); +echo $output->render($tabs); +echo $output->render($filtercontrols); +echo $output->render($reportdata); +echo $output->footer(); + diff --git a/mod/attendance/resetcalendar.php b/mod/attendance/resetcalendar.php new file mode 100644 index 0000000..129b5dd --- /dev/null +++ b/mod/attendance/resetcalendar.php @@ -0,0 +1,92 @@ +<?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/>. + +/** + * Reset Calendar events. + * + * @package mod_attendance + * @copyright 2017 onwards Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot.'/mod/attendance/lib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + +$action = optional_param('action', '', PARAM_ALPHA); + +admin_externalpage_setup('managemodules'); +$context = context_system::instance(); + +// Check permissions. +require_capability('mod/attendance:viewreports', $context); + +$exportfilename = 'attendance-absentee.csv'; + +$PAGE->set_url('/mod/attendance/resetcalendar.php'); + +$PAGE->set_heading($SITE->fullname); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('resetcalendar', 'mod_attendance')); +$tabmenu = attendance_print_settings_tabs('resetcalendar'); +echo $tabmenu; + +if (get_config('attendance', 'enablecalendar')) { + // Check to see if all sessions that need them have calendar events. + if ($action == 'create' && confirm_sesskey()) { + $sessions = $DB->get_recordset('attendance_sessions', array('caleventid' => 0, 'calendarevent' => 1)); + foreach ($sessions as $session) { + attendance_create_calendar_event($session); + if ($session->caleventid) { + $DB->update_record('attendance_sessions', $session); + } + } + $sessions->close(); + echo $OUTPUT->notification(get_string('eventscreated', 'mod_attendance'), 'notifysuccess'); + } else { + if ($DB->record_exists('attendance_sessions', array('caleventid' => 0, 'calendarevent' => 1))) { + $createurl = new moodle_url('/mod/attendance/resetcalendar.php', array('action' => 'create')); + $returnurl = new moodle_url("/{$CFG->admin}/settings.php", array('section' => 'modsettingattendance')); + + echo $OUTPUT->confirm(get_string('resetcaledarcreate', 'mod_attendance'), $createurl, $returnurl); + } else { + echo $OUTPUT->box(get_string("noeventstoreset", "mod_attendance")); + } + } +} else { + if ($action == 'delete' && confirm_sesskey()) { + $caleventids = $DB->get_records_select_menu('attendance_sessions', 'caleventid > 0', array(), + '', 'caleventid, caleventid as id2'); + $DB->delete_records_list('event', 'id', $caleventids); + $DB->execute("UPDATE {attendance_sessions} set caleventid = 0"); + echo $OUTPUT->notification(get_string('eventsdeleted', 'mod_attendance'), 'notifysuccess'); + } else { + // Check to see if there are any events that need to be deleted. + if ($DB->record_exists_select('attendance_sessions', 'caleventid > 0')) { + $deleteurl = new moodle_url('/mod/attendance/resetcalendar.php', array('action' => 'delete')); + $returnurl = new moodle_url("/{$CFG->admin}/settings.php", array('section' => 'modsettingattendance')); + + echo $OUTPUT->confirm(get_string('resetcaledardelete', 'mod_attendance'), $deleteurl, $returnurl); + } else { + echo $OUTPUT->box(get_string("noeventstoreset", "mod_attendance")); + } + } + +} + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/attendance/sessions.php b/mod/attendance/sessions.php new file mode 100644 index 0000000..091c7bb --- /dev/null +++ b/mod/attendance/sessions.php @@ -0,0 +1,227 @@ +<?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/>. + +/** + * Adding attendance sessions + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); +require_once($CFG->dirroot.'/lib/formslib.php'); + +$pageparams = new mod_attendance_sessions_page_params(); + +$id = required_param('id', PARAM_INT); +$pageparams->action = required_param('action', PARAM_INT); + +if (optional_param('deletehiddensessions', false, PARAM_TEXT)) { + $pageparams->action = mod_attendance_sessions_page_params::ACTION_DELETE_HIDDEN; +} + +if (empty($pageparams->action)) { + // The form on manage.php can submit with the "choose" option - this should be fixed in the long term, + // but in the meantime show a useful error and redirect when it occurs. + $url = new moodle_url('/mod/attendance/view.php', array('id' => $id)); + redirect($url, get_string('invalidaction', 'mod_attendance'), 2); +} + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +require_login($course, true, $cm); + +$context = context_module::instance($cm->id); +require_capability('mod/attendance:manageattendances', $context); + +$att = new mod_attendance_structure($att, $cm, $course, $context, $pageparams); + +$PAGE->set_url($att->url_sessions(array('action' => $pageparams->action))); +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->force_settings_menu(true); +$PAGE->set_cacheable(true); +$PAGE->navbar->add($att->name); + +$currenttab = attendance_tabs::TAB_ADD; +$formparams = array('course' => $course, 'cm' => $cm, 'modcontext' => $context, 'att' => $att); +switch ($att->pageparams->action) { + case mod_attendance_sessions_page_params::ACTION_ADD: + $url = $att->url_sessions(array('action' => mod_attendance_sessions_page_params::ACTION_ADD)); + $mform = new \mod_attendance\form\addsession($url, $formparams); + + if ($mform->is_cancelled()) { + redirect($att->url_manage()); + } + + if ($formdata = $mform->get_data()) { + $sessions = attendance_construct_sessions_data_for_add($formdata, $att); + $att->add_sessions($sessions); + if (count($sessions) == 1) { + $message = get_string('sessiongenerated', 'attendance'); + } else { + $message = get_string('sessionsgenerated', 'attendance', count($sessions)); + } + + mod_attendance_notifyqueue::notify_success($message); + // Redirect to the sessions tab always showing all sessions. + $SESSION->attcurrentattview[$cm->course] = ATT_VIEW_ALL; + redirect($att->url_manage()); + } + break; + case mod_attendance_sessions_page_params::ACTION_UPDATE: + $sessionid = required_param('sessionid', PARAM_INT); + + $url = $att->url_sessions(array('action' => mod_attendance_sessions_page_params::ACTION_UPDATE, 'sessionid' => $sessionid)); + $formparams['sessionid'] = $sessionid; + $mform = new \mod_attendance\form\updatesession($url, $formparams); + + if ($mform->is_cancelled()) { + redirect($att->url_manage()); + } + + if ($formdata = $mform->get_data()) { + if (empty($formdata->autoassignstatus)) { + $formdata->autoassignstatus = 0; + } + $att->update_session_from_form_data($formdata, $sessionid); + + mod_attendance_notifyqueue::notify_success(get_string('sessionupdated', 'attendance')); + redirect($att->url_manage()); + } + $currenttab = attendance_tabs::TAB_UPDATE; + break; + case mod_attendance_sessions_page_params::ACTION_DELETE: + $sessionid = required_param('sessionid', PARAM_INT); + $confirm = optional_param('confirm', null, PARAM_INT); + + if (isset($confirm) && confirm_sesskey()) { + $att->delete_sessions(array($sessionid)); + attendance_update_users_grade($att); + redirect($att->url_manage(), get_string('sessiondeleted', 'attendance')); + } + + $sessinfo = $att->get_session_info($sessionid); + + $message = get_string('deletecheckfull', 'attendance', get_string('session', 'attendance')); + $message .= str_repeat(html_writer::empty_tag('br'), 2); + $message .= userdate($sessinfo->sessdate, get_string('strftimedmyhm', 'attendance')); + $message .= html_writer::empty_tag('br'); + $message .= $sessinfo->description; + + $params = array('action' => $att->pageparams->action, 'sessionid' => $sessionid, 'confirm' => 1, 'sesskey' => sesskey()); + + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); + echo $OUTPUT->confirm($message, $att->url_sessions($params), $att->url_manage()); + echo $OUTPUT->footer(); + exit; + case mod_attendance_sessions_page_params::ACTION_DELETE_SELECTED: + $confirm = optional_param('confirm', null, PARAM_INT); + $message = get_string('deletecheckfull', 'attendance', get_string('sessions', 'attendance')); + + if (isset($confirm) && confirm_sesskey()) { + $sessionsids = required_param('sessionsids', PARAM_ALPHANUMEXT); + $sessionsids = explode('_', $sessionsids); + if ($att->pageparams->action == mod_attendance_sessions_page_params::ACTION_DELETE_SELECTED) { + $att->delete_sessions($sessionsids); + attendance_update_users_grade($att); + redirect($att->url_manage(), get_string('sessiondeleted', 'attendance')); + } + } + $sessid = optional_param_array('sessid', '', PARAM_SEQUENCE); + if (empty($sessid)) { + print_error('nosessionsselected', 'attendance', $att->url_manage()); + } + $sessionsinfo = $att->get_sessions_info($sessid); + + $message .= html_writer::empty_tag('br'); + foreach ($sessionsinfo as $sessinfo) { + $message .= html_writer::empty_tag('br'); + $message .= userdate($sessinfo->sessdate, get_string('strftimedmyhm', 'attendance')); + $message .= html_writer::empty_tag('br'); + $message .= $sessinfo->description; + } + + $sessionsids = implode('_', $sessid); + $params = array('action' => $att->pageparams->action, 'sessionsids' => $sessionsids, + 'confirm' => 1, 'sesskey' => sesskey()); + + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); + echo $OUTPUT->confirm($message, $att->url_sessions($params), $att->url_manage()); + echo $OUTPUT->footer(); + exit; + case mod_attendance_sessions_page_params::ACTION_CHANGE_DURATION: + $sessid = optional_param_array('sessid', '', PARAM_SEQUENCE); + $ids = optional_param('ids', '', PARAM_ALPHANUMEXT); + + $slist = !empty($sessid) ? implode('_', $sessid) : ''; + + $url = $att->url_sessions(array('action' => mod_attendance_sessions_page_params::ACTION_CHANGE_DURATION)); + $formparams['ids'] = $slist; + $mform = new mod_attendance\form\duration($url, $formparams); + + if ($mform->is_cancelled()) { + redirect($att->url_manage()); + } + + if ($formdata = $mform->get_data()) { + $sessionsids = explode('_', $ids); + $duration = $formdata->durtime['hours'] * HOURSECS + $formdata->durtime['minutes'] * MINSECS; + $att->update_sessions_duration($sessionsids, $duration); + redirect($att->url_manage(), get_string('sessionupdated', 'attendance')); + } + + if ($slist === '') { + print_error('nosessionsselected', 'attendance', $att->url_manage()); + } + + break; + case mod_attendance_sessions_page_params::ACTION_DELETE_HIDDEN: + $confirm = optional_param('confirm', null, PARAM_INT); + if ($confirm && confirm_sesskey()) { + $sessions = $att->get_hidden_sessions(); + $att->delete_sessions(array_keys($sessions)); + redirect($att->url_manage(), get_string('hiddensessionsdeleted', 'attendance')); + } + + $a = new stdClass(); + $a->count = $att->get_hidden_sessions_count(); + $a->date = userdate($course->startdate); + $message = get_string('confirmdeletehiddensessions', 'attendance', $a); + + $params = array('action' => $att->pageparams->action, 'confirm' => 1, 'sesskey' => sesskey()); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); + echo $OUTPUT->confirm($message, $att->url_sessions($params), $att->url_manage()); + echo $OUTPUT->footer(); + exit; +} + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, $currenttab); +echo $output->header(); +echo $output->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); +echo $output->render($tabs); + +$mform->display(); + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/attendance/settings.php b/mod/attendance/settings.php new file mode 100644 index 0000000..9713075 --- /dev/null +++ b/mod/attendance/settings.php @@ -0,0 +1,210 @@ +<?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/>. + +/** + * Attendance plugin settings + * + * @package mod_attendance + * @copyright 2013 Netspot, Tim Lock. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + require_once(dirname(__FILE__).'/lib.php'); + require_once(dirname(__FILE__).'/locallib.php'); + require_once($CFG->dirroot . '/user/profile/lib.php'); + + $tabmenu = attendance_print_settings_tabs(); + + $settings->add(new admin_setting_heading('attendance_header', '', $tabmenu)); + + $plugininfos = core_plugin_manager::instance()->get_plugins_of_type('local'); + + // Paging options. + $options = array( + 0 => get_string('donotusepaging', 'attendance'), + 25 => 25, + 50 => 50, + 75 => 75, + 100 => 100, + 250 => 250, + 500 => 500, + 1000 => 1000, + ); + + $settings->add(new admin_setting_configselect('attendance/resultsperpage', + get_string('resultsperpage', 'attendance'), get_string('resultsperpage_desc', 'attendance'), 25, $options)); + + $settings->add(new admin_setting_configcheckbox('attendance/studentscanmark', + get_string('studentscanmark', 'attendance'), get_string('studentscanmark_desc', 'attendance'), 1)); + + $settings->add(new admin_setting_configtext('attendance/rotateqrcodeinterval', + get_string('rotateqrcodeinterval', 'attendance'), + get_string('rotateqrcodeinterval_desc', 'attendance'), '15', PARAM_INT)); + + $settings->add(new admin_setting_configtext('attendance/rotateqrcodeexpirymargin', + get_string('rotateqrcodeexpirymargin', 'attendance'), + get_string('rotateqrcodeexpirymargin_desc', 'attendance'), '2', PARAM_INT)); + + $settings->add(new admin_setting_configcheckbox('attendance/studentscanmarksessiontime', + get_string('studentscanmarksessiontime', 'attendance'), + get_string('studentscanmarksessiontime_desc', 'attendance'), 1)); + + $settings->add(new admin_setting_configtext('attendance/studentscanmarksessiontimeend', + get_string('studentscanmarksessiontimeend', 'attendance'), + get_string('studentscanmarksessiontimeend_desc', 'attendance'), '60', PARAM_INT)); + + $settings->add(new admin_setting_configcheckbox('attendance/subnetactivitylevel', + get_string('subnetactivitylevel', 'attendance'), + get_string('subnetactivitylevel_desc', 'attendance'), 1)); + + $options = array( + ATT_VIEW_ALL => get_string('all', 'attendance'), + ATT_VIEW_ALLPAST => get_string('allpast', 'attendance'), + ATT_VIEW_NOTPRESENT => get_string('below', 'attendance', 'X'), + ATT_VIEW_MONTHS => get_string('months', 'attendance'), + ATT_VIEW_WEEKS => get_string('weeks', 'attendance'), + ATT_VIEW_DAYS => get_string('days', 'attendance') + ); + + $settings->add(new admin_setting_configselect('attendance/defaultview', + get_string('defaultview', 'attendance'), + get_string('defaultview_desc', 'attendance'), ATT_VIEW_WEEKS, $options)); + + $settings->add(new admin_setting_configcheckbox('attendance/multisessionexpanded', + get_string('multisessionexpanded', 'attendance'), + get_string('multisessionexpanded_desc', 'attendance'), 0)); + + $settings->add(new admin_setting_configcheckbox('attendance/showsessiondescriptiononreport', + get_string('showsessiondescriptiononreport', 'attendance'), + get_string('showsessiondescriptiononreport_desc', 'attendance'), 0)); + + $settings->add(new admin_setting_configcheckbox('attendance/studentrecordingexpanded', + get_string('studentrecordingexpanded', 'attendance'), + get_string('studentrecordingexpanded_desc', 'attendance'), 1)); + + $settings->add(new admin_setting_configcheckbox('attendance/enablecalendar', + get_string('enablecalendar', 'attendance'), + get_string('enablecalendar_desc', 'attendance'), 1)); + + $settings->add(new admin_setting_configcheckbox('attendance/enablewarnings', + get_string('enablewarnings', 'attendance'), + get_string('enablewarnings_desc', 'attendance'), 0)); + + $fields = array('id' => get_string('studentid', 'attendance')); + $customfields = profile_get_custom_fields(); + foreach ($customfields as $field) { + $fields[$field->shortname] = format_string($field->name); + } + + $settings->add(new admin_setting_configmultiselect('attendance/customexportfields', + new lang_string('customexportfields', 'attendance'), + new lang_string('customexportfields_help', 'attendance'), + array('id'), $fields) + ); + + $name = new lang_string('mobilesettings', 'mod_attendance'); + $description = new lang_string('mobilesettings_help', 'mod_attendance'); + $settings->add(new admin_setting_heading('mobilesettings', $name, $description)); + + $settings->add(new admin_setting_configduration('attendance/mobilesessionfrom', + get_string('mobilesessionfrom', 'attendance'), get_string('mobilesessionfrom_help', 'attendance'), + 6 * HOURSECS, PARAM_RAW)); + + $settings->add(new admin_setting_configduration('attendance/mobilesessionto', + get_string('mobilesessionto', 'attendance'), get_string('mobilesessionto_help', 'attendance'), + 24 * HOURSECS, PARAM_RAW)); + + $name = new lang_string('defaultsettings', 'mod_attendance'); + $description = new lang_string('defaultsettings_help', 'mod_attendance'); + $settings->add(new admin_setting_heading('defaultsettings', $name, $description)); + + $settings->add(new admin_setting_configtext('attendance/subnet', + get_string('requiresubnet', 'attendance'), get_string('requiresubnet_help', 'attendance'), '', PARAM_RAW)); + + $name = new lang_string('defaultsessionsettings', 'mod_attendance'); + $description = new lang_string('defaultsessionsettings_help', 'mod_attendance'); + $settings->add(new admin_setting_heading('defaultsessionsettings', $name, $description)); + + $settings->add(new admin_setting_configcheckbox('attendance/calendarevent_default', + get_string('calendarevent', 'attendance'), '', 1)); + + $settings->add(new admin_setting_configcheckbox('attendance/absenteereport_default', + get_string('includeabsentee', 'attendance'), '', 1)); + + $settings->add(new admin_setting_configcheckbox('attendance/studentscanmark_default', + get_string('studentscanmark', 'attendance'), '', 0)); + + $options = attendance_get_automarkoptions(); + + $settings->add(new admin_setting_configselect('attendance/automark_default', + get_string('automark', 'attendance'), '', 0, $options)); + + $settings->add(new admin_setting_configcheckbox('attendance/randompassword_default', + get_string('randompassword', 'attendance'), '', 0)); + + $settings->add(new admin_setting_configcheckbox('attendance/includeqrcode_default', + get_string('includeqrcode', 'attendance'), '', 0)); + + $settings->add(new admin_setting_configcheckbox('attendance/rotateqrcode_default', + get_string('rotateqrcode', 'attendance'), '', 0)); + + $settings->add(new admin_setting_configcheckbox('attendance/autoassignstatus', + get_string('autoassignstatus', 'attendance'), '', 0)); + + $options = attendance_get_sharedipoptions(); + $settings->add(new admin_setting_configselect('attendance/preventsharedip', + get_string('preventsharedip', 'attendance'), + '', ATTENDANCE_SHAREDIP_DISABLED, $options)); + + $settings->add(new admin_setting_configtext('attendance/preventsharediptime', + get_string('preventsharediptime', 'attendance'), get_string('preventsharediptime_help', 'attendance'), '', PARAM_RAW)); + + $name = new lang_string('defaultwarningsettings', 'mod_attendance'); + $description = new lang_string('defaultwarningsettings_help', 'mod_attendance'); + $settings->add(new admin_setting_heading('defaultwarningsettings', $name, $description)); + + $options = array(); + for ($i = 1; $i <= 100; $i++) { + $options[$i] = "$i%"; + } + $settings->add(new admin_setting_configselect('attendance/warningpercent', + get_string('warningpercent', 'attendance'), get_string('warningpercent_help', 'attendance'), 70, $options)); + + $options = array(); + for ($i = 1; $i <= 50; $i++) { + $options[$i] = "$i"; + } + $settings->add(new admin_setting_configselect('attendance/warnafter', + get_string('warnafter', 'attendance'), get_string('warnafter_help', 'attendance'), 5, $options)); + + $settings->add(new admin_setting_configselect('attendance/maxwarn', + get_string('maxwarn', 'attendance'), get_string('maxwarn_help', 'attendance'), 1, $options)); + + $settings->add(new admin_setting_configcheckbox('attendance/emailuser', + get_string('emailuser', 'attendance'), get_string('emailuser_help', 'attendance'), 1)); + + $settings->add(new admin_setting_configtext('attendance/emailsubject', + get_string('emailsubject', 'attendance'), get_string('emailsubject_help', 'attendance'), + get_string('emailsubject_default', 'attendance'), PARAM_RAW)); + + + $settings->add(new admin_setting_configtextarea('attendance/emailcontent', + get_string('emailcontent', 'attendance'), get_string('emailcontent_help', 'attendance'), + get_string('emailcontent_default', 'attendance'), PARAM_RAW)); +} diff --git a/mod/attendance/styles.css b/mod/attendance/styles.css new file mode 100644 index 0000000..e635046 --- /dev/null +++ b/mod/attendance/styles.css @@ -0,0 +1,302 @@ +.path-mod-attendance .attbtn { + border: 1px solid #aaa; + border-radius: 5px; + margin-left: 2px; + margin-right: 2px; + padding: 5px; + display: inline-block; +} + +.path-mod-attendance .attcurbtn { + margin-left: 2px; + margin-right: 2px; + padding: 5px; +} + +.path-mod-attendance .attfiltercontrols { + margin-bottom: 10px; + margin-left: auto; + margin-right: auto; +} + +.path-mod-attendance .attfiltercontrols #currentdate { + display: inline; +} + +.path-mod-attendance .attwidth { + margin: auto; +} + +.path-mod-attendance .userwithoutenrol, +.path-mod-attendance .userwithoutenrol a { + color: gray; +} + +.path-mod-attendance .userwithoutdata, +.path-mod-attendance .userwithoutdata a { + color: red; +} + +.path-mod-attendance .takelist td { + vertical-align: middle; +} + +.path-mod-attendance .takelist .userpicture { + margin: 0 3px; + vertical-align: middle; +} + +.path-mod-attendance .takegrid input { + margin: 0 3px 0 6px; +} + +.path-mod-attendance .takegrid .fullname { + font-size: 0.8em; +} + +.path-mod-attendance div.allsessionssummary + form#attendancetakeform > div { + width: 100%; +} + +.path-mod-attendance table.controls { + text-align: center; + width: 100%; +} + +.path-mod-attendance table.controls tr { + vertical-align: top; +} + +.path-mod-attendance table.controls td.right, +.path-mod-attendance table.controls td.left { + padding: 4px; +} + +.path-mod-attendance table.controls .right { + text-align: right; +} +/* for IE7*/ +.path-mod-attendance .filtercontrols td { + padding: 6px; +} + +.path-mod-attendance .takecontrols { + margin: 0 auto 20px auto; + width: 800px; +} +.path-mod-attendance .takecontrols table { + margin: 0 auto; +} +.path-mod-attendance .takecontrols .c0 { + text-align: left; + width: 500px; +} +.path-mod-attendance .takecontrols .c1 { + text-align: right; +} + +.path-mod-attendance .inline, +.path-mod-attendance .inline form, +.path-mod-attendance .inline div { + display: inline; +} + +.path-mod-attendance table.userinfobox { + border: 1px solid #eee; + padding: 0; +} +.path-mod-attendance table.userinfobox td.left { + background-color: #eee; + padding: 30px 10px; +} +.path-mod-attendance table.userinfobox .userpicture { + margin: 0; +} +.path-mod-attendance table.attlist td.c0 { + text-align: right; +} +.path-mod-attendance table.allsessions tr.grouper td { + background-color: #eee; +} +.path-mod-attendance table.allsessions td.groupheading { + font-weight: bold; +} +.path-mod-attendance .allsessionssummary > * { + display: inline-block; +} +.path-mod-attendance .allsessionssummary .float-right { + float: right; +} +.path-mod-attendance .allsessionssummary .float-left { + float: left; +} + +#page-mod-attendance-preferences .generalbox { + text-align: center; +} + +.path-mod-attendance .attsessions_manage_table .action-icon img.smallicon { + margin-left: 5px; +} +#page-mod-attendance-sessions input[type="checkbox"] { + margin-right: 2px; +} + +.path-mod-attendance .setallstatuses { + text-align: right; +} + +.path-mod-attendance .remarkholder { + position: relative; +} + +.path-mod-attendance .remarkholder .remarkcontent { + background-color: white; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 3px 3px 5px #ccc; + display: none; + left: 20px; + padding: 5px; + position: absolute; + top: 0; + width: 150px; + z-index: 5000; +} + +.path-mod-attendance .remarkholder:hover .remarkcontent { + display: inline-block; +} + +.path-mod-attendance .attendancestatus-P { + color: green; +} + +.path-mod-attendance .attendancestatus-E { + color: #00aee3; +} + +.path-mod-attendance .attendancestatus-L { + color: #f7931e; +} + +.path-mod-attendance .attendancestatus-A { + color: red; +} + +.path-mod-attendance .attreport .contrast { + background-color: #eaeaea; +} + +.path-mod-attendance .attreport .center { + text-align: center; +} + +.path-mod-attendance .attreport .left { + text-align: left; +} + +.path-mod-attendance .attreport .bottom { + vertical-align: bottom; +} + +.path-mod-attendance .attreport .nowrap { + white-space: nowrap; +} + +.path-mod-attendance .attreport .narrow { + width: 1px; +} + +.path-mod-attendance .attreport img.userpicture { + max-width: inherit; +} + +.path-mod-attendance .student-password { + font-size: x-large; + text-align: center; +} +.path-mod-attendance .ungraded { + font-size: smaller; + font-style: italic; +} +#page-mod-attendance-sessions .statusgroup .statusdesc { + margin-right: 12px; +} + +#page-mod-attendance-view .averageattendance { + font-weight: bold; +} + +#page-mod-attendance-preferences .form-control { + width: inherit; + display: inherit; +} + +@media (max-width: 767px) { + .path-mod-attendance .remarkscol { + display: none; + } + + .path-mod-attendance .statusgroup .form-check-inline { + display: block; + padding-top: 10px; + padding-bottom: 10px; + } + + #page-mod-attendance-view .colatt { + display: none; + } + + .path-mod-attendance .attfiltercontrols, + .path-mod-attendance .attwidth { + width: 100%; + } +} + +@media (max-width: 480px) { + .path-mod-attendance .desccol { + display: none; + } + + .path-mod-attendance .pointscol { + display: none; + } + + .path-mod-attendance .attfiltercontrols #currentdate { + display: none; + } + + #page-mod-attendance-view .colsessionscompleted, + #page-mod-attendance-view .colpointssessionscompleted { + display: none; + } +} + +#page-mod-attendance-report div[role=main] { + position: relative; +} + +#page-mod-attendance-report .attendancereporttable { + overflow-x: scroll; + overflow-y: visible; + padding: 0; + margin-left: 180px; +} + +#page-mod-attendance-report .attendancereporttable .headcol { + position: absolute; + width: 200px; + left: 0; + top: auto; + border-top-width: 1px; +} + +#page-mod-attendance-report .attendancereporttable .headcol input[type='checkbox'] { + margin-right: 4px; + +} + +.attendancereporttable img.icon { + padding-left: 5px; +} diff --git a/mod/attendance/take.php b/mod/attendance/take.php new file mode 100644 index 0000000..238613e --- /dev/null +++ b/mod/attendance/take.php @@ -0,0 +1,108 @@ +<?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/>. + +/** + * Take Attendance + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); + +$pageparams = new mod_attendance_take_page_params(); + +$id = required_param('id', PARAM_INT); +$pageparams->sessionid = required_param('sessionid', PARAM_INT); +$pageparams->grouptype = required_param('grouptype', PARAM_INT); +$pageparams->sort = optional_param('sort', ATT_SORT_DEFAULT, PARAM_INT); +$pageparams->copyfrom = optional_param('copyfrom', null, PARAM_INT); +$pageparams->viewmode = optional_param('viewmode', null, PARAM_INT); +$pageparams->gridcols = optional_param('gridcols', null, PARAM_INT); +$pageparams->page = optional_param('page', 1, PARAM_INT); +$pageparams->perpage = optional_param('perpage', get_config('attendance', 'resultsperpage'), PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); +// Check this is a valid session for this attendance. +$session = $DB->get_record('attendance_sessions', array('id' => $pageparams->sessionid, 'attendanceid' => $att->id), + '*', MUST_EXIST); + +require_login($course, true, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/attendance:takeattendances', $context); + +$pageparams->group = groups_get_activity_group($cm, true); + +$pageparams->init($course->id); +$att = new mod_attendance_structure($att, $cm, $course, $PAGE->context, $pageparams); + +$allowedgroups = groups_get_activity_allowed_groups($cm); +if (!empty($pageparams->grouptype) && !array_key_exists($pageparams->grouptype, $allowedgroups)) { + $group = groups_get_group($pageparams->grouptype); + throw new moodle_exception('cannottakeforgroup', 'attendance', '', $group->name); +} + +if (($formdata = data_submitted()) && confirm_sesskey()) { + $att->take_from_form_data($formdata); + + $group = 0; + if ($att->pageparams->grouptype != mod_attendance_structure::SESSION_COMMON) { + $group = $att->pageparams->grouptype; + } else { + if ($att->pageparams->group) { + $group = $att->pageparams->group; + } + } + + $totalusers = count_enrolled_users(context_module::instance($cm->id), 'mod/attendance:canbelisted', $group); + $usersperpage = $att->pageparams->perpage; + + if (!empty($att->pageparams->page) && $att->pageparams->page && $totalusers && $usersperpage) { + $numberofpages = ceil($totalusers / $usersperpage); + if ($att->pageparams->page < $numberofpages) { + $params = array( + 'sessionid' => $att->pageparams->sessionid, + 'grouptype' => $att->pageparams->grouptype); + $params['page'] = $att->pageparams->page + 1; + redirect($att->url_take($params), get_string('moreattendance', 'attendance')); + } + } + + redirect($att->url_manage(), get_string('attendancesuccess', 'attendance')); +} + +$PAGE->set_url($att->url_take()); +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add($att->name); + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att); +$sesstable = new attendance_take_data($att); + +// Output starts here. + +echo $output->header(); +echo $output->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); +echo $output->render($tabs); +echo $output->render($sesstable); + +echo $output->footer(); diff --git a/mod/attendance/tempedit.php b/mod/attendance/tempedit.php new file mode 100644 index 0000000..d4fc287 --- /dev/null +++ b/mod/attendance/tempedit.php @@ -0,0 +1,113 @@ +<?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/>. + +/** + * Attendance tempedit + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + +$id = required_param('id', PARAM_INT); +$userid = required_param('userid', PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHA); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); +$tempuser = $DB->get_record('attendance_tempusers', array('id' => $userid), '*', MUST_EXIST); + +$att = new mod_attendance_structure($att, $cm, $course); + +$params = array('userid' => $tempuser->id); +if ($action) { + $params['action'] = $action; +} +$PAGE->set_url($att->url_tempedit($params)); + +require_login($course, true, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/attendance:managetemporaryusers', $context); + +$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusersedit', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('tempusersedit', 'attendance')); + +/** @var mod_attendance_renderer $output */ +$output = $PAGE->get_renderer('mod_attendance'); + +if ($action == 'delete') { + if (optional_param('confirm', false, PARAM_BOOL)) { + require_sesskey(); + + // Remove the user from the grades table, the attendance log and the tempusers table. + $DB->delete_records('grade_grades', array('userid' => $tempuser->studentid)); + $DB->delete_records('attendance_log', array('studentid' => $tempuser->studentid)); + $DB->delete_records('attendance_tempusers', array('id' => $tempuser->id)); + + redirect($att->url_managetemp()); + } else { + + $info = (object)array( + 'fullname' => $tempuser->fullname, + 'email' => $tempuser->email, + ); + $msg = get_string('confirmdeleteuser', 'attendance', $info); + $continue = new moodle_url($PAGE->url, array('confirm' => 1, 'sesskey' => sesskey())); + + echo $output->header(); + echo $output->confirm($msg, $continue, $att->url_managetemp()); + echo $output->footer(); + + die(); + } +} + +$formdata = new stdClass(); +$formdata->id = $cm->id; +$formdata->tname = $tempuser->fullname; +$formdata->userid = $tempuser->id; +$formdata->temail = $tempuser->email; + +$mform = new \mod_attendance\form\tempuseredit(); +$mform->set_data($formdata); + +if ($mform->is_cancelled()) { + redirect($att->url_managetemp()); +} else if ($tempuser = $mform->get_data()) { + global $DB; + $updateuser = new stdClass(); + $updateuser->id = $tempuser->userid; + $updateuser->fullname = $tempuser->tname; + $updateuser->email = $tempuser->temail; + $DB->update_record('attendance_tempusers', $updateuser); + redirect($att->url_managetemp()); +} + +$tabs = new attendance_tabs($att, attendance_tabs::TAB_TEMPORARYUSERS); + +echo $output->header(); +echo $output->heading(get_string('tempusersedit', 'attendance').' : '.format_string($course->fullname)); +echo $output->render($tabs); +$mform->display(); +echo $output->footer($course); + diff --git a/mod/attendance/templates/attendance_password_icon.mustache b/mod/attendance/templates/attendance_password_icon.mustache new file mode 100644 index 0000000..6c8e911 --- /dev/null +++ b/mod/attendance/templates/attendance_password_icon.mustache @@ -0,0 +1,21 @@ +{{! + @template attendance/attendance_password_icon + + attendance_password icon. + + Example context (json): + { + "title": "Help with something", + "url": "http://example.org/help", + "linktext": "", + "icon":{ + "attributes": [ + {"name": "src", "value": "../pix/key.svg"}, + {"name": "alt", "value": "Password icon"} + ] + } + } +}} +<span class="helptooltip"> + <a href="{{url}}" title={{#quote}}{{title}}{{/quote}} aria-haspopup="true" target="_blank">{{#icon}}{{>core/pix_icon}}{{/icon}}{{#linktext}}{{.}}{{/linktext}}</a> +</span> \ No newline at end of file diff --git a/mod/attendance/templates/attendance_password_icon_boost.mustache b/mod/attendance/templates/attendance_password_icon_boost.mustache new file mode 100644 index 0000000..319b897 --- /dev/null +++ b/mod/attendance/templates/attendance_password_icon_boost.mustache @@ -0,0 +1,28 @@ +{{! + @template attendance/attendance_password_icon Boost Example. + This is an example of a template you could copy into a boost based theme to use proper popover. + At the moment we cannot specify different templates to use in plugin so we use + a cross-compatible link based pop-up for the password. + + attendance_password icon. + + Example context (json): + { + "title": "Help with something", + "url": "http://example.org/help", + "linktext": "", + "icon":{ + "attributes": [ + {"name": "class", "value": "iconhelp"}, + {"name": "src", "value": "../../../pix/help.svg"}, + {"name": "alt", "value": "Help icon"} + ] + } + } +}} +<a class="btn btn-link p-a-0" role="button" + data-container="body" data-toggle="popover" + data-placement="{{#ltr}}left{{/ltr}}{{^ltr}}right{{/ltr}}" data-content="<span class='student-pass'>{{text}}</span> {{completedoclink}}" + data-html="true" tabindex="0" data-trigger="focus"> + {{#pix}}key, attendance, {{alt}}{{/pix}} +</a> diff --git a/mod/attendance/templates/mobile_teacher_form.mustache b/mod/attendance/templates/mobile_teacher_form.mustache new file mode 100644 index 0000000..bd1d8bb --- /dev/null +++ b/mod/attendance/templates/mobile_teacher_form.mustache @@ -0,0 +1,105 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_attendance/mobile_user_form + + The page to take attendance + + Classes required for JS: + * None + + Data attibutes required for JS: + * All data attributes are required + + Context variables required for this template: + * attendance + * summary + * cmid + + Example context (json): + { + "attendance": { + "id": "1", + "course": "2", + "name": "Class Attendance", + "intro": "Intro" + }, + "cmid": "25", + "courseid": "4", + "sessid": "43", + "btnargs" : "" + } +}} +{{=<% %>=}} +<div class="attendance_mobile_teacher_form"> + <span class="description"> + <core-course-module-description description="<% attendance.intro %>" component="mod_attendance" componentId="<% cmid %>"></core-course-module-description> + </span> + <%#showmessage%> + <%#messages%> + <span class="messages"> + <ion-item> + {{ 'plugin.mod_attendance.<% string %>' | translate }} + </ion-item> + </span> + <%/messages%> + <%/showmessage%> + <span class="attendance_selectall"> + <ion-item> + {{ 'plugin.mod_attendance.setallstatuses' | translate }} + </ion-item> + <ion-list radio-group> + <%#statuses%> + + <span class="radiolabel"> + <ion-item> + <ion-label><% acronym %></ion-label> + <ion-radio (ionSelect)="<% selectall %>" value="<% stid %>"></ion-radio> + </ion-item> + </span> + <%/statuses%> + </ion-list> + </span> + <%#users%> + <span class="attendance_user_row"> + <!-- User and status of the submission. --> + <span ion-item text-wrap title="<% fullname %>"> + <ion-avatar item-start> + <img src="<% profileimageurl %>" core-external-content role="presentation" onError="this.src='assets/img/user-avatar.png'"> + </ion-avatar> + <h2><% fullname %></h2> + <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> + </span> + <ion-list radio-group [(ngModel)]="CONTENT_OTHERDATA.status<% userid %>"> + <%#statuses%> + <span class="radiolabel"> + <ion-item> + <ion-label><% acronym %></ion-label> + <ion-radio value="<% stid %>"></ion-radio> + </ion-item> + </span> + <%/statuses%> + </ion-list> + </span> + <%/users%> + <ion-item> + <button ion-button core-site-plugins-new-content component="mod_attendance" method="mobile_view_activity" [args]="{cmid: <% cmid %>, courseid: <% courseid %>, sessid: <% sessid %><% btnargs %>}"> + {{ 'plugin.mod_attendance.submitattendance' | translate }} + </button> + </ion-item> + +</div> \ No newline at end of file diff --git a/mod/attendance/templates/mobile_user_form.mustache b/mod/attendance/templates/mobile_user_form.mustache new file mode 100644 index 0000000..b47502d --- /dev/null +++ b/mod/attendance/templates/mobile_user_form.mustache @@ -0,0 +1,82 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_attendance/mobile_user_form + + The page to take attendance + + Classes required for JS: + * None + + Data attibutes required for JS: + * All data attributes are required + + Context variables required for this template: + * attendance + * summary + * cmid + + Example context (json): + { + "attendance": { + "id": "1", + "course": "2", + "name": "Class Attendance", + "intro": "Intro" + }, + "cmid": "25", + "courseid": "4", + "sessid": "43" + } +}} +{{=<% %>=}} +<div class="attendance_mobile_user_form"> + <core-course-module-description description="<% attendance.intro %>" component="mod_attendance" componentId="<% cmid %>"></core-course-module-description> + <%#showmessage%> + <%#messages%> + <span class="messages"> + <ion-item> + {{ 'plugin.mod_attendance.<% string %>' | translate }} + </ion-item> + </span> + <%/messages%> + <%/showmessage%> + <%#showpassword%> + <ion-list [(ngModel)]="studentpass"> + <ion-item> + <ion-label>{{ 'plugin.mod_attendance.enterpassword' | translate }}:</ion-label> + <ion-input type="text" name="studentpass"></ion-input> + </ion-item> + </ion-list> + <%/showpassword%> + <%#showstatuses%> + <ion-list radio-group [(ngModel)]="status"> + <%#statuses%> + <ion-item> + <ion-label><% description %></ion-label> + <ion-radio value="<% stid %>"></ion-radio> + </ion-item> + <%/statuses%> + </ion-list> + <button ion-button core-site-plugins-new-content component="mod_attendance" method="mobile_view_activity" [args]="{cmid: <% cmid %>, courseid: <% courseid %>, sessid: <% sessid %>, status: status, studentpass: studentpass}"> + {{ 'plugin.mod_attendance.submitattendance' | translate }} + </button> + <%/showstatuses%> + <%#disabledduetotime%> + {{ 'plugin.mod_attendance.somedisabledstatus' | translate }} + <%/disabledduetotime%> +</div> \ No newline at end of file diff --git a/mod/attendance/templates/mobile_view_page.mustache b/mod/attendance/templates/mobile_view_page.mustache new file mode 100644 index 0000000..39b9d87 --- /dev/null +++ b/mod/attendance/templates/mobile_view_page.mustache @@ -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/>. +}} +{{! + @template mod_attendance/mobile_view_page + + The main page to view the attendance activity + + Classes required for JS: + * None + + Data attibutes required for JS: + * All data attributes are required + + Context variables required for this template: + * attendance + * summary + * cmid + + Example context (json): + { + "attendance": { + "id": "1", + "course": "2", + "name": "Class Attendance", + "intro": "Intro" + }, + "summary": { + "numtakensessions": "1", + "pointssessionscompleted": "2", + "percentagesessionscompleted": "2" + }, + "cmid": "25", + "timestamp": "1234" + } +}} +{{=<% %>=}} +<div class="attendance_mobile_view_page"> + <core-course-module-description description="<% attendance.intro %>" component="mod_attendance" componentId="<% cmid %>"></core-course-module-description> + <%#showmessage%> + <%#messages%> + <span class="messages"> + <ion-item> + {{ 'plugin.mod_attendance.<% string %>' | translate }} + </ion-item> + </span> + <%/messages%> + <%/showmessage%> + <%#sessions%> + <ion-item> + <h2><% time %></h2> + <h3><% groupname %></h3> + <h3><% currentstatus %></h3> + <%#sessid%> + <button ion-button core-site-plugins-new-content component="mod_attendance" method="<% attendancefunction %>" [args]="{cmid: <% cmid %>, courseid: <% courseid %>, sessid: <% sessid %>, timestamp: <% timestamp %>}"> + {{ 'plugin.mod_attendance.submitattendance' | translate }} + </button> + <%/sessid%> + </ion-item> + <%/sessions%> + <ion-item> + <ion-grid> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.sessionscompleted' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.numtakensessions %> + </ion-col> + </ion-row> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.pointssessionscompleted' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.pointssessionscompleted %> + </ion-col> + </ion-row> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.percentagesessionscompleted' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.percentagesessionscompleted %> + </ion-col> + </ion-row> + + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.sessionstotal' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.numallsessions %> + </ion-col> + </ion-row> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.pointsallsessions' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.percentagesessionscompleted %> + </ion-col> + </ion-row> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.percentageallsessions' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.allsessionspercentage %> + </ion-col> + </ion-row> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.maxpossiblepoints' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.maxpossiblepoints %> + </ion-col> + </ion-row> + <ion-row> + <ion-col col-9 class="text-left"> + {{ 'plugin.mod_attendance.maxpossiblepercentage' | translate }} + </ion-col> + <ion-col col-2 class="text-left"> + <% summary.maxpossiblepercentage %> + </ion-col> + </ion-row> + + </ion-grid> + </ion-item> +</div> \ No newline at end of file diff --git a/mod/attendance/tempmerge.php b/mod/attendance/tempmerge.php new file mode 100644 index 0000000..b983d1a --- /dev/null +++ b/mod/attendance/tempmerge.php @@ -0,0 +1,101 @@ +<?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/>. + +/** + * Merge temporary user with real user. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + +$id = required_param('id', PARAM_INT); +$userid = required_param('userid', PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); +$tempuser = $DB->get_record('attendance_tempusers', array('id' => $userid), '*', MUST_EXIST); + +$att = new mod_attendance_structure($att, $cm, $course); +$params = array('userid' => $tempuser->id); +$PAGE->set_url($att->url_tempmerge($params)); + +require_login($course, true, $cm); + +$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusermerge', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('tempusermerge', 'attendance')); + +$formdata = (object)array( + 'id' => $cm->id, + 'userid' => $tempuser->id, +); + +$custom = array( + 'description' => format_string($tempuser->fullname).' ('.format_string($tempuser->email).')', +); +$mform = new mod_attendance\form\tempmerge(null, $custom); +$mform->set_data($formdata); + +if ($mform->is_cancelled()) { + redirect($att->url_managetemp()); + +} else if ($data = $mform->get_data()) { + + $sql = "SELECT s.id, lr.id AS reallogid, lt.id AS templogid + FROM {attendance_sessions} s + LEFT JOIN {attendance_log} lr ON lr.sessionid = s.id AND lr.studentid = :realuserid + LEFT JOIN {attendance_log} lt ON lt.sessionid = s.id AND lt.studentid = :tempuserid + WHERE s.attendanceid = :attendanceid AND lt.id IS NOT NULL + ORDER BY s.id"; + $params = array( + 'realuserid' => $data->participant, + 'tempuserid' => $tempuser->studentid, + 'attendanceid' => $att->id, + ); + $logs = $DB->get_recordset_sql($sql, $params); + + foreach ($logs as $log) { + if (!is_null($log->reallogid)) { + // Remove the existing attendance for the real user for this session. + $DB->delete_records('attendance_log', array('id' => $log->reallogid)); + } + // Adjust the 'temp user' attendance record to point at the real user. + $DB->set_field('attendance_log', 'studentid', $data->participant, array('id' => $log->templogid)); + } + + // Delete the temp user. + $DB->delete_records('attendance_tempusers', array('id' => $tempuser->id)); + $att->update_users_grade(array($data->participant)); // Update the gradebook after the merge. + + redirect($att->url_managetemp()); +} + +/** @var mod_attendance_renderer $output */ +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_TEMPORARYUSERS); + +echo $output->header(); +echo $output->heading(get_string('tempusermerge', 'attendance').' : '.format_string($course->fullname)); +echo $output->render($tabs); +$mform->display(); +echo $output->footer($course); \ No newline at end of file diff --git a/mod/attendance/tempusers.php b/mod/attendance/tempusers.php new file mode 100644 index 0000000..93cb336 --- /dev/null +++ b/mod/attendance/tempusers.php @@ -0,0 +1,134 @@ +<?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/>. + +/** + * Temporary user management. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + +$id = required_param('id', PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +$att = new mod_attendance_structure($att, $cm, $course); +$PAGE->set_url($att->url_managetemp()); + +require_login($course, true, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/attendance:managetemporaryusers', $context); + +$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusers', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->force_settings_menu(true); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('tempusers', 'attendance')); + +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_TEMPORARYUSERS); + +$formdata = (object)array( + 'id' => $cm->id, +); +$mform = new mod_attendance\form\tempuser(); +$mform->set_data($formdata); + +if ($data = $mform->get_data()) { + // Create temp user in main user table. + $user = new stdClass(); + $user->auth = 'manual'; + $user->confirmed = 1; + $user->deleted = 1; + $user->email = time().'@attendance.danmarsden.com'; + $user->username = time().'@attendance.danmarsden.com'; + $user->idnumber = 'tempghost'; + $user->mnethostid = $CFG->mnet_localhost_id; + $studentid = $DB->insert_record('user', $user); + + // Create the temporary user record. + $newtempuser = new stdClass(); + $newtempuser->fullname = $data->tname; + $newtempuser->courseid = $COURSE->id; + $newtempuser->email = $data->temail; + $newtempuser->created = time(); + $newtempuser->studentid = $studentid; + $DB->insert_record('attendance_tempusers', $newtempuser); + + redirect($att->url_managetemp()); +} + +// Output starts here. +echo $output->header(); +echo $output->heading(get_string('tempusers', 'attendance').' : '.format_string($course->fullname)); +echo $output->render($tabs); +$mform->display(); + +$tempusers = $DB->get_records('attendance_tempusers', array('courseid' => $course->id), 'fullname, email'); + +echo '<div>'; +echo '<p style="margin-left:10%;">'.get_string('tempuserslist', 'attendance').'</p>'; +if ($tempusers) { + attendance_print_tempusers($tempusers, $att); +} +echo '</div>'; +echo $output->footer($course); + +/** + * Print list of users. + * + * @param stdClass $tempusers + * @param mod_attendance_structure $att + */ +function attendance_print_tempusers($tempusers, mod_attendance_structure $att) { + echo '<p></p>'; + echo '<table border="1" bordercolor="#EEEEEE" style="background-color:#fff" cellpadding="2" align="center"'. + 'width="80%" summary="'.get_string('temptable', 'attendance').'"><tr>'; + echo '<th class="header">'.get_string('tusername', 'attendance').'</th>'; + echo '<th class="header">'.get_string('tuseremail', 'attendance').'</th>'; + echo '<th class="header">'.get_string('tcreated', 'attendance').'</th>'; + echo '<th class="header">'.get_string('tactions', 'attendance').'</th>'; + echo '</tr>'; + + $even = false; // Used to colour rows. + foreach ($tempusers as $tempuser) { + if ($even) { + echo '<tr style="background-color: #FCFCFC">'; + } else { + echo '<tr>'; + } + $even = !$even; + echo '<td>'.format_string($tempuser->fullname).'</td>'; + echo '<td>'.format_string($tempuser->email).'</td>'; + echo '<td>'.userdate($tempuser->created, get_string('strftimedatetime')).'</td>'; + $params = array('userid' => $tempuser->id); + $editlink = html_writer::link($att->url_tempedit($params), get_string('edituser', 'attendance')); + $deletelink = html_writer::link($att->url_tempdelete($params), get_string('deleteuser', 'attendance')); + $mergelink = html_writer::link($att->url_tempmerge($params), get_string('mergeuser', 'attendance')); + echo '<td>'.$editlink.' | '.$deletelink.' | '.$mergelink.'</td>'; + echo '</tr>'; + } + echo '</table>'; +} + + diff --git a/mod/attendance/tests/behat/attendance_mod.feature b/mod/attendance/tests/behat/attendance_mod.feature new file mode 100644 index 0000000..276593c --- /dev/null +++ b/mod/attendance/tests/behat/attendance_mod.feature @@ -0,0 +1,161 @@ +@javascript @mod @uon @mod_attendance +Feature: Teachers and Students can record session attendance + In order to record session attendance + As a student + I need to be able to mark my own attendance to a session + And as a teacher + I need to be able to mark any students attendance to a session + In order to report on session attendance + As a teacher + I need to be able to export session attendance and run reports + In order to contact students with poor attendance + As a teacher + I need the ability to message a group of students with low attendance + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | timecreated | timemodified | + | Course 1 | C1 | Prove the attendance activity works | 0 | ##yesterday## | ##yesterday## | + And the following "users" exist: + | username | firstname | lastname | email | idnumber | department | institution | + | student1 | Sam | Student | student1@asd.com | 1234 | computer science | University of Nottingham | + | teacher1 | Teacher | One | teacher1@asd.com | 5678 | computer science | University of Nottingham | + And the following "course enrolments" exist: + | course | user | role | timestart | + | C1 | student1 | student | ##yesterday## | + | C1 | teacher1 | editingteacher | ##yesterday## | + + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Add a block" + And I follow "Administration" + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Attendance | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I should see "Attendance" + And I log out + + Scenario: Students can mark their own attendance and teacher can hide specific status from students. + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Add" + And I set the field "Allow students to record own attendance" to "1" + And I set the following fields to these values: + | id_sestime_starthour | 00 | + | id_sestime_endhour | 23 | + | id_sestime_endminute | 55 | + And I click on "id_submitbutton" "button" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Submit attendance" + And I should see "Excused" + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Status set" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[5]/input" to "0" + And I press "Update" + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Submit attendance" + And I should not see "Excused" + And I set the field "Present" to "1" + And I press "Save changes" + And I should see "Self-recorded" + And I log out + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I expand "Reports" node + And I follow "Logs" + And I click on "Get these logs" "button" + Then "Attendance taken by student" "link" should exist + + Scenario: Teachers can view below % report and send a message + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Add" + And I set the following fields to these values: + | id_sestime_starthour | 01 | + | id_sestime_endhour | 02 | + And I click on "id_submitbutton" "button" + And I follow "Report" + And I follow "Below" + And I set the field "cb_selector" to "1" + And I click on "Send a message" "button" + And I should see "Message body" + And I should see "student1@asd.com" + And I follow "Course 1" + And I expand "Reports" node + And I follow "Logs" + And I click on "Get these logs" "button" + Then "Attendance report viewed" "link" should exist + + Scenario: Export report includes id number, department and institution + Given I log in as "admin" + And I navigate to "Users > Permissions > User policies" in site administration + And the following config values are set as admin: + | showuseridentity | idnumber,email,phone1,phone2,department,institution | + + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Add" + And I set the following fields to these values: + | id_sestime_starthour | 01 | + | id_sestime_endhour | 02 | + And I click on "id_submitbutton" "button" + And I follow "Export" + Then the field "id_ident_idnumber" matches value "1" + And the field "id_ident_institution" matches value "1" + And the field "id_ident_department" matches value "1" + + Scenario: Test enabling custom user profile field + # Add custom field. + Given I log in as "admin" + And I navigate to "Users > Accounts > User profile fields" in site administration + And I set the field "datatype" to "Text input" + And I set the following fields to these values: + | Short name | superfield | + | Name | Super field | + And I click on "Save changes" "button" + + And I navigate to "Plugins > Activity modules > Attendance" in site administration + And the "Export custom user profile fields" select box should contain "Super field" + + Scenario: Test adding custom user profile + # Add custom field. + Given I log in as "admin" + And I navigate to "Users > Accounts > User profile fields" in site administration + And I set the field "datatype" to "Text input" + And I set the following fields to these values: + | Short name | superfield | + | Name | Super field | + And I click on "Save changes" "button" + + And the following config values are set as admin: + | customexportfields | superfield | attendance | + + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I follow "Add" + And I set the following fields to these values: + | id_sestime_starthour | 01 | + | id_sestime_endhour | 02 | + And I click on "id_submitbutton" "button" + And I follow "Export" + Then the field "id_ident_superfield" matches value "1" + + # Removed dependency on behat_download to allow automated Travis CI tests to pass. + # It would be good to add these back at some point. diff --git a/mod/attendance/tests/behat/calendar_features.feature b/mod/attendance/tests/behat/calendar_features.feature new file mode 100644 index 0000000..5e97214 --- /dev/null +++ b/mod/attendance/tests/behat/calendar_features.feature @@ -0,0 +1,41 @@ +@mod @mod_attendance @javascript +Feature: Test the calendar related features in the attendance module + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | timecreated | timemodified | + | Course 1 | C1 | Prove the attendance activity works | 0 | ##yesterday## | ##yesterday## | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "course enrolments" exist: + | course | user | role | timestart | + | C1 | student1 | student | ##yesterday## | + | C1 | teacher1 | editingteacher | ##yesterday## | + + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add the "Upcoming events" block + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Test attendance | + And I log out + + Scenario: Calendar events can be created automatically with sessions creation + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test attendance" + And I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 23 | + | id_sestime_startminute | 00 | + | id_sestime_endhour | 23 | + | id_sestime_endminute | 55 | + And I click on "id_submitbutton" "button" + And I am on "Course 1" course homepage + And I follow "Go to calendar" + And I should see "Test attendance" + And I log out + And I log in as "student1" + And I follow "Go to calendar" + Then I should see "Test attendance" diff --git a/mod/attendance/tests/behat/defaultstatus.feature b/mod/attendance/tests/behat/defaultstatus.feature new file mode 100644 index 0000000..5af3534 --- /dev/null +++ b/mod/attendance/tests/behat/defaultstatus.feature @@ -0,0 +1,30 @@ +@mod @mod_attendance +Feature: Admin can set default status set for use in new attendance + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | timecreated | timemodified | + | Course 1 | C1 | Prove the attendance activity works | 0 | ##yesterday## | ##yesterday## | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | course | user | role | timestart | + | C1 | teacher1 | editingteacher | ##yesterday## | + And I log in as "admin" + And I navigate to "Plugins > Attendance" in site administration + And I follow "Default status set" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[3]/input" to "customstatusdescription" + And I click on "Update" "button" in the "#preferencesform" "css_element" + And I should see "Status updated" + And I log out + + @javascript + Scenario: Modified default status set added to new attendance + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Attendance1 | + And I follow "Attendance1" + And I follow "Status set" + Then the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[3]/input" matches value "customstatusdescription" diff --git a/mod/attendance/tests/behat/extra_features.feature b/mod/attendance/tests/behat/extra_features.feature new file mode 100644 index 0000000..75535bc --- /dev/null +++ b/mod/attendance/tests/behat/extra_features.feature @@ -0,0 +1,233 @@ +@mod @mod_attendance @javascript +Feature: Test the various new features in the attendance module + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | timecreated | timemodified | + | Course 1 | C1 | Prove the attendance activity works | 0 | ##yesterday## | ##yesterday## | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | student4 | Student | 4 | student4@example.com | + | student5 | Student | 5 | student5@example.com | + And the following "course enrolments" exist: + | course | user | role | timestart | + | C1 | teacher1 | editingteacher | ##yesterday## | + | C1 | student1 | student | ##yesterday## | + | C1 | student2 | student | ##yesterday## | + | C1 | student3 | student | ##yesterday## | + + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Test attendance | + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Test2 attendance | + And I log out + + Scenario: A teacher can create and update temporary users + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test attendance" + And I follow "Temporary users" + + When I set the following fields to these values: + | Full name | Temporary user 1 | + | Email | | + And I press "Add user" + And I set the following fields to these values: + | Full name | Temporary user test 2 | + | Email | tempuser2test@example.com | + And I press "Add user" + Then I should see "Temporary user 1" + And "tempuser2test@example.com" "text" should exist in the "Temporary user test 2" "table_row" + + When I click on "Edit user" "link" in the "Temporary user test 2" "table_row" + And the following fields match these values: + | Full name | Temporary user test 2 | + | Email | tempuser2test@example.com | + And I set the following fields to these values: + | Full name | Temporary user 2 | + | Email | tempuser2@example.com | + And I press "Edit user" + Then "tempuser2@example.com" "text" should exist in the "Temporary user 2" "table_row" + + When I click on "Delete user" "link" in the "Temporary user 1" "table_row" + And I press "Continue" + Then I should not see "Temporary user 1" + And I should see "Temporary user 2" + + Scenario: A teacher can take attendance for temporary users + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test attendance" + And I follow "Temporary users" + And I set the following fields to these values: + | Full name | Temporary user 1 | + | Email | | + And I press "Add user" + And I set the following fields to these values: + | Full name | Temporary user 2 | + | Email | tempuser2@example.com | + And I press "Add user" + + And I follow "Add" + And I set the following fields to these values: + | id_addmultiply | 0 | + And I click on "submitbutton" "button" + + And I follow "Take attendance" + # Present + And I click on "td.cell.c3 input" "css_element" in the "Student 1" "table_row" + # Late + And I click on "td.cell.c4 input" "css_element" in the "Student 2" "table_row" + # Excused + And I click on "td.cell.c5 input" "css_element" in the "Temporary user 1" "table_row" + # Absent + And I click on "td.cell.c6 input" "css_element" in the "Temporary user 2" "table_row" + And I press "Save attendance" + And I follow "Report" + And "P" "text" should exist in the "Student 1" "table_row" + And "L" "text" should exist in the "Student 2" "table_row" + And "E" "text" should exist in the "Temporary user 1" "table_row" + And "A" "text" should exist in the "Temporary user 2" "table_row" + + And I follow "Temporary user 2" + And I should see "Absent" + + # Merge user. + When I follow "Test attendance" + And I follow "Temporary users" + And I click on "Merge user" "link" in the "Temporary user 2" "table_row" + And I set the field "Participant" to "Student 3" + And I press "Merge user" + And I follow "Report" + + And "P" "text" should exist in the "Student 1" "table_row" + And "L" "text" should exist in the "Student 2" "table_row" + And "E" "text" should exist in the "Temporary user 1" "table_row" + And "A" "text" should exist in the "Student 3" "table_row" + Then I should not see "Temporary user 2" + + Scenario: A teacher can select a subset of users for export + Given the following "groups" exist: + | course | name | idnumber | + | C1 | Group1 | Group1 | + | C1 | Group2 | Group2 | + And the following "group members" exist: + | group | user | + | Group1 | student1 | + | Group1 | student2 | + | Group2 | student2 | + | Group2 | student3 | + + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test attendance" + And I follow "Add" + And I set the following fields to these values: + | id_addmultiply | 0 | + And I click on "submitbutton" "button" + + And I follow "Export" + + When I set the field "Export specific users" to "Yes" + And I set the field "Group" to "Group1" + Then the "Users to export" select box should contain "Student 1" + And the "Users to export" select box should contain "Student 2" + And the "Users to export" select box should not contain "Student 3" + + When I set the field "Group" to "Group2" + Then the "Users to export" select box should contain "Student 2" + And the "Users to export" select box should contain "Student 3" + And the "Users to export" select box should not contain "Student 1" + # Ideally the download would be tested here, but that is difficult to configure. + + Scenario: A teacher can create and use multiple status lists + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test attendance" + And I follow "Status set" + And I set the field "jump" to "New set of statuses" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[1]/td[2]/input" to "G" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[1]/td[3]/input" to "Great" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[1]/td[4]/input" to "3" + And I click on "Add" "button" in the ".lastrow" "css_element" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[2]/input" to "O" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[3]/input" to "OK" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[4]/input" to "2" + And I click on "Add" "button" in the ".lastrow" "css_element" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[2]/input" to "B" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[3]/input" to "Bad" + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[4]/input" to "0" + And I click on "Add" "button" in the ".lastrow" "css_element" + And I click on "Update" "button" in the "#preferencesform" "css_element" + + And I follow "Add" + And I set the following fields to these values: + | id_addmultiply | 0 | + | Status set | Status set 1 (P L E A) | + | id_sestime_starthour | 10 | + | id_sestime_startminute | 0 | + | id_sestime_endhour | 11 | + And I click on "submitbutton" "button" + And I follow "Add" + And I set the following fields to these values: + | id_addmultiply | 0 | + | Status set | Status set 2 (G O B) | + | id_sestime_starthour | 12 | + | id_sestime_startminute | 0 | + | id_sestime_endhour | 13 | + And I click on "submitbutton" "button" + + When I click on "Take attendance" "link" in the "10AM" "table_row" + Then "Set status to «Present»" "link" should exist + And "Set status to «Late»" "link" should exist + And "Set status to «Excused»" "link" should exist + And "Set status to «Absent»" "link" should exist + + When I follow "Sessions" + And I click on "Take attendance" "link" in the "12PM" "table_row" + Then "Set status to «Great»" "link" should exist + And "Set status to «OK»" "link" should exist + And "Set status to «Bad»" "link" should exist + + Scenario: A teacher can use the radio buttons to set attendance values for all users + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test attendance" + And I follow "Add" + And I set the following fields to these values: + | id_addmultiply | 0 | + And I click on "submitbutton" "button" + And I click on "Take attendance" "link" + And I set the field "Set status for" to "all" + When I click on "setallstatuses" "field" in the ".takelist tbody td.c3" "css_element" + And I press "Save attendance" + And I follow "Report" + Then "L" "text" should exist in the "Student 1" "table_row" + And "L" "text" should exist in the "Student 2" "table_row" + And "L" "text" should exist in the "Student 3" "table_row" + + Scenario: A teacher can use the radio buttons to set attendance values for unselected users + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test2 attendance" + And I follow "Add" + And I set the following fields to these values: + | id_addmultiply | 0 | + And I click on "submitbutton" "button" + And I click on "Take attendance" "link" + And I set the field "Set status for" to "unselected" + # Set student 1 as present. + And I click on "td.cell.c3 input" "css_element" in the "Student 1" "table_row" + And I click on "setallstatuses" "field" in the ".takelist tbody td.c3" "css_element" + And I wait until the page is ready + And I press "Save attendance" + When I follow "Report" + Then "P" "text" should exist in the "Student 1" "table_row" + And "L" "text" should exist in the "Student 2" "table_row" + And "L" "text" should exist in the "Student 3" "table_row" diff --git a/mod/attendance/tests/behat/preferences.feature b/mod/attendance/tests/behat/preferences.feature new file mode 100644 index 0000000..ae529db --- /dev/null +++ b/mod/attendance/tests/behat/preferences.feature @@ -0,0 +1,56 @@ +@mod @uon @mod_attendance @mod_attendance_preferences +Feature: Teachers can't change status variables to have empty acronyms or descriptions + In order to update status variables + As a teacher + I need to see an error notice below each acronym / description that I try to set to be empty + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | timecreated | timemodified | + | Course 1 | C1 | Prove the attendance activity works | 0 | ##yesterday## | ##yesterday## | + And the following "users" exist: + | username | firstname | lastname | + | student1 | Sam | Student | + | teacher1 | Teacher | One | + And the following "course enrolments" exist: + | course | user | role | timestart | + | C1 | student1 | student | ##yesterday## | + | C1 | teacher1 | editingteacher | ##yesterday## | + + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Attendancepreftest | + And I follow "Attendancepreftest" + And I follow "Status set" + + @javascript + Scenario: Teachers can add status variables + # Set the second status acronym to be empty + Given I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[2]/input" to "" + # Set the second status description to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[3]/input" to "" + # Set the second status grade to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[2]/td[4]/input" to "" + When I click on "Update" "button" in the "#preferencesform" "css_element" + Then I should see "Empty acronyms are not allowed" in the "//*[@id='preferencesform']/table/tbody/tr[2]/td[2]/p" "xpath_element" + And I should see "Empty descriptions are not allowed" in the "//*[@id='preferencesform']/table/tbody/tr[2]/td[3]/p" "xpath_element" + And I click on "Update" "button" in the "#preferencesform" "css_element" + + # Set the first status acronym to be empty + Given I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[1]/td[2]/input" to "" + # Set the first status description to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[1]/td[3]/input" to "" + # Set the first status grade to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[1]/td[4]/input" to "" + # Set the third status acronym to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[2]/input" to "" + # Set the third status description to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[3]/input" to "" + # Set the third status grade to be empty + And I set the field with xpath "//*[@id='preferencesform']/table/tbody/tr[3]/td[4]/input" to "" + When I click on "Update" "button" in the "#preferencesform" "css_element" + Then I should see "Empty acronyms are not allowed" in the "//*[@id='preferencesform']/table/tbody/tr[1]/td[2]/p" "xpath_element" + And I should see "Empty descriptions are not allowed" in the "//*[@id='preferencesform']/table/tbody/tr[1]/td[3]/p" "xpath_element" + And I should see "Empty acronyms are not allowed" in the "//*[@id='preferencesform']/table/tbody/tr[3]/td[2]/p" "xpath_element" + And I should see "Empty descriptions are not allowed" in the "//*[@id='preferencesform']/table/tbody/tr[3]/td[3]/p" "xpath_element" diff --git a/mod/attendance/tests/behat/report.feature b/mod/attendance/tests/behat/report.feature new file mode 100644 index 0000000..b9da498 --- /dev/null +++ b/mod/attendance/tests/behat/report.feature @@ -0,0 +1,216 @@ +@javascript @mod @uon @mod_attendance +Feature: Visiting reports + As a teacher I visit the reports + + Background: + Given the following "courses" exist: + | fullname | shortname | summary | category | timecreated | timemodified | + | Course 1 | C1 | Prove the attendance activity works | 0 | ##yesterday## | ##yesterday## | + And the following "users" exist: + | username | firstname | lastname | email | idnumber | department | institution | + | student1 | Student | 1 | student1@asd.com | 1234 | computer science | University of Nottingham | + | teacher1 | Teacher | 1 | teacher1@asd.com | 5678 | computer science | University of Nottingham | + And the following "course enrolments" exist: + | course | user | role | timestart | + | C1 | student1 | student | ##yesterday## | + | C1 | teacher1 | editingteacher | ##yesterday## | + And the following config values are set as admin: + | enablewarnings | 1 | attendance | + + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Attendance | + And I follow "Attendance" + And I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 01 | + | id_sestime_endhour | 02 | + And I click on "id_submitbutton" "button" + And I follow "Warnings set" + And I press "Add warning" + And I set the following fields to these values: + | id_warningpercent | 84 | + | id_warnafter | 2 | + And I click on "id_submitbutton" "button" + And I log out + + Scenario: Teacher takes attendance + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I navigate to "Edit settings" in current page administration + Then I set the following fields to these values: + | id_grade_modgrade_type | Point | + | id_grade_modgrade_point | 50 | + And I press "Save and display" + + When I follow "Report" + Then "0 / 0" "text" should exist in the "Student 1" "table_row" + And "0.0%" "text" should exist in the "Student 1" "table_row" + + When I follow "Grades" in the user menu + And I follow "Course 1" + And "-" "text" should exist in the "Student 1" "table_row" + + When I follow "Attendance" + Then I click on "Take attendance" "link" in the "1AM - 2AM" "table_row" + # Late + And I click on "td.cell.c4 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Report" + Then "1 / 2" "text" should exist in the "Student 1" "table_row" + And "50.0%" "text" should exist in the "Student 1" "table_row" + + When I follow "Grades" in the user menu + And I follow "Course 1" + And "25.00" "text" should exist in the "Student 1" "table_row" + + And I log out + + Scenario: Teacher take attendance of group session + Given the following "groups" exist: + | course | name | idnumber | + | C1 | Group1 | Group1 | + And the following "group members" exist: + | group | user | + | Group1 | student1 | + + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | id_grade_modgrade_type | Point | + | id_grade_modgrade_point | 50 | + | id_groupmode | Visible groups | + And I press "Save and display" + + When I follow "Attendance" + Then I click on "Take attendance" "link" in the "1AM - 2AM" "table_row" + # Excused + And I click on "td.cell.c4 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 03 | + | id_sestime_endhour | 04 | + | id_sessiontype_1 | 1 | + | id_groups | Group1 | + And I click on "id_submitbutton" "button" + Then I should see "3AM - 4AM" + And "Group: Group1" "text" should exist in the "3AM - 4AM" "table_row" + + When I click on "Take attendance" "link" in the "3AM - 4AM" "table_row" + # Present + And I click on "td.cell.c3 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Report" + Then "3 / 4" "text" should exist in the "Student 1" "table_row" + And "75.0%" "text" should exist in the "Student 1" "table_row" + + When I follow "Grades" in the user menu + And I follow "Course 1" + Then "37.50" "text" should exist in the "Student 1" "table_row" + + And I log out + + Scenario: Teacher visit summary report and absentee report + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | id_grade_modgrade_type | Point | + | id_grade_modgrade_point | 50 | + And I press "Save and display" + + When I click on "Take attendance" "link" in the "1AM - 2AM" "table_row" + # Late + And I click on "td.cell.c4 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 03 | + | id_sestime_endhour | 04 | + And I click on "id_submitbutton" "button" + Then I should see "3AM - 4AM" + + When I click on "Take attendance" "link" in the "3AM - 4AM" "table_row" + # Present + And I click on "td.cell.c3 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 05 | + | id_sestime_endhour | 06 | + And I click on "id_submitbutton" "button" + Then I should see "5AM - 6AM" + + When I follow "Report" + And I click on "Summary" "link" in the "All" "table_row" + + Then "3 / 6" "text" should exist in the "Student 1" "table_row" + And "50.0%" "text" should exist in the "Student 1" "table_row" + And "5 / 6" "text" should exist in the "Student 1" "table_row" + And "83.3%" "text" should exist in the "Student 1" "table_row" + + And I follow "Absentee report" + And I should see "Student 1" + + And I log out + + Scenario: Student visit user report + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I navigate to "Edit settings" in current page administration + Then I set the following fields to these values: + | id_grade_modgrade_type | Point | + | id_grade_modgrade_point | 50 | + And I press "Save and display" + + When I click on "Take attendance" "link" in the "1AM - 2AM" "table_row" + # Late + And I click on "td.cell.c4 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 03 | + | id_sestime_endhour | 04 | + And I click on "id_submitbutton" "button" + + When I click on "Take attendance" "link" in the "3AM - 4AM" "table_row" + # Present + And I click on "td.cell.c3 input" "css_element" in the "Student 1" "table_row" + And I press "Save attendance" + + When I follow "Add session" + And I set the following fields to these values: + | id_sestime_starthour | 05 | + | id_sestime_endhour | 06 | + And I click on "id_submitbutton" "button" + + Then I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Attendance" + And I click on "All" "link" in the ".attfiltercontrols" "css_element" + + Then "2" "text" should exist in the "Taken sessions" "table_row" + And "3 / 4" "text" should exist in the "Points over taken sessions:" "table_row" + And "75.0%" "text" should exist in the "Percentage over taken sessions:" "table_row" + And "3" "text" should exist in the "Total number of sessions:" "table_row" + And "3 / 6" "text" should exist in the "Points over all sessions:" "table_row" + And "50.0%" "text" should exist in the "Percentage over all sessions:" "table_row" + And "5 / 6" "text" should exist in the "Maximum possible points:" "table_row" + And "83.3%" "text" should exist in the "Maximum possible percentage:" "table_row" + + And I log out diff --git a/mod/attendance/tests/externallib_test.php b/mod/attendance/tests/externallib_test.php new file mode 100644 index 0000000..e7262e8 --- /dev/null +++ b/mod/attendance/tests/externallib_test.php @@ -0,0 +1,474 @@ +<?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/>. + +/** + * External functions test for attendance plugin. + * + * @package mod_attendance + * @category test + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->dirroot . '/mod/attendance/classes/attendance_webservices_handler.php'); +require_once($CFG->dirroot . '/mod/attendance/classes/structure.php'); +require_once($CFG->dirroot . '/mod/attendance/externallib.php'); + +/** + * This class contains the test cases for webservices. + * + * @package mod_attendance + * @category test + * @copyright 2015 Caio Bressan Doneda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @group mod_attendance + */ +class mod_attendance_external_testcase extends externallib_advanced_testcase { + /** @var core_course_category */ + protected $category; + /** @var stdClass */ + protected $course; + /** @var stdClass */ + protected $attendance; + /** @var stdClass */ + protected $teacher; + /** @var array */ + protected $students; + /** @var array */ + protected $sessions; + + /** + * Setup class. + */ + public function setUp(): void { + global $DB; + $this->category = $this->getDataGenerator()->create_category(); + $this->course = $this->getDataGenerator()->create_course(array('category' => $this->category->id)); + $att = $this->getDataGenerator()->create_module('attendance', array('course' => $this->course->id)); + $cm = $DB->get_record('course_modules', array('id' => $att->cmid), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $this->attendance = new mod_attendance_structure($att, $cm, $course); + + $this->create_and_enrol_users(); + + $this->setUser($this->teacher); + + $session = new stdClass(); + $session->sessdate = time(); + $session->duration = 6000; + $session->description = ""; + $session->descriptionformat = 1; + $session->descriptionitemid = 0; + $session->timemodified = time(); + $session->statusset = 0; + $session->groupid = 0; + $session->absenteereport = 1; + $session->calendarevent = 0; + + // Creating session. + $this->sessions[] = $session; + + $this->attendance->add_sessions($this->sessions); + } + + /** Creating 10 students and 1 teacher. */ + protected function create_and_enrol_users() { + $this->students = array(); + for ($i = 0; $i < 10; $i++) { + $this->students[] = $this->getDataGenerator()->create_and_enrol($this->course, 'student'); + } + + $this->teacher = $this->getDataGenerator()->create_and_enrol($this->course, 'editingteacher'); + } + + public function test_get_courses_with_today_sessions() { + $this->resetAfterTest(true); + + // Just adding the same session again to check if the method returns the right amount of instances. + $this->attendance->add_sessions($this->sessions); + + $courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id); + $courseswithsessions = external_api::clean_returnvalue(mod_attendance_external::get_courses_with_today_sessions_returns(), + $courseswithsessions); + + $this->assertTrue(is_array($courseswithsessions)); + $this->assertEquals(count($courseswithsessions), 1); + $course = array_pop($courseswithsessions); + $this->assertEquals($course['fullname'], $this->course->fullname); + $attendanceinstance = array_pop($course['attendance_instances']); + $this->assertEquals(count($attendanceinstance['today_sessions']), 2); + } + + public function test_get_courses_with_today_sessions_multiple_instances() { + global $DB; + $this->resetAfterTest(true); + + // Make another attendance. + $att = $this->getDataGenerator()->create_module('attendance', array('course' => $this->course->id)); + $cm = $DB->get_record('course_modules', array('id' => $att->cmid), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $second = new mod_attendance_structure($att, $cm, $course); + + // Just add the same session. + $secondsession = clone $this->sessions[0]; + $secondsession->sessdate += 3600; + + $second->add_sessions([$secondsession]); + + $courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id); + $courseswithsessions = external_api::clean_returnvalue(mod_attendance_external::get_courses_with_today_sessions_returns(), + $courseswithsessions); + + $this->assertTrue(is_array($courseswithsessions)); + $this->assertEquals(count($courseswithsessions), 1); + $course = array_pop($courseswithsessions); + $this->assertEquals(count($course['attendance_instances']), 2); + } + + public function test_get_session() { + $this->resetAfterTest(true); + + $courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id); + $courseswithsessions = external_api::clean_returnvalue(mod_attendance_external::get_courses_with_today_sessions_returns(), + $courseswithsessions); + + $course = array_pop($courseswithsessions); + $attendanceinstance = array_pop($course['attendance_instances']); + $session = array_pop($attendanceinstance['today_sessions']); + + $sessioninfo = attendance_handler::get_session($session['id']); + $sessioninfo = external_api::clean_returnvalue(mod_attendance_external::get_session_returns(), + $sessioninfo); + + $this->assertEquals($this->attendance->id, $sessioninfo['attendanceid']); + $this->assertEquals($session['id'], $sessioninfo['id']); + $this->assertEquals(count($sessioninfo['users']), 10); + } + + public function test_get_session_with_group() { + $this->resetAfterTest(true); + + // Create a group in our course, and add some students to it. + $group = new stdClass(); + $group->courseid = $this->course->id; + $group = $this->getDataGenerator()->create_group($group); + + for ($i = 0; $i < 5; $i++) { + $member = new stdClass; + $member->groupid = $group->id; + $member->userid = $this->students[$i]->id; + $this->getDataGenerator()->create_group_member($member); + } + + // Add a session that's identical to the first, but with a group. + $midnight = usergetmidnight(time()); // Check if this test is running during midnight. + $session = clone $this->sessions[0]; + $session->groupid = $group->id; + $session->sessdate += 3600; // Make sure it appears second in the list. + $this->attendance->add_sessions([$session]); + + $courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id); + + // This test is fragile when running over midnight - check that it is still the same day, if not, run this again. + // This isn't really ideal code, but will hopefully still give a valid test. + if (empty($courseswithsessions) && $midnight !== usergetmidnight(time())) { + $this->attendance->add_sessions([$session]); + $courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id); + } + $courseswithsessions = external_api::clean_returnvalue(mod_attendance_external::get_courses_with_today_sessions_returns(), + $courseswithsessions); + + $course = array_pop($courseswithsessions); + $attendanceinstance = array_pop($course['attendance_instances']); + $session = array_pop($attendanceinstance['today_sessions']); + + $sessioninfo = attendance_handler::get_session($session['id']); + $sessioninfo = external_api::clean_returnvalue(mod_attendance_external::get_session_returns(), + $sessioninfo); + + $this->assertEquals($session['id'], $sessioninfo['id']); + $this->assertEquals($group->id, $sessioninfo['groupid']); + $this->assertEquals(count($sessioninfo['users']), 5); + } + + public function test_update_user_status() { + $this->resetAfterTest(true); + + $courseswithsessions = attendance_handler::get_courses_with_today_sessions($this->teacher->id); + $courseswithsessions = external_api::clean_returnvalue(mod_attendance_external::get_courses_with_today_sessions_returns(), + $courseswithsessions); + + $course = array_pop($courseswithsessions); + $attendanceinstance = array_pop($course['attendance_instances']); + $session = array_pop($attendanceinstance['today_sessions']); + + $sessioninfo = attendance_handler::get_session($session['id']); + $sessioninfo = external_api::clean_returnvalue(mod_attendance_external::get_session_returns(), + $sessioninfo); + + $student = array_pop($sessioninfo['users']); + $status = array_pop($sessioninfo['statuses']); + $statusset = $sessioninfo['statusset']; + + $result = mod_attendance_external::update_user_status($session['id'], $student['id'], $this->teacher->id, + $status['id'], $statusset); + $result = external_api::clean_returnvalue(mod_attendance_external::update_user_status_returns(), $result); + + $sessioninfo = attendance_handler::get_session($session['id']); + $sessioninfo = external_api::clean_returnvalue(mod_attendance_external::get_session_returns(), + $sessioninfo); + + $log = array_pop($sessioninfo['attendance_log']); + $this->assertEquals($student['id'], $log['studentid']); + $this->assertEquals($status['id'], $log['statusid']); + } + + public function test_add_attendance() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + + // Become a teacher. + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + $this->setUser($teacher); + + // Check attendance does not exist. + $this->assertCount(0, $DB->get_records('attendance', ['course' => $course->id])); + + // Create attendance. + $result = mod_attendance_external::add_attendance($course->id, 'test', 'test', NOGROUPS); + $result = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), $result); + + // Check attendance exist. + $this->assertCount(1, $DB->get_records('attendance', ['course' => $course->id])); + $record = $DB->get_record('attendance', ['id' => $result['attendanceid']]); + $this->assertEquals($record->name, 'test'); + + // Check group. + $cm = get_coursemodule_from_instance('attendance', $result['attendanceid'], 0, false, MUST_EXIST); + $groupmode = (int)groups_get_activity_groupmode($cm); + $this->assertEquals($groupmode, NOGROUPS); + + // Create attendance with "separate groups" group mode. + $result = mod_attendance_external::add_attendance($course->id, 'testsepgrp', 'testsepgrp', SEPARATEGROUPS); + $result = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), $result); + + // Check attendance exist. + $this->assertCount(2, $DB->get_records('attendance', ['course' => $course->id])); + $record = $DB->get_record('attendance', ['id' => $result['attendanceid']]); + $this->assertEquals($record->name, 'testsepgrp'); + + // Check group. + $cm = get_coursemodule_from_instance('attendance', $result['attendanceid'], 0, false, MUST_EXIST); + $groupmode = (int)groups_get_activity_groupmode($cm); + $this->assertEquals($groupmode, SEPARATEGROUPS); + + // Create attendance with wrong group mode. + $this->expectException('invalid_parameter_exception'); + $result = mod_attendance_external::add_attendance($course->id, 'test1', 'test1', 100); + } + + public function test_remove_attendance() { + global $DB; + $this->resetAfterTest(true); + + // Become a teacher. + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, $teacherrole->id); + $this->setUser($teacher); + + // Check attendance exists. + $this->assertCount(1, $DB->get_records('attendance', ['course' => $this->course->id])); + $this->assertCount(1, $DB->get_records('attendance_sessions', ['attendanceid' => $this->attendance->id])); + + // Remove attendance. + $result = mod_attendance_external::remove_attendance($this->attendance->id); + $result = external_api::clean_returnvalue(mod_attendance_external::remove_attendance_returns(), $result); + + // Check attendance removed. + $this->assertCount(0, $DB->get_records('attendance', ['course' => $this->course->id])); + $this->assertCount(0, $DB->get_records('attendance_sessions', ['attendanceid' => $this->attendance->id])); + } + + public function test_add_session() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); + + // Become a teacher. + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + $this->setUser($teacher); + + // Create attendance with separate groups mode. + $attendancesepgroups = mod_attendance_external::add_attendance($course->id, 'sepgroups', 'test', SEPARATEGROUPS); + $attendancesepgroups = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), + $attendancesepgroups); + + // Check attendance exist. + $this->assertCount(1, $DB->get_records('attendance', ['course' => $course->id])); + + // Create session and validate record. + $time = time(); + $duration = 3600; + $result = mod_attendance_external::add_session($attendancesepgroups['attendanceid'], + 'testsession', $time, $duration, $group->id, true); + $result = external_api::clean_returnvalue(mod_attendance_external::add_session_returns(), $result); + + $this->assertCount(1, $DB->get_records('attendance_sessions', ['id' => $result['sessionid']])); + $record = $DB->get_record('attendance_sessions', ['id' => $result['sessionid']]); + $this->assertEquals($record->description, 'testsession'); + $this->assertEquals($record->attendanceid, $attendancesepgroups['attendanceid']); + $this->assertEquals($record->groupid, $group->id); + $this->assertEquals($record->sessdate, $time); + $this->assertEquals($record->duration, $duration); + $this->assertEquals($record->calendarevent, 1); + + // Create session with no group in "separate groups" attendance. + $this->expectException('invalid_parameter_exception'); + mod_attendance_external::add_session($attendancesepgroups['attendanceid'], 'test', time(), 3600, 0, false); + } + + + public function test_add_session_group_in_no_group_exception() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); + + // Become a teacher. + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + $this->setUser($teacher); + + // Create attendance with no groups mode. + $attendancenogroups = mod_attendance_external::add_attendance($course->id, 'nogroups', + 'test', NOGROUPS); + $attendancenogroups = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), + $attendancenogroups); + + // Check attendance exist. + $this->assertCount(1, $DB->get_records('attendance', ['course' => $course->id])); + + // Create session with group in "no groups" attendance. + $this->expectException('invalid_parameter_exception'); + mod_attendance_external::add_session($attendancenogroups['attendanceid'], 'test', time(), 3600, $group->id, false); + } + + public function test_add_session_invalid_group_exception() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); + + // Become a teacher. + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + $this->setUser($teacher); + + // Create attendance with visible groups mode. + $attendancevisgroups = mod_attendance_external::add_attendance($course->id, 'visgroups', 'test', VISIBLEGROUPS); + $attendancevisgroups = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), + $attendancevisgroups); + + // Check attendance exist. + $this->assertCount(1, $DB->get_records('attendance', ['course' => $course->id])); + + // Create session with invalid group in "visible groups" attendance. + $this->expectException('invalid_parameter_exception'); + mod_attendance_external::add_session($attendancevisgroups['attendanceid'], 'test', time(), 3600, $group->id + 100, false); + } + + public function test_remove_session() { + global $DB; + $this->resetAfterTest(true); + + // Create attendance with no groups mode. + $attendance = mod_attendance_external::add_attendance($this->course->id, 'test', 'test', NOGROUPS); + $attendance = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), $attendance); + + // Create sessions. + $result0 = mod_attendance_external::add_session($attendance['attendanceid'], 'test0', time(), 3600, 0, false); + $result0 = external_api::clean_returnvalue(mod_attendance_external::add_session_returns(), $result0); + $result1 = mod_attendance_external::add_session($attendance['attendanceid'], 'test1', time(), 3600, 0, false); + $result1 = external_api::clean_returnvalue(mod_attendance_external::add_session_returns(), $result1); + + $this->assertCount(2, $DB->get_records('attendance_sessions', ['attendanceid' => $attendance['attendanceid']])); + + // Delete session 0. + $result = mod_attendance_external::remove_session($result0['sessionid']); + $result = external_api::clean_returnvalue(mod_attendance_external::remove_session_returns(), $result); + $this->assertCount(1, $DB->get_records('attendance_sessions', ['attendanceid' => $attendance['attendanceid']])); + + // Delete session 1. + $result = mod_attendance_external::remove_session($result1['sessionid']); + $result = external_api::clean_returnvalue(mod_attendance_external::remove_session_returns(), $result); + $this->assertCount(0, $DB->get_records('attendance_sessions', ['attendanceid' => $attendance['attendanceid']])); + } + + public function test_add_session_creates_calendar_event() { + global $DB; + $this->resetAfterTest(true); + + // Create attendance with no groups mode. + $attendance = mod_attendance_external::add_attendance($this->course->id, 'test', 'test', NOGROUPS); + $attendance = external_api::clean_returnvalue(mod_attendance_external::add_attendance_returns(), $attendance); + + // Prepare events tracing. + $sink = $this->redirectEvents(); + + // Create session with no calendar event. + $result = mod_attendance_external::add_session($attendance['attendanceid'], 'test0', time(), 3600, 0, false); + $result = external_api::clean_returnvalue(mod_attendance_external::add_session_returns(), $result); + + // Capture the event. + $events = $sink->get_events(); + $sink->clear(); + + // Validate. + $this->assertCount(1, $events); + $this->assertInstanceOf('\mod_attendance\event\session_added', $events[0]); + + // Create session with calendar event. + $result = mod_attendance_external::add_session($attendance['attendanceid'], 'test0', time(), 3600, 0, true); + $result = external_api::clean_returnvalue(mod_attendance_external::add_session_returns(), $result); + + // Capture the event. + $events = $sink->get_events(); + $sink->clear(); + + // Validate the event. + $this->assertCount(2, $events); + $this->assertInstanceOf('\core\event\calendar_event_created', $events[0]); + $this->assertInstanceOf('\mod_attendance\event\session_added', $events[1]); + } +} diff --git a/mod/attendance/tests/generator/lib.php b/mod/attendance/tests/generator/lib.php new file mode 100644 index 0000000..b69a0a0 --- /dev/null +++ b/mod/attendance/tests/generator/lib.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * mod_attendance data generator + * + * @package mod_attendance + * @category test + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * mod_attendance data generator + * + * @package mod_attendance + * @category test + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_attendance_generator extends testing_module_generator { + + /** + * Create new attendance module instance + * + * @param array|stdClass $record + * @param array $options + * @return stdClass mod_attendance_structure + */ + public function create_instance($record = null, array $options = null) { + global $CFG, $DB; + require_once($CFG->dirroot.'/mod/attendance/lib.php'); + + $this->instancecount++; + $i = $this->instancecount; + + $record = (object)(array)$record; + $options = (array)$options; + + if (empty($record->course)) { + throw new coding_exception('module generator requires $record->course'); + } + if (!isset($record->name)) { + $record->name = get_string('pluginname', 'attendance').' '.$i; + } + if (!isset($record->grade)) { + $record->grade = 100; + } + + return parent::create_instance($record, (array)$options); + } +} diff --git a/mod/attendance/thirdpartylibs.xml b/mod/attendance/thirdpartylibs.xml new file mode 100644 index 0000000..3e5ea69 --- /dev/null +++ b/mod/attendance/thirdpartylibs.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<libraries> + <library> + <location>js/qrcode</location> + <name>jquery.qrcode.js</name> + <version>1.0</version> + <license>MIT</license> + <licenseversion></licenseversion> + </library> +</libraries> \ No newline at end of file diff --git a/mod/attendance/version.php b/mod/attendance/version.php new file mode 100644 index 0000000..6a49dff --- /dev/null +++ b/mod/attendance/version.php @@ -0,0 +1,31 @@ +<?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/>. + +/** + * Version information + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020120300; +$plugin->requires = 2019072500; // Requires 3.8. +$plugin->release = '3.9.1'; +$plugin->maturity = MATURITY_STABLE; +$plugin->cron = 0; +$plugin->component = 'mod_attendance'; diff --git a/mod/attendance/view.php b/mod/attendance/view.php new file mode 100644 index 0000000..dea964d --- /dev/null +++ b/mod/attendance/view.php @@ -0,0 +1,158 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Prints attendance info for particular user + * + * @package mod_attendance + * @copyright 2011 Artem Andreev <andreev.artem@gmail.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__).'/../../config.php'); +require_once(dirname(__FILE__).'/locallib.php'); + +$pageparams = new mod_attendance_view_page_params(); + +$id = required_param('id', PARAM_INT); +$edit = optional_param('edit', -1, PARAM_BOOL); +$pageparams->studentid = optional_param('studentid', null, PARAM_INT); +$pageparams->mode = optional_param('mode', mod_attendance_view_page_params::MODE_THIS_COURSE, PARAM_INT); +$pageparams->view = optional_param('view', null, PARAM_INT); +$pageparams->curdate = optional_param('curdate', null, PARAM_INT); +$pageparams->groupby = optional_param('groupby', 'course', PARAM_ALPHA); +$pageparams->sesscourses = optional_param('sesscourses', 'current', PARAM_ALPHA); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$attendance = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +require_login($course, true, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/attendance:view', $context); + +$pageparams->init($cm); +$att = new mod_attendance_structure($attendance, $cm, $course, $context, $pageparams); + +// Not specified studentid for displaying attendance? +// Redirect to appropriate page if can. +if (!$pageparams->studentid) { + $capabilities = array( + 'mod/attendance:manageattendances', + 'mod/attendance:takeattendances', + 'mod/attendance:changeattendances' + ); + if (has_any_capability($capabilities, $context)) { + redirect($att->url_manage()); + } else if (has_capability('mod/attendance:viewreports', $context)) { + redirect($att->url_report()); + } +} + +if (isset($pageparams->studentid) && $USER->id != $pageparams->studentid) { + // Only users with proper permissions should be able to see any user's individual report. + require_capability('mod/attendance:viewreports', $context); + $userid = $pageparams->studentid; +} else { + // A valid request to see another users report has not been sent, show the user's own. + $userid = $USER->id; +} + +$url = $att->url_view($pageparams->get_significant_params()); +$PAGE->set_url($url); + +$buttons = ''; +$capabilities = array('mod/attendance:takeattendances', 'mod/attendance:changeattendances'); +if (has_any_capability($capabilities, $context) && + $pageparams->mode == mod_attendance_view_page_params::MODE_ALL_SESSIONS) { + + if (!isset($USER->attendanceediting)) { + $USER->attendanceediting = false; + } + + if (($edit == 1) and confirm_sesskey()) { + $USER->attendanceediting = true; + } else if ($edit == 0 and confirm_sesskey()) { + $USER->attendanceediting = false; + } + + if ($USER->attendanceediting) { + $options['edit'] = 0; + $string = get_string('turneditingoff'); + } else { + $options['edit'] = 1; + $string = get_string('turneditingon'); + } + $options['sesskey'] = sesskey(); + $button = new single_button(new moodle_url($PAGE->url, $options), $string, 'post'); + $PAGE->set_button($OUTPUT->render($button)); +} + +$userdata = new attendance_user_data($att, $userid); + +// Create url for link in log screen. +$filterparams = array( + 'view' => $userdata->pageparams->view, + 'curdate' => $userdata->pageparams->curdate, + 'startdate' => $userdata->pageparams->startdate, + 'enddate' => $userdata->pageparams->enddate +); +$params = array_merge($userdata->pageparams->get_significant_params(), $filterparams); + +$header = new mod_attendance_header($att); + +if (empty($userdata->pageparams->studentid)) { + $relateduserid = $USER->id; +} else { + $relateduserid = $userdata->pageparams->studentid; +} + +if (($formdata = data_submitted()) && confirm_sesskey() && $edit == -1) { + $userdata->take_sessions_from_form_data($formdata); + + // Trigger updated event. + $event = \mod_attendance\event\session_report_updated::create(array( + 'relateduserid' => $relateduserid, + 'context' => $context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $cm); + $event->trigger(); + + redirect($url, get_string('attendancesuccess', 'attendance')); +} else { + // Trigger viewed event. + $event = \mod_attendance\event\session_report_viewed::create(array( + 'relateduserid' => $relateduserid, + 'context' => $context, + 'other' => $params)); + $event->add_record_snapshot('course_modules', $cm); + $event->trigger(); +} + +$PAGE->set_title($course->shortname. ": ".$att->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('attendancereport', 'attendance')); + +$output = $PAGE->get_renderer('mod_attendance'); + +echo $output->header(); + +echo $output->render($header); +echo $output->render($userdata); + +echo $output->footer(); diff --git a/mod/attendance/warnings.php b/mod/attendance/warnings.php new file mode 100644 index 0000000..bfb1ab8 --- /dev/null +++ b/mod/attendance/warnings.php @@ -0,0 +1,203 @@ +<?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/>. + +/** + * Allows default warnings to be modified. + * + * @package mod_attendance + * @copyright 2017 Dan Marsden http://danmarsden.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__.'/../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/mod/attendance/lib.php'); +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); + +$action = optional_param('action', '', PARAM_ALPHA); +$notid = optional_param('notid', 0, PARAM_INT); +$id = optional_param('id', 0, PARAM_INT); + +$url = new moodle_url('/mod/attendance/warnings.php'); + +// This page is used for configuring default set and for configuring attendance level set. +if (empty($id)) { + // This is the default status set - show appropriate admin stuff and check admin permissions. + admin_externalpage_setup('managemodules'); + + $output = $PAGE->get_renderer('mod_attendance'); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('defaultwarnings', 'mod_attendance')); + $tabmenu = attendance_print_settings_tabs('defaultwarnings'); + echo $tabmenu; + +} else { + // This is an attendance level config. + $cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + + require_login($course, false, $cm); + $context = context_module::instance($cm->id); + require_capability('mod/attendance:changepreferences', $context); + + $att = new mod_attendance_structure($att, $cm, $course, $PAGE->context); + + $PAGE->set_url($url); + $PAGE->set_title($course->shortname. ": ".$att->name); + $PAGE->set_heading($course->fullname); + $PAGE->navbar->add($att->name); + + $output = $PAGE->get_renderer('mod_attendance'); + $tabs = new attendance_tabs($att, attendance_tabs::TAB_WARNINGS); + echo $output->header(); + echo $output->heading(get_string('attendanceforthecourse', 'attendance').' :: ' .format_string($course->fullname)); + echo $output->render($tabs); + +} + +$mform = new mod_attendance\form\addwarning($url, array('notid' => $notid, 'id' => $id)); + +if ($data = $mform->get_data()) { + if (empty($data->notid)) { + // Insert new record. + $notify = new stdClass(); + if (empty($id)) { + $notify->idnumber = 0; + } else { + $notify->idnumber = $att->id; + } + + $notify->warningpercent = $data->warningpercent; + $notify->warnafter = $data->warnafter; + $notify->maxwarn = $data->maxwarn; + $notify->emailuser = empty($data->emailuser) ? 0 : $data->emailuser; + $notify->emailsubject = $data->emailsubject; + $notify->emailcontent = $data->emailcontent['text']; + $notify->emailcontentformat = $data->emailcontent['format']; + $notify->thirdpartyemails = ''; + if (!empty($data->thirdpartyemails)) { + $notify->thirdpartyemails = implode(',', $data->thirdpartyemails); + } + $existingrecord = $DB->record_exists('attendance_warning', array('idnumber' => $notify->idnumber, + 'warningpercent' => $notify->warningpercent, + 'warnafter' => $notify->warnafter)); + if (empty($existingrecord)) { + $DB->insert_record('attendance_warning', $notify); + echo $OUTPUT->notification(get_string('warningupdated', 'mod_attendance'), 'success'); + } else { + echo $OUTPUT->notification(get_string('warningfailed', 'mod_attendance'), 'warning'); + } + + } else { + $notify = $DB->get_record('attendance_warning', array('id' => $data->notid)); + if (!empty($id) && $data->idnumber != $att->id) { + // Someone is trying to update a record for a different attendance. + print_error('invalidcoursemodule'); + } else { + $notify = new stdClass(); + $notify->id = $data->notid; + $notify->idnumber = $data->idnumber; + $notify->warningpercent = $data->warningpercent; + $notify->warnafter = $data->warnafter; + $notify->maxwarn = $data->maxwarn; + $notify->emailuser = empty($data->emailuser) ? 0 : $data->emailuser; + $notify->emailsubject = $data->emailsubject; + $notify->emailcontentformat = $data->emailcontent['format']; + $notify->emailcontent = $data->emailcontent['text']; + $notify->thirdpartyemails = ''; + if (!empty($data->thirdpartyemails)) { + $notify->thirdpartyemails = implode(',', $data->thirdpartyemails); + } + $existingrecord = $DB->get_record('attendance_warning', array('idnumber' => $notify->idnumber, + 'warningpercent' => $notify->warningpercent, 'warnafter' => $notify->warnafter)); + if (empty($existingrecord) || $existingrecord->id == $notify->id) { + $DB->update_record('attendance_warning', $notify); + echo $OUTPUT->notification(get_string('warningupdated', 'mod_attendance'), 'success'); + } else { + echo $OUTPUT->notification(get_string('warningfailed', 'mod_attendance'), 'error'); + } + } + } +} +if ($action == 'delete' && !empty($notid)) { + if (!optional_param('confirm', false, PARAM_BOOL)) { + $cancelurl = $url; + $url->params(array('action' => 'delete', 'notid' => $notid, 'sesskey' => sesskey(), 'confirm' => true, 'id' => $id)); + echo $OUTPUT->confirm(get_string('deletewarningconfirm', 'mod_attendance'), $url, $cancelurl); + echo $OUTPUT->footer(); + exit; + } else { + require_sesskey(); + $params = array('id' => $notid); + if (!empty($att)) { + // Add id/level to array. + $params['idnumber'] = $att->id; + } + $DB->delete_records('attendance_warning', $params); + echo $OUTPUT->notification(get_string('warningdeleted', 'mod_attendance'), 'success'); + } +} +if ($action == 'update' && !empty($notid)) { + $existing = $DB->get_record('attendance_warning', array('id' => $notid)); + $content = $existing->emailcontent; + $existing->emailcontent = array(); + $existing->emailcontent['text'] = $content; + $existing->emailcontent['format'] = $existing->emailcontentformat; + $existing->notid = $existing->id; + $existing->id = $id; + $mform->set_data($existing); + $mform->display(); +} else if ($action == 'add' && confirm_sesskey()) { + $mform->display(); +} else { + if (empty($id)) { + $warningdesc = get_string('warningdesc', 'mod_attendance'); + $idnumber = 0; + } else { + $warningdesc = get_string('warningdesc_course', 'mod_attendance'); + $idnumber = $att->id; + } + echo $OUTPUT->box($warningdesc, 'generalbox attendancedesc', 'notice'); + $existingnotifications = $DB->get_records('attendance_warning', + array('idnumber' => $idnumber), + 'warningpercent'); + + if (!empty($existingnotifications)) { + $table = new html_table(); + $table->head = array(get_string('warningthreshold', 'mod_attendance'), + get_string('numsessions', 'mod_attendance'), + get_string('emailsubject', 'mod_attendance'), + ''); + foreach ($existingnotifications as $notification) { + $url->params(array('action' => 'delete', 'notid' => $notification->id, 'id' => $id)); + $actionbuttons = $OUTPUT->action_icon($url, new pix_icon('t/delete', + get_string('delete', 'attendance')), null, null); + $url->params(array('action' => 'update', 'notid' => $notification->id, 'id' => $id)); + $actionbuttons .= $OUTPUT->action_icon($url, new pix_icon('t/edit', + get_string('update', 'attendance')), null, null); + $table->data[] = array($notification->warningpercent, $notification->warnafter, + $notification->emailsubject, $actionbuttons); + } + echo html_writer::table($table); + } + $addurl = new moodle_url('/mod/attendance/warnings.php', array('action' => 'add', 'id' => $id)); + echo $OUTPUT->single_button($addurl, get_string('addwarning', 'mod_attendance')); + +} + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-debug.js b/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-debug.js new file mode 100644 index 0000000..6bb50aa --- /dev/null +++ b/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-debug.js @@ -0,0 +1,46 @@ +YUI.add('moodle-mod_attendance-groupfilter', function (Y, NAME) { + +/* global M */ +// eslint-disable-next-line camelcase +M.mod_attendance = M.mod_attendance || {}; +M.mod_attendance.groupfilter = { + groupmappings: null, + + init: function(opts) { + "use strict"; + + this.groupmappings = opts.groupmappings; + Y.one('#id_group').after('change', this.update_user_list, this); + }, + + /** + * Update the user list with those found in the selected group. + */ + update_user_list: function() { // eslint-disable-line camelcase + "use strict"; + var groupid, userlist, users, userid, opt; + + // Get the list of users in the current group. + groupid = Y.one('#id_group').get('value'); + users = this.groupmappings[groupid]; + + // Remove the options from the users select. + userlist = Y.one('#id_users'); + userlist.get('options').remove(); + + // Repopulate the users select with those users in the selected group (if any). + if (users !== undefined) { + for (userid in users) { + if (users.hasOwnProperty(userid)) { + opt = Y.Node.create('<option></option>'); + opt.set('value', userid); + opt.set('text', users[userid]); + userlist.appendChild(opt); + } + } + } + } +}; + + +}, '@VERSION@', {"requires": ["base", "node"]}); diff --git a/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-min.js b/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-min.js new file mode 100644 index 0000000..af80444 --- /dev/null +++ b/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_attendance-groupfilter",function(a,e){M.mod_attendance=M.mod_attendance||{},M.mod_attendance.groupfilter={groupmappings:null,init:function(e){"use strict";this.groupmappings=e.groupmappings,a.one("#id_group").after("change",this.update_user_list,this)},update_user_list:function(){"use strict";var e,t,n,o,i;if(e=a.one("#id_group").get("value"),n=this.groupmappings[e],(t=a.one("#id_users")).get("options").remove(),n!==undefined)for(o in n)n.hasOwnProperty(o)&&((i=a.Node.create("<option></option>")).set("value",o),i.set("text",n[o]),t.appendChild(i))}}},"@VERSION@",{requires:["base","node"]}); \ No newline at end of file diff --git a/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter.js b/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter.js new file mode 100644 index 0000000..6bb50aa --- /dev/null +++ b/mod/attendance/yui/build/moodle-mod_attendance-groupfilter/moodle-mod_attendance-groupfilter.js @@ -0,0 +1,46 @@ +YUI.add('moodle-mod_attendance-groupfilter', function (Y, NAME) { + +/* global M */ +// eslint-disable-next-line camelcase +M.mod_attendance = M.mod_attendance || {}; +M.mod_attendance.groupfilter = { + groupmappings: null, + + init: function(opts) { + "use strict"; + + this.groupmappings = opts.groupmappings; + Y.one('#id_group').after('change', this.update_user_list, this); + }, + + /** + * Update the user list with those found in the selected group. + */ + update_user_list: function() { // eslint-disable-line camelcase + "use strict"; + var groupid, userlist, users, userid, opt; + + // Get the list of users in the current group. + groupid = Y.one('#id_group').get('value'); + users = this.groupmappings[groupid]; + + // Remove the options from the users select. + userlist = Y.one('#id_users'); + userlist.get('options').remove(); + + // Repopulate the users select with those users in the selected group (if any). + if (users !== undefined) { + for (userid in users) { + if (users.hasOwnProperty(userid)) { + opt = Y.Node.create('<option></option>'); + opt.set('value', userid); + opt.set('text', users[userid]); + userlist.appendChild(opt); + } + } + } + } +}; + + +}, '@VERSION@', {"requires": ["base", "node"]}); diff --git a/mod/attendance/yui/src/groupfilter/build.json b/mod/attendance/yui/src/groupfilter/build.json new file mode 100644 index 0000000..9545400 --- /dev/null +++ b/mod/attendance/yui/src/groupfilter/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_attendance-groupfilter", + "builds": { + "moodle-mod_attendance-groupfilter": { + "jsfiles": [ + "groupfilter.js" + ] + } + } +} \ No newline at end of file diff --git a/mod/attendance/yui/src/groupfilter/js/groupfilter.js b/mod/attendance/yui/src/groupfilter/js/groupfilter.js new file mode 100644 index 0000000..5cd7ade --- /dev/null +++ b/mod/attendance/yui/src/groupfilter/js/groupfilter.js @@ -0,0 +1,41 @@ +/* global M */ +// eslint-disable-next-line camelcase +M.mod_attendance = M.mod_attendance || {}; +M.mod_attendance.groupfilter = { + groupmappings: null, + + init: function(opts) { + "use strict"; + + this.groupmappings = opts.groupmappings; + Y.one('#id_group').after('change', this.update_user_list, this); + }, + + /** + * Update the user list with those found in the selected group. + */ + update_user_list: function() { // eslint-disable-line camelcase + "use strict"; + var groupid, userlist, users, userid, opt; + + // Get the list of users in the current group. + groupid = Y.one('#id_group').get('value'); + users = this.groupmappings[groupid]; + + // Remove the options from the users select. + userlist = Y.one('#id_users'); + userlist.get('options').remove(); + + // Repopulate the users select with those users in the selected group (if any). + if (users !== undefined) { + for (userid in users) { + if (users.hasOwnProperty(userid)) { + opt = Y.Node.create('<option></option>'); + opt.set('value', userid); + opt.set('text', users[userid]); + userlist.appendChild(opt); + } + } + } + } +}; diff --git a/mod/attendance/yui/src/groupfilter/meta/groupfilter.json b/mod/attendance/yui/src/groupfilter/meta/groupfilter.json new file mode 100644 index 0000000..5308751 --- /dev/null +++ b/mod/attendance/yui/src/groupfilter/meta/groupfilter.json @@ -0,0 +1,8 @@ +{ + "moodle-mod_attendance-groupfilter": { + "requires": [ + "base", + "node" + ] + } +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/.gitignore b/mod/bigbluebuttonbn/.gitignore new file mode 100644 index 0000000..618417a --- /dev/null +++ b/mod/bigbluebuttonbn/.gitignore @@ -0,0 +1,14 @@ +/.buildpath +/.project +/.settings* +/config.php +/deploy* +/moodle-plugin-ci/ +/output* +/node_modules/ +/ci + +/vendor/* +!/vendor/firebase/ + +.DS_Store diff --git a/mod/bigbluebuttonbn/.travis.yml b/mod/bigbluebuttonbn/.travis.yml new file mode 100644 index 0000000..12d8ce3 --- /dev/null +++ b/mod/bigbluebuttonbn/.travis.yml @@ -0,0 +1,196 @@ +language: php + +dist: bionic +sudo: required + +services: + - mysql + - postgresql + +addons: + chrome: stable +# firefox: "47.0.1" + postgresql: "9.6" + apt: + packages: + - openjdk-8-jre-headless + - chromium-chromedriver + +cache: + +env: + global: + - MUSTACHE_IGNORE_NAMES=broken.mustache + - COVERAGE=false + +jobs: + include: + - # MOODLE_32_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_32_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_32_STABLE DB=mysqli + - # MOODLE_32_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_32_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_32_STABLE DB=pgsql + + - # MOODLE_33_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_33_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_33_STABLE DB=mysqli + - # MOODLE_33_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_33_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_33_STABLE DB=pgsql + + - # MOODLE_34_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_34_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_34_STABLE DB=mysqli + - # MOODLE_34_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_34_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_34_STABLE DB=pgsql + - # MOODLE_34_STABLE with mysql using php 7.2 + if: env(ALL) = true OR env(MOODLE_34_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_34_STABLE DB=mysqli + - # MOODLE_34_STABLE with pgsql using php 7.2 + if: env(ALL) = true OR env(MOODLE_34_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_34_STABLE DB=pgsql + + - # MOODLE_35_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_35_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_35_STABLE DB=mysqli + - # MOODLE_35_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_35_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_35_STABLE DB=pgsql + - # MOODLE_35_STABLE with mysql using php 7.2 + if: env(ALL) = true OR env(MOODLE_35_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_35_STABLE DB=mysqli + - # MOODLE_35_STABLE with pgsql using php 7.2 + if: env(ALL) = true OR env(MOODLE_35_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_35_STABLE DB=pgsql + + - # MOODLE_36_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_36_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_36_STABLE DB=mysqli + - # MOODLE_36_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_36_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_36_STABLE DB=pgsql + - # MOODLE_36_STABLE with mysql using php 7.2 + if: env(ALL) = true OR env(MOODLE_36_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_36_STABLE DB=mysqli + - # MOODLE_36_STABLE with pgsql using php 7.2 + if: env(ALL) = true OR env(MOODLE_36_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_36_STABLE DB=pgsql + + - # MOODLE_37_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_37_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_37_STABLE DB=mysqli + - # MOODLE_37_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_37_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_37_STABLE DB=pgsql + - # MOODLE_37_STABLE with mysql using php 7.2 + if: env(ALL) = true OR env(MOODLE_37_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_37_STABLE DB=mysqli + - # MOODLE_37_STABLE with pgsql using php 7.2 + if: env(ALL) = true OR env(MOODLE_37_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_37_STABLE DB=pgsql + + - # MOODLE_38_STABLE with mysql using php 7.1 + if: env(ALL) = true OR env(MOODLE_38_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_38_STABLE DB=mysqli + - # MOODLE_38_STABLE with pgsql using php 7.1 + if: env(ALL) = true OR env(MOODLE_38_STABLE) = true + php: 7.1 + env: MOODLE_BRANCH=MOODLE_38_STABLE DB=pgsql + - # MOODLE_38_STABLE with mysql using php 7.2 + if: env(ALL) = true OR env(MOODLE_38_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_38_STABLE DB=mysqli + - # MOODLE_38_STABLE with pgsql using php 7.2 + if: env(ALL) = true OR env(MOODLE_38_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_38_STABLE DB=pgsql + + - # MOODLE_39_STABLE with mysql using php 7.2 + if: env(ALL) = true OR env(MOODLE_39_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_39_STABLE DB=mysqli + - # MOODLE_39_STABLE with pgsql using php 7.2 + if: env(ALL) = true OR env(MOODLE_39_STABLE) = true + php: 7.2 + env: MOODLE_BRANCH=MOODLE_39_STABLE DB=pgsql + - # MOODLE_39_STABLE with mysql using php 7.3 + if: env(ALL) = true OR env(MOODLE_39_STABLE) = true + php: 7.3 + env: MOODLE_BRANCH=MOODLE_39_STABLE DB=mysqli + - # MOODLE_39_STABLE with pgsql using php 7.3 + if: env(ALL) = true OR env(MOODLE_39_STABLE) = true + php: 7.3 + env: MOODLE_BRANCH=MOODLE_39_STABLE DB=pgsql + - # MOODLE_39_STABLE with mysql using php 7.4 + if: env(ALL) = true OR env(MOODLE_39_STABLE) = true + php: 7.4 + env: MOODLE_BRANCH=MOODLE_39_STABLE DB=mysqli + - # MOODLE_39_STABLE with pgsql using php 7.4 + if: env(ALL) = true OR env(MOODLE_39_STABLE) = true + php: 7.4 + env: MOODLE_BRANCH=MOODLE_39_STABLE DB=pgsql + + - # master with mysql using php 7.4 + php: 7.4 + env: MOODLE_BRANCH=master DB=mysqli + - # master with pgsql using php 7.4 + php: 7.4 + env: MOODLE_BRANCH=master DB=pgsql + +before_install: + - phpenv config-rm xdebug.ini + - | + if [ $MOODLE_BRANCH == "MOODLE_32_STABLE" ] || [ $MOODLE_BRANCH == "MOODLE_33_STABLE" ] || [ $MOODLE_BRANCH == "MOODLE_34_STABLE" ] || [ $MOODLE_BRANCH == "MOODLE_36_STABLE" ]; then + nvm install 8.9.4 + else + nvm install 14.15.0 + fi + - npm install + - cd ../.. + - composer self-update 1.10.12 + - composer create-project -n --no-dev --prefer-dist blackboard-open-source/moodle-plugin-ci:v2 ci ^2 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci phpdoc + - moodle-plugin-ci behat --profile chrome + # - | + # # Grunt fails since version 3.8 + # # https://moodle.org/mod/forum/discuss.php?d=389744 + # if [ $MOODLE_BRANCH != "master" && $MOODLE_BRANCH != "MOODLE_38_STABLE" && $MOODLE_BRANCH != "MOODLE_39_STABLE"]; then + # moodle-plugin-ci grunt + # fi + # - moodle-plugin-ci mustache # Not working + # - moodle-plugin-ci phpunit # Not working diff --git a/mod/bigbluebuttonbn/CHANGES b/mod/bigbluebuttonbn/CHANGES new file mode 100644 index 0000000..8fc9284 --- /dev/null +++ b/mod/bigbluebuttonbn/CHANGES @@ -0,0 +1,35 @@ +2.4-beta2 (2019101004) +Bug: Patch for adding timestamp to join. +Bug: Fix for issue after bug fixed on BBB post_events script (CONTRIB-7457). +Bug: Fix for check capabilities also in module context (CONTRIB-8371 & GITHUB-315). +Bug: Fix for preventing bugged displays on recordings (GITHUB-321). +Enhancement: Update to external.php. +Enhancement: Add default welcome message to settings (CONTRIB-7845). + +2.4-beta1 (2019101003) +Bug: Fix for issue with search on updated recordings only works if page is refreshed (CONTRIB-7700). +Bug: Fix for end session action not always working (CONTRIB-7895). +Bug: Fix for message sending triggered by recordng ready callback hangs (CONTRIB-7905). +Bug: Fix for issue with mod_bigbluebuttonbn\privacy\provider::get_contexts_for_userid (CONTRIB-7915). +Bug: Fix for participation report is not correctly implemented (CONTRIB-7951). +Bug: Fix for "Recordings Only" instances have the feature [Restrict Access] disabled (CONTRIB-7960). +Bug: Fix for missing strings for the new recording formats (CONTRIB-7962). +Bug: Fix for "Moderator by default" setting does not recognise custom roles (CONTRIB-7966). +Bug: Fix for BBB hard-coded dependency on mod_lti (CONTRIB-7969). +Bug: Fix for possible CSRF vulnerability in BBB ajax intermediate script (CONTRIB-7971). +Bug: Fix for BBB add participant module setting doesn't respect group mode/capabilities (CONTRIB-7972). +Bug: Fix for error while creating or updating a BBB activity (CONTRIB-8019). +Bug: Fix for '&' breaks the password when randomly assigned (CONTRIB-8049). +Bug: Fix for validation of recording links fails some times (CONTRIB-8051). +Bug: Fix for problem of List Table Pagination (CONTRIB-8053). +Bug: Fix for improve performance on view (CONTRIB-8157). +Enhancement: Add a way to reprocess completion validation (CONTRIB-7917). +Enhancement: Completion by engagement should include configurable metrics (CONTRIB-7919). +Enhancement: BBB should only include core libraries as required (CONTRIB-7970). +Enhancement: Notification should contain link to BBB (CONTRIB-8029). +Enhancement: Corrected problem of Import List Same Course Names (CONTRIB-8052). +Enhancement: Recording parameters in activity form (CONTRIB-8116). +Enhancement: Change the UI for Participants to User/Role Mapping (CONTRIB-8164). +New Feature: Completion should be based on events captured from the live session (CONTRIB-7457). +New Feature: Completion should be extended for supporting engagement (CONTRIB-7908). +New Feature: Configure meeting room from the plugin (CONTRIB-8239). diff --git a/mod/bigbluebuttonbn/LICENSE b/mod/bigbluebuttonbn/LICENSE new file mode 100644 index 0000000..ef7e7ef --- /dev/null +++ b/mod/bigbluebuttonbn/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program 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. + + This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/mod/bigbluebuttonbn/README.md b/mod/bigbluebuttonbn/README.md new file mode 100644 index 0000000..bcc307a --- /dev/null +++ b/mod/bigbluebuttonbn/README.md @@ -0,0 +1,68 @@ +[](https://travis-ci.org/blindsidenetworks/moodle-mod_bigbluebuttonbn) +[](https://scrutinizer-ci.com/g/blindsidenetworks/moodle-mod_bigbluebuttonbn/?branch=master) + +BigBlueButtonBN Activity Module for Moodle +========================================== +BigBlueButton is an open source web conferencing system that enables universities and colleges to deliver a high-quality learning experience to remote students. + +These instructions describe how to install the BigBlueButtonBN Activity Module for Moodle. This module is developed and supported by Blindside Networks, the company that started the BigBlueButton project in 2007. + +With the latest version of this plugin you can + +- Create links in any course that can be used to create rooms/sessions in a BigBlueButton server +- Specify join open/close dates that will appear in the Moodle calendar +- Create a custom welcome messages that appears in the chat window when users join the session +- Launch BigBlueButton in its own tab or window +- Assign the role uses will have in BigBlueButton (moderator, viewer) per user or role in Moodle +- Pre-upload presentations +- Monitor the active sessions for the course and end any session (eject all users) +- Record and playback your lectures +- Access and manage recorded lectures +- Import recording links from a different course, and more. + + +Note that on previous versions of Moodle you will need to use the specific version of this plugin. + +| Moodle Version | Branch | Version | +|-------------------|--------------|-------------------------| +| Moodle 2.0 - 2.5 | v1.1-stable | v1.1.1 (2015062101) | +| Moodle 2.6 | v2.0-stable | v2.0.4 (2015080611) | +| Moodle 2.7 - 2.9 | v2.1-stable | v2.1.15 (2016051920) | +| Moodle 3.0 - 3.1 | v2.2-stable | v2.2.12 (2017101020) | +| Moodle 3.2 - 3.9 | v2.3-stable | v2.3.4 (2019042009) | +| Moodle 3.4 - 3.9 | v2.4-beta | v2.4-beta1 (2019101003) | + +Prerequisites +============= +You need: + +1. A server running Moodle +2. A BigBlueButton 0.8 (or later) server running on a separate server (not on the same server as your Moodle site) + +Blindside Networks provides you a test BigBlueButton server for testing this plugin. To use this test server, just accept the default settings when configuring the activity module. The default settings are + + url: http://test-install.blindsidenetworks.com/bigbluebutton/ + + salt: 8cd8ef52e8e101574e400365b55e11a6 + +For information on how to setup your own BigBlueButton server see + +http://bigbluebutton.org/ + +Obtaining the source +==================== +This GitHub repository at + +https://github.com/blindsidenetworks/moodle-mod_bigbluebuttonbn/ + +contains the latest source. We recommend to download the latest snapshot from the Moodle Plugin Directory. + + +Note: Since version 2.2 the use of the RecordingsBN activity module to access recordings is no longer needed. But when running an older version, it is requiered in order to gain access to the recordings. + + +Contact Us +========== +If you have feedback, enhancement requests, or would like commercial support for hosting, integrating, customizing, branding, or scaling BigBlueButton, contact us at + +http://blindsidenetworks.com/ diff --git a/mod/bigbluebuttonbn/RELEASENOTES b/mod/bigbluebuttonbn/RELEASENOTES new file mode 100644 index 0000000..0275780 --- /dev/null +++ b/mod/bigbluebuttonbn/RELEASENOTES @@ -0,0 +1,375 @@ +2.4-beta2 (2019101004) +Bug: Patch for adding timestamp to join. +Bug: Fix for issue after bug fixed on BBB post_events script (CONTRIB-7457). +Bug: Fix for check capabilities also in module context (CONTRIB-8371 & GITHUB-315). +Bug: Fix for preventing bugged displays on recordings (GITHUB-321). +Enhancement: Update to external.php. +Enhancement: Add default welcome message to settings (CONTRIB-7845). + +2.4-beta1 (2019101003) +Bug: Fix for issue with search on updated recordings only works if page is refreshed (CONTRIB-7700). +Bug: Fix for end session action not always working (CONTRIB-7895). +Bug: Fix for message sending triggered by recordng ready callback hangs (CONTRIB-7905). +Bug: Fix for issue with mod_bigbluebuttonbn\privacy\provider::get_contexts_for_userid (CONTRIB-7915). +Bug: Fix for participation report is not correctly implemented (CONTRIB-7951). +Bug: Fix for "Recordings Only" instances have the feature [Restrict Access] disabled (CONTRIB-7960). +Bug: Fix for missing strings for the new recording formats (CONTRIB-7962). +Bug: Fix for "Moderator by default" setting does not recognise custom roles (CONTRIB-7966). +Bug: Fix for BBB hard-coded dependency on mod_lti (CONTRIB-7969). +Bug: Fix for possible CSRF vulnerability in BBB ajax intermediate script (CONTRIB-7971). +Bug: Fix for BBB add participant module setting doesn't respect group mode/capabilities (CONTRIB-7972). +Bug: Fix for error while creating or updating a BBB activity (CONTRIB-8019). +Bug: Fix for '&' breaks the password when randomly assigned (CONTRIB-8049). +Bug: Fix for validation of recording links fails some times (CONTRIB-8051). +Bug: Fix for problem of List Table Pagination (CONTRIB-8053). +Bug: Fix for improve performance on view (CONTRIB-8157). +Enhancement: Add a way to reprocess completion validation (CONTRIB-7917). +Enhancement: Completion by engagement should include configurable metrics (CONTRIB-7919). +Enhancement: BBB should only include core libraries as required (CONTRIB-7970). +Enhancement: Notification should contain link to BBB (CONTRIB-8029). +Enhancement: Corrected problem of Import List Same Course Names (CONTRIB-8052). +Enhancement: Recording parameters in activity form (CONTRIB-8116). +Enhancement: Change the UI for Participants to User/Role Mapping (CONTRIB-8164). +New Feature: Completion should be based on events captured from the live session (CONTRIB-7457). +New Feature: Completion should be extended for supporting engagement (CONTRIB-7908). +New Feature: Configure meeting room from the plugin (CONTRIB-8239). + +2.3.6 (2019042011) +Bug: Fix for issue with users joining meetings with static URLs on mobile devices. + +2.3.5 (2019042010) +Bug: Fix for issue with users joining meetings with static URLs. + +2.3.4 (2019042009) +Bug: Fix for issue with regression BBB incorrectly distributes moderator rights to student-role (CONTRIB-7627). +Bug: Fix for issue with regression meeting name is not required in the recording list (CONTRIB-7703). +Bug: Fix for issue with imported recordings shows all the "Imported recordings" in the course (CONTRIB-7961). +Bug: Fix for issue with administrator and manager-role cannot see recordings when groups are used (CONTRIB-8038). +Bug: Fix for issue with BigBlueButton notifications include the sender email-address (CONTRIB-8042). +Bug: Fix for issue with notification when activity is updated not respecting enrolment status (CONTRIB-8046). +Bug: Fix for issue with wrong role ids in Website Administration (CONTRIB-8047). +Bug: Fix for issue with performance on view (CONTRIB-8157). +Bug: Fix for issue with closingtime shown on the Dashboard if it isn't set in the activity instance (CONTRIB-8158). +Bug: Fix for issue with "wait for moderator" not allowing join meetings from mobile app (CONTRIB-8162). +Bug: Fix for issue with option 'Session can be recorded' not shown for 'Room/Activity only' (CONTRIB-8174). +Enhancement: Added option for turn off sending notifications when recording ready feature. (CONTRIB-8026). + +2.3.3 (2019042008) +Bug: Fix for issue with instances not being saved on Moodle 3.5 and earlier (CONTRIB-8019). + +2.3.2 (2019042007) +Bug: Fix for issue with Restrict Access setting on "Recording Only" and "Room/Activity Only" modes (CONTRIB-7960). +Bug: Fix for issue with HAndle missing language strings for new recording formats (CONTRIB-7962). +Bug: Fix for issue with hard-coded dependency on mod_lti (CONTRIB-7969). +Bug: Fix for issue with missing sesskey on ajax requests (CONTRIB-7971). +Bug: Fix for issue with participant module setting doesn't respect group mode/capabilities (CONTRIB-7972). +Bug: Fix for issue with recordings table doesn't respect forced group mode (CONTRIB-7973). +Bug: Fix for issue with fails core privacy provider testcase (CONTRIB-7983). + +2.3.1 (2019042006) +Bug: Fix for issue with notifications on creation (CONTRIB-7874). +Bug: Fix for issue with unit test failure on v2.2 stable (CONTRIB-7875). +Bug: Fix for issue with DML when using Oracle or MSSQL (CONTRIB-7894). + +2.3 (2019042005) +Tag: Bumped version and release tags and updated maturity to stable. + +2.3-rc1 (2019042004) +Tag: Bumped version and release tags and updated maturity to release candidate. + +2.3-beta2 (2019042003) +Bug: Fix for issue with importing recordings on 3.7 (CONTRIB-7754). +Bug: Fix for regression on recording ready notifications (CONTRIB-7740). + +2.3-beta2 (2019042002) +Bug: Fix for recording ready notifications not working (CONTRIB-7740). +Bug: Fix for false positive sent to the logs (CONTRIB-7742). +Bug: Fix for recording settings showing empty when no feature can be edited (CONTRIB-7701). +Bug: Fix for notifications to be sent only to users allowed to see the activity (CONTRIB-7733). + +2.3-beta1 (2019042001) +Bug: Fix for issue with JWT library clashing in Moodle 3.7 (CONTRIB-7716). +Bug: Fix for implementation of resetting function (CONTRIB-7695). +Bug: Fix for for issue with delete instances with recycle bin disabled (CONTRIB-7660). +Bug: Fix for hard-coded string in room.js and old reference to RecordingsBN (GIT-130). + +2.3-beta0 (2019042000) +Bug: Fix for instanceTypeProfiles array used by keys but NOT defined so (CONTRIB-7399). +Bug: Fix for hard-coded strings in recordings table based on YUI datatables (CONTRIB-7518). +Bug: Fix for Join session timeline button don't work (CONTRIB-7538). +Bug: Fix for Index page does not show actions (CONTRIB-7579). +Enhancement: Playback recordings should be shown in a new tab (CONTRIB-7386). +Enhancement: Limit instance type repertoire by capabilities (CONTRIB-7400). +Enhancement: Media files in the description are not shown (CONTRIB-7440). +Enhancement: Code cleanup and refactoring (CONTRIB-7625). +New Feature: Being able to define a default presentation file for a Moodle site through settings (CONTRIB-6970). +New Feature: Users being able to see recordings of only the rooms they have access to (CONTRIB-7225). +New Feature: Add support for mobile application (CONTRIB-7227). +New Feature: Add support for muteOnStart (CONTRIB-7519). +New Feature: Add option for forcing all the sessions to be recorded (with no button) (CONTRIB-7520). +New Feature: Add support for recordings of breakout rooms (CONTRIB-7521). +New Feature: Recording list should give an option for searching recordings (CONTRIB-7522). + +2.2.13 (2017101021) +Bug: Fix for issue with users joining meetings with static URLs. + +2.2.12 (2017101020) +Bug: Fix for issue with JWT library clashing in Moodle 3.7 (CONTRIB-7716). +Bug: Fix for implementation of resetting function (CONTRIB-7695). +Bug: Fix for for issue with delete instances with recycle bin disabled (CONTRIB-7660). +Bug: Fix for hard-coded string in room.js and old reference to RecordingsBN (GIT-130). + +2.2.11 (2017101019) +Bug: Fix for participation rules for users applied as rules for roles (CONTRIB-7627). +Bug: Fix for some themes broken updated prefix for icon-bigbluebutton (CONTRIB-7616). +Bug: Fix on missing strings for new recording formats (CONTRIB-7617). +Bug: Fix notification messages send in FOMAT_MOODLE instead of FORMAT_HTML. + +2.2.10 (2017101018) +Bug: Fixed issue JWT library conflicting with other plugins using it (CONTRIB-7612). + +2.2.9 (2017101017) +Bug: Fixed issue with media files not shown in the description (CONTRIB-7454). +Bug: Fixed issue with date-time shown in calendar and timeline (CONTRIB-7589). + +2.2.8 (2017101016) +Enhancement: Added support for new privacy methods in Moodle 3.6, 3.5.3, 3.4.6 (CONTRIB-7549). + +2.2.7 (2017101015) +Bug: Fixed issue with recording-ready notifications sometimes sent more than once (CONTRIB-7438). +Bug: Fixed issue with rooms not being created when using Oracle (CONTRIB-7481). +Bug: Fixed issue with privacy requests not being processed when the user was already deleted (CONTRIB-7527). +Bug: Fixed issue with recordings from other Moodle server may be wrongly included (CONTRIB-7528). +Bug. Fixed issue with warning message in Moodle 3.2 and previous when instance was created or updated (CONTRIB-7512). +Enhancement: Added support for HTML5 client that now can be set to be used by default or per room (CONTRIB-7413). + +2.2.6 (2017101014) +Hot-fix: Fixed issue with room viewing too slow when several recordings where included (CONTRIB-7435). +Bug: Fixed regression issue with activities still not listed in Timeline tab (CONTRIB-7377). +Bug: Fixed issue with default settings for moderator not applied to existent activities (CONTRIB-7437). + +2.2.5 (2017101013) +Bug: Fixed issue with recording lenght shown as 0 in some cases (CONTRIB-7383). +Bug: Fixed issus with activities not listed in Timeline tab of Course overview block (CONTRIB-7377). +Bug: Fixed issue with filter "Show only imported links" not working work (CONTRIB-7424) +Bug: Fixed issue with thumbnails overriding a css class in Boost theme. +Enhancement: Validates and notifies when recordings are incorrectly migrated from one BBB server to another. + +2.2.4 (2017101012) +Bug: Fixed issue with error message shown for some rooms when displayed (CONTRIB-7243). +Bug: Fixed issue with recordings are accessible from copied activities (CONTRIB-7118). +Bug: Fixed issue with edit name or description of an recording under firefox (CONTRIB-7279). +Bug: Fixed issue with Hide/Show images not changing when publishing unpublishing recordings (CONTRIB-7297). +Bug: Fixed issue when preview is disabled, on publish/unpublish recordings js fails (CONTRIB-7298). +Bug: Fixed issue with diverged install and upgrade schema (CONTRIB-7302). +Feature: Added support for privacy API in compliance with GDPR (CONTRIB-7290). + +2.2.3 (2017101011) +Bug: Fixed issue with Groups not working properly in BigBlueButtonBN (CONTRIB-7220). +Bug: Fixed issue with Time out when migrating from 2.1.14 to 2.2.2 (CONTRIB-7221). +Bug: Fixed issue with When recordings are unpublished they disappear (CONTRIB-7234). + +2.2.2 (2017101010) +Bug: Fixed issue with YUI datatable not deleting recordings correctly. +Bug: Fixed issue with events not being properly described. +Bug: Fixed issue with guest users not being able to view or use rooms. +Bug: Fixed issue with thumbnails overriding a css class on Essential theme. +Bug: Fixed issue with separated groups showing all the recordings when the user is allowed to manage them. +Feature: Show a warning message to administrators when using the BBB server pre-configured by default. +DB migration: Fix issue with internal passwords that may have not been generated on instance creation. + +2.2.1 (2017101009) +Bug: Fixed issue with voice bridge not accepted when editing the instance. +Bug: Added fa-circle as the default icon for Totara. +Bug: Fixed icon for delete action broken in Participant List. +Bug: Fixed issue with tags not working when class core_tag_tag not defined. +Bug: Fixed flickering on recording preview. +Bug: Fixed issue with JavaScript for modform not working with participantList in old versions. +Bug: Fixed issue with not correctly formed BBB url and trailing white characters in shared_secret. +Feature: Recording preview can now be enabled/disabled by configuration (Updates the db). + +2.2 (2017101008) +Bug: Fix for issue with JS not supported by IE11!. +Bug: Fix for flicking page when waiting for moderator enabled. + +2.2 (2017101007) +Enhancement: Added alert boxed warning message in configuration that is shown when the old cfg global variable is used. +Enhancement: Added filter for restricting access to links when recordings include multiple formats. +Bug: Fix for issue with recording option showing up on 'Room/Activity only' instances. + +2.2 (2017101006) +Bug: Make sure the common settings are wiped out when type is changed to 'recordings only' +Bug: Fixed issue with import recordings not working with postgresql +Bug: Fixed issue with recordings_imported_enabled not being shown in mod_form +Bug: Fixed issue with filter for recordings when groups enabled +Bug: Fixed issue with previews shown even with servers not implementing the feature +Bug: Fixed general warning to be shown only to selected roles +Bug: Fixed issue with 'protect' button not shown on imported links +Bug: Fixed issue with turning on/off importing links by configuration + +2.2 (2017101005) +Bug: Fix for issue with internal passwords not being generated for instances created with 2017101003. +Bug: Fix for issue with notifications not being send when the recording is ready. + +2.2 (2017101004) +Hot-Fix: Fix for issue with internal passwords not being generated on instance creation. + +2.2 (2017101003) +Bug: Fix for issue when using BN servers, Recording name and description could not be edited. +Bug: Fix for issue with html in the name when multi-language filter is used. +Bug: Fix for issue when editing instance the passwords were overridden. + +2.2 (2017101002) +Bug: Fix for issues raised in the previous release. +Bug: Fix for issues when importing link and using PostgreSQL. + +2.2 (2017101001) +Enhancement: A general warning message can be set up by configuration. + +2.2 (2017101000) +Enhancement: Included the parameter userID as part of the join request. +Enhancement: Added preview thumbnails to recordings. +Enhancement: Merged RecordingsBN by adding a type to differentiate if the instance implements "Room & Recordings"/"Only room"/"Only recordings". +Enhancement: Recording name and description can be edited. +Enhancement: Make use of Moodle tags for tagging recordings. +Enhancement: Imported links to real recordings can also be imported. +Enhancement: Links can be imported from activities in the same course. +Enhancement: When users playback a recording an event for reports is logged. +Enhancement: Guest users may are able to join a meeting based on Moodle permissions. +Enhancement: Added automated tests. +Refactoring: Major refactoring was applied in order to improve the quality of the code. + +2.1.14 (2016051919) +Bug: Fixed issue participant list not considering rules based on Moodle roles when user had multiple. +Bug: Fixed issue participant list not applying the default rule correctly. + +2.1.13 (2016051918) +Bug: Fixed issue with hint on hovering participant list button +Bug: Fixed issue participant list not considering rules based on Moodle roles. + +2.1.12 (2016051917) +Hot fix: After some changes introduced in v2.1.7 the way participants are handled changed. As the permissions are now based in role and user id, previous settings were ignored. (CONTRIB-6925) +Bug: Fixed issue with mod_form not being rendered when participants were not set (happened with activities imported from old versions). +Bug: Fixed issue with all recordings shown to all users when separated groups were set. + +2.1.11 (2016051916) +Bug: Fixed issue with role names not properly shown in Totara. +Bug: Fixed issue with roles in participant list not showing correctly. +Bug: Fixed issue with users enrolled and no active being included in participant list. +Bug: Fixed issue with moderators being considered viewers. +Bug: Fixed issue with message shown to non-moderators when the room is not open yet. +Bug: Fixed issue with messages not properly sent when the room/activity was added/updated. + +2.1.10 (2016051915) +Hot fix: After some changes introduced in v2.1.7 the way participants are handled changed. This affected the way the new UI renders the settings form. (CONTRIB-6900) +Bug: Fixed Participants for style in mod_form not working in all versions. +Bug: Fixed issue with action buttons not being updated on publish/unpublish and delete recordings +Bug: Fixed style in 'Import recordings' and 'Go back' buttons. + +2.1.9 (2016051914) +Bug: Fixed Participants for style in mod_form and buttons in view. +Bug: Fixed Participants for style in mod_form not working in all versions. + +2.1.8 (2016051913) +Bug: Fixed issue with custom role names failing when they were edited after being used. +Bug: Fixed issue with separated groups. + +2.1.7 (2016051911) +Bug: Fixed issue with roles not shown correctly in Totara and any custom implementation. +Bug: Fixed issue with a string not properly localized. + +2.1.7 (2016051911) +Bug: Fixed issue with roles not shown correctly in Totara and any custom implementation. +Bug: Fixed issue with a string not properly localised. + +2.1.6 (2016051910) +Bug: Fixed issue with groups not shown correctly when set up as separated and no groups where created. (CONTRIB-6652) +Bug: Fixed issue with references to recordings and imported links duplicated when a backup is restored in a different course. +Bug: Fixed issue with references to recordings and imported links not being properly restored when using the option import from backups. +Bug: Fixed issue with guest users not able to join as moderators even though the role is matched with the role with BBB . + +2.1.5 (2016051909) +Bug: Fixed issue with actions for recording management not working in instances with schedule. (CONTRIB-6654) +Bug: Standardized output in intermediate page for activities not started/ended. +Bug: Fixed issue with past BBB sessions listed as upcoming in the course overview block. (CONTRIB-6650) +Bug: Fixed issue with default bigbluebutton server settings. (CONTRIB-6630) +Bug: Rework to prevent potential issue with recordings not fetched when the course has too many groups. + +2.1.4 (2016051908) +Bug: Reverted changes made to the language strings in previous release. + +2.1.3 (2016051907) +Bug: Fixed issue with course logs not being backup/restore. +Bug: Fixed issue with localized event description affecting multi-language sites. +Bug: Fixed issue with activities not shown in the dashboard block in Moodle 3.x. + +2.1.2 (2016051906) +Bug: Fixed version format for recording list so it works for formats used in non western countries. +Bug: Fixed issue with user limit not being correctly enforced when default limit was updated and edition was not allowed. (This bug only affected sites making use of the user limit feature) +Bug: Fixed issue in "wait for moderator" feature. The only way to enable the join session was to refresh the page manually. + +2.1.1 (2016051905) +Bug: Fixed issue with database upgrade when updating from versions older than 2.1.x. (This bug only affected users upgrading directly from 1.x to 2.1.0). + +2.1.0 (2016051904) +Tag: Bumped version and release tags and updated maturity to stable + +2.1.0-rc2 (2016051903) +Bug: Fixed missing variable triggering warning message showing up on in development mode +Bug: Fixed bug in format of imported recordings on YUI table +Bug: Fixed a bug in import recording. Recordings from deleted activities were not considered. + +2.1.0-rc1 (2016051902) +Tag: Bumped release tag and updated maturity + +2.1.0-b3 (2016051902) +Bug: Considers all the imported recordings corresponding to a course. +Bug: Duration was not shown properly in recordings +Bug: All events are localized +Bug: recordings not showing up after restore +Bug: backup/restore were not working on logs +Bug: Added missing event + +2.1.0-b2 (2016051901) +Bug: Fix for action management not working in recordings. + +2.1.0-b1 (2016051900) +Enhancement: Links to recordings can now be imported into a room/activity from a different course room/activity. +Bug: Fixed backups. The logs, a secondary table dependent of mdl_bigbluebuttonbn used for tracking meeting creation is now included as part of the backup. +Bug: Fixed issue with database prefix. + +2.0.4 (2015080611) +Bug: Fixed a bug introduced in the previous revision that was making pre-uploaded presentations to be ignored when a new activity was created. +Bug: Changed wording for message shown when user is waiting for moderator. + +2.0.3 (2015080610) +Enhancement: Make sure the php-curl extension is installed. For pre-uploading slides, the use of the php-curl extension is required. If it is not installed or enabled the feature won't be available. +Bug: Toggle form for tagging sessions was flicking for a second when the activity page was shown. +Bug: When creating a new and pre-uploading presentation is turned off an error messages comes up. +Bug: When for security reasons either the webserver or the proxy ahead of the webserver(s) adds the header x-content-type-options: nosniff to force the MIME Type verification, the javascript in the client doesn't work in Chrome/IE. + +2.0.2 (2015080609) +Bug: When using localization in a language different than English, the recording table produced for RecordingsBN showed an 'Invalid date' message. + +2.0.1 (2015080608) +Bug: Adding a presentation file while creating a new BigBlueButtonBN activity failed. Only after editing the activity and adding a presentation file again would let the feature work. +Bug: Fixed warning message was shown in the logs when joining a session + +1.0.7 (April 8, 2012) +Enhancement: Added Spanish and French. +Enhancement: Synced API calls with BigBlueButton 0.8-beta-4. + +1.0.6 (October 31, 2011) +Enhancement: Extended supported languages (ru), fixed a bug in bigbluebuttonbn/index.php + +1.0.5 (October 19, 2011) +Enhancement: Extended supported languages + +1.0.4 (October 12, 2011) +Enhancement: Added support for separate groups + +1.0-b2 (September 16, 2011) +Enhancement: Removed unnecesarry code related to grade scales + +1.0-b1 (September 13, 2011) +Tag: Beta release for use with BigBlueButton 0.71a and BigBlueButton 0.8-beta servers diff --git a/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php new file mode 100644 index 0000000..d43b22f --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_activity_task.class.php @@ -0,0 +1,81 @@ +<?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 for backup BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php'); + +/** + * Backup task that provides all the settings and steps to perform one complete backup of the activity. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_bigbluebuttonbn_activity_task extends backup_activity_task +{ + /** + * Define (add) particular settings this activity can have. + * + * @return void + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have. + * + * @return void + */ + protected function define_my_steps() { + // Choice only has one structure step. + $this->add_step(new backup_bigbluebuttonbn_activity_structure_step('bigbluebuttonbn_structure', 'bigbluebuttonbn.xml')); + } + + /** + * Code the transformations to perform in the activity in order to get transportable (encoded) links. + * + * @param string $content + * + * @return string + */ + public static function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot.'/mod/bigbluebuttonbn', '#'); + + // Link to the list of bigbluebuttonbns. + $pattern = '#('.$base."\/index.php\?id\=)([0-9]+)#"; + $content = preg_replace($pattern, '$@BIGBLUEBUTTONBNINDEX*$2@$', $content); + + // Link to bigbluebuttonbn view by moduleid. + $pattern = '#('.$base."\/view.php\?id\=)([0-9]+)#"; + $content = preg_replace($pattern, '$@BIGBLUEBUTTONBNVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php new file mode 100644 index 0000000..3bab6e2 --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/backup_bigbluebuttonbn_stepslib.php @@ -0,0 +1,85 @@ +<?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 for the structure used for backup BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define all the backup steps that will be used by the backup_bigbluebuttonbn_activity_task. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_bigbluebuttonbn_activity_structure_step extends backup_activity_structure_step +{ + /** + * Define the complete bigbluebuttonbn structure for backup, with file and id annotations. + * + * @return object + */ + protected function define_structure() { + + // To know if we are including userinfo. + $userinfo = $this->get_setting_value('userinfo'); + + // Define each element separated. + $bigbluebuttonbn = new backup_nested_element('bigbluebuttonbn', array('id'), array( + 'type', 'course', 'name', 'intro', 'introformat', 'meetingid', + 'moderatorpass', 'viewerpass', 'wait', 'record', 'recordallfromstart', + 'recordhidebutton', 'welcome', 'voicebridge', 'openingtime', 'closingtime', 'timecreated', + 'timemodified', 'presentation', 'participants', 'userlimit', + 'recordings_html', 'recordings_deleted', 'recordings_imported', 'recordings_preview', + 'clienttype', 'muteonstart', 'completionattendance', + 'completionengagementchats', 'completionengagementtalks', 'completionengagementraisehand', + 'completionengagementpollvotes', 'completionengagementemojis')); + + $logs = new backup_nested_element('logs'); + + $log = new backup_nested_element('log', array('id'), array( + 'courseid', 'bigbluebuttonbnid', 'userid', 'timecreated', 'meetingid', 'log', 'meta', )); + + // Build the tree. + $bigbluebuttonbn->add_child($logs); + $logs->add_child($log); + + // Define sources. + $bigbluebuttonbn->set_source_table('bigbluebuttonbn', array('id' => backup::VAR_ACTIVITYID)); + + // This source definition only happen if we are including user info. + if ($userinfo) { + $log->set_source_table('bigbluebuttonbn_logs', array('bigbluebuttonbnid' => backup::VAR_PARENTID)); + } + + // Define id annotations. + $log->annotate_ids('user', 'userid'); + + // Define file annotations. + $bigbluebuttonbn->annotate_files('mod_bigbluebuttonbn', 'intro', null); + + // Return the root element (bigbluebuttonbn), wrapped into standard activity structure. + return $this->prepare_activity_structure($bigbluebuttonbn); + } +} diff --git a/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php new file mode 100644 index 0000000..8c151a4 --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_activity_task.class.php @@ -0,0 +1,107 @@ +<?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 for restore BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php'); + +/** + * Restore task that provides all the settings and steps to perform one complete restore of the activity. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_bigbluebuttonbn_activity_task extends restore_activity_task +{ + /** + * Define (add) particular settings this activity can have. + * + * @return void + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have. + * + * @return void + */ + protected function define_my_steps() { + // BigBlueButtonBN only has one structure step. + $this->add_step(new restore_bigbluebuttonbn_activity_structure_step('bigbluebuttonbn_structure', 'bigbluebuttonbn.xml')); + } + + /** + * Define the contents in the activity that must be processed by the link decoder. + * + * @return array + */ + public static function define_decode_contents() { + $contents = array(); + $contents[] = new restore_decode_content('bigbluebuttonbn', array('intro'), 'bigbluebuttonbn'); + $contents[] = new restore_decode_content('bigbluebuttonbn_logs', array('log'), 'bigbluebuttonbn_logs'); + return $contents; + } + + /** + * Define the decoding rules for links belonging to the activity to be executed by the link decoder. + * + * @return array + */ + public static function define_decode_rules() { + $rules = array(); + $rules[] = new restore_decode_rule('BIGBLUEBUTTONBNVIEWBYID', '/mod/bigbluebuttonbn/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('BIGBLUEBUTTONBNINDEX', '/mod/bigbluebuttonbn/index.php?id=$1', 'course'); + return $rules; + } + + /** + * Define the restoring rules for logs belonging to the activity to be executed by the link decoder. + * + * @return array + */ + public static function define_restore_log_rules() { + $rules = array(); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'add', 'view.php?id={course_module}', '{bigbluebuttonbn}'); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'update', 'view.php?id={course_module}', '{bigbluebuttonbn}'); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'view', 'view.php?id={course_module}', '{bigbluebuttonbn}'); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'report', 'report.php?id={course_module}', '{bigbluebuttonbn}'); + return $rules; + } + + /** + * Define the restoring rules for course associated to the activity to be executed by the link decoder. + * + * @return array + */ + public static function define_restore_log_rules_for_course() { + $rules = array(); + $rules[] = new restore_log_rule('bigbluebuttonbn', 'view all', 'index.php?id={course}', null); + return $rules; + } +} diff --git a/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php new file mode 100644 index 0000000..1adaca0 --- /dev/null +++ b/mod/bigbluebuttonbn/backup/moodle2/restore_bigbluebuttonbn_stepslib.php @@ -0,0 +1,97 @@ +<?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 for the structure used for restore BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define all the restore steps that will be used by the restore_url_activity_task. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_bigbluebuttonbn_activity_structure_step extends restore_activity_structure_step +{ + /** + * Structure step to restore one bigbluebuttonbn activity. + * + * @return array + */ + protected function define_structure() { + $paths = array(); + $paths[] = new restore_path_element('bigbluebuttonbn', '/activity/bigbluebuttonbn'); + $paths[] = new restore_path_element('bigbluebuttonbn_logs', '/activity/bigbluebuttonbn/logs/log'); + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * Process a bigbluebuttonbn restore. + * + * @param object $data The data in object form + * @return void + */ + protected function process_bigbluebuttonbn($data) { + global $DB; + $data = (object) $data; + $data->course = $this->get_courseid(); + $data->timemodified = $this->apply_date_offset($data->timemodified); + // Insert the bigbluebuttonbn record. + $newitemid = $DB->insert_record('bigbluebuttonbn', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * Process a bigbluebuttonbn_logs restore (additional table). + * + * @param object $data The data in object form + * @return void + */ + protected function process_bigbluebuttonbn_logs($data) { + global $DB; + $data = (object) $data; + // Apply modifications. + $data->courseid = $this->get_mappingid('course', $data->courseid); + $data->bigbluebuttonbnid = $this->get_new_parentid('bigbluebuttonbn'); + $data->userid = $this->get_mappingid('user', $data->userid); + $data->timecreated = $this->apply_date_offset($data->timecreated); + // Insert the bigbluebuttonbn_logs record. + $newitemid = $DB->insert_record('bigbluebuttonbn_logs', $data); + // Immediately after inserting associated record, call this. + $this->set_mapping('bigbluebuttonbn_logs', $data->id, $newitemid); + } + + /** + * Actions to be executed after the restore is completed + * + * @return array + */ + protected function after_execute() { + // Add bigbluebuttonbn related files, no need to match by itemname (just internally handled context). + $this->add_related_files('mod_bigbluebuttonbn', 'intro', null); + } +} diff --git a/mod/bigbluebuttonbn/bbb_ajax.php b/mod/bigbluebuttonbn/bbb_ajax.php new file mode 100644 index 0000000..0aad083 --- /dev/null +++ b/mod/bigbluebuttonbn/bbb_ajax.php @@ -0,0 +1,141 @@ +<?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/>. + +/** + * Intermediator for handling ajax requests resulting on BigBlueButton actions. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +define('AJAX_SCRIPT', true); + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/locallib.php'); +require_once(__DIR__.'/brokerlib.php'); + +global $PAGE, $USER, $CFG, $SESSION, $DB; + +$params['action'] = optional_param('action', '', PARAM_TEXT); +$params['callback'] = optional_param('callback', '', PARAM_TEXT); +$params['id'] = optional_param('id', '', PARAM_TEXT); +$params['idx'] = optional_param('idx', '', PARAM_TEXT); +$params['bigbluebuttonbn'] = optional_param('bigbluebuttonbn', 0, PARAM_INT); +$params['signed_parameters'] = optional_param('signed_parameters', '', PARAM_TEXT); +$params['updatecache'] = optional_param('updatecache', 'false', PARAM_TEXT); +$params['meta'] = optional_param('meta', '', PARAM_TEXT); + +require_login(null, true); +require_sesskey(); + +if (empty($params['action'])) { + header('HTTP/1.0 400 Bad Request. Parameter ['.$params['action'].'] was not included'); + return; +} + +$error = bigbluebuttonbn_broker_validate_parameters($params); +if (!empty($error)) { + header('HTTP/1.0 400 Bad Request. '.$error); + return; +} + +if ($params['bigbluebuttonbn']) { + $bbbbrokerinstance = bigbluebuttonbn_view_instance_bigbluebuttonbn($params['bigbluebuttonbn']); + $cm = $bbbbrokerinstance['cm']; + $bigbluebuttonbn = $bbbbrokerinstance['bigbluebuttonbn']; + $context = context_module::instance($cm->id); +} + +if (!isset($SESSION->bigbluebuttonbn_bbbsession) || is_null($SESSION->bigbluebuttonbn_bbbsession)) { + header('HTTP/1.0 400 Bad Request. No session variable set'); + return; +} +$bbbsession = $SESSION->bigbluebuttonbn_bbbsession; + +$userid = $USER->id; +if (!isloggedin() && $PAGE->course->id == SITEID) { + $userid = guest_user()->id; +} +$hascourseaccess = ($PAGE->course->id == SITEID) || can_access_course($PAGE->course, $userid); + +if (!$hascourseaccess) { + header('HTTP/1.0 401 Unauthorized'); + return; +} + +$type = null; +if (isset($bbbsession['bigbluebuttonbn']->type)) { + $type = $bbbsession['bigbluebuttonbn']->type; +} + +$typeprofiles = bigbluebuttonbn_get_instance_type_profiles(); +$enabledfeatures = bigbluebuttonbn_get_enabled_features($typeprofiles, $type); +try { + header('Content-Type: application/javascript; charset=utf-8'); + $a = strtolower($params['action']); + if ($a == 'meeting_info') { + $meetinginfo = bigbluebuttonbn_broker_meeting_info($bbbsession, $params, ($params['updatecache'] == 'true')); + echo $meetinginfo; + return; + } + if ($a == 'meeting_end') { + $meetingend = bigbluebuttonbn_broker_meeting_end($bbbsession, $params); + echo $meetingend; + return; + } + if ($a == 'recording_play') { + $recordingplay = bigbluebuttonbn_broker_recording_play($params); + echo $recordingplay; + return; + } + if ($a == 'recording_links') { + $recordinglinks = bigbluebuttonbn_broker_recording_links($bbbsession, $params); + echo $recordinglinks; + return; + } + if ($a == 'recording_info') { + $recordinginfo = bigbluebuttonbn_broker_recording_info($bbbsession, $params, $enabledfeatures['showroom']); + echo $recordinginfo; + return; + } + if ($a == 'recording_publish' || $a == 'recording_unpublish' || + $a == 'recording_delete' || $a == 'recording_edit' || + $a == 'recording_protect' || $a == 'recording_unprotect') { + $recordingaction = bigbluebuttonbn_broker_recording_action($bbbsession, $params, $enabledfeatures['showroom']); + echo $recordingaction; + return; + } + if ($a == 'recording_import') { + echo bigbluebuttonbn_broker_recording_import($bbbsession, $params); + return; + } + if ($a == 'recording_list_table') { + $PAGE->set_context(context_course::instance($PAGE->course->id)); + $recordingdata = bigbluebuttonbn_broker_get_recording_data($bbbsession, $params, $enabledfeatures); + echo $recordingdata; + return; + } + if ($a == 'completion_validate') { + $completionvalidate = bigbluebuttonbn_broker_completion_validate($bigbluebuttonbn, $params); + echo $completionvalidate; + return; + } + header('HTTP/1.0 400 Bad request. The action '. $a . ' doesn\'t exist'); +} catch (Exception $e) { + header('HTTP/1.0 500 Internal Server Error. '.$e->getMessage()); +} diff --git a/mod/bigbluebuttonbn/bbb_broker.php b/mod/bigbluebuttonbn/bbb_broker.php new file mode 100644 index 0000000..dc43ac7 --- /dev/null +++ b/mod/bigbluebuttonbn/bbb_broker.php @@ -0,0 +1,77 @@ +<?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/>. + +/** + * Intermediator for handling requests from the BigBlueButton server. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/locallib.php'); +require_once(__DIR__.'/brokerlib.php'); + +use \Firebase\JWT\JWT; + +global $PAGE, $USER, $CFG, $SESSION, $DB; + +$params = $_REQUEST; + +if (!isset($params['action']) || empty($params['action'])) { + header('HTTP/1.0 400 Bad Request. Parameter ['.$params['action'].'] was not included'); + return; +} + +// The endpoints for ajax requests are now implemented in bbb_ajax.php. +// The endpoints for recording_ready and meeting_events callbacks must be moved to services (CONTRIB-7440). +// But in order to support the transition, requests other than the callbacks are redirected to bbb_ajax.php. +if ($params['action'] != 'recording_ready' && $params['action'] != 'meeting_events') { + $url = $CFG->wwwroot . '/mod/bigbluebuttonbn/bbb_ajax.php?' . http_build_query($params, '', '&'); + header("Location: " . $url); + exit; +} + +$error = bigbluebuttonbn_broker_validate_parameters($params); +if (!empty($error)) { + header('HTTP/1.0 400 Bad Request. '.$error); + return; +} + +$bbbbrokerinstance = bigbluebuttonbn_view_instance_bigbluebuttonbn($params['bigbluebuttonbn']); +$bigbluebuttonbn = $bbbbrokerinstance['bigbluebuttonbn']; +$context = context_course::instance($bigbluebuttonbn->course); +$PAGE->set_context($context); + +try { + $a = strtolower($params['action']); + if ($a == 'recording_ready') { + bigbluebuttonbn_broker_recording_ready($params, $bigbluebuttonbn); + return; + } + if ($a == 'meeting_events') { + // When meeting_events callback is implemented by BigBlueButton, Moodle receives a POST request + // which is processed in the function using super globals. + bigbluebuttonbn_broker_meeting_events($bigbluebuttonbn); + return; + } + header('HTTP/1.0 400 Bad request. The action '. $a . ' doesn\'t exist'); +} catch (Exception $e) { + header('HTTP/1.0 500 Internal Server Error. '.$e->getMessage()); +} diff --git a/mod/bigbluebuttonbn/bbb_view.php b/mod/bigbluebuttonbn/bbb_view.php new file mode 100644 index 0000000..bb4ca2d --- /dev/null +++ b/mod/bigbluebuttonbn/bbb_view.php @@ -0,0 +1,454 @@ +<?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 for BigBlueButton interaction. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once(dirname(__FILE__).'/locallib.php'); + +global $SESSION; + +$action = required_param('action', PARAM_TEXT); +$id = optional_param('id', 0, PARAM_INT); +$bn = optional_param('bn', 0, PARAM_INT); +$href = optional_param('href', '', PARAM_TEXT); +$mid = optional_param('mid', '', PARAM_TEXT); +$rid = optional_param('rid', '', PARAM_TEXT); +$rtype = optional_param('rtype', 'presentation', PARAM_TEXT); +$errors = optional_param('errors', '', PARAM_TEXT); +$timeline = optional_param('timeline', 0, PARAM_INT); +$index = optional_param('index', 0, PARAM_INT); +$group = optional_param('group', -1, PARAM_INT); + +$bbbviewinstance = bigbluebuttonbn_view_validator($id, $bn); +if (!$bbbviewinstance) { + print_error(get_string('view_error_url_missing_parameters', 'bigbluebuttonbn')); +} + +$cm = $bbbviewinstance['cm']; +$course = $bbbviewinstance['course']; +$bigbluebuttonbn = $bbbviewinstance['bigbluebuttonbn']; +$context = context_module::instance($cm->id); + +require_login($course, true, $cm); + +$bbbsession = null; +if (isset($SESSION->bigbluebuttonbn_bbbsession)) { + $bbbsession = $SESSION->bigbluebuttonbn_bbbsession; +} + +if ($timeline || $index) { + // Validates if the BigBlueButton server is working. + $serverversion = bigbluebuttonbn_get_server_version(); + if (is_null($serverversion)) { + if ($bbbsession['administrator']) { + print_error('view_error_unable_join', 'bigbluebuttonbn', + $CFG->wwwroot.'/admin/settings.php?section=modsettingbigbluebuttonbn'); + exit; + } + if ($bbbsession['moderator']) { + print_error('view_error_unable_join_teacher', 'bigbluebuttonbn', + $CFG->wwwroot.'/course/view.php?id='.$bigbluebuttonbn->course); + exit; + } + print_error('view_error_unable_join_student', 'bigbluebuttonbn', + $CFG->wwwroot.'/course/view.php?id='.$bigbluebuttonbn->course); + exit; + } + + $bbbsession = mod_bigbluebuttonbn\locallib\bigbluebutton::build_bbb_session($cm, $course, $bigbluebuttonbn); + + // Check status and set extra values. + $activitystatus = bigbluebuttonbn_view_get_activity_status($bbbsession); + if ($activitystatus == 'ended') { + $bbbsession['presentation'] = bigbluebuttonbn_get_presentation_array( + $bbbsession['context'], $bbbsession['bigbluebuttonbn']->presentation); + } else if ($activitystatus == 'open') { + $bbbsession['presentation'] = bigbluebuttonbn_get_presentation_array( + $bbbsession['context'], $bbbsession['bigbluebuttonbn']->presentation, $bbbsession['bigbluebuttonbn']->id); + } + + // Check group. + if ($group >= 0) { + $bbbsession['group'] = $group; + $groupname = get_string('allparticipants'); + if ($bbbsession['group'] != 0) { + $groupname = groups_get_group_name($bbbsession['group']); + } + + // Assign group default values. + $bbbsession['meetingid'] .= '['.$bbbsession['group'].']'; + $bbbsession['meetingname'] .= ' ('.$groupname.')'; + } + + // Initialize session variable used across views. + $SESSION->bigbluebuttonbn_bbbsession = $bbbsession; +} + +// Print the page header. +$PAGE->set_context($context); +$PAGE->set_url('/mod/bigbluebuttonbn/bbb_view.php', array('id' => $cm->id, 'bigbluebuttonbn' => $bigbluebuttonbn->id)); +$PAGE->set_title(format_string($bigbluebuttonbn->name)); +$PAGE->set_cacheable(false); +$PAGE->set_heading($course->fullname); +$PAGE->blocks->show_only_fake_blocks(); + +switch (strtolower($action)) { + case 'logout': + if (isset($errors) && $errors != '') { + bigbluebuttonbn_bbb_view_errors($errors, $id); + break; + } + if (is_null($bbbsession)) { + bigbluebuttonbn_bbb_view_close_window_manually(); + break; + } + // Moodle event logger: Create an event for meeting left. + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['meeting_left'], $bigbluebuttonbn); + // Update the cache. + $meetinginfo = bigbluebuttonbn_get_meeting_info($bbbsession['meetingid'], BIGBLUEBUTTONBN_UPDATE_CACHE); + // Check the origin page. + $select = "userid = ? AND log = ?"; + $params = array( + 'userid' => $bbbsession['userID'], + 'log' => BIGBLUEBUTTONBN_LOG_EVENT_JOIN, + ); + $accesses = $DB->get_records_select('bigbluebuttonbn_logs', $select, $params, 'id ASC', 'id, meta', 1); + $lastaccess = end($accesses); + $lastaccess = json_decode($lastaccess->meta); + // If the user acceded from Timeline it should be redirected to the Dashboard. + if (isset($lastaccess->origin) && $lastaccess->origin == BIGBLUEBUTTON_ORIGIN_TIMELINE) { + redirect($CFG->wwwroot . '/my/'); + } + // Close the tab or window where BBB was opened. + bigbluebuttonbn_bbb_view_close_window(); + break; + case 'join': + if (is_null($bbbsession)) { + print_error('view_error_unable_join', 'bigbluebuttonbn'); + break; + } + // Check the origin page. + $origin = BIGBLUEBUTTON_ORIGIN_BASE; + if ($timeline) { + $origin = BIGBLUEBUTTON_ORIGIN_TIMELINE; + } else if ($index) { + $origin = BIGBLUEBUTTON_ORIGIN_INDEX; + } + // See if the session is in progress. + if (bigbluebuttonbn_is_meeting_running($bbbsession['meetingid'])) { + // Since the meeting is already running, we just join the session. + bigbluebuttonbn_bbb_view_join_meeting($bbbsession, $bigbluebuttonbn, $origin); + break; + } + // If user is not administrator nor moderator (user is steudent) and waiting is required. + if (!$bbbsession['administrator'] && !$bbbsession['moderator'] && $bbbsession['wait']) { + header('Location: '.$bbbsession['logoutURL']); + break; + } + // As the meeting doesn't exist, try to create it. + $response = bigbluebuttonbn_get_create_meeting_array( + bigbluebuttonbn_bbb_view_create_meeting_data($bbbsession), + bigbluebuttonbn_bbb_view_create_meeting_metadata($bbbsession), + $bbbsession['presentation']['name'], + $bbbsession['presentation']['url'] + ); + if (empty($response)) { + // The server is unreachable. + if ($bbbsession['administrator']) { + print_error('view_error_unable_join', 'bigbluebuttonbn', + $CFG->wwwroot.'/admin/settings.php?section=modsettingbigbluebuttonbn'); + break; + } + if ($bbbsession['moderator']) { + print_error('view_error_unable_join_teacher', 'bigbluebuttonbn', + $CFG->wwwroot.'/admin/settings.php?section=modsettingbigbluebuttonbn'); + break; + } + print_error('view_error_unable_join_student', 'bigbluebuttonbn', + $CFG->wwwroot.'/admin/settings.php?section=modsettingbigbluebuttonbn'); + break; + } + if ($response['returncode'] == 'FAILED') { + // The meeting was not created. + if (!$printerrorkey) { + print_error($response['message'], 'bigbluebuttonbn'); + break; + } + $printerrorkey = bigbluebuttonbn_get_error_key($response['messageKey'], 'view_error_create'); + print_error($printerrorkey, 'bigbluebuttonbn'); + break; + } + if ($response['hasBeenForciblyEnded'] == 'true') { + print_error(get_string('index_error_forciblyended', 'bigbluebuttonbn')); + break; + } + // Moodle event logger: Create an event for meeting created. + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['meeting_create'], $bigbluebuttonbn); + // Internal logger: Insert a record with the meeting created. + $overrides = array('meetingid' => $bbbsession['meetingid']); + $meta = '{"record":'.($bbbsession['record'] ? 'true' : 'false').'}'; + bigbluebuttonbn_log($bbbsession['bigbluebuttonbn'], BIGBLUEBUTTONBN_LOG_EVENT_CREATE, $overrides, $meta); + // Since the meeting is already running, we just join the session. + bigbluebuttonbn_bbb_view_join_meeting($bbbsession, $bigbluebuttonbn, $origin); + break; + case 'play': + $href = bigbluebuttonbn_bbb_view_playback_href($href, $mid, $rid, $rtype); + // Moodle event logger: Create an event for meeting left. + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['recording_play'], $bigbluebuttonbn, + ['other' => $rid]); + // Internal logger: Instert a record with the playback played. + $overrides = array('meetingid' => $bbbsession['meetingid']); + bigbluebuttonbn_log($bbbsession['bigbluebuttonbn'], BIGBLUEBUTTONBN_LOG_EVENT_PLAYED, $overrides); + // Execute the redirect. + header('Location: '.urldecode($href)); + break; + default: + bigbluebuttonbn_bbb_view_close_window(); +} + +/** + * Helper for getting the playback url that corresponds to an specific type. + * + * @param string $href + * @param string $mid + * @param string $rid + * @param string $rtype + * @return string + */ +function bigbluebuttonbn_bbb_view_playback_href($href, $mid, $rid, $rtype) { + if ($href != '' || $mid == '' || $rid == '') { + return $href; + } + $recordings = bigbluebuttonbn_get_recordings_array($mid, $rid); + if (empty($recordings)) { + return ''; + } + return bigbluebuttonbn_bbb_view_playback_href_lookup($recordings[$rid]['playbacks'], $rtype); +} + +/** + * Helper for looking up playback url in the recording playback array. + * + * @param array $playbacks + * @param string $type + * @return string + */ +function bigbluebuttonbn_bbb_view_playback_href_lookup($playbacks, $type) { + foreach ($playbacks as $playback) { + if ($playback['type'] == $type) { + return $playback['url']; + } + } + return ''; +} + +/** + * Helper for closing the tab or window when the user lefts the meeting. + * + * @return string + */ +function bigbluebuttonbn_bbb_view_close_window() { + global $OUTPUT, $PAGE; + echo $OUTPUT->header(); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-rooms', 'M.mod_bigbluebuttonbn.rooms.windowClose'); + echo $OUTPUT->footer(); +} + +/** + * Helper for showing a message when the tab or window can not be closed. + * + * @return string + */ +function bigbluebuttonbn_bbb_view_close_window_manually() { + echo get_string('view_message_tab_close', 'bigbluebuttonbn'); +} + +/** + * Helper for preparing data used for creating the meeting. + * + * @param array $bbbsession + * @return object + */ +function bigbluebuttonbn_bbb_view_create_meeting_data(&$bbbsession) { + $data = ['meetingID' => $bbbsession['meetingid'], + 'name' => bigbluebuttonbn_html2text($bbbsession['meetingname'], 64), + 'attendeePW' => $bbbsession['viewerPW'], + 'moderatorPW' => $bbbsession['modPW'], + 'logoutURL' => $bbbsession['logoutURL'], + ]; + $data['record'] = bigbluebuttonbn_bbb_view_create_meeting_data_record($bbbsession['record']); + // Check if auto_start_record is enable. + if ($data['record'] == 'true' && $bbbsession['recordallfromstart']) { + $data['autoStartRecording'] = 'true'; + // Check if hide_record_button is enable. + if ($bbbsession['recordhidebutton']) { + $data['allowStartStopRecording'] = 'false'; + } + } + + $data['welcome'] = trim($bbbsession['welcome']); + // Set the duration for the meeting. + $durationtime = bigbluebuttonbn_bbb_view_create_meeting_data_duration($bbbsession['bigbluebuttonbn']->closingtime); + if ($durationtime > 0) { + $data['duration'] = $durationtime; + $data['welcome'] .= '<br><br>'; + $data['welcome'] .= str_replace( + '%duration%', + (string) $durationtime, + get_string('bbbdurationwarning', 'bigbluebuttonbn') + ); + } + $voicebridge = intval($bbbsession['voicebridge']); + if ($voicebridge > 0 && $voicebridge < 79999) { + $data['voiceBridge'] = $voicebridge; + } + $maxparticipants = intval($bbbsession['userlimit']); + if ($maxparticipants > 0) { + $data['maxParticipants'] = $maxparticipants; + } + if ($bbbsession['muteonstart']) { + $data['muteOnStart'] = 'true'; + } + // Lock settings. + if ($bbbsession['disablecam']) { + $data['lockSettingsDisableCam'] = 'true'; + } + if ($bbbsession['disablemic']) { + $data['lockSettingsDisableMic'] = 'true'; + } + if ($bbbsession['disableprivatechat']) { + $data['lockSettingsDisablePrivateChat'] = 'true'; + } + if ($bbbsession['disablepublicchat']) { + $data['lockSettingsDisablePublicChat'] = 'true'; + } + if ($bbbsession['disablenote']) { + $data['lockSettingsDisableNote'] = 'true'; + } + if ($bbbsession['hideuserlist']) { + $data['lockSettingsHideUserList'] = 'true'; + } + if ($bbbsession['lockedlayout']) { + $data['lockSettingsLockedLayout'] = 'true'; + } + if ($bbbsession['lockonjoin']) { + $data['lockSettingsLockOnJoin'] = 'false'; + } + if ($bbbsession['lockonjoinconfigurable']) { + $data['lockSettingsLockOnJoinConfigurable'] = 'true'; + } + return $data; +} + +/** + * Helper for returning the flag to know if the meeting is recorded. + * + * @param boolean $record + * @return string + */ +function bigbluebuttonbn_bbb_view_create_meeting_data_record($record) { + if ((boolean)\mod_bigbluebuttonbn\locallib\config::recordings_enabled() && $record) { + return 'true'; + } + return 'false'; +} + +/** + * Helper for returning the duration expected for the meeting. + * + * @param string $closingtime + * @return integer + */ +function bigbluebuttonbn_bbb_view_create_meeting_data_duration($closingtime) { + if ((boolean)\mod_bigbluebuttonbn\locallib\config::get('scheduled_duration_enabled')) { + return bigbluebuttonbn_get_duration($closingtime); + } + return 0; +} + +/** + * Helper for preparing metadata used while creating the meeting. + * + * @param array $bbbsession + * @return array + */ +function bigbluebuttonbn_bbb_view_create_meeting_metadata(&$bbbsession) { + return bigbluebuttonbn_create_meeting_metadata($bbbsession); +} + +/** + * Helper for preparing data used while joining the meeting. + * + * @param array $bbbsession + * @param object $bigbluebuttonbn + * @param integer $origin + */ +function bigbluebuttonbn_bbb_view_join_meeting($bbbsession, $bigbluebuttonbn, $origin = 0) { + // Update the cache. + $meetinginfo = bigbluebuttonbn_get_meeting_info($bbbsession['meetingid'], BIGBLUEBUTTONBN_UPDATE_CACHE); + if ($bbbsession['userlimit'] > 0 && intval($meetinginfo['participantCount']) >= $bbbsession['userlimit']) { + // No more users allowed to join. + header('Location: '.$bbbsession['logoutURL']); + return; + } + // Build the URL. + $password = $bbbsession['viewerPW']; + if ($bbbsession['administrator'] || $bbbsession['moderator']) { + $password = $bbbsession['modPW']; + } + $bbbsession['createtime'] = $meetinginfo['createTime']; + $joinurl = bigbluebuttonbn_get_join_url($bbbsession['meetingid'], $bbbsession['username'], + $password, $bbbsession['logoutURL'], null, $bbbsession['userID'], $bbbsession['clienttype'], $bbbsession['createtime']); + // Moodle event logger: Create an event for meeting joined. + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['meeting_join'], $bigbluebuttonbn); + // Internal logger: Instert a record with the meeting created. + $overrides = array('meetingid' => $bbbsession['meetingid']); + $meta = '{"origin":'.$origin.'}'; + bigbluebuttonbn_log($bbbsession['bigbluebuttonbn'], BIGBLUEBUTTONBN_LOG_EVENT_JOIN, $overrides, $meta); + // Before executing the redirect, increment the number of participants. + bigbluebuttonbn_participant_joined($bbbsession['meetingid'], + ($bbbsession['administrator'] || $bbbsession['moderator'])); + // Execute the redirect. + header('Location: '.$joinurl); +} + +/** + * Helper for showinf error messages if any. + * + * @param string $serrors + * @param string $id + * @return string + */ +function bigbluebuttonbn_bbb_view_errors($serrors, $id) { + global $CFG, $OUTPUT; + $errors = (array) json_decode(urldecode($serrors)); + $msgerrors = ''; + foreach ($errors as $error) { + $msgerrors .= html_writer::tag('p', $error->{'message'}, array('class' => 'alert alert-danger'))."\n"; + } + echo $OUTPUT->header(); + print_error('view_error_bigbluebutton', 'bigbluebuttonbn', + $CFG->wwwroot.'/mod/bigbluebuttonbn/view.php?id='.$id, $msgerrors, $serrors); + echo $OUTPUT->footer(); +} diff --git a/mod/bigbluebuttonbn/brokerlib.php b/mod/bigbluebuttonbn/brokerlib.php new file mode 100644 index 0000000..2386d36 --- /dev/null +++ b/mod/bigbluebuttonbn/brokerlib.php @@ -0,0 +1,904 @@ +<?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/>. + +/** + * Broker helper methods. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Callback for meeting info. + * + * @param array $bbbsession + * @param array $params + * @param boolean $updatecache + * + * @return string + */ +function bigbluebuttonbn_broker_meeting_info($bbbsession, $params, $updatecache) { + $callbackresponse = array(); + $info = bigbluebuttonbn_get_meeting_info($params['id'], $updatecache); + $callbackresponse['info'] = $info; + $running = false; + if ($info['returncode'] == 'SUCCESS') { + $running = ($info['running'] === 'true'); + } + $callbackresponse['running'] = $running; + $status = array(); + $status["join_url"] = $bbbsession['joinURL']; + $status["join_button_text"] = get_string('view_conference_action_join', 'bigbluebuttonbn'); + $status["end_button_text"] = get_string('view_conference_action_end', 'bigbluebuttonbn'); + $participantcount = 0; + if (isset($info['participantCount'])) { + $participantcount = $info['participantCount']; + } + $canjoin = bigbluebuttonbn_broker_meeting_info_can_join($bbbsession, $running, $participantcount); + $status["can_join"] = $canjoin["can_join"]; + $status["message"] = $canjoin["message"]; + $canend = bigbluebuttonbn_broker_meeting_info_can_end($bbbsession, $running); + $status["can_end"] = $canend["can_end"]; + $callbackresponse['status'] = $status; + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Helper for evaluating if meeting can be joined, it is used by meeting info callback. + * + * @param array $bbbsession + * @param boolean $running + * @param boolean $participantcount + * + * @return array + */ +function bigbluebuttonbn_broker_meeting_info_can_join($bbbsession, $running, $participantcount) { + $status = array("can_join" => false); + if ($running) { + $status["message"] = get_string('view_error_userlimit_reached', 'bigbluebuttonbn'); + if ($bbbsession['userlimit'] == 0 || $participantcount < $bbbsession['userlimit']) { + $status["message"] = get_string('view_message_conference_in_progress', 'bigbluebuttonbn'); + $status["can_join"] = true; + } + return $status; + } + // If user is administrator, moderator or if is viewer and no waiting is required. + $status["message"] = get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn'); + if ($bbbsession['administrator'] || $bbbsession['moderator'] || !$bbbsession['wait']) { + $status["message"] = get_string('view_message_conference_room_ready', 'bigbluebuttonbn'); + $status["can_join"] = true; + } + return $status; +} + +/** + * Helper for evaluating if meeting can be ended, it is used by meeting info callback. + * + * @param array $bbbsession + * @param boolean $running + * + * @return boolean + */ +function bigbluebuttonbn_broker_meeting_info_can_end($bbbsession, $running) { + if ($running && ($bbbsession['administrator'] || $bbbsession['moderator'])) { + return array("can_end" => true); + } + return array("can_end" => false); +} + +/** + * Callback for meeting end. + * + * @param array $bbbsession + * @param array $params + * + * @return string + */ +function bigbluebuttonbn_broker_meeting_end($bbbsession, $params) { + if (!$bbbsession['administrator'] && !$bbbsession['moderator']) { + header('HTTP/1.0 401 Unauthorized. User not authorized to execute end command'); + return; + } + // Execute the end command. + bigbluebuttonbn_end_meeting($params['id'], $bbbsession['modPW']); + // Moodle event logger: Create an event for meeting ended. + if (isset($bbbsession['bigbluebuttonbn'])) { + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['meeting_end'], $bbbsession['bigbluebuttonbn']); + } + // Update the cache. + bigbluebuttonbn_get_meeting_info($params['id'], BIGBLUEBUTTONBN_UPDATE_CACHE); + $callbackresponse = array('status' => true); + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Callback for recording links. + * + * @param array $bbbsession + * @param array $params + * + * @return string + */ +function bigbluebuttonbn_broker_recording_links($bbbsession, $params) { + if (!$bbbsession['managerecordings']) { + header('HTTP/1.0 401 Unauthorized. User not authorized to execute update command'); + return; + } + $callbackresponse = array('status' => false); + if (isset($params['id']) && $params['id'] != '') { + $importedall = bigbluebuttonbn_get_recording_imported_instances($params['id']); + $callbackresponse['status'] = true; + $callbackresponse['links'] = count($importedall); + } + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Callback for recording info. + * + * @param array $bbbsession + * @param array $params + * @param boolean $showroom + * + * @return string + */ +function bigbluebuttonbn_broker_recording_info($bbbsession, $params, $showroom) { + if (!$bbbsession['managerecordings']) { + header('HTTP/1.0 401 Unauthorized. User not authorized to execute command'); + return; + } + $callbackresponse = array('status' => true, 'found' => false); + $courseid = $bbbsession['course']->id; + $bigbluebuttonbnid = null; + if ($showroom) { + $bigbluebuttonbnid = $bbbsession['bigbluebuttonbn']->id; + } + $includedeleted = $bbbsession['bigbluebuttonbn']->recordings_deleted; + // Retrieve the array of imported recordings. + $recordings = bigbluebuttonbn_get_allrecordings($courseid, $bigbluebuttonbnid, $showroom, $includedeleted); + if (array_key_exists($params['id'], $recordings)) { + // Look up for an update on the imported recording. + if (!array_key_exists('messageKey', $recordings[$params['id']])) { + // The recording was found. + $callbackresponse = bigbluebuttonbn_broker_recording_info_current($recordings[$params['id']], $params); + } + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; + } + // As the recordingid was not identified as imported recording link, look up for a real recording. + $recordings = bigbluebuttonbn_get_recordings_array($params['idx'], $params['id']); + if (array_key_exists($params['id'], $recordings)) { + // The recording was found. + $callbackresponse = bigbluebuttonbn_broker_recording_info_current($recordings[$params['id']], $params); + } + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Data used as for the callback for recording info. + * + * @param array $recording + * @param array $params + * + * @return string + */ +function bigbluebuttonbn_broker_recording_info_current($recording, $params) { + $callbackresponse['status'] = true; + $callbackresponse['found'] = true; + $callbackresponse['published'] = (string) $recording['published']; + if (!isset($params['meta']) || empty($params['meta'])) { + return $callbackresponse; + } + $meta = json_decode($params['meta'], true); + foreach (array_keys($meta) as $key) { + $callbackresponse[$key] = ''; + if (isset($recording[$key])) { + $callbackresponse[$key] = trim($recording[$key]); + } + } + return $callbackresponse; +} + +/** + * Callback for recording play. + * + * @param array $params + * + * @return string + */ +function bigbluebuttonbn_broker_recording_play($params) { + $callbackresponse = array('status' => true, 'found' => false); + $recordings = bigbluebuttonbn_get_recordings_array($params['idx'], $params['id']); + if (array_key_exists($params['id'], $recordings)) { + // The recording was found. + $callbackresponse = bigbluebuttonbn_broker_recording_info_current($recordings[$params['id']], $params); + } + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Callback for recording action. + * (publush/unpublish/protect/unprotect/edit/delete) + * + * @param array $bbbsession + * @param array $params + * @param boolean $showroom + * + * @return string + */ +function bigbluebuttonbn_broker_recording_action($bbbsession, $params, $showroom) { + if (!$bbbsession['managerecordings']) { + header('HTTP/1.0 401 Unauthorized. User not authorized to execute end command'); + return; + } + // Retrieve array of recordings that includes real and imported. + $bigbluebuttonbnid = null; + if ($showroom) { + $bigbluebuttonbnid = $bbbsession['bigbluebuttonbn']->id; + } + $recordings = bigbluebuttonbn_get_allrecordings( + $bbbsession['course']->id, + $bigbluebuttonbnid, + $showroom, + $bbbsession['bigbluebuttonbn']->recordings_deleted + ); + + $action = strtolower($params['action']); + // Excecute action. + $callbackresponse = bigbluebuttonbn_broker_recording_action_perform($action, $params, $recordings); + if ($callbackresponse['status']) { + // Moodle event logger: Create an event for action performed on recording. + bigbluebuttonbn_event_log( + \mod_bigbluebuttonbn\event\events::$events[$action], + $bbbsession['bigbluebuttonbn'], + ['other' => $params['id']] + ); + } + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Helper for performing actions on recordings. + * (publush/unpublish/protect/unprotect/edit/delete) + * + * @param string $action + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_perform($action, $params, $recordings) { + if ($action == 'recording_publish') { + return bigbluebuttonbn_broker_recording_action_publish($params, $recordings); + } + if ($action == 'recording_unpublish') { + return bigbluebuttonbn_broker_recording_action_unpublish($params, $recordings); + } + if ($action == 'recording_edit') { + return bigbluebuttonbn_broker_recording_action_edit($params, $recordings); + } + if ($action == 'recording_delete') { + return bigbluebuttonbn_broker_recording_action_delete($params, $recordings); + } + if ($action == 'recording_protect') { + return bigbluebuttonbn_broker_recording_action_protect($params, $recordings); + } + if ($action == 'recording_unprotect') { + return bigbluebuttonbn_broker_recording_action_unprotect($params, $recordings); + } +} + +/** + * Helper for performing publish on recordings. + * + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_publish($params, $recordings) { + if (bigbluebuttonbn_broker_recording_is_imported($recordings, $params['id'])) { + // Execute publish on imported recording link, if the real recording is published. + $realrecordings = bigbluebuttonbn_get_recordings_array( + $recordings[$params['id']]['meetingID'], + $recordings[$params['id']]['recordID'] + ); + // Only if the physical recording exist and it is published, execute publish on imported recording link. + if (!isset($realrecordings[$params['id']])) { + return array( + 'status' => false, + 'message' => get_string('view_recording_publish_link_deleted', 'bigbluebuttonbn') + ); + } + if ($realrecordings[$params['id']]['published'] !== 'true') { + return array( + 'status' => false, + 'message' => get_string('view_recording_publish_link_not_published', 'bigbluebuttonbn') + ); + } + return array( + 'status' => bigbluebuttonbn_publish_recording_imported( + $recordings[$params['id']]['imported'], + true + ) + ); + } + // As the recordingid was not identified as imported recording link, execute actual publish. + return array( + 'status' => bigbluebuttonbn_publish_recordings( + $params['id'], + 'true' + ) + ); +} + +/** + * Helper for performing unprotect on recordings. + * + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_unprotect($params, $recordings) { + if (bigbluebuttonbn_broker_recording_is_imported($recordings, $params['id'])) { + // Execute unprotect on imported recording link, if the real recording is unprotected. + $realrecordings = bigbluebuttonbn_get_recordings_array( + $recordings[$params['id']]['meetingID'], + $recordings[$params['id']]['recordID'] + ); + // Only if the physical recording exist and it is published, execute unprotect on imported recording link. + if (!isset($realrecordings[$params['id']])) { + return array( + 'status' => false, + 'message' => get_string('view_recording_unprotect_link_deleted', 'bigbluebuttonbn') + ); + } + if ($realrecordings[$params['id']]['protected'] === 'true') { + return array( + 'status' => false, + 'message' => get_string('view_recording_unprotect_link_not_unprotected', 'bigbluebuttonbn') + ); + } + return array( + 'status' => bigbluebuttonbn_protect_recording_imported( + $recordings[$params['id']]['imported'], + false + ) + ); + } + // As the recordingid was not identified as imported recording link, execute actual uprotect. + return array( + 'status' => bigbluebuttonbn_update_recordings( + $params['id'], + array('protect' => 'false') + ) + ); +} + +/** + * Helper for performing unpublish on recordings. + * + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_unpublish($params, $recordings) { + global $DB; + if (bigbluebuttonbn_broker_recording_is_imported($recordings, $params['id'])) { + // Execute unpublish or protect on imported recording link. + return array( + 'status' => bigbluebuttonbn_publish_recording_imported( + $recordings[$params['id']]['imported'], + false + ) + ); + } + // As the recordingid was not identified as imported recording link, execute unpublish on a real recording. + // First: Unpublish imported links associated to the recording. + $importedall = bigbluebuttonbn_get_recording_imported_instances($params['id']); + foreach ($importedall as $key => $record) { + $meta = json_decode($record->meta, true); + // Prepare data for the update. + $meta['recording']['published'] = 'false'; + $importedall[$key]->meta = json_encode($meta); + // Proceed with the update. + $DB->update_record('bigbluebuttonbn_logs', $importedall[$key]); + } + // Second: Execute the actual unpublish. + return array( + 'status' => bigbluebuttonbn_publish_recordings( + $params['id'], + 'false' + ) + ); +} + +/** + * Helper for performing protect on recordings. + * + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_protect($params, $recordings) { + global $DB; + if (bigbluebuttonbn_broker_recording_is_imported($recordings, $params['id'])) { + // Execute unpublish or protect on imported recording link. + return array( + 'status' => bigbluebuttonbn_protect_recording_imported( + $recordings[$params['id']]['imported'], + true + ) + ); + } + // As the recordingid was not identified as imported recording link, execute protect on a real recording. + // First: Protect imported links associated to the recording. + $importedall = bigbluebuttonbn_get_recording_imported_instances($params['id']); + foreach ($importedall as $key => $record) { + $meta = json_decode($record->meta, true); + // Prepare data for the update. + $meta['recording']['protected'] = 'true'; + $importedall[$key]->meta = json_encode($meta); + // Proceed with the update. + $DB->update_record('bigbluebuttonbn_logs', $importedall[$key]); + } + // Second: Execute the actual protect. + return array( + 'status' => bigbluebuttonbn_update_recordings( + $params['id'], + array('protect' => 'true') + ) + ); +} + +/** + * Helper for performing delete on recordings. + * + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_delete($params, $recordings) { + global $DB; + if (bigbluebuttonbn_broker_recording_is_imported($recordings, $params['id'])) { + // Execute delete on imported recording link. + return array( + 'status' => bigbluebuttonbn_delete_recording_imported( + $recordings[$params['id']]['imported'] + ) + ); + } + // As the recordingid was not identified as imported recording link, execute delete on a real recording. + // First: Delete imported links associated to the recording. + $importedall = bigbluebuttonbn_get_recording_imported_instances($params['id']); + if ($importedall > 0) { + foreach (array_keys($importedall) as $key) { + // Execute delete on imported links. + $DB->delete_records('bigbluebuttonbn_logs', array('id' => $key)); + } + } + // Second: Execute the actual delete. + return array( + 'status' => bigbluebuttonbn_delete_recordings($params['id']) + ); +} + +/** + * Helper for performing edit on recordings. + * + * @param array $params + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_broker_recording_action_edit($params, $recordings) { + if (bigbluebuttonbn_broker_recording_is_imported($recordings, $params['id'])) { + // Execute update on imported recording link. + return array( + 'status' => bigbluebuttonbn_update_recording_imported( + $recordings[$params['id']]['imported'], + json_decode($params['meta'], true) + ) + ); + } + + // As the recordingid was not identified as imported recording link, execute update on a real recording. + // (No need to update imported links as the update only affects the actual recording). + // Execute update on actual recording. + return array( + 'status' => bigbluebuttonbn_update_recordings( + $params['id'], + json_decode($params['meta']) + ) + ); +} + +/** + * Helper for responding when recording ready is performed. + * + * @param array $params + * @param object $bigbluebuttonbn + * + * @return void + */ +function bigbluebuttonbn_broker_recording_ready($params, $bigbluebuttonbn) { + // Decodes the received JWT string. + try { + $decodedparameters = \Firebase\JWT\JWT::decode( + $params['signed_parameters'], + \mod_bigbluebuttonbn\locallib\config::get('shared_secret'), + array('HS256') + ); + } catch (Exception $e) { + $error = 'Caught exception: '.$e->getMessage(); + header('HTTP/1.0 400 Bad Request. '.$error); + return; + } + // Validate that the bigbluebuttonbn activity corresponds to the meeting_id received. + $meetingidelements = explode('[', $decodedparameters->meeting_id); + $meetingidelements = explode('-', $meetingidelements[0]); + + if (!isset($bigbluebuttonbn) || $bigbluebuttonbn->meetingid != $meetingidelements[0]) { + header('HTTP/1.0 410 Gone. The activity may have been deleted'); + return; + } + // Sends the messages. + try { + // Workaround for CONTRIB-7438. + // Proceed as before when no record_id is provided. + if (!isset($decodedparameters->record_id)) { + bigbluebuttonbn_send_notification_recording_ready($bigbluebuttonbn); + header('HTTP/1.0 202 Accepted'); + return; + } + // We make sure messages are sent only once. + if (bigbluebuttonbn_get_count_callback_event_log($decodedparameters->record_id) == 0) { + bigbluebuttonbn_send_notification_recording_ready($bigbluebuttonbn); + } + $overrides = array('meetingid' => $decodedparameters->meeting_id); + $meta['recordid'] = $decodedparameters->record_id; + $meta['callback'] = 'recording_ready'; + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTON_LOG_EVENT_CALLBACK, $overrides, json_encode($meta)); + header('HTTP/1.0 202 Accepted'); + } catch (Exception $e) { + $error = 'Caught exception: '.$e->getMessage(); + header('HTTP/1.0 503 Service Unavailable. '.$error); + } +} + +/** + * Helper for performing import on recordings. + * + * @param array $bbbsession + * @param array $params + * + * @return string + */ +function bigbluebuttonbn_broker_recording_import($bbbsession, $params) { + global $SESSION; + if (!$bbbsession['managerecordings']) { + header('HTTP/1.0 401 Unauthorized. User not authorized to execute end command'); + return; + } + $importrecordings = $SESSION->bigbluebuttonbn_importrecordings; + if (!isset($importrecordings[$params['id']])) { + $error = "Recording {$params['id']} could not be found. It can not be imported"; + header('HTTP/1.0 404 Not found. '.$error); + return; + } + $callbackresponse = array('status' => true); + $importrecordings[$params['id']]['imported'] = true; + $overrides = array('meetingid' => $importrecordings[$params['id']]['meetingID']); + $meta = '{"recording":'.json_encode($importrecordings[$params['id']]).'}'; + bigbluebuttonbn_log($bbbsession['bigbluebuttonbn'], BIGBLUEBUTTONBN_LOG_EVENT_IMPORT, $overrides, $meta); + // Moodle event logger: Create an event for recording imported. + if (isset($bbbsession['bigbluebutton']) && isset($bbbsession['cm'])) { + bigbluebuttonbn_event_log( + \mod_bigbluebuttonbn\event\events::$events['recording_import'], + $bbbsession['bigbluebuttonbn'], + ['other' => $params['id']] + ); + } + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Helper for responding when storing live meeting events is requested. + * + * The callback with a POST request includes: + * - Authentication: Bearer <A JWT token containing {"exp":<TIMESTAMP>} encoded with HS512> + * - Content Type: application/json + * - Body: <A JSON Object> + * + * @param object $bigbluebuttonbn + * + * @return void + */ +function bigbluebuttonbn_broker_meeting_events($bigbluebuttonbn) { + // Decodes the received JWT string. + try { + // Get the HTTP headers (getallheaders is a PHP function that may only work with Apache). + $headers = getallheaders(); + + // Pull the Bearer from the headers. + if (!array_key_exists('Authorization', $headers)) { + $msg = 'Authorization failed'; + header('HTTP/1.0 400 Bad Request. ' . $msg); + return; + } + $authorization = explode(" ", $headers['Authorization']); + + // Verify the authenticity of the request. + $token = \Firebase\JWT\JWT::decode( + $authorization[1], + \mod_bigbluebuttonbn\locallib\config::get('shared_secret'), + array('HS512') + ); + + // Get JSON string from the body. + $jsonstr = file_get_contents('php://input'); + + // Convert JSON string to a JSON object. + $jsonobj = json_decode($jsonstr); + } catch (Exception $e) { + $msg = 'Caught exception: ' . $e->getMessage(); + header('HTTP/1.0 400 Bad Request. ' . $msg); + return; + } + + // Validate that the bigbluebuttonbn activity corresponds to the meeting_id received. + $meetingidelements = explode('[', $jsonobj->{'meeting_id'}); + $meetingidelements = explode('-', $meetingidelements[0]); + if (!isset($bigbluebuttonbn) || $bigbluebuttonbn->meetingid != $meetingidelements[0]) { + $msg = 'The activity may have been deleted'; + header('HTTP/1.0 410 Gone. ' . $msg); + return; + } + + // We make sure events are processed only once. + $overrides = array('meetingid' => $jsonobj->{'meeting_id'}); + $meta['recordid'] = $jsonobj->{'internal_meeting_id'}; + $meta['callback'] = 'meeting_events'; + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTON_LOG_EVENT_CALLBACK, $overrides, json_encode($meta)); + if (bigbluebuttonbn_get_count_callback_event_log($meta['recordid'], 'meeting_events') == 1) { + // Process the events. + bigbluebuttonbn_process_meeting_events($bigbluebuttonbn, $jsonobj); + header('HTTP/1.0 200 Accepted. Enqueued.'); + return; + } + + header('HTTP/1.0 202 Accepted. Already processed.'); +} + +/** + * Helper for validating the parameters received. + * + * @param array $params + * + * @return string + */ +function bigbluebuttonbn_broker_validate_parameters($params) { + $action = strtolower($params['action']); + $requiredparams = bigbluebuttonbn_broker_required_parameters(); + if (!array_key_exists($action, $requiredparams)) { + return 'Action '.$params['action'].' can not be performed.'; + } + return bigbluebuttonbn_broker_validate_parameters_message($params, $requiredparams[$action]); +} + +/** + * Helper for responding after the parameters received are validated. + * + * @param array $params + * @param array $requiredparams + * + * @return string + */ +function bigbluebuttonbn_broker_validate_parameters_message($params, $requiredparams) { + foreach ($requiredparams as $param => $message) { + if (!array_key_exists($param, $params) || $params[$param] == '') { + return $message; + } + } +} + +/** + * Helper for definig rules for validating required parameters. + */ +function bigbluebuttonbn_broker_required_parameters() { + $params['server_ping'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The meetingID must be specified.' + ]; + $params['meeting_info'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The meetingID must be specified.' + ]; + $params['meeting_end'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The meetingID must be specified.' + ]; + $params['recording_play'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_info'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_links'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_publish'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_unpublish'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_delete'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_protect'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_unprotect'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_edit'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.', + 'meta' => 'A meta parameter should be included' + ]; + $params['recording_import'] = [ + 'callback' => 'This request must include a javascript callback.', + 'id' => 'The recordingID must be specified.' + ]; + $params['recording_ready'] = [ + 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.', + 'signed_parameters' => 'A JWT encoded string must be included as [signed_parameters].' + ]; + $params['recording_list_table'] = [ + 'id' => 'The Bigbluebutton activity id must be specified.', + 'callback' => 'This request must include a javascript callback.', + ]; + $params['meeting_events'] = [ + 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.' + ]; + $params['completion_validate'] = [ + 'callback' => 'This request must include a javascript callback.', + 'bigbluebuttonbn' => 'The BigBlueButtonBN instance ID must be specified.' + ]; + return $params; +} + +/** + * Helper for validating if a recording is an imported link or a real one. + * + * @param array $recordings + * @param string $recordingid + * + * @return boolean + */ +function bigbluebuttonbn_broker_recording_is_imported($recordings, $recordingid) { + return (isset($recordings[$recordingid]) && isset($recordings[$recordingid]['imported'])); +} + +/** + * Helper for performing validation of completion. + * + * @param object $bigbluebuttonbn + * @param array $params + * + * @return void + */ +function bigbluebuttonbn_broker_completion_validate($bigbluebuttonbn, $params) { + $context = \context_course::instance($bigbluebuttonbn->course); + // Get list with all the users enrolled in the course. + list($sort, $sqlparams) = users_order_by_sql('u'); + $users = get_enrolled_users($context, 'mod/bigbluebuttonbn:view', 0, 'u.*', $sort); + foreach ($users as $user) { + // Enqueue a task for processing the completion. + bigbluebuttonbn_enqueue_completion_update($bigbluebuttonbn, $user->id); + } + $callbackresponse['status'] = 200; + $callbackresponsedata = json_encode($callbackresponse); + return "{$params['callback']}({$callbackresponsedata});"; +} + +/** + * Helper function builds the data used by the recording table. + * + * @param array $bbbsession + * @param array $params + * @param array $enabledfeatures + * + * @return array + * @throws coding_exception + */ +function bigbluebuttonbn_broker_get_recording_data($bbbsession, $params, $enabledfeatures) { + $tools = ['protect', 'publish', 'delete']; + $recordings = bigbluebutton_get_recordings_for_table_view($bbbsession, $enabledfeatures); + $tabledata = array(); + $typeprofiles = bigbluebuttonbn_get_instance_type_profiles(); + $tabledata['activity'] = bigbluebuttonbn_view_get_activity_status($bbbsession); + $tabledata['ping_interval'] = (int) \mod_bigbluebuttonbn\locallib\config::get('waitformoderator_ping_interval') * 1000; + $tabledata['locale'] = bigbluebuttonbn_get_localcode(); + $tabledata['profile_features'] = $typeprofiles[0]['features']; + $tabledata['recordings_html'] = $bbbsession['bigbluebuttonbn']->recordings_html == '1'; + + $data = array(); + // Build table content. + if (isset($recordings) && !array_key_exists('messageKey', $recordings)) { + // There are recordings for this meeting. + foreach ($recordings as $recording) { + $rowdata = bigbluebuttonbn_get_recording_data_row($bbbsession, $recording, $tools); + if (!empty($rowdata)) { + array_push($data, $rowdata); + } + } + } + + $columns = array(); + // Initialize table headers. + $columns[] = array('key' => 'playback', 'label' => get_string('view_recording_playback', 'bigbluebuttonbn'), + 'width' => '125px', 'allowHTML' => true); // Note: here a strange bug noted whilst changing the columns, ref CONTRIB. + $columns[] = array('key' => 'recording', 'label' => get_string('view_recording_name', 'bigbluebuttonbn'), + 'width' => '125px', 'allowHTML' => true); + $columns[] = array('key' => 'description', 'label' => get_string('view_recording_description', 'bigbluebuttonbn'), + 'sortable' => true, 'width' => '250px', 'allowHTML' => true); + if (bigbluebuttonbn_get_recording_data_preview_enabled($bbbsession)) { + $columns[] = array('key' => 'preview', 'label' => get_string('view_recording_preview', 'bigbluebuttonbn'), + 'width' => '250px', 'allowHTML' => true); + } + $columns[] = array('key' => 'date', 'label' => get_string('view_recording_date', 'bigbluebuttonbn'), + 'sortable' => true, 'width' => '225px', 'allowHTML' => true); + $columns[] = array('key' => 'duration', 'label' => get_string('view_recording_duration', 'bigbluebuttonbn'), + 'width' => '50px'); + if ($bbbsession['managerecordings']) { + $columns[] = array('key' => 'actionbar', 'label' => get_string('view_recording_actionbar', 'bigbluebuttonbn'), + 'width' => '120px', 'allowHTML' => true); + } + + $tabledata['data'] = array( + 'columns' => $columns, + 'data' => $data + ); + $callbackresponsedata = json_encode($tabledata); + return "{$params['callback']}({$callbackresponsedata});"; +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php b/mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php new file mode 100644 index 0000000..268ff5e --- /dev/null +++ b/mod/bigbluebuttonbn/classes/analytics/indicator/activity_base.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Activity base class. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Activity base class. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { + + /** + * No need to fetch grades for resources. + * + * @return bool + */ + public function feedback_check_grades() { + // BigBlueButtonBN's feedback is not contained in grades. + return false; + } +} diff --git a/mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php b/mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php new file mode 100644 index 0000000..0ed7786 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/analytics/indicator/cognitive_depth.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Cognitive depth indicator - BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Cognitive depth indicator - bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cognitive_depth extends activity_base { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return object + */ + public static function get_name() : \lang_string { + return new \lang_string('indicator:cognitivedepth', 'mod_bigbluebuttonbn'); + } + + /** + * Returns the indicator type. + * + * @return integer + */ + public function get_indicator_type() { + return self::INDICATOR_COGNITIVE; + } + + /** + * Returns the cognitive depth level. + * + * @param cm_info $cm + * + * @return integer + */ + public function get_cognitive_depth_level(\cm_info $cm) { + return self::COGNITIVE_LEVEL_4; + } +} diff --git a/mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php b/mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php new file mode 100644 index 0000000..4a0aac4 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/analytics/indicator/social_breadth.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Social breadth indicator - BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\analytics\indicator; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Social breadth indicator - BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class social_breadth extends activity_base { + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return object + */ + public static function get_name() : \lang_string { + return new \lang_string('indicator:socialbreadth', 'mod_bigbluebuttonbn'); + } + + /** + * Returns the indicator type. + * + * @return integer + */ + public function get_indicator_type() { + return self::INDICATOR_SOCIAL; + } + + /** + * Returns the social breadth level. + * + * @param cm_info $cm + * + * @return integer + */ + public function get_social_breadth_level(\cm_info $cm) { + return self::SOCIAL_LEVEL_1; + } +} diff --git a/mod/bigbluebuttonbn/classes/event/activity_viewed.php b/mod/bigbluebuttonbn/classes/event/activity_viewed.php new file mode 100644 index 0000000..45345ec --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/activity_viewed.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn viewed event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn activity viewed event (triggered by view.php). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity_viewed extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' viewed the bigbluebuttonbn activity " . + "with id '##objectid' for the course id '##courseid'."; + } + + /** + * Return event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_activity_viewed', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/base.php b/mod/bigbluebuttonbn/classes/event/base.php new file mode 100644 index 0000000..369753d --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/base.php @@ -0,0 +1,144 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn abstract base event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn abstract base event class. Most mod_bigbluebuttonbn events can extend this class. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base extends \core\event\base { + + /** @var \bigbluebuttonbn */ + protected $bigbluebuttonbn; + + /** + * Description. + * + * @var string + */ + protected $description; + + /** + * Object Id Mapping. + * + * @var array + */ + protected static $objectidmapping = array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + + /** + * Legacy log data. + * + * @var array + */ + protected $legacylogdata; + + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + $this->data['crud'] = $crud; + $this->data['edulevel'] = $edulevel; + $this->data['objecttable'] = 'bigbluebuttonbn'; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $vars = array( + 'userid' => $this->userid, + 'courseid' => $this->courseid, + 'objectid' => $this->objectid, + 'contextinstanceid' => $this->contextinstanceid, + 'other' => $this->other + ); + $string = $this->description; + foreach ($vars as $key => $value) { + $string = str_replace("##" . $key, $value, $string); + } + return $string; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/bigbluebuttonbn/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Sets the legacy event log data. + * + * @param string $action The current action + * @param string $info A detailed description of the change. But no more than 255 characters. + * @param string $url The url to the assign module instance. + */ + public function set_legacy_logdata($action = '', $info = '', $url = '') { + $fullurl = 'view.php?id=' . $this->contextinstanceid; + if ($url != '') { + $fullurl .= '&' . $url; + } + + $this->legacylogdata = array($this->courseid, 'bigbluebuttonbn', $action, $fullurl, $info, $this->contextinstanceid); + } + + /** + * Return legacy data for add_to_log(). + * + * @return array + */ + protected function get_legacy_logdata() { + if (isset($this->legacylogdata)) { + return $this->legacylogdata; + } + + return null; + } + + /** + * Custom validation. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} diff --git a/mod/bigbluebuttonbn/classes/event/bigbluebuttonbn_activity_management_viewed.php b/mod/bigbluebuttonbn/classes/event/bigbluebuttonbn_activity_management_viewed.php new file mode 100644 index 0000000..324fd8a --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/bigbluebuttonbn_activity_management_viewed.php @@ -0,0 +1,66 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn activity management viewed event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010-2017 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn activity management viewed event (triggered by index.php). + * + * @copyright 2010-2017 Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later + */ +class bigbluebuttonbn_activity_management_viewed extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' viewed the bigbluebuttonbn activity management page for ". + "the course module id '##contextinstanceid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return 'BigBlueButtonBN activity management viewed'; + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/events.php b/mod/bigbluebuttonbn/classes/event/events.php new file mode 100644 index 0000000..829897f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/events.php @@ -0,0 +1,63 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn event/events. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * The mod_bigbluebuttonbn class for event name definition. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class events { + + /** + * Event name matcher. + * @var $events + */ + public static $events = array( + 'create' => 'activity_created', + 'view' => 'activity_viewed', + 'update' => 'activity_updated', + 'delete' => 'activity_deleted', + 'meeting_create' => 'meeting_created', + 'meeting_end' => 'meeting_ended', + 'meeting_join' => 'meeting_joined', + 'meeting_left' => 'meeting_left', + 'recording_delete' => 'recording_deleted', + 'recording_import' => 'recording_imported', + 'recording_protect' => 'recording_protected', + 'recording_publish' => 'recording_published', + 'recording_unprotect' => 'recording_unprotected', + 'recording_unpublish' => 'recording_unpublished', + 'recording_edit' => 'recording_edited', + 'recording_play' => 'recording_viewed', + 'live_session' => 'live_session' + ); +} diff --git a/mod/bigbluebuttonbn/classes/event/live_session_event.php b/mod/bigbluebuttonbn/classes/event/live_session_event.php new file mode 100644 index 0000000..09cd0f4 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/live_session_event.php @@ -0,0 +1,68 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn live session event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn live_session (Experimental: for being triggered when external events are received). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class live_session_event extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' triggered action ##other in a ". + "bigbluebutton meeting for the bigbluebuttonbn activity with id ". + "'##objectid' for the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_live_session', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_created.php b/mod/bigbluebuttonbn/classes/event/meeting_created.php new file mode 100644 index 0000000..b17f56f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_created.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn meeting created event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn meeting created event (triggered by bbb_view.php when the meeting is created before join). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_created extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' created a bigbluebutton meeting for ". + "the bigbluebuttonbn activity with id '##objectid' for the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_created', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_ended.php b/mod/bigbluebuttonbn/classes/event/meeting_ended.php new file mode 100644 index 0000000..dc29ffd --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_ended.php @@ -0,0 +1,68 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn meeting ended event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn meeting ended event (triggered by bbb_ajax.php and index.php when the meeting is ended by the user). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_ended extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "A bigbluebutton meeting for the bigbluebuttonbn activity with id ". + "'##objectid' for the course id '##courseid' has been forcibly ". + "ended by the user with id '##userid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_ended', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_joined.php b/mod/bigbluebuttonbn/classes/event/meeting_joined.php new file mode 100644 index 0000000..07ef240 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_joined.php @@ -0,0 +1,68 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn meeting joined event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn meeting joined event (triggered by bbb_view.php when the user joins the session). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_joined extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has joined a bigbluebutton meeting for ". + "the bigbluebuttonbn activity with id '##objectid' for the course id ". + "'##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_joined', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/meeting_left.php b/mod/bigbluebuttonbn/classes/event/meeting_left.php new file mode 100644 index 0000000..7efd322 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/meeting_left.php @@ -0,0 +1,68 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn meeting left event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn meeting left event (triggered by bbb_view.php when the user lefts the meeting using the logout button). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class meeting_left extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_PARTICIPATING) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has left a bigbluebutton meeting for ". + "the bigbluebuttonbn activity with id '##objectid' for the course id ". + "'##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_meeting_left', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_deleted.php b/mod/bigbluebuttonbn/classes/event/recording_deleted.php new file mode 100644 index 0000000..6214874 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_deleted.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording deleted event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording deleted event (triggered when a recording is deleted). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_deleted extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has deleted a recording with id ". + "'##other' from the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_deleted', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_edited.php b/mod/bigbluebuttonbn/classes/event/recording_edited.php new file mode 100644 index 0000000..6daaf4f --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_edited.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording edited event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording edited event (triggered when a recording is updated). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_edited extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has edited a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_edited', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_imported.php b/mod/bigbluebuttonbn/classes/event/recording_imported.php new file mode 100644 index 0000000..001cde9 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_imported.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording imported event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording imported event (triggered when a recording is imported). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_imported extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has imported a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_imported', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_protected.php b/mod/bigbluebuttonbn/classes/event/recording_protected.php new file mode 100644 index 0000000..c56721a --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_protected.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording protected event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording protected event (triggered when a recording is protected). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_protected extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has protected a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_protected', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_published.php b/mod/bigbluebuttonbn/classes/event/recording_published.php new file mode 100644 index 0000000..b97a7a9 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_published.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording published event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording published event (triggered when a recording is published). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_published extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has published a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_published', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_unprotected.php b/mod/bigbluebuttonbn/classes/event/recording_unprotected.php new file mode 100644 index 0000000..4614cbe --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_unprotected.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording unprotected event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording unprotected event (triggered when a recording is unprotected). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_unprotected extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has unprotected a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_unprotected', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_unpublished.php b/mod/bigbluebuttonbn/classes/event/recording_unpublished.php new file mode 100644 index 0000000..5a5d6bc --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_unpublished.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording unpublished event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording unpublished event (triggered when a recording is unpublished). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_unpublished extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has unpublished a recording with id ". + "'##other' in the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_unpublished', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/event/recording_viewed.php b/mod/bigbluebuttonbn/classes/event/recording_viewed.php new file mode 100644 index 0000000..66d6063 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/event/recording_viewed.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn recording viewed event. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_bigbluebuttonbn recording viewed event (triggered when a recording is viewed). + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class recording_viewed extends base +{ + /** + * Init method. + * @param string $crud + * @param integer $edulevel + */ + protected function init($crud = 'r', $edulevel = self::LEVEL_OTHER) { + parent::init($crud, $edulevel); + $this->description = "The user with id '##userid' has viewed a recording with id ". + "'##other' from the course id '##courseid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_recording_viewed', 'bigbluebuttonbn'); + } + + /** + * Return objectid mapping. + * + * @return string + */ + public static function get_objectid_mapping() { + return array('db' => 'bigbluebuttonbn', 'restore' => 'bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/classes/external.php b/mod/bigbluebuttonbn/classes/external.php new file mode 100644 index 0000000..e02f2b5 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/external.php @@ -0,0 +1,255 @@ +<?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/>. + +/** + * BigBlueButtonBN external API + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once("$CFG->libdir/externallib.php"); + +/** + * BigBlueButtonBN external functions + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_bigbluebuttonbn_external extends external_api { + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.0 + */ + public static function view_bigbluebuttonbn_parameters() { + return new external_function_parameters( + array( + 'bigbluebuttonbnid' => new external_value(PARAM_INT, 'bigbluebuttonbn instance id') + ) + ); + } + + /** + * Trigger the course module viewed event and update the module completion status. + * + * @param int $bigbluebuttonbnid the bigbluebuttonbn instance id + * @return array of warnings and status result + * @since Moodle 3.0 + * @throws moodle_exception + */ + public static function view_bigbluebuttonbn($bigbluebuttonbnid) { + global $DB, $CFG; + require_once($CFG->dirroot . "/mod/bigbluebuttonbn/lib.php"); + + $params = self::validate_parameters(self::view_bigbluebuttonbn_parameters(), + array( + 'bigbluebuttonbnid' => $bigbluebuttonbnid + )); + $warnings = array(); + + // Request and permission validation. + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $params['bigbluebuttonbnid']), '*', MUST_EXIST); + list($course, $cm) = get_course_and_cm_from_instance($bigbluebuttonbn, 'bigbluebuttonbn'); + + $context = context_module::instance($cm->id); + self::validate_context($context); + + require_capability('mod/bigbluebuttonbn:view', $context); + + // Call the bigbluebuttonbn/lib API. + bigbluebuttonbn_view($bigbluebuttonbn, $course, $cm, $context); + + $result = array(); + $result['status'] = true; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 3.0 + */ + public static function view_bigbluebuttonbn_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings() + ) + ); + } + + /** + * Describes the parameters for get_bigbluebuttonbns_by_courses. + * + * @return external_function_parameters + * @since Moodle 3.3 + */ + public static function get_bigbluebuttonbns_by_courses_parameters() { + return new external_function_parameters ( + array( + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, array() + ), + ) + ); + } + + /** + * Returns a list of bigbluebuttonbns in a provided list of courses. + * If no list is provided all bigbluebuttonbns that the user can view will be returned. + * + * @param array $courseids course ids + * @return array of warnings and bigbluebuttonbns + * @since Moodle 3.3 + */ + public static function get_bigbluebuttonbns_by_courses($courseids = array()) { + + $warnings = array(); + $returnedbigbluebuttonbns = array(); + + $params = array( + 'courseids' => $courseids, + ); + $params = self::validate_parameters(self::get_bigbluebuttonbns_by_courses_parameters(), $params); + + $mycourses = array(); + if (empty($params['courseids'])) { + $mycourses = enrol_get_my_courses(); + $params['courseids'] = array_keys($mycourses); + } + + // Ensure there are courseids to loop through. + if (!empty($params['courseids'])) { + + list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses); + + // Get the bigbluebuttonbns in this course, this function checks users visibility permissions. + // We can avoid then additional validate_context calls. + $bigbluebuttonbns = get_all_instances_in_courses("bigbluebuttonbn", $courses); + foreach ($bigbluebuttonbns as $bigbluebuttonbn) { + $context = context_module::instance($bigbluebuttonbn->coursemodule); + // Entry to return. + $bigbluebuttonbn->name = external_format_string($bigbluebuttonbn->name, $context->id); + + list($bigbluebuttonbn->intro, $bigbluebuttonbn->introformat) = external_format_text($bigbluebuttonbn->intro, + $bigbluebuttonbn->introformat, $context->id, 'mod_bigbluebuttonbn', 'intro', null); + $bigbluebuttonbn->introfiles = external_util::get_area_files($context->id, + 'mod_bigbluebuttonbn', 'intro', false, false); + + $returnedbigbluebuttonbns[] = $bigbluebuttonbn; + } + } + + $result = array( + 'bigbluebuttonbns' => $returnedbigbluebuttonbns, + 'warnings' => $warnings + ); + return $result; + } + + /** + * Describes the get_bigbluebuttonbns_by_courses return value. + * + * @return external_single_structure + * @since Moodle 3.3 + */ + public static function get_bigbluebuttonbns_by_courses_returns() { + return new external_single_structure( + array( + 'bigbluebuttonbns' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'Module id'), + 'coursemodule' => new external_value(PARAM_INT, 'Course module id'), + 'course' => new external_value(PARAM_INT, 'Course id'), + 'name' => new external_value(PARAM_RAW, 'Name'), + 'intro' => new external_value(PARAM_RAW, 'Description'), + 'meetingid' => new external_value(PARAM_RAW, 'Meeting id'), + 'introformat' => new external_format_value('intro', 'Summary format'), + 'introfiles' => new external_files('Files in the introduction text'), + 'timemodified' => new external_value(PARAM_INT, 'Last time the instance was modified'), + 'section' => new external_value(PARAM_INT, 'Course section id'), + 'visible' => new external_value(PARAM_INT, 'Module visibility'), + 'groupmode' => new external_value(PARAM_INT, 'Group mode'), + 'groupingid' => new external_value(PARAM_INT, 'Grouping id'), + ) + ) + ), + 'warnings' => new external_warnings(), + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.0 + */ + public static function can_join_parameters() { + return new external_function_parameters( + array( + 'cmid' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED) + ) + ); + } + + /** + * This will check if current user can join the session from this module + * @param int $cmid + * @throws coding_exception + * @throws dml_exception + */ + public static function can_join($cmid) { + global $SESSION, $CFG; + require_once($CFG->dirroot . "/mod/bigbluebuttonbn/locallib.php"); + + $params = self::validate_parameters(self::can_join_parameters(), + array( + 'cmid' => $cmid + )); + $canjoin = \mod_bigbluebuttonbn\locallib\bigbluebutton::can_join_meeting($cmid); + $canjoin['cmid'] = $cmid; + return $canjoin; + } + + /** + * Return value for can join function + * + * @return external_single_structure + * @since Moodle 3.3 + */ + public static function can_join_returns() { + return new external_single_structure( + array( + 'can_join' => new external_value(PARAM_BOOL, 'Can join session'), + 'message' => new external_value(PARAM_RAW, 'Message if we cannot join', VALUE_OPTIONAL), + 'cmid' => new external_value(PARAM_INT, 'course module id', VALUE_REQUIRED) + ) + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/locallib/bigbluebutton.php b/mod/bigbluebuttonbn/classes/locallib/bigbluebutton.php new file mode 100644 index 0000000..599e244 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/locallib/bigbluebutton.php @@ -0,0 +1,293 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn locallib/bigbluebutton. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\locallib; + +use context_module; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Wrapper for executing http requests on a BigBlueButton server. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class bigbluebutton { + + /** + * Returns the right URL for the action specified. + * + * @param string $action + * @param array $data + * @param array $metadata + * @return string + */ + public static function action_url($action = '', $data = array(), $metadata = array()) { + $baseurl = self::sanitized_url() . $action . '?'; + $metadata = array_combine( + array_map( + function($k) { + return 'meta_' . $k; + } + , array_keys($metadata) + ), + $metadata + ); + $params = http_build_query($data + $metadata, '', '&'); + return $baseurl . $params . '&checksum=' . sha1($action . $params . self::sanitized_secret()); + } + + /** + * Makes sure the url used doesn't is in the format required. + * + * @return string + */ + public static function sanitized_url() { + $serverurl = trim(config::get('server_url')); + if (substr($serverurl, -1) == '/') { + $serverurl = rtrim($serverurl, '/'); + } + if (substr($serverurl, -4) == '/api') { + $serverurl = rtrim($serverurl, '/api'); + } + return $serverurl . '/api/'; + } + + /** + * Makes sure the shared_secret used doesn't have trailing white characters. + * + * @return string + */ + public static function sanitized_secret() { + return trim(config::get('shared_secret')); + } + + /** + * Returns the BigBlueButton server root URL. + * + * @return string + */ + public static function root() { + $pserverurl = parse_url(trim(config::get('server_url'))); + $pserverurlport = ""; + if (isset($pserverurl['port'])) { + $pserverurlport = ":" . $pserverurl['port']; + } + return $pserverurl['scheme'] . "://" . $pserverurl['host'] . $pserverurlport . "/"; + } + + /** + * Get BBB session information from viewinstance + * + * @param object $viewinstance + * @return mixed + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + * @throws \require_login_exception + * @throws \required_capability_exception + */ + public static function build_bbb_session_fromviewinstance($viewinstance) { + $cm = $viewinstance['cm']; + $course = $viewinstance['course']; + $bigbluebuttonbn = $viewinstance['bigbluebuttonbn']; + return self::build_bbb_session($cm, $course, $bigbluebuttonbn); + } + + /** + * Get BBB session from parameters + * + * @param \course_modinfo $cm + * @param object $course + * @param object $bigbluebuttonbn + * @return mixed + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + * @throws \require_login_exception + * @throws \required_capability_exception + */ + public static function build_bbb_session($cm, $course, $bigbluebuttonbn) { + global $CFG; + $context = context_module::instance($cm->id); + require_login($course->id, false, $cm, true, true); + require_capability('mod/bigbluebuttonbn:join', $context); + + // Add view event. + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['view'], $bigbluebuttonbn); + + // Create array bbbsession with configuration for BBB server. + $bbbsession['course'] = $course; + $bbbsession['coursename'] = $course->fullname; + $bbbsession['cm'] = $cm; + $bbbsession['bigbluebuttonbn'] = $bigbluebuttonbn; + self::view_bbbsession_set($context, $bbbsession); + + $serverversion = bigbluebuttonbn_get_server_version(); + $bbbsession['serverversion'] = (string) $serverversion; + + // Operation URLs. + $bbbsession['bigbluebuttonbnURL'] = $CFG->wwwroot . '/mod/bigbluebuttonbn/view.php?id=' . $cm->id; + $bbbsession['logoutURL'] = $CFG->wwwroot . '/mod/bigbluebuttonbn/bbb_view.php?action=logout&id=' . $cm->id . + '&bn=' . $bbbsession['bigbluebuttonbn']->id; + $bbbsession['recordingReadyURL'] = $CFG->wwwroot . '/mod/bigbluebuttonbn/bbb_broker.php?action=recording_' . + 'ready&bigbluebuttonbn=' . $bbbsession['bigbluebuttonbn']->id; + $bbbsession['meetingEventsURL'] = $CFG->wwwroot . '/mod/bigbluebuttonbn/bbb_broker.php?action=meeting' . + '_events&bigbluebuttonbn=' . $bbbsession['bigbluebuttonbn']->id; + $bbbsession['joinURL'] = $CFG->wwwroot . '/mod/bigbluebuttonbn/bbb_view.php?action=join&id=' . $cm->id . + '&bn=' . $bbbsession['bigbluebuttonbn']->id; + + return $bbbsession; + } + + /** + * Build standard array with configurations required for BBB server. + * + * @param \context $context + * @param array $bbbsession + * @throws \coding_exception + * @throws \dml_exception + */ + public static function view_bbbsession_set($context, &$bbbsession) { + + global $CFG, $USER; + + $bbbsession['username'] = fullname($USER); + $bbbsession['userID'] = $USER->id; + $bbbsession['administrator'] = is_siteadmin($bbbsession['userID']); + $participantlist = bigbluebuttonbn_get_participant_list($bbbsession['bigbluebuttonbn'], $context); + $bbbsession['moderator'] = bigbluebuttonbn_is_moderator($context, $participantlist); + $bbbsession['managerecordings'] = ($bbbsession['administrator'] + || has_capability('mod/bigbluebuttonbn:managerecordings', $context)); + $bbbsession['importrecordings'] = ($bbbsession['managerecordings']); + $bbbsession['modPW'] = $bbbsession['bigbluebuttonbn']->moderatorpass; + $bbbsession['viewerPW'] = $bbbsession['bigbluebuttonbn']->viewerpass; + $bbbsession['meetingid'] = $bbbsession['bigbluebuttonbn']->meetingid.'-'.$bbbsession['course']->id.'-'. + $bbbsession['bigbluebuttonbn']->id; + $bbbsession['meetingname'] = $bbbsession['bigbluebuttonbn']->name; + $bbbsession['meetingdescription'] = $bbbsession['bigbluebuttonbn']->intro; + $bbbsession['userlimit'] = intval((int) config::get('userlimit_default')); + if ((boolean) config::get('userlimit_editable')) { + $bbbsession['userlimit'] = intval($bbbsession['bigbluebuttonbn']->userlimit); + } + $bbbsession['voicebridge'] = $bbbsession['bigbluebuttonbn']->voicebridge; + if ($bbbsession['bigbluebuttonbn']->voicebridge > 0) { + $bbbsession['voicebridge'] = 70000 + $bbbsession['bigbluebuttonbn']->voicebridge; + } + $bbbsession['wait'] = $bbbsession['bigbluebuttonbn']->wait; + $bbbsession['record'] = $bbbsession['bigbluebuttonbn']->record; + $bbbsession['recordallfromstart'] = $CFG->bigbluebuttonbn_recording_all_from_start_default; + if ($CFG->bigbluebuttonbn_recording_all_from_start_editable) { + $bbbsession['recordallfromstart'] = $bbbsession['bigbluebuttonbn']->recordallfromstart; + } + $bbbsession['recordhidebutton'] = $CFG->bigbluebuttonbn_recording_hide_button_default; + if ($CFG->bigbluebuttonbn_recording_hide_button_editable) { + $bbbsession['recordhidebutton'] = $bbbsession['bigbluebuttonbn']->recordhidebutton; + } + $bbbsession['welcome'] = $bbbsession['bigbluebuttonbn']->welcome; + if (!isset($bbbsession['welcome']) || $bbbsession['welcome'] == '') { + $bbbsession['welcome'] = get_string('mod_form_field_welcome_default', 'bigbluebuttonbn'); + } + if ($bbbsession['bigbluebuttonbn']->record) { + // Check if is enable record all from start. + if ($bbbsession['recordallfromstart']) { + $bbbsession['welcome'] .= '<br><br>'.get_string('bbbrecordallfromstartwarning', + 'bigbluebuttonbn'); + } else { + $bbbsession['welcome'] .= '<br><br>'.get_string('bbbrecordwarning', 'bigbluebuttonbn'); + } + } + $bbbsession['openingtime'] = $bbbsession['bigbluebuttonbn']->openingtime; + $bbbsession['closingtime'] = $bbbsession['bigbluebuttonbn']->closingtime; + $bbbsession['muteonstart'] = $bbbsession['bigbluebuttonbn']->muteonstart; + // Lock settings. + $bbbsession['disablecam'] = $bbbsession['bigbluebuttonbn']->disablecam; + $bbbsession['disablemic'] = $bbbsession['bigbluebuttonbn']->disablemic; + $bbbsession['disableprivatechat'] = $bbbsession['bigbluebuttonbn']->disableprivatechat; + $bbbsession['disablepublicchat'] = $bbbsession['bigbluebuttonbn']->disablepublicchat; + $bbbsession['disablenote'] = $bbbsession['bigbluebuttonbn']->disablenote; + $bbbsession['hideuserlist'] = $bbbsession['bigbluebuttonbn']->hideuserlist; + $bbbsession['lockedlayout'] = $bbbsession['bigbluebuttonbn']->lockedlayout; + $bbbsession['lockonjoin'] = $bbbsession['bigbluebuttonbn']->lockonjoin; + $bbbsession['lockonjoinconfigurable'] = $bbbsession['bigbluebuttonbn']->lockonjoinconfigurable; + // Additional info related to the course. + $bbbsession['context'] = $context; + // Metadata (origin). + $bbbsession['origin'] = 'Moodle'; + $bbbsession['originVersion'] = $CFG->release; + $parsedurl = parse_url($CFG->wwwroot); + $bbbsession['originServerName'] = $parsedurl['host']; + $bbbsession['originServerUrl'] = $CFG->wwwroot; + $bbbsession['originServerCommonName'] = ''; + $bbbsession['originTag'] = 'moodle-mod_bigbluebuttonbn ('.get_config('mod_bigbluebuttonbn', 'version').')'; + $bbbsession['bnserver'] = bigbluebuttonbn_is_bn_server(); + // Setting for clienttype, assign flash if not enabled, or default if not editable. + $bbbsession['clienttype'] = config::get('clienttype_default'); + if (config::get('clienttype_editable')) { + $bbbsession['clienttype'] = $bbbsession['bigbluebuttonbn']->clienttype; + } + if (!config::clienttype_enabled()) { + $bbbsession['clienttype'] = BIGBLUEBUTTON_CLIENTTYPE_FLASH; + } + } + + /** + * Can join meeting. + * + * @param int $cmid + * @return array|bool[] + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + * @throws \require_login_exception + * @throws \required_capability_exception + */ + public static function can_join_meeting($cmid) { + global $CFG; + $canjoin = array('can_join' => false, 'message' => ''); + + $viewinstance = bigbluebuttonbn_view_validator($cmid, null); + if ($viewinstance) { + $bbbsession = self::build_bbb_session_fromviewinstance($viewinstance); + if ($bbbsession) { + require_once($CFG->dirroot . "/mod/bigbluebuttonbn/brokerlib.php"); + $info = bigbluebuttonbn_get_meeting_info($bbbsession['meetingid'], false); + $running = false; + if ($info['returncode'] == 'SUCCESS') { + $running = ($info['running'] === 'true'); + } + $participantcount = 0; + if (isset($info['participantCount'])) { + $participantcount = $info['participantCount']; + } + $canjoin = bigbluebuttonbn_broker_meeting_info_can_join($bbbsession, $running, $participantcount); + } + } + return $canjoin; + } +} diff --git a/mod/bigbluebuttonbn/classes/locallib/config.php b/mod/bigbluebuttonbn/classes/locallib/config.php new file mode 100644 index 0000000..5120273 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/locallib/config.php @@ -0,0 +1,252 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn locallib/config. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\locallib; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Handles the global configuration based on config.php. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class config { + + /** + * Returns moodle version. + * + * @return string + */ + public static function get_moodle_version_major() { + global $CFG; + $versionarray = explode('.', $CFG->version); + return $versionarray[0]; + } + + /** + * Returns configuration default values. + * + * @return array + */ + public static function defaultvalues() { + return array( + 'server_url' => (string) BIGBLUEBUTTONBN_DEFAULT_SERVER_URL, + 'shared_secret' => (string) BIGBLUEBUTTONBN_DEFAULT_SHARED_SECRET, + 'voicebridge_editable' => false, + 'importrecordings_enabled' => false, + 'importrecordings_from_deleted_enabled' => false, + 'waitformoderator_default' => false, + 'waitformoderator_editable' => true, + 'waitformoderator_ping_interval' => '10', + 'waitformoderator_cache_ttl' => '60', + 'userlimit_default' => '0', + 'userlimit_editable' => false, + 'preuploadpresentation_enabled' => false, + 'sendnotifications_enabled' => false, + 'recordingready_enabled' => false, + 'recordingstatus_enabled' => false, + 'meetingevents_enabled' => false, + 'participant_moderator_default' => '0', + 'scheduled_duration_enabled' => false, + 'scheduled_duration_compensation' => '10', + 'scheduled_pre_opening' => '10', + 'recordings_enabled' => true, + 'recordings_html_default' => false, + 'recordings_html_editable' => false, + 'recordings_deleted_default' => false, + 'recordings_deleted_editable' => false, + 'recordings_imported_default' => false, + 'recordings_imported_editable' => false, + 'recordings_preview_default' => true, + 'recordings_preview_editable' => false, + 'recordings_validate_url' => true, + 'recording_default' => true, + 'recording_editable' => true, + 'recording_icons_enabled' => true, + 'recording_all_from_start_default' => false, + 'recording_all_from_start_editable' => false, + 'recording_hide_button_default' => false, + 'recording_hide_button_editable' => false, + 'general_warning_message' => '', + 'general_warning_roles' => 'editingteacher,teacher', + 'general_warning_box_type' => 'info', + 'general_warning_button_text' => '', + 'general_warning_button_href' => '', + 'general_warning_button_class' => '', + 'clienttype_enabled' => false, + 'clienttype_default' => '0', + 'clienttype_editable' => true, + 'muteonstart_default' => false, + 'muteonstart_editable' => false, + 'disablecam_default' => false, + 'disablecam_editable' => true, + 'disablemic_default' => false, + 'disablemic_editable' => true, + 'disableprivatechat_default' => false, + 'disableprivatechat_editable' => true, + 'disablepublicchat_default' => false, + 'disablepublicchat_editable' => true, + 'disablenote_default' => false, + 'disablenote_editable' => true, + 'hideuserlist_default' => false, + 'hideuserlist_editable' => true, + 'lockedlayout_default' => false, + 'lockedlayout_editable' => true, + 'lockonjoin_default' => false, + 'lockonjoin_editable' => true, + 'lockonjoinconfigurable_default' => false, + 'lockonjoinconfigurable_editable' => true, + 'welcome_default' => '', + ); + } + + /** + * Returns default value for an specific setting. + * + * @param string $setting + * @return string + */ + public static function defaultvalue($setting) { + $defaultvalues = self::defaultvalues(); + if (!array_key_exists($setting, $defaultvalues)) { + return; + } + return $defaultvalues[$setting]; + } + + /** + * Returns value for an specific setting. + * + * @param string $setting + * @return string + */ + public static function get($setting) { + global $CFG; + if (isset($CFG->bigbluebuttonbn[$setting])) { + return (string)$CFG->bigbluebuttonbn[$setting]; + } + if (isset($CFG->{'bigbluebuttonbn_'.$setting})) { + return (string)$CFG->{'bigbluebuttonbn_'.$setting}; + } + return self::defaultvalue($setting); + } + + /** + * Validates if recording settings are enabled. + * + * @return boolean + */ + public static function recordings_enabled() { + return (boolean)self::get('recordings_enabled'); + } + + /** + * Validates if imported recording settings are enabled. + * + * @return boolean + */ + public static function importrecordings_enabled() { + return (boolean)self::get('importrecordings_enabled'); + } + + /** + * Validates if clienttype settings are enabled. + * + * @return boolean + */ + public static function clienttype_enabled() { + return (boolean)self::get('clienttype_enabled'); + } + + /** + * Wraps current settings in an array. + * + * @return array + */ + public static function get_options() { + return array( + 'version_major' => self::get_moodle_version_major(), + 'voicebridge_editable' => self::get('voicebridge_editable'), + 'importrecordings_enabled' => self::get('importrecordings_enabled'), + 'importrecordings_from_deleted_enabled' => self::get('importrecordings_from_deleted_enabled'), + 'waitformoderator_default' => self::get('waitformoderator_default'), + 'waitformoderator_editable' => self::get('waitformoderator_editable'), + 'userlimit_default' => self::get('userlimit_default'), + 'userlimit_editable' => self::get('userlimit_editable'), + 'preuploadpresentation_enabled' => self::get('preuploadpresentation_enabled'), + 'sendnotifications_enabled' => self::get('sendnotifications_enabled'), + 'recordings_enabled' => self::get('recordings_enabled'), + 'meetingevents_enabled' => self::get('meetingevents_enabled'), + 'recordings_html_default' => self::get('recordings_html_default'), + 'recordings_html_editable' => self::get('recordings_html_editable'), + 'recordings_deleted_default' => self::get('recordings_deleted_default'), + 'recordings_deleted_editable' => self::get('recordings_deleted_editable'), + 'recordings_imported_default' => self::get('recordings_imported_default'), + 'recordings_imported_editable' => self::get('recordings_imported_editable'), + 'recordings_preview_default' => self::get('recordings_preview_default'), + 'recordings_preview_editable' => self::get('recordings_preview_editable'), + 'recordings_validate_url' => self::get('recordings_validate_url'), + 'recording_default' => self::get('recording_default'), + 'recording_editable' => self::get('recording_editable'), + 'recording_icons_enabled' => self::get('recording_icons_enabled'), + 'recording_all_from_start_default' => self::get('recording_all_from_start_default'), + 'recording_all_from_start_editable' => self::get('recording_all_from_start_editable'), + 'recording_hide_button_default' => self::get('recording_hide_button_default'), + 'recording_hide_button_editable' => self::get('recording_hide_button_editable'), + 'general_warning_message' => self::get('general_warning_message'), + 'general_warning_box_type' => self::get('general_warning_box_type'), + 'general_warning_button_text' => self::get('general_warning_button_text'), + 'general_warning_button_href' => self::get('general_warning_button_href'), + 'general_warning_button_class' => self::get('general_warning_button_class'), + 'clienttype_enabled' => self::get('clienttype_enabled'), + 'clienttype_editable' => self::get('clienttype_editable'), + 'clienttype_default' => self::get('clienttype_default'), + 'muteonstart_editable' => self::get('muteonstart_editable'), + 'muteonstart_default' => self::get('muteonstart_default'), + 'disablecam_editable' => self::get('disablecam_editable'), + 'disablecam_default' => self::get('disablecam_default'), + 'disablemic_editable' => self::get('disablemic_editable'), + 'disablemic_default' => self::get('disablemic_default'), + 'disableprivatechat_editable' => self::get('disableprivatechat_editable'), + 'disableprivatechat_default' => self::get('disableprivatechat_default'), + 'disablepublicchat_editable' => self::get('disablepublicchat_editable'), + 'disablepublicchat_default' => self::get('disablepublicchat_default'), + 'disablenote_editable' => self::get('disablenote_editable'), + 'disablenote_default' => self::get('disablenote_default'), + 'hideuserlist_editable' => self::get('hideuserlist_editable'), + 'hideuserlist_default' => self::get('hideuserlist_default'), + 'lockedlayout_editable' => self::get('lockedlayout_editable'), + 'lockedlayout_default' => self::get('lockedlayout_default'), + 'lockonjoin_editable' => self::get('lockonjoin_editable'), + 'lockonjoin_default' => self::get('lockonjoin_default'), + 'lockonjoinconfigurable_editable' => self::get('lockonjoinconfigurable_editable'), + 'lockonjoinconfigurable_default' => self::get('lockonjoinconfigurable_default'), + 'welcome_default' => self::get('welcome_default'), + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/locallib/mobileview.php b/mod/bigbluebuttonbn/classes/locallib/mobileview.php new file mode 100644 index 0000000..c17d7d4 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/locallib/mobileview.php @@ -0,0 +1,160 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn locallib/mobileview. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_bigbluebuttonbn\locallib; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Methods used to render view BBB in mobile. + * + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mobileview { + + /** + * Build url for join to session. + * This method is similar to "join_meeting()" in bbb_view. + * @param array $bbbsession + * @return string + */ + public static function build_url_join_session(&$bbbsession) { + $password = $bbbsession['viewerPW']; + if ($bbbsession['administrator'] || $bbbsession['moderator']) { + $password = $bbbsession['modPW']; + } + $joinurl = bigbluebuttonbn_get_join_url($bbbsession['meetingid'], $bbbsession['username'], + $password, $bbbsession['logoutURL'], null, $bbbsession['userID'], $bbbsession['clienttype'], + $bbbsession['createtime']); + + return($joinurl); + } + + /** + * Return the status of an activity [open|not_started|ended]. + * + * @param array $bbbsession + * @return string + */ + public static function get_activity_status(&$bbbsession) { + $now = time(); + if (!empty($bbbsession['bigbluebuttonbn']->openingtime) && $now < $bbbsession['bigbluebuttonbn']->openingtime) { + // The activity has not been opened. + return 'not_started'; + } + if (!empty($bbbsession['bigbluebuttonbn']->closingtime) && $now > $bbbsession['bigbluebuttonbn']->closingtime) { + // The activity has been closed. + return 'ended'; + } + // The activity is open. + return 'open'; + } + + /** + * Helper for preparing metadata used while creating the meeting. + * + * @param array $bbbsession + * @return array + */ + public static function create_meeting_metadata(&$bbbsession) { + return bigbluebuttonbn_create_meeting_metadata($bbbsession); + } + + /** + * Helper to prepare data used for create meeting. + * @param array $bbbsession + * @return array + * @throws \coding_exception + */ + public static function create_meeting_data(&$bbbsession) { + $data = ['meetingID' => $bbbsession['meetingid'], + 'name' => bigbluebuttonbn_html2text($bbbsession['meetingname'], 64), + 'attendeePW' => $bbbsession['viewerPW'], + 'moderatorPW' => $bbbsession['modPW'], + 'logoutURL' => $bbbsession['logoutURL'], + ]; + $data['record'] = self::create_meeting_data_record($bbbsession['record']); + // Check if auto_start_record is enable. + if ($data['record'] == 'true' && $bbbsession['recordallfromstart']) { + $data['autoStartRecording'] = 'true'; + // Check if hide_record_button is enable. + if ($bbbsession['recordallfromstart'] && $bbbsession['recordhidebutton']) { + $data['allowStartStopRecording'] = 'false'; + } + } + $data['welcome'] = trim($bbbsession['welcome']); + // Set the duration for the meeting. + $durationtime = self::create_meeting_data_duration($bbbsession['bigbluebuttonbn']->closingtime); + if ($durationtime > 0) { + $data['duration'] = $durationtime; + $data['welcome'] .= '<br><br>'; + $data['welcome'] .= str_replace( + '%duration%', + (string) $durationtime, + get_string('bbbdurationwarning', 'bigbluebuttonbn') + ); + } + $voicebridge = intval($bbbsession['voicebridge']); + if ($voicebridge > 0 && $voicebridge < 79999) { + $data['voiceBridge'] = $voicebridge; + } + $maxparticipants = intval($bbbsession['userlimit']); + if ($maxparticipants > 0) { + $data['maxParticipants'] = $maxparticipants; + } + if ($bbbsession['muteonstart']) { + $data['muteOnStart'] = 'true'; + } + return $data; + } + + /** + * Helper for returning the flag to know if the meeting is recorded. + * + * @param boolean $record + * @return string + */ + public static function create_meeting_data_record($record) { + if ((boolean)\mod_bigbluebuttonbn\locallib\config::recordings_enabled() && $record) { + return 'true'; + } + return 'false'; + } + + /** + * Helper for returning the duration expected for the meeting. + * + * @param string $closingtime + * @return integer + */ + public static function create_meeting_data_duration($closingtime) { + if ((boolean)\mod_bigbluebuttonbn\locallib\config::get('scheduled_duration_enabled')) { + return bigbluebuttonbn_get_duration($closingtime); + } + return 0; + } +} diff --git a/mod/bigbluebuttonbn/classes/locallib/notifier.php b/mod/bigbluebuttonbn/classes/locallib/notifier.php new file mode 100644 index 0000000..06fcdcf --- /dev/null +++ b/mod/bigbluebuttonbn/classes/locallib/notifier.php @@ -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/>. + +/** + * The mod_bigbluebuttonbn locallib/notifier. + * + * @package mod_bigbluebuttonbn + * @copyright 2017 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\locallib; + +use html_writer; +use mod_bigbluebuttonbn\plugin; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Helper class for sending notifications. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class notifier +{ + /** + * Prepares html message body for instance updated notification. + * + * @param object $msg + * @return string + */ + public static function htmlmsg_instance_updated($msg) { + $messagetext = '<p>'.get_string('pluginname', 'bigbluebuttonbn'). + ' <b>'.$msg->activity_url.'</b> '. + get_string('email_body_notification_meeting_has_been', 'bigbluebuttonbn').' '.$msg->action.'.</p>'."\n"; + $messagetext .= '<p>'.get_string('email_body_notification_meeting_details', 'bigbluebuttonbn').':'."\n"; + $messagetext .= '<table border="0" style="margin: 5px 0 0 20px"><tbody>'."\n"; + $messagetext .= '<tr><td style="font-weight:bold;color:#555;">'. + get_string('email_body_notification_meeting_title', 'bigbluebuttonbn').': </td><td>'."\n"; + $messagetext .= $msg->activity_title.'</td></tr>'."\n"; + $messagetext .= '<tr><td style="font-weight:bold;color:#555;">'. + get_string('email_body_notification_meeting_description', 'bigbluebuttonbn').': </td><td>'."\n"; + $messagetext .= $msg->activity_description.'</td></tr>'."\n"; + $messagetext .= '<tr><td style="font-weight:bold;color:#555;">'. + get_string('email_body_notification_meeting_start_date', 'bigbluebuttonbn').': </td><td>'."\n"; + $messagetext .= $msg->activity_openingtime.'</td></tr>'."\n"; + $messagetext .= '<tr><td style="font-weight:bold;color:#555;">'. + get_string('email_body_notification_meeting_end_date', 'bigbluebuttonbn').': </td><td>'."\n"; + $messagetext .= $msg->activity_closingtime.'</td></tr>'."\n"; + $messagetext .= '<tr><td style="font-weight:bold;color:#555;">'.$msg->action.' '. + get_string('email_body_notification_meeting_by', 'bigbluebuttonbn').': </td><td>'."\n"; + $messagetext .= $msg->activity_owner.'</td></tr></tbody></table></p>'."\n"; + $messagetext .= '<p><hr/><br/>'.get_string('email_footer_sent_by', 'bigbluebuttonbn').' '. + $msg->user_name.' '; + $messagetext .= get_string('email_footer_sent_from', 'bigbluebuttonbn').' '.$msg->course_name.'.</p>'; + return $messagetext; + } + + /** + * Starts the notification process. + * + * @param object $bigbluebuttonbn + * @param string $action + * @return void + */ + public static function notify_instance_updated($bigbluebuttonbn, $action) { + global $USER; + $coursemodinfo = \course_modinfo::instance($bigbluebuttonbn->course); + $course = $coursemodinfo->get_course($bigbluebuttonbn->course); + $sender = $USER; + // Prepare message. + $msg = (object) array(); + // Build the message_body. + $msg->action = $action; + $msg->activity_url = html_writer::link( + plugin::necurl('/mod/bigbluebuttonbn/view.php', ['id' => $bigbluebuttonbn->coursemodule]), + format_string($bigbluebuttonbn->name) + ); + $msg->activity_title = format_string($bigbluebuttonbn->name); + // Add the meeting details to the message_body. + $msg->action = ucfirst($action); + $msg->activity_description = ''; + if (!empty($bigbluebuttonbn->intro)) { + $msg->activity_description = format_string(trim($bigbluebuttonbn->intro)); + } + $msg->activity_openingtime = bigbluebuttonbn_format_activity_time($bigbluebuttonbn->openingtime); + $msg->activity_closingtime = bigbluebuttonbn_format_activity_time($bigbluebuttonbn->closingtime); + $msg->activity_owner = fullname($sender); + + $msg->user_name = fullname($sender); + $msg->user_email = $sender->email; + $msg->course_name = $course->fullname; + + // Send notification to all users enrolled. + self::enqueue_notifications($bigbluebuttonbn, $sender, self::htmlmsg_instance_updated($msg)); + } + + /** + * Prepares html message body for recording ready notification. + * + * @param object $bigbluebuttonbn + * + * @return void + */ + public static function htmlmsg_recording_ready($bigbluebuttonbn) { + return '<p>'.get_string('email_body_recording_ready_for', 'bigbluebuttonbn'). + ' "' . $bigbluebuttonbn->name . '" '. + get_string('email_body_recording_ready_is_ready', 'bigbluebuttonbn').'.</p>'; + } + + /** + * Helper function triggers a send notification when the recording is ready. + * + * @param object $bigbluebuttonbn + * + * @return void + */ + public static function notify_recording_ready($bigbluebuttonbn) { + // Instead of get_admin, the firs user enrolled with editing privileges may be used as the sender. + $sender = get_admin(); + $htmlmsg = self::htmlmsg_recording_ready($bigbluebuttonbn); + self::enqueue_notifications($bigbluebuttonbn, $sender, $htmlmsg); + } + + /** + * Enqueue notifications to be sent to all users in a context where the instance belongs. + * + * @param object $bigbluebuttonbn + * @param object $sender + * @param string $htmlmsg + * @return void + */ + public static function enqueue_notifications($bigbluebuttonbn, $sender, $htmlmsg) { + foreach (self::receivers($bigbluebuttonbn->course) as $receiver) { + if ($sender->id != $receiver->id) { + // Enqueue a task for sending a notification. + try { + // Create the instance of completion_update_state task. + $task = new \mod_bigbluebuttonbn\task\send_notification(); + // Add custom data. + $data = array( + 'sender' => $sender, + 'receiver' => $receiver, + 'htmlmsg' => $htmlmsg + ); + $task->set_custom_data($data); + // Enqueue it. + \core\task\manager::queue_adhoc_task($task); + } catch (Exception $e) { + mtrace("Error while enqueuing completion_uopdate_state task. " . (string) $e); + } + } + } + } + + /** + * Sends notification to a user. + * + * @param object $sender + * @param object $receiver + * @param object $htmlmsg + * @return void + */ + public static function send_notification($sender, $receiver, $htmlmsg) { + // Send the message. + message_post_message($sender, $receiver, $htmlmsg, FORMAT_HTML); + } + + /** + * Define users to be notified. + * + * @param object $courseid + * @return array + */ + public static function receivers($courseid) { + $context = \context_course::instance($courseid); + $users = array(); + // Potential users should be active users only. + $users = get_enrolled_users($context, 'mod/bigbluebuttonbn:view', 0, 'u.*', null, 0, 0, true); + return $users; + } +} diff --git a/mod/bigbluebuttonbn/classes/output/import_view.php b/mod/bigbluebuttonbn/classes/output/import_view.php new file mode 100644 index 0000000..d3f85f6 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/import_view.php @@ -0,0 +1,125 @@ +<?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/>. + +/** + * Renderer. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +namespace mod_bigbluebuttonbn\output; + +use renderable; +use renderer_base; +use templatable; +use html_table; +use html_writer; +use stdClass; +use coding_exception; +use mod_bigbluebuttonbn\plugin; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/bigbluebuttonbn/locallib.php'); + +/** + * Class import_view + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +class import_view implements renderable, templatable { + + /** @var array */ + private $context = []; + + /** + * import_view constructor. + * @param stdClass $course + * @param stdClass $bigbluebuttonbn + * @param int $tc + */ + public function __construct($course, $bigbluebuttonbn, $tc) { + global $SESSION, $PAGE; + $bbbsession = $SESSION->bigbluebuttonbn_bbbsession; + $options = bigbluebuttonbn_import_get_courses_for_select($bbbsession); + $selected = isset($options[$tc]) ? $tc : ''; + $this->context['backactionurl'] = plugin::necurl('/mod/bigbluebuttonbn/view.php'); + $this->context['cmid'] = $PAGE->cm->id; + if (!empty($options)) { + $selectoptions = []; + $toadd = ['value' => '', 'label' => get_string('choosedots')]; + if ('' == $tc) { + $toadd['selected'] = true; + } + $selectoptions[] = $toadd; + foreach ($options as $key => $option) { + $toadd = ['value' => $key, 'label' => $option]; + if ($key == $tc) { + $toadd['selected'] = true; + } + $selectoptions[] = $toadd; + } + $this->context['hascontent'] = true; + $this->context['selectoptions'] = $selectoptions; + // Get course recordings. + $bigbluebuttonbnid = null; + if ($course->id == $selected) { + $bigbluebuttonbnid = $bigbluebuttonbn->id; + } + $recordings = bigbluebuttonbn_get_allrecordings( + $selected, $bigbluebuttonbnid, false, + (boolean)\mod_bigbluebuttonbn\locallib\config::get('importrecordings_from_deleted_enabled') + ); + // Exclude the ones that are already imported. + if (!empty($recordings)) { + $recordings = bigbluebuttonbn_unset_existent_recordings_already_imported( + $recordings, $course->id, $bigbluebuttonbn->id + ); + } + // Store recordings (indexed) in a session variable. + $SESSION->bigbluebuttonbn_importrecordings = $recordings; + // Proceed with rendering. + if (!empty($recordings)) { + $this->context['recordings'] = true; + $this->context['recordingtable'] = bigbluebuttonbn_output_recording_table($bbbsession, $recordings, ['import']); + } + // JavaScript for locales. + $PAGE->requires->strings_for_js(array_keys(bigbluebuttonbn_get_strings_for_js()), 'bigbluebuttonbn'); + // Require JavaScript modules. + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-imports', 'M.mod_bigbluebuttonbn.imports.init', + array(array('bn' => $bigbluebuttonbn->id, 'tc' => $selected))); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-broker', 'M.mod_bigbluebuttonbn.broker.init', + array()); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-recordings', 'M.mod_bigbluebuttonbn.recordings.init', + array('recordings_html' => true)); + } + } + + /** + * Defer to template. + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output) { + return $this->context; + } + +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/classes/output/index.php b/mod/bigbluebuttonbn/classes/output/index.php new file mode 100644 index 0000000..c93daaf --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/index.php @@ -0,0 +1,233 @@ +<?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/>. + +/** + * Renderer. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\output; + +use renderable; +use html_table; +use html_writer; +use stdClass; +use coding_exception; +use mod_bigbluebuttonbn\plugin; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/bigbluebuttonbn/locallib.php'); + +/** + * Class index + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +class index implements renderable { + + /** @var html_table */ + public $table = null; + + /** + * index constructor. + * @param stdClass $course + * @throws coding_exception + */ + public function __construct($course) { + global $PAGE; + + // Get all the appropriate data. + if (!$bigbluebuttonbns = get_all_instances_in_course('bigbluebuttonbn', $course)) { + notice( + get_string('index_error_noinstances', plugin::COMPONENT), + plugin::necurl('/course/view.php', ['id' => $course->id]) + ); + } + + // Print the list of instances. + $strweek = get_string('week'); + $headingname = get_string('index_heading_name', plugin::COMPONENT); + $headinggroup = get_string('index_heading_group', plugin::COMPONENT); + $headingusers = get_string('index_heading_users', plugin::COMPONENT); + $headingviewer = get_string('index_heading_viewer', plugin::COMPONENT); + $headingmoderator = get_string('index_heading_moderator', plugin::COMPONENT); + $headingactions = get_string('index_heading_actions', plugin::COMPONENT); + $headingrecording = get_string('index_heading_recording', plugin::COMPONENT); + + $table = new html_table(); + $table->head = array($strweek, $headingname, $headinggroup, $headingusers, $headingviewer, $headingmoderator, + $headingrecording, $headingactions); + $table->align = array('center', 'left', 'center', 'center', 'center', 'center', 'center'); + + foreach ($bigbluebuttonbns as $bigbluebuttonbn) { + if ($bigbluebuttonbn->visible) { + $cm = get_coursemodule_from_id('bigbluebuttonbn', $bigbluebuttonbn->coursemodule, 0, false, MUST_EXIST); + // User roles. + $participantlist = bigbluebuttonbn_get_participant_list($bigbluebuttonbn, $PAGE->context); + $moderator = bigbluebuttonbn_is_moderator($PAGE->context, $participantlist); + $administrator = is_siteadmin(); + $canmoderate = ($administrator || $moderator); + // Add a the data for the bigbluebuttonbn instance. + $groupobj = null; + if (groups_get_activity_groupmode($cm) > 0) { + $groupobj = (object) array('id' => 0, 'name' => get_string('allparticipants')); + } + $table->data[] = self::bigbluebuttonbn_index_display_room($canmoderate, $course, $bigbluebuttonbn, $groupobj); + // Add a the data for the groups belonging to the bigbluebuttonbn instance, if any. + $groups = groups_get_activity_allowed_groups($cm); + foreach ($groups as $group) { + $table->data[] = self::bigbluebuttonbn_index_display_room($canmoderate, $course, $bigbluebuttonbn, $group); + } + } + } + + $this->table = $table; + } + + /** + * Displays the general view. + * + * @param boolean $moderator + * @param object $course + * @param object $bigbluebuttonbn + * @param object $groupobj + * @return array + */ + public static function bigbluebuttonbn_index_display_room($moderator, $course, $bigbluebuttonbn, $groupobj = null) { + $meetingid = sprintf('%s-%d-%d', $bigbluebuttonbn->meetingid, $course->id, $bigbluebuttonbn->id); + $groupname = ''; + $urlparams = ['id' => $bigbluebuttonbn->coursemodule]; + if ($groupobj) { + $meetingid .= sprintf('[%d]', $groupobj->id); + $urlparams['group'] = $groupobj->id; + $groupname = $groupobj->name; + } + $meetinginfo = bigbluebuttonbn_get_meeting_info_array($meetingid); + if (empty($meetinginfo)) { + // The server was unreachable. + print_error('index_error_unable_display', plugin::COMPONENT); + } + if (isset($meetinginfo['messageKey']) && ($meetinginfo['messageKey'] == 'checksumError')) { + // There was an error returned. + print_error('index_error_checksum', plugin::COMPONENT); + } + // Output Users in the meeting. + $joinurl = html_writer::link( + plugin::necurl('/mod/bigbluebuttonbn/view.php', $urlparams), + format_string($bigbluebuttonbn->name) + ); + $group = $groupname; + $users = ''; + $viewerlist = ''; + $moderatorlist = ''; + $recording = ''; + $actions = ''; + // The meeting info was returned. + if (array_key_exists('running', $meetinginfo) && $meetinginfo['running'] == 'true') { + $users = self::bigbluebuttonbn_index_display_room_users($meetinginfo); + $viewerlist = self::bigbluebuttonbn_index_display_room_users_attendee_list($meetinginfo, 'VIEWER'); + $moderatorlist = self::bigbluebuttonbn_index_display_room_users_attendee_list($meetinginfo, 'MODERATOR'); + $recording = self::bigbluebuttonbn_index_display_room_recordings($meetinginfo); + $actions = self::bigbluebuttonbn_index_display_room_actions($moderator, $course, $bigbluebuttonbn, $groupobj); + } + return array($bigbluebuttonbn->section, $joinurl, $group, $users, $viewerlist, $moderatorlist, $recording, $actions); + } + + /** + * Count the number of users in the meeting. + * + * @param array $meetinginfo + * @return integer + */ + public static function bigbluebuttonbn_index_display_room_users($meetinginfo) { + $users = ''; + if (count($meetinginfo['attendees']) && count($meetinginfo['attendees']->attendee)) { + $users = count($meetinginfo['attendees']->attendee); + } + return $users; + } + + /** + * Returns attendee list. + * + * @param array $meetinginfo + * @param string $role + * @return string + */ + public static function bigbluebuttonbn_index_display_room_users_attendee_list($meetinginfo, $role) { + $attendeelist = ''; + if (count($meetinginfo['attendees']) && count($meetinginfo['attendees']->attendee)) { + $attendeecount = 0; + foreach ($meetinginfo['attendees']->attendee as $attendee) { + if ($attendee->role == $role) { + $attendeelist .= ($attendeecount++ > 0 ? ', ' : '').$attendee->fullName; + } + } + } + return $attendeelist; + } + + /** + * Returns indication of recording enabled. + * + * @param array $meetinginfo + * @return string + */ + public static function bigbluebuttonbn_index_display_room_recordings($meetinginfo) { + $recording = ''; + if (isset($meetinginfo['recording']) && $meetinginfo['recording'] === 'true') { + // If it has been set when meeting created, set the variable on/off. + $recording = get_string('index_enabled', 'bigbluebuttonbn'); + } + return $recording; + } + + /** + * Returns room actions. + * + * @param boolean $moderator + * @param object $course + * @param object $bigbluebuttonbn + * @param object $groupobj + * @return string + */ + public static function bigbluebuttonbn_index_display_room_actions($moderator, $course, $bigbluebuttonbn, $groupobj = null) { + $actions = ''; + if ($moderator) { + $actions .= '<form name="form1" method="post" action="">'."\n"; + $actions .= ' <INPUT type="hidden" name="id" value="'.$course->id.'">'."\n"; + $actions .= ' <INPUT type="hidden" name="a" value="'.$bigbluebuttonbn->id.'">'."\n"; + $actions .= ' <INPUT type="hidden" name="action" value="end">'."\n"; + if ($groupobj != null) { + $actions .= ' <INPUT type="hidden" name="g" value="'.$groupobj->id.'">'."\n"; + } + $actions .= ' <INPUT type="submit" name="submit" value="' . + get_string('view_conference_action_end', 'bigbluebuttonbn') . + '" class="btn btn-primary btn-sm" onclick="return confirm(\'' . + get_string('index_confirm_end', 'bigbluebuttonbn') . '\')">' . "\n"; + $actions .= '</form>'."\n"; + } + return $actions; + } +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/classes/output/mobile.php b/mod/bigbluebuttonbn/classes/output/mobile.php new file mode 100644 index 0000000..995e7dd --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/mobile.php @@ -0,0 +1,290 @@ +<?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/>. + +/** + * Mobile output class for bigbluebuttonbn + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\output; + +defined('MOODLE_INTERNAL') || die(); + +use context_module; +use mod_bigbluebuttonbn\locallib\bigbluebutton; + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); +require_once($CFG->dirroot . '/lib/grouplib.php'); + +/** + * Mobile output class for bigbluebuttonbn + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class mobile { + + /** + * Returns the bigbluebuttonbn course view for the mobile app. + * @param mixed $args + * @return array HTML, javascript and other data. + * @throws \coding_exception + * @throws \moodle_exception + * @throws \require_login_exception + * @throws \required_capability_exception + */ + public static function mobile_course_view($args) { + + global $OUTPUT, $SESSION; + + $args = (object) $args; + $viewinstance = bigbluebuttonbn_view_validator($args->cmid, null); + if (!$viewinstance) { + $error = get_string('view_error_url_missing_parameters', 'bigbluebuttonbn'); + return(self::mobile_print_error($error)); + } + + $bbbsession = bigbluebutton::build_bbb_session_fromviewinstance($viewinstance); + + // Create variables for easy access. + $bigbluebuttonbn = $viewinstance['bigbluebuttonbn']; + $serverversion = $bbbsession['serverversion']; + $cm = $bbbsession['cm']; + $course = $bbbsession['course']; + $context = $bbbsession['context']; + + // Check activity status. + $activitystatus = \mod_bigbluebuttonbn\locallib\mobileview::get_activity_status($bbbsession); + if ($activitystatus == 'not_started') { + $message = get_string('view_message_conference_not_started', 'bigbluebuttonbn'); + + $notstarted = array(); + $notstarted['starts_at'] = ''; + $notstarted['ends_at'] = ''; + if (!empty($bigbluebuttonbn->openingtime)) { + $notstarted['starts_at'] = sprintf( + '%s: %s', + get_string('mod_form_field_openingtime', 'bigbluebuttonbn'), + userdate($bigbluebuttonbn->openingtime) + ); + } + if (!empty($bigbluebuttonbn->closingtime)) { + $notstarted['ends_at'] = sprintf( + '%s: %s', + get_string('mod_form_field_closingtime', 'bigbluebuttonbn'), + userdate($bigbluebuttonbn->closingtime) + ); + } + + return(self::mobile_print_notification($bigbluebuttonbn, $cm, $message, $notstarted)); + } + if ($activitystatus == 'ended') { + $message = get_string('view_message_conference_has_ended', 'bigbluebuttonbn'); + return(self::mobile_print_notification($bigbluebuttonbn, $cm, $message)); + } + + // Check if the BBB server is working. + if (is_null($serverversion)) { + + if ($bbbsession['administrator']) { + $error = get_string('view_error_unable_join', 'bigbluebuttonbn'); + } else if ($bbbsession['moderator']) { + $error = get_string('view_error_unable_join_teacher', 'bigbluebuttonbn'); + } else { + $error = get_string('view_error_unable_join_student', 'bigbluebuttonbn'); + } + + return(self::mobile_print_error($error)); + } + + // Mark viewed by user (if required). + $completion = new \completion_info($course); + $completion->set_module_viewed($cm); + + // Validate if the user is in a role allowed to join. + if (!has_capability('moodle/category:manage', $context) && + !has_capability('mod/bigbluebuttonbn:join', $context)) { + $error = get_string('view_nojoin', 'bigbluebuttonbn'); + return(self::mobile_print_error($error)); + } + + // Initialize session variable used across views. + $SESSION->bigbluebuttonbn_bbbsession = $bbbsession; + + // Logic of bbb_view for join to session. + // If user is not administrator nor moderator (user is student) and waiting is required. + if (!$bbbsession['administrator'] && !$bbbsession['moderator'] && $bbbsession['wait']) { + $canjoin = \mod_bigbluebuttonbn\locallib\bigbluebutton::can_join_meeting($args->cmid); + if (!$canjoin['can_join']) { + $message = get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn'); + return (self::mobile_print_notification($bigbluebuttonbn, $cm, $message)); + } + } + + // See if the BBB session is already in progress. + if (!bigbluebuttonbn_is_meeting_running($bbbsession['meetingid'])) { + + // The meeting doesnt exist in BBB server, must be created. + $response = bigbluebuttonbn_get_create_meeting_array( + \mod_bigbluebuttonbn\locallib\mobileview::create_meeting_data($bbbsession), + \mod_bigbluebuttonbn\locallib\mobileview::create_meeting_metadata($bbbsession), + $bbbsession['presentation']['name'], + $bbbsession['presentation']['url'] + ); + + if (empty($response)) { + // The BBB server is failing. + if ($bbbsession['administrator']) { + $e = get_string('view_error_unable_join', 'bigbluebuttonbn'); + } else if ($bbbsession['moderator']) { + $e = get_string('view_error_unable_join_teacher', 'bigbluebuttonbn'); + } else { + $e = get_string('view_error_unable_join_student', 'bigbluebuttonbn'); + } + return(self::mobile_print_error($e)); + } + if ($response['returncode'] == 'FAILED') { + // The meeting could not be created. + $errorkey = bigbluebuttonbn_get_error_key($response['messageKey'], 'view_error_create'); + $e = get_string($errorkey, 'bigbluebuttonbn'); + return(self::mobile_print_error($e)); + } + if ($response['hasBeenForciblyEnded'] == 'true') { + $e = get_string('index_error_forciblyended', 'bigbluebuttonbn'); + return(self::mobile_print_error($e)); + } + + // Event meeting created. + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['meeting_create'], $bigbluebuttonbn); + // Insert a record that meeting was created. + $overrides = array('meetingid' => $bbbsession['meetingid']); + $meta = '{"record":'.($bbbsession['record'] ? 'true' : 'false').'}'; + bigbluebuttonbn_log($bbbsession['bigbluebuttonbn'], BIGBLUEBUTTONBN_LOG_EVENT_CREATE, $overrides, $meta); + } + + // It is part of 'bigbluebuttonbn_bbb_view_join_meeting' in bbb_view. + // Update the cache. + $meetinginfo = bigbluebuttonbn_get_meeting_info($bbbsession['meetingid'], BIGBLUEBUTTONBN_UPDATE_CACHE); + if ($bbbsession['userlimit'] > 0 && intval($meetinginfo['participantCount']) >= $bbbsession['userlimit']) { + // No more users allowed to join. + $message = get_string('view_error_userlimit_reached', 'bigbluebuttonbn'); + return(self::mobile_print_notification($bigbluebuttonbn, $cm, $message)); + } + + // Build final url to BBB. + $bbbsession['createtime'] = $meetinginfo['createTime']; + $urltojoin = \mod_bigbluebuttonbn\locallib\mobileview::build_url_join_session($bbbsession); + + // Check groups access and show message. + $msjgroup = array(); + $groupmode = groups_get_activity_groupmode($bbbsession['cm']); + if ($groupmode != NOGROUPS) { + $msjgroup = array("message" => get_string('view_mobile_message_groups_not_supported', + 'bigbluebuttonbn')); + } + + $data = array( + 'bigbluebuttonbn' => $bigbluebuttonbn, + 'bbbsession' => (object) $bbbsession, + 'msjgroup' => $msjgroup, + 'urltojoin' => $urltojoin, + 'cmid' => $cm->id, + 'courseid' => $course->id + ); + + // We want to show a notification when user excedded 45 seconds without click button. + $jstimecreatedmeeting = 'setTimeout(function(){ + document.getElementById("bigbluebuttonbn-mobile-notifications").style.display = "block"; + document.getElementById("bigbluebuttonbn-mobile-join").disabled = true; + document.getElementById("bigbluebuttonbn-mobile-meetingready").style.display = "none"; + }, 45000);'; + + return array( + 'templates' => array( + array( + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_bigbluebuttonbn/mobile_view_page', $data), + ), + ), + 'javascript' => $jstimecreatedmeeting, + 'otherdata' => '', + 'files' => '' + ); + } + + /** + * Returns the view for errors. + * @param string $error Error to display. + * + * @return array HTML, javascript and otherdata + */ + protected static function mobile_print_error($error) { + + global $OUTPUT; + $data = array( + 'error' => $error + ); + + return array( + 'templates' => array( + array( + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_bigbluebuttonbn/mobile_view_error', $data), + ), + ), + 'javascript' => '', + 'otherdata' => '', + 'files' => '' + ); + } + + /** + * Returns the view for messages. + * @param object $bigbluebuttonbn + * @param stdClass $cm + * @param string $message Message to display. + * @param array $notstarted Extra messages for not started session. + * @return array HTML, javascript and otherdata + */ + protected static function mobile_print_notification($bigbluebuttonbn, $cm, $message, $notstarted = array()) { + + global $OUTPUT, $CFG; + $data = array( + 'bigbluebuttonbn' => $bigbluebuttonbn, + 'cmid' => $cm->id, + 'message' => $message, + 'not_started' => $notstarted + ); + + return array( + 'templates' => array( + array( + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_bigbluebuttonbn/mobile_view_notification', $data), + ), + ), + 'javascript' => file_get_contents($CFG->dirroot . '/mod/bigbluebuttonbn/mobile.notification.js'), + 'otherdata' => '', + 'files' => '' + ); + } +} diff --git a/mod/bigbluebuttonbn/classes/output/renderer.php b/mod/bigbluebuttonbn/classes/output/renderer.php new file mode 100644 index 0000000..8dde416 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/output/renderer.php @@ -0,0 +1,62 @@ +<?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/>. + +/** + * Renderer. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +namespace mod_bigbluebuttonbn\output; + +use html_writer; +use html_table; +use plugin_renderer_base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class renderer + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +class renderer extends plugin_renderer_base { + + /** + * Renderer for index. + * @param index $indexobj + * @return string + */ + protected function render_index(index $indexobj) { + return html_writer::table($indexobj->table); + } + + /** + * Renderer for import_view. + * @param import_view $widget + * @return string + */ + protected function render_import_view(import_view $widget) { + $context = $widget->export_for_template($this); + return $this->render_from_template('mod_bigbluebuttonbn/import_view', $context); + } + +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/classes/plugin.php b/mod/bigbluebuttonbn/classes/plugin.php new file mode 100644 index 0000000..972db58 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/plugin.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn plugin helper. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +namespace mod_bigbluebuttonbn; + +use moodle_url; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class plugin. + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ +abstract class plugin { + + /** + * Component name. + */ + const COMPONENT = 'mod_bigbluebuttonbn'; + + /** + * Outputs url with plain parameters. + * @param string $url + * @param array $params + * @param string $anchor + * @return string + * @throws \moodle_exception + */ + public static function necurl($url, $params = null, $anchor = null) { + $lurl = new moodle_url($url, $params, $anchor); + return $lurl->out(false); + } + +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/classes/privacy/provider.php b/mod/bigbluebuttonbn/classes/privacy/provider.php new file mode 100644 index 0000000..9c04b39 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/privacy/provider.php @@ -0,0 +1,361 @@ +<?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 mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\privacy; + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\helper; +use \core_privacy\local\request\transform; +use core_privacy\local\request\userlist; +use \core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/* + * This part is to be eliminated as soon as possible but allows the phpunit test to pass Ok on MOODLE_33 and below WHILST allowing + * also the privacy/tests/provider_test.php tests to pass + * (vendor/bin/phpunit --fail-on-risky --disallow-test-output -v privacy/tests/provider_test.php). + * Downside we add a new warning to the code checker. This is not ideal but will be ok until we stop supporting MOODLE_33 or we + * change the test in provider_test.php so to cater for classes which are implementing the right method but not necessarily + * inheriting from the new interface setup in MOODLE_34 (\core_privacy\local\request\core_userlist_provider). + * This is linked to CONTRIB-7983 + */ +if (!interface_exists("\\core_privacy\\local\\request\\core_userlist_provider")) { + interface core_userlist_provider { + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist); + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist); + } +} else { + interface core_userlist_provider extends \core_privacy\local\request\core_userlist_provider { + + } +} + +/** + * Privacy class for requesting user data. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class provider implements + // This plugin has data. + \core_privacy\local\metadata\provider, + + // This plugin currently implements the original plugin\provider interface. + \core_privacy\local\request\plugin\provider, + + // This plugin is capable of determining which users have data within it. + core_userlist_provider { + + // This trait must be included. + use \core_privacy\local\legacy_polyfill; + + /** + * Returns metadata. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function _get_metadata(collection $collection) { + + // The table bigbluebuttonbn stores only the room properties. + // However, there is a chance that some personal information is stored as metadata. + // This would be done in the column 'participants' where rules can be set to define BBB roles. + // It is fair to say that only the userid is stored, which is useless if user is removed. + // But if this is a concern a refactoring on the way the rules are stored will be required. + $collection->add_database_table('bigbluebuttonbn', [ + 'participants' => 'privacy:metadata:bigbluebuttonbn:participants', + ], 'privacy:metadata:bigbluebuttonbn'); + + // The table bigbluebuttonbn_logs stores events triggered by users when using the plugin. + // Some personal information along with the resource accessed is stored. + $collection->add_database_table('bigbluebuttonbn_logs', [ + 'userid' => 'privacy:metadata:bigbluebuttonbn_logs:userid', + 'timecreated' => 'privacy:metadata:bigbluebuttonbn_logs:timecreated', + 'meetingid' => 'privacy:metadata:bigbluebuttonbn_logs:meetingid', + 'log' => 'privacy:metadata:bigbluebuttonbn_logs:log', + 'meta' => 'privacy:metadata:bigbluebuttonbn_logs:meta', + ], 'privacy:metadata:bigbluebuttonbn_logs'); + + // Personal information has to be passed to BigBlueButton. + // This includes the user ID and fullname. + $collection->add_external_location_link('bigbluebutton', [ + 'userid' => 'privacy:metadata:bigbluebutton:userid', + 'fullname' => 'privacy:metadata:bigbluebutton:fullname', + ], 'privacy:metadata:bigbluebutton'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The list of contexts used in this plugin. + */ + public static function _get_contexts_for_userid(int $userid) { + // If user was already deleted, do nothing. + if (!\core_user::get_user($userid)) { + return; + } + // Fetch all bigbluebuttonbn logs. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm + ON cm.id = c.instanceid + AND c.contextlevel = :contextlevel + INNER JOIN {modules} m + ON m.id = cm.module + AND m.name = :modname + INNER JOIN {bigbluebuttonbn} bigbluebuttonbn + ON bigbluebuttonbn.id = cm.instance + INNER JOIN {bigbluebuttonbn_logs} bigbluebuttonbnlogs + ON bigbluebuttonbnlogs.bigbluebuttonbnid = bigbluebuttonbn.id + WHERE bigbluebuttonbnlogs.userid = :userid"; + + $params = [ + 'modname' => 'bigbluebuttonbn', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + ]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function _export_user_data(approved_contextlist $contextlist) { + self::_export_user_data_bigbliebuttonbn_logs($contextlist); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function _delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if (!$context instanceof \context_module) { + return; + } + + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid]); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function _delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + $count = $contextlist->count(); + if (empty($count)) { + return; + } + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + if (!$context instanceof \context_module) { + return; + } + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $instanceid, 'userid' => $userid]); + } + } + + /** + * Export personal data for the given approved_contextlist related to bigbluebuttonbn logs. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + protected static function _export_user_data_bigbliebuttonbn_logs(approved_contextlist $contextlist) { + global $DB; + + // Filter out any contexts that are not related to modules. + $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + $carry[] = $context->instanceid; + } + return $carry; + }, []); + + if (empty($cmids)) { + return; + } + + $user = $contextlist->get_user(); + + // Get all the bigbluebuttonbn activities associated with the above course modules. + $instanceidstocmids = self::get_instance_ids_to_cmids_from_cmids($cmids); + $instanceids = array_keys($instanceidstocmids); + + list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED); + $params = array_merge($inparams, ['userid' => $user->id]); + $recordset = $DB->get_recordset_select( + 'bigbluebuttonbn_logs', "bigbluebuttonbnid $insql AND userid = :userid", $params, 'timecreated, id'); + self::recordset_loop_and_export($recordset, 'bigbluebuttonbnid', [], + function($carry, $record) use ($user, $instanceidstocmids) { + $carry[] = [ + 'timecreated' => transform::datetime($record->timecreated), + 'meetingid' => $record->meetingid, + 'log' => $record->log, + 'meta' => $record->meta, + ]; + return $carry; + }, + function($instanceid, $data) use ($user, $instanceidstocmids) { + $context = \context_module::instance($instanceidstocmids[$instanceid]); + $contextdata = helper::get_context_data($context, $user); + $finaldata = (object) array_merge((array) $contextdata, ['logs' => $data]); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $finaldata); + } + ); + } + + /** + * Return a dict of bigbluebuttonbn IDs mapped to their course module ID. + * + * @param array $cmids The course module IDs. + * @return array In the form of [$bigbluebuttonbnid => $cmid]. + */ + protected static function get_instance_ids_to_cmids_from_cmids(array $cmids) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); + $sql = "SELECT bigbluebuttonbn.id, cm.id AS cmid + FROM {bigbluebuttonbn} bigbluebuttonbn + JOIN {modules} m + ON m.name = :bigbluebuttonbn + JOIN {course_modules} cm + ON cm.instance = bigbluebuttonbn.id + AND cm.module = m.id + WHERE cm.id $insql"; + $params = array_merge($inparams, ['bigbluebuttonbn' => 'bigbluebuttonbn']); + + return $DB->get_records_sql_menu($sql, $params); + } + + /** + * Loop and export from a recordset. + * + * @param \moodle_recordset $recordset The recordset. + * @param string $splitkey The record key to determine when to export. + * @param mixed $initial The initial data to reduce from. + * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. + * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. + * @return void + */ + protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, + callable $reducer, callable $export) { + $data = $initial; + $lastid = null; + + foreach ($recordset as $record) { + if ($lastid && $record->{$splitkey} != $lastid) { + $export($lastid, $data); + $data = $initial; + } + $data = $reducer($data, $record); + $lastid = $record->{$splitkey}; + } + $recordset->close(); + + if (!empty($lastid)) { + $export($lastid, $data); + } + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) { + + $context = $userlist->get_context(); + + if (!$context instanceof \context_module) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + 'modulename' => 'bigbluebuttonbn', + ]; + + $sql = "SELECT bnl.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {bigbluebuttonbn} bn ON bn.id = cm.instance + JOIN {bigbluebuttonbn_logs} bnl ON bnl.bigbluebuttonbnid = bn.id + WHERE cm.id = :instanceid"; + + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); + $params = array_merge(['bigbluebuttonbnid' => $cm->instance], $userinparams); + $sql = "bigbluebuttonbnid = :bigbluebuttonbnid AND userid {$userinsql}"; + + $DB->delete_records_select('bigbluebuttonbn_logs', $sql, $params); + } +} diff --git a/mod/bigbluebuttonbn/classes/search/activity.php b/mod/bigbluebuttonbn/classes/search/activity.php new file mode 100644 index 0000000..c893d4a --- /dev/null +++ b/mod/bigbluebuttonbn/classes/search/activity.php @@ -0,0 +1,47 @@ +<?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/>. + +/** + * Search area for mod_bigbluebuttonbn activities. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\search; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Search area for mod_bigbluebuttonbn activities. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity extends \core_search\base_activity { + + /** + * Returns true if this area uses file indexing. + * + * @return bool + */ + public function uses_file_indexing() { + return true; + } +} diff --git a/mod/bigbluebuttonbn/classes/search/tags.php b/mod/bigbluebuttonbn/classes/search/tags.php new file mode 100644 index 0000000..9d963ea --- /dev/null +++ b/mod/bigbluebuttonbn/classes/search/tags.php @@ -0,0 +1,128 @@ +<?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/>. + +/** + * Search area for mod_bigbluebuttonbn tags. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Pablo Pagnone (pablodp84 [at] gmail [dt] com) + */ + +namespace mod_bigbluebuttonbn\search; +use core_tag\output\tag; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Search area for mod_bigbluebuttonbn tags. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tags extends \core_search\base_activity { + + /** + * Returns true if this area uses file indexing. + * + * @return bool + */ + public function uses_file_indexing() { + return false; + } + + /** + * Overwritting get_document_recordset() + * In this search implementation, we need to re-index all instances (and not only the last modified) because we + * are working with core tags and these can be removed from "manage tags" without change the timemodified in + * BBB instances. + * @param int $modifiedfrom + * @param \context|null $context + * @return \moodle_recordset|null + * @throws \coding_exception + * @throws \dml_exception + */ + public function get_document_recordset($modifiedfrom = 0, \context $context = null) { + global $DB; + list ($contextjoin, $contextparams) = $this->get_context_restriction_sql( + $context, $this->get_module_name(), 'modtable'); + if ($contextjoin === null) { + return null; + } + + $result = $DB->get_recordset_sql('SELECT modtable.* FROM {' . $this->get_module_name() . + '} modtable ' . $contextjoin, array_merge($contextparams)); + + return($result); + } + + /** + * Overriding method to index tags of module as string separated by comma. + * + * @param stdClass $record + * @param array $options + * @return \core_search\document + */ + public function get_document($record, $options = array()) { + + try { + $cm = $this->get_cm($this->get_module_name(), $record->id, $record->course); + $context = \context_module::instance($cm->id); + + $tags = \core_tag_tag::get_tags_by_area_in_contexts("core", "course_modules", [$context]); + $tagsstring = ""; + if (!empty($tags)) { + $res = array(); + foreach ($tags as $t) { + $res[] = $t->name; + } + $tagsstring = implode(", ", $res); + } + + } catch (\dml_missing_record_exception $ex) { + // Notify it as we run here as admin, we should see everything. + debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' . + $ex->getMessage(), DEBUG_DEVELOPER); + return false; + } catch (\dml_exception $ex) { + // Notify it as we run here as admin, we should see everything. + debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); + return false; + } + + // Prepare associative array with data from DB. + $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname); + $doc->set('title', content_to_text($record->name, false)); + $doc->set('content', $tagsstring); + $doc->set('contextid', $context->id); + $doc->set('courseid', $record->course); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); + $doc->set('modified', $record->{static::MODIFIED_FIELD_NAME}); + + // Check if this document should be considered new. + if (isset($options['lastindexedtime'])) { + $createdfield = static::CREATED_FIELD_NAME; + if (!empty($createdfield) && ($options['lastindexedtime'] < $record->{$createdfield})) { + // If the document was created after the last index time, it must be new. + $doc->set_is_new(true); + } + } + + return $doc; + } +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/classes/settings/renderer.php b/mod/bigbluebuttonbn/classes/settings/renderer.php new file mode 100644 index 0000000..55f5114 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/settings/renderer.php @@ -0,0 +1,223 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn settings/renderer. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\settings; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); +require_once($CFG->libdir.'/adminlib.php'); + +/** + * Helper class for rendering HTML for settings.php. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer { + + /** + * @var $settings stores the settings as they come from settings.php + */ + private $settings; + + /** + * Constructor. + * + * @param object $settings + */ + public function __construct(&$settings) { + $this->settings = $settings; + } + + /** + * Render the header for a group. + * + * @param string $name + * @param string $itemname + * @param string $itemdescription + * + * @return void + */ + public function render_group_header($name, $itemname = null, $itemdescription = null) { + if ($itemname === null) { + $itemname = get_string('config_' . $name, 'bigbluebuttonbn'); + } + if ($itemdescription === null) { + $itemdescription = get_string('config_' .$name . '_description', 'bigbluebuttonbn'); + } + $item = new \admin_setting_heading('bigbluebuttonbn_config_' . $name, $itemname, $itemdescription); + $this->settings->add($item); + } + + /** + * Render an element in a group. + * + * @param string $name + * @param object $item + * + * @return void + */ + public function render_group_element($name, $item) { + global $CFG; + if (!isset($CFG->bigbluebuttonbn[$name])) { + $this->settings->add($item); + } + } + + /** + * Render a text element in a group. + * + * @param string $name + * @param object $default + * @param string $type + * + * @return Object + */ + public function render_group_element_text($name, $default = null, $type = PARAM_RAW) { + $item = new \admin_setting_configtext('bigbluebuttonbn_' . $name, + get_string('config_' . $name, 'bigbluebuttonbn'), + get_string('config_' . $name . '_description', 'bigbluebuttonbn'), + $default, $type); + return $item; + } + + /** + * Render a html editor element in a group. + * + * @param string $name + * @param object $default + * @param string $type + * + * @return Object + */ + public function render_group_element_textarea($name, $default = null, $type = PARAM_RAW) { + $item = new \admin_setting_configtextarea('bigbluebuttonbn_' . $name, + get_string('config_' . $name, 'bigbluebuttonbn'), + get_string('config_' . $name . '_description', 'bigbluebuttonbn'), + $default, $type); + return $item; + } + + /** + * Render a checkbox element in a group. + * + * @param string $name + * @param object $default + * + * @return Object + */ + public function render_group_element_checkbox($name, $default = null) { + $item = new \admin_setting_configcheckbox('bigbluebuttonbn_' . $name, + get_string('config_' . $name, 'bigbluebuttonbn'), + get_string('config_' . $name . '_description', 'bigbluebuttonbn'), + $default); + return $item; + } + + /** + * Render a multiselect element in a group. + * + * @param string $name + * @param object $defaultsetting + * @param object $choices + * + * @return Object + */ + public function render_group_element_configmultiselect($name, $defaultsetting, $choices) { + $item = new \admin_setting_configmultiselect('bigbluebuttonbn_' . $name, + get_string('config_' . $name, 'bigbluebuttonbn'), + get_string('config_' . $name . '_description', 'bigbluebuttonbn'), + $defaultsetting, $choices); + return $item; + } + + /** + * Render a select element in a group. + * + * @param string $name + * @param object $defaultsetting + * @param object $choices + * + * @return Object + */ + public function render_group_element_configselect($name, $defaultsetting, $choices) { + $item = new \admin_setting_configselect('bigbluebuttonbn_' . $name, + get_string('config_' . $name, 'bigbluebuttonbn'), + get_string('config_' . $name . '_description', 'bigbluebuttonbn'), + $defaultsetting, $choices); + return $item; + } + + /** + * Render a general warning message. + * + * @param string $name + * @param string $message + * @param string $type + * @param boolean $closable + * + * @return Object + */ + public function render_warning_message($name, $message, $type = 'warning', $closable = true) { + $output = $this->output->box_start('box boxalignleft adminerror alert alert-' . $type . ' alert-block fade in', + 'bigbluebuttonbn_' . $name)."\n"; + if ($closable) { + $output .= ' <button type="button" class="close" data-dismiss="alert">×</button>' . "\n"; + } + $output .= ' ' . $message . "\n"; + $output .= $this->output->box_end() . "\n"; + $item = new \admin_setting_heading('bigbluebuttonbn_' . $name, '', $output); + $this->settings->add($item); + return $item; + } + + /** + * Render a general manage file for use as default presentation. + * + * @param string $name + * + * @return Object + */ + public function render_filemanager_default_file_presentation($name) { + + $filemanageroptions = array(); + $filemanageroptions['accepted_types'] = '*'; + $filemanageroptions['maxbytes'] = 0; + $filemanageroptions['subdirs'] = 0; + $filemanageroptions['maxfiles'] = 1; + $filemanageroptions['mainfile'] = true; + + $filemanager = new \admin_setting_configstoredfile('mod_bigbluebuttonbn/presentationdefault', + get_string('config_' . $name, 'bigbluebuttonbn'), + get_string('config_' . $name . '_description', 'bigbluebuttonbn'), + 'presentationdefault', + 0, + $filemanageroptions); + + $this->settings->add($filemanager); + return $filemanager; + } +} diff --git a/mod/bigbluebuttonbn/classes/settings/validator.php b/mod/bigbluebuttonbn/classes/settings/validator.php new file mode 100644 index 0000000..f057731 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/settings/validator.php @@ -0,0 +1,310 @@ +<?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/>. + +/** + * The mod_bigbluebuttonbn settings/validator. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +namespace mod_bigbluebuttonbn\settings; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); +require_once($CFG->libdir.'/adminlib.php'); + +/** + * Helper class for validating settings used HTML for settings.php. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class validator { + + /** + * Validate if general section will be shown. + * + * @return boolean + */ + public static function section_general_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['server_url']) || + !isset($CFG->bigbluebuttonbn['shared_secret'])); + } + + /** + * Validate if record meeting section will be shown. + * + * @return boolean + */ + public static function section_record_meeting_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['recording_default']) || + !isset($CFG->bigbluebuttonbn['recording_editable']) || + !isset($CFG->bigbluebuttonbn['recording_icons_enabled']) || + !isset($CFG->bigbluebuttonbn['recording_all_from_start_default']) || + !isset($CFG->bigbluebuttonbn['recording_all_from_start_editable']) || + !isset($CFG->bigbluebuttonbn['recording_hide_button_default']) || + !isset($CFG->bigbluebuttonbn['recording_hide_button_editable']) ); + } + + /** + * Validate if import recording section will be shown. + * + * @return boolean + */ + public static function section_import_recordings_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['importrecordings_enabled']) || + !isset($CFG->bigbluebuttonbn['importrecordings_from_deleted_enabled'])); + } + + /** + * Validate if show recording section will be shown. + * + * @return boolean + */ + public static function section_show_recordings_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['recordings_html_default']) || + !isset($CFG->bigbluebuttonbn['recordings_html_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_deleted_default']) || + !isset($CFG->bigbluebuttonbn['recordings_deleted_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_imported_default']) || + !isset($CFG->bigbluebuttonbn['recordings_imported_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_preview_default']) || + !isset($CFG->bigbluebuttonbn['recordings_preview_editable']) || + !isset($CFG->bigbluebuttonbn['recordings_validate_url']) + ); + } + + /** + * Validate if wait moderator section will be shown. + * + * @return boolean + */ + public static function section_wait_moderator_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['waitformoderator_default']) || + !isset($CFG->bigbluebuttonbn['waitformoderator_editable']) || + !isset($CFG->bigbluebuttonbn['waitformoderator_ping_interval']) || + !isset($CFG->bigbluebuttonbn['waitformoderator_cache_ttl'])); + } + + /** + * Validate if static voice bridge section will be shown. + * + * @return boolean + */ + public static function section_static_voice_bridge_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['voicebridge_editable'])); + } + + /** + * Validate if preupload presentation section will be shown. + * + * @return boolean + */ + public static function section_preupload_presentation_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['preuploadpresentation_enabled'])); + } + + /** + * Validate if user limit section will be shown. + * + * @return boolean + */ + public static function section_user_limit_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['userlimit_default']) || + !isset($CFG->bigbluebuttonbn['userlimit_editable'])); + } + + /** + * Validate if scheduled duration section will be shown. + * + * @return boolean + */ + public static function section_scheduled_duration_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['scheduled_duration_enabled'])); + } + + /** + * Validate if moderator default section will be shown. + * + * @return boolean + */ + public static function section_moderator_default_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['participant_moderator_default'])); + } + + /** + * Validate if send notification section will be shown. + * + * @return boolean + */ + public static function section_send_notifications_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['sendnotifications_enabled'])); + } + + /** + * Validate if clienttype section will be shown. + * + * @return boolean + */ + public static function section_clienttype_shown() { + global $CFG; + if (!isset($CFG->bigbluebuttonbn['clienttype_enabled']) || + !$CFG->bigbluebuttonbn['clienttype_enabled']) { + return false; + } + if (!bigbluebuttonbn_has_html5_client()) { + return false; + } + return (!isset($CFG->bigbluebuttonbn['clienttype_default']) || + !isset($CFG->bigbluebuttonbn['clienttype_editable'])); + } + + /** + * Validate if settings extended section will be shown. + * + * @return boolean + */ + public static function section_settings_extended_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['recordingready_enabled']) || + !isset($CFG->bigbluebuttonbn['meetingevents_enabled'])); + } + + /** + * Validate if muteonstart section will be shown. + * + * @return boolean + */ + public static function section_muteonstart_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['muteonstart_default']) || + !isset($CFG->bigbluebuttonbn['muteonstart_editable'])); + } + + /** + * Validate if disablecam section will be shown. + * + * @return boolean + */ + public static function section_disablecam_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablecam_default']) || + !isset($CFG->bigbluebuttonbn['disablecam_editable'])); + } + + /** + * Validate if disablemic section will be shown. + * + * @return boolean + */ + public static function section_disablemic_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablemic_default']) || + !isset($CFG->bigbluebuttonbn['disablemic_editable'])); + } + + /** + * Validate if disableprivatechat section will be shown. + * + * @return boolean + */ + public static function section_disableprivatechat_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disableprivatechat_default']) || + !isset($CFG->bigbluebuttonbn['disableprivatechat_editable'])); + } + + /** + * Validate if disablepublicchat section will be shown. + * + * @return boolean + */ + public static function section_disablepublicchat_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablepublicchat_default']) || + !isset($CFG->bigbluebuttonbn['disablepublicchat_editable'])); + } + + /** + * Validate if disablenote section will be shown. + * + * @return boolean + */ + public static function section_disablenote_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['disablenote_default']) || + !isset($CFG->bigbluebuttonbn['disablenote_editable'])); + } + + /** + * Validate if hideuserlist section will be shown. + * + * @return boolean + */ + public static function section_hideuserlist_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['hideuserlist_default']) || + !isset($CFG->bigbluebuttonbn['hideuserlist_editable'])); + } + + /** + * Validate if lockedlayout section will be shown. + * + * @return boolean + */ + public static function section_lockedlayout_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['lockedlayout_default']) || + !isset($CFG->bigbluebuttonbn['lockedlayout_editable'])); + } + + /** + * Validate if lockonjoin section will be shown. + * + * @return boolean + */ + public static function section_lockonjoin_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['lockonjoin_default']) || + !isset($CFG->bigbluebuttonbn['lockonjoin_editable'])); + } + + /** + * Validate if lockonjoinconfigurable section will be shown. + * + * @return boolean + */ + public static function section_lockonjoinconfigurable_shown() { + global $CFG; + return (!isset($CFG->bigbluebuttonbn['lockonjoinconfigurable_default']) || + !isset($CFG->bigbluebuttonbn['lockonjoinconfigurable_editable'])); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/completion_update_state.php b/mod/bigbluebuttonbn/classes/task/completion_update_state.php new file mode 100644 index 0000000..1088dc1 --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/completion_update_state.php @@ -0,0 +1,61 @@ +<?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/>. + +/** + * Internal library of functions for module BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +namespace mod_bigbluebuttonbn\task; + +use core\task\adhoc_task; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class completion_update_state extends adhoc_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('completionupdatestate', 'mod_bigbluebuttonbn'); + } + + /** + * Run bigbluebuttonbn cron. + */ + public function execute() { + // Get the custom data. + $data = $this->get_custom_data(); + mtrace("Task completion_update_state running for user {$data->userid}"); + // Process the completion. + bigbluebuttonbn_completion_update_state($data->bigbluebuttonbn, $data->userid); + } +} diff --git a/mod/bigbluebuttonbn/classes/task/send_notification.php b/mod/bigbluebuttonbn/classes/task/send_notification.php new file mode 100644 index 0000000..19ec1fa --- /dev/null +++ b/mod/bigbluebuttonbn/classes/task/send_notification.php @@ -0,0 +1,63 @@ +<?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/>. + +/** + * Internal library of functions for module BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +namespace mod_bigbluebuttonbn\task; + +use core\task\adhoc_task; +use \mod_bigbluebuttonbn\locallib\notifier; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Class containing the scheduled task for lti module. + * + * @package mod_bigbluebuttonbn + * @copyright 2019 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class send_notification extends adhoc_task +{ + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('sendnotification', 'mod_bigbluebuttonbn'); + } + + /** + * Run bigbluebuttonbn cron. + */ + public function execute() { + // Get the custom data. + $data = $this->get_custom_data(); + mtrace("Execute send_notification task: Sending notification to user {$data->receiver->id}"); + // Process the completion. + \mod_bigbluebuttonbn\locallib\notifier::send_notification($data->sender, $data->receiver, $data->htmlmsg); + } +} diff --git a/mod/bigbluebuttonbn/composer.json b/mod/bigbluebuttonbn/composer.json new file mode 100644 index 0000000..fce905e --- /dev/null +++ b/mod/bigbluebuttonbn/composer.json @@ -0,0 +1,21 @@ +{ + "name": "blindsidenetworks/moodle-mod_bigbluebuttonbn", + "type": "moodle-mod", + "require": { + "firebase/php-jwt": "^4.0" + }, + "require-dev": { + "composer/installers": "~1.0", + "phpunit/phpunit": "5.5.*", + "phpunit/dbunit": "1.4.*", + "moodlehq/behat-extension": "3.34.0", + "friendsofphp/php-cs-fixer": "~1.10", + "phpmd/phpmd": "@stable", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "jakub-onderka/php-console-highlighter": "^0.3.2", + "phpdocumentor/phpdocumentor": "2.*" + }, + "extra": { + "installer-name": "bigbluebuttonbn" + } +} diff --git a/mod/bigbluebuttonbn/composer.lock b/mod/bigbluebuttonbn/composer.lock new file mode 100644 index 0000000..e33b488 --- /dev/null +++ b/mod/bigbluebuttonbn/composer.lock @@ -0,0 +1,6747 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "7cffb944cbebb3deced57bfa337f4535", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/dccf163dc8ed7ed6a00afc06c51ee5186a428d35", + "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v4.0.0" + }, + "time": "2016-07-18T04:51:16+00:00" + } + ], + "packages-dev": [ + { + "name": "behat/behat", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "44a58c1480d6144b2dc2c2bf02b9cef73c83840d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/44a58c1480d6144b2dc2c2bf02b9cef73c83840d", + "reference": "44a58c1480d6144b2dc2c2bf02b9cef73c83840d", + "shasum": "" + }, + "require": { + "behat/gherkin": "^4.4.4", + "behat/transliterator": "^1.2", + "container-interop/container-interop": "^1.1", + "ext-mbstring": "*", + "php": ">=5.3.3", + "symfony/class-loader": "~2.1||~3.0", + "symfony/config": "~2.3||~3.0", + "symfony/console": "~2.5||~3.0", + "symfony/dependency-injection": "~2.1||~3.0", + "symfony/event-dispatcher": "~2.1||~3.0", + "symfony/translation": "~2.3||~3.0", + "symfony/yaml": "~2.1||~3.0" + }, + "require-dev": { + "herrera-io/box": "~1.6.1", + "phpunit/phpunit": "~4.5", + "symfony/process": "~2.5|~3.0" + }, + "suggest": { + "behat/mink-extension": "for integration with Mink testing framework", + "behat/symfony2-extension": "for integration with Symfony2 web framework", + "behat/yii-extension": "for integration with Yii web framework" + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "support": { + "issues": "https://github.com/Behat/Behat/issues", + "source": "https://github.com/Behat/Behat/tree/master" + }, + "time": "2017-05-15T16:49:16+00:00" + }, + { + "name": "behat/gherkin", + "version": "v4.6.2", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/51ac4500c4dc30cbaaabcd2f25694299df666a31", + "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3|~4", + "symfony/yaml": "~2.3|~3|~4" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "support": { + "issues": "https://github.com/Behat/Gherkin/issues", + "source": "https://github.com/Behat/Gherkin/tree/master" + }, + "time": "2020-03-17T14:03:26+00:00" + }, + { + "name": "behat/mink", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20", + "symfony/debug": "^2.7|^3.0|^4.0", + "symfony/phpunit-bridge": "^3.4.38 || ^5.0.5" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)", + "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Browser controller/emulator abstraction for PHP", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "support": { + "issues": "https://github.com/minkphp/Mink/issues", + "source": "https://github.com/minkphp/Mink/tree/v1.8.1" + }, + "time": "2020-03-11T15:45:53+00:00" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "v1.3.4", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "e3b90840022ebcd544c7b394a3c9597ae242cbee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/e3b90840022ebcd544c7b394a3c9597ae242cbee", + "reference": "e3b90840022ebcd544c7b394a3c9597ae242cbee", + "shasum": "" + }, + "require": { + "behat/mink": "^1.7.1@dev", + "php": ">=5.3.6", + "symfony/browser-kit": "~2.3|~3.0|~4.0", + "symfony/dom-crawler": "~2.3|~3.0|~4.0" + }, + "require-dev": { + "mink/driver-testsuite": "dev-master", + "symfony/debug": "^2.7|^3.0|^4.0", + "symfony/http-kernel": "~2.3|~3.0|~4.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "support": { + "issues": "https://github.com/minkphp/MinkBrowserKitDriver/issues", + "source": "https://github.com/minkphp/MinkBrowserKitDriver/tree/v1.3.4" + }, + "time": "2020-03-11T09:49:45+00:00" + }, + { + "name": "behat/mink-extension", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/Behat/MinkExtension.git", + "reference": "80f7849ba53867181b7e412df9210e12fba50177" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/80f7849ba53867181b7e412df9210e12fba50177", + "reference": "80f7849ba53867181b7e412df9210e12fba50177", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.5", + "behat/mink": "^1.5", + "php": ">=5.3.2", + "symfony/config": "^2.7|^3.0|^4.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "^1.1", + "phpspec/phpspec": "^2.0" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\MinkExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "Mink extension for Behat", + "homepage": "http://extensions.behat.org/mink", + "keywords": [ + "browser", + "gui", + "test", + "web" + ], + "support": { + "issues": "https://github.com/Behat/MinkExtension/issues", + "source": "https://github.com/Behat/MinkExtension/tree/master" + }, + "time": "2018-02-06T15:36:30+00:00" + }, + { + "name": "behat/mink-goutte-driver", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0|~3.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "support": { + "issues": "https://github.com/minkphp/MinkGoutteDriver/issues", + "source": "https://github.com/minkphp/MinkGoutteDriver/tree/master" + }, + "time": "2016-03-05T09:04:22+00:00" + }, + { + "name": "behat/mink-selenium2-driver", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkSelenium2Driver.git", + "reference": "312a967dd527f28980cce40850339cd5316da092" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/312a967dd527f28980cce40850339cd5316da092", + "reference": "312a967dd527f28980cce40850339cd5316da092", + "shasum": "" + }, + "require": { + "behat/mink": "~1.7@dev", + "instaclick/php-webdriver": "~1.1", + "php": ">=5.4" + }, + "require-dev": { + "mink/driver-testsuite": "dev-master" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pete Otaqui", + "email": "pete@otaqui.com", + "homepage": "https://github.com/pete-otaqui" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Selenium2 (WebDriver) driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "javascript", + "selenium", + "testing", + "webdriver" + ], + "support": { + "issues": "https://github.com/minkphp/MinkSelenium2Driver/issues", + "source": "https://github.com/minkphp/MinkSelenium2Driver/tree/v1.4.0" + }, + "time": "2020-03-11T14:43:21+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "3c4ec1d77c3d05caa1f0bf8fb3aae4845005c7fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/3c4ec1d77c3d05caa1f0bf8fb3aae4845005c7fc", + "reference": "3c4ec1d77c3d05caa1f0bf8fb3aae4845005c7fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0", + "phpunit/phpunit": "^4.8.36|^6.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Transliterator\\": "src/Behat/Transliterator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "support": { + "issues": "https://github.com/Behat/Transliterator/issues", + "source": "https://github.com/Behat/Transliterator/tree/v1.3.0" + }, + "time": "2020-01-14T16:39:13+00:00" + }, + { + "name": "cilex/cilex", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Cilex/Cilex.git", + "reference": "7acd965a609a56d0345e8b6071c261fbdb926cb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Cilex/Cilex/zipball/7acd965a609a56d0345e8b6071c261fbdb926cb5", + "reference": "7acd965a609a56d0345e8b6071c261fbdb926cb5", + "shasum": "" + }, + "require": { + "cilex/console-service-provider": "1.*", + "php": ">=5.3.3", + "pimple/pimple": "~1.0", + "symfony/finder": "~2.1", + "symfony/process": "~2.1" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*", + "symfony/validator": "~2.1" + }, + "suggest": { + "monolog/monolog": ">=1.0.0", + "symfony/validator": ">=1.0.0", + "symfony/yaml": ">=1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "Cilex": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "description": "The PHP micro-framework for Command line tools based on the Symfony2 Components", + "homepage": "http://cilex.github.com", + "keywords": [ + "cli", + "microframework" + ], + "support": { + "issues": "https://github.com/Cilex/Cilex/issues", + "source": "https://github.com/Cilex/Cilex/tree/master" + }, + "time": "2014-03-29T14:03:13+00:00" + }, + { + "name": "cilex/console-service-provider", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Cilex/console-service-provider.git", + "reference": "25ee3d1875243d38e1a3448ff94bdf944f70d24e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Cilex/console-service-provider/zipball/25ee3d1875243d38e1a3448ff94bdf944f70d24e", + "reference": "25ee3d1875243d38e1a3448ff94bdf944f70d24e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "pimple/pimple": "1.*@dev", + "symfony/console": "~2.1" + }, + "require-dev": { + "cilex/cilex": "1.*@dev", + "silex/silex": "1.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "Cilex\\Provider\\Console": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "description": "Console Service Provider", + "keywords": [ + "cilex", + "console", + "pimple", + "service-provider", + "silex" + ], + "support": { + "issues": "https://github.com/Cilex/console-service-provider/issues", + "source": "https://github.com/Cilex/console-service-provider/tree/master" + }, + "time": "2012-12-19T10:50:58+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.2.8", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "8a7ecad675253e4654ea05505233285377405215" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", + "reference": "8a7ecad675253e4654ea05505233285377405215", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.2.8" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-08-23T12:54:47+00:00" + }, + { + "name": "composer/installers", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.6.* || 2.0.*@dev", + "composer/semver": "1.0.* || 2.0.*@dev", + "phpunit/phpunit": "^4.8.36", + "sebastian/comparator": "^1.2.4", + "symfony/process": "^2.3" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "modulework", + "modx", + "moodle", + "osclass", + "phpbb", + "piwik", + "ppi", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "symfony", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.9.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-04-07T06:57:05+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "f28d44c286812c714741478d968104c5e604a1d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4", + "reference": "f28d44c286812c714741478d968104c5e604a1d4", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-11-13T08:04:11+00:00" + }, + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "support": { + "issues": "https://github.com/container-interop/container-interop/issues", + "source": "https://github.com/container-interop/container-interop/tree/master" + }, + "abandoned": "psr/container", + "time": "2017-02-14T19:40:03+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^9.1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.11.1" + }, + "time": "2020-10-26T10:28:16+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" + }, + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" + }, + { + "name": "fabpot/goutte", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "80a23b64f44d54dd571d114c473d9d7e9ed84ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/80a23b64f44d54dd571d114c473d9d7e9ed84ca5", + "reference": "80a23b64f44d54dd571d114c473d9d7e9ed84ca5", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=7.1.3", + "symfony/browser-kit": "^4.4|^5.0", + "symfony/css-selector": "^4.4|^5.0", + "symfony/dom-crawler": "^4.4|^5.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + }, + "exclude-from-classmap": [ + "Goutte/Tests" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "support": { + "issues": "https://github.com/FriendsOfPHP/Goutte/issues", + "source": "https://github.com/FriendsOfPHP/Goutte/tree/v3.3.1" + }, + "time": "2020-11-01T09:30:18+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v1.11.8", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "117137e9970054d022b7656209f094dab852b90c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/117137e9970054d022b7656209f094dab852b90c", + "reference": "117137e9970054d022b7656209f094dab852b90c", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.6", + "sebastian/diff": "~1.1", + "symfony/console": "~2.3|~3.0", + "symfony/event-dispatcher": "~2.1|~3.0", + "symfony/filesystem": "~2.1|~3.0", + "symfony/finder": "~2.1|~3.0", + "symfony/process": "~2.3|~3.0", + "symfony/stopwatch": "~2.5|~3.0" + }, + "conflict": { + "hhvm": "<3.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.5|^5", + "satooshi/php-coveralls": "^0.7.1" + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "Symfony\\CS\\": "Symfony/CS/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v1.11.8" + }, + "time": "2016-08-16T23:31:05+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5" + }, + "time": "2020-06-16T21:01:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "60d379c243457e073cff02bc323a2a86cb355631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", + "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.0" + }, + "time": "2020-09-30T07:37:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.7.0" + }, + "time": "2020-09-30T07:37:11+00:00" + }, + { + "name": "instaclick/php-webdriver", + "version": "1.4.7", + "source": { + "type": "git", + "url": "https://github.com/instaclick/php-webdriver.git", + "reference": "b5f330e900e9b3edfc18024a5ec8c07136075712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/b5f330e900e9b3edfc18024a5ec8c07136075712", + "reference": "b5f330e900e9b3edfc18024a5ec8c07136075712", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "^4.8", + "satooshi/php-coveralls": "^1.0||^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "WebDriver": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Justin Bishop", + "email": "jubishop@gmail.com", + "role": "Developer" + }, + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "role": "Fork Maintainer" + } + ], + "description": "PHP WebDriver for Selenium 2", + "homepage": "http://instaclick.com/", + "keywords": [ + "browser", + "selenium", + "webdriver", + "webtest" + ], + "support": { + "issues": "https://github.com/instaclick/php-webdriver/issues", + "source": "https://github.com/instaclick/php-webdriver/tree/1.x" + }, + "time": "2019-09-25T09:05:11+00:00" + }, + { + "name": "jakub-onderka/php-console-color", + "version": "v0.2", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191", + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "jakub-onderka/php-code-style": "1.0", + "jakub-onderka/php-parallel-lint": "1.0", + "jakub-onderka/php-var-dump-check": "0.*", + "phpunit/phpunit": "~4.3", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "JakubOnderka\\PhpConsoleColor\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "jakub.onderka@gmail.com" + } + ], + "support": { + "issues": "https://github.com/JakubOnderka/PHP-Console-Color/issues", + "source": "https://github.com/JakubOnderka/PHP-Console-Color/tree/master" + }, + "abandoned": "php-parallel-lint/php-console-color", + "time": "2018-09-29T17:23:10+00:00" + }, + { + "name": "jakub-onderka/php-console-highlighter", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", + "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "shasum": "" + }, + "require": { + "jakub-onderka/php-console-color": "~0.1", + "php": ">=5.3.0" + }, + "require-dev": { + "jakub-onderka/php-code-style": "~1.0", + "jakub-onderka/php-parallel-lint": "~0.5", + "jakub-onderka/php-var-dump-check": "~0.1", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JakubOnderka\\PhpConsoleHighlighter": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "acci@acci.cz", + "homepage": "http://www.acci.cz/" + } + ], + "support": { + "issues": "https://github.com/JakubOnderka/PHP-Console-Highlighter/issues", + "source": "https://github.com/JakubOnderka/PHP-Console-Highlighter/tree/master" + }, + "abandoned": "php-parallel-lint/php-console-highlighter", + "time": "2015-04-20T18:58:01+00:00" + }, + { + "name": "jakub-onderka/php-parallel-lint", + "version": "v0.9.2", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Parallel-Lint.git", + "reference": "2ead2e4043ab125bee9554f356e0a86742c2d4fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Parallel-Lint/zipball/2ead2e4043ab125bee9554f356e0a86742c2d4fa", + "reference": "2ead2e4043ab125bee9554f356e0a86742c2d4fa", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "jakub-onderka/php-console-highlighter": "~0.3", + "nette/tester": "~1.3" + }, + "suggest": { + "jakub-onderka/php-console-highlighter": "Highlight syntax in code snippet" + }, + "bin": [ + "parallel-lint" + ], + "type": "library", + "autoload": { + "classmap": [ + "./" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "jakub.onderka@gmail.com" + } + ], + "description": "This tool check syntax of PHP files about 20x faster than serial check.", + "homepage": "https://github.com/JakubOnderka/PHP-Parallel-Lint", + "support": { + "issues": "https://github.com/JakubOnderka/PHP-Parallel-Lint/issues", + "source": "https://github.com/JakubOnderka/PHP-Parallel-Lint/tree/master" + }, + "abandoned": "php-parallel-lint/php-parallel-lint", + "time": "2015-12-15T10:42:16+00:00" + }, + { + "name": "jms/metadata", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "support": { + "issues": "https://github.com/schmittjoh/metadata/issues", + "source": "https://github.com/schmittjoh/metadata/tree/1.x" + }, + "time": "2018-10-26T12:40:10+00:00" + }, + { + "name": "jms/parser-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/parser-lib.git", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "shasum": "" + }, + "require": { + "phpoption/phpoption": ">=0.9,<2.0-dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "A library for easily creating recursive-descent parsers.", + "support": { + "issues": "https://github.com/schmittjoh/parser-lib/issues", + "source": "https://github.com/schmittjoh/parser-lib/tree/1.0.0" + }, + "time": "2012-11-18T18:08:43+00:00" + }, + { + "name": "jms/serializer", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "4fad8bbbe76e05de3b79ffa3db027058ed3813ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/4fad8bbbe76e05de3b79ffa3db027058ed3813ff", + "reference": "4fad8bbbe76e05de3b79ffa3db027058ed3813ff", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/instantiator": "^1.0.3", + "jms/metadata": "~1.1", + "jms/parser-lib": "1.*", + "php": ">=5.5.0", + "phpcollection/phpcollection": "~0.1", + "phpoption/phpoption": "^1.1" + }, + "conflict": { + "jms/serializer-bundle": "<1.2.1", + "twig/twig": "<1.12" + }, + "require-dev": { + "doctrine/orm": "~2.1", + "doctrine/phpcr-odm": "^1.3|^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "phpunit/phpunit": "^4.8|^5.0", + "propel/propel1": "~1.7", + "symfony/expression-language": "^2.6|^3.0", + "symfony/filesystem": "^2.1", + "symfony/form": "~2.1|^3.0", + "symfony/translation": "^2.1|^3.0", + "symfony/validator": "^2.2|^3.0", + "symfony/yaml": "^2.1|^3.0", + "twig/twig": "~1.12|~2.0" + }, + "suggest": { + "doctrine/cache": "Required if you like to use cache functionality.", + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", + "symfony/yaml": "Required if you'd like to serialize data to YAML format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\Serializer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "support": { + "issues": "https://github.com/schmittjoh/serializer/issues", + "source": "https://github.com/schmittjoh/serializer/tree/master" + }, + "time": "2017-05-15T08:35:42+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.25.5", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1817faadd1846cd08be9a49e905dc68823bc38c0", + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "php-parallel-lint/php-parallel-lint": "^1.0", + "phpunit/phpunit": "~4.5", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/1.25.5" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-07-23T08:35:51+00:00" + }, + { + "name": "moodlehq/behat-extension", + "version": "v3.34.0", + "source": { + "type": "git", + "url": "https://github.com/moodlehq/moodle-behat-extension.git", + "reference": "a1f956fb13ef4c430ceb37c6c1ffcd355d956a22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/a1f956fb13ef4c430ceb37c6c1ffcd355d956a22", + "reference": "a1f956fb13ef4c430ceb37c6c1ffcd355d956a22", + "shasum": "" + }, + "require": { + "behat/behat": "3.3.*", + "behat/mink": "~1.7", + "behat/mink-extension": "~2.2", + "behat/mink-goutte-driver": "~1.2", + "behat/mink-selenium2-driver": "~1.3", + "php": ">=5.4.4", + "symfony/process": "2.8.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "Moodle\\BehatExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPLv3" + ], + "authors": [ + { + "name": "David Monllaó", + "email": "david.monllao@gmail.com", + "homepage": "http://moodle.com", + "role": "Developer" + } + ], + "description": "Moodle behat extension", + "keywords": [ + "BDD", + "Behat", + "moodle" + ], + "support": { + "source": "https://github.com/moodlehq/moodle-behat-extension/tree/v3.33.1" + }, + "time": "2017-01-20T02:48:22+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v1.4.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51", + "reference": "f78af2c9c86107aa1a34cd1dbb5bbe9eeb0d9f51", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "files": [ + "lib/bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/1.x" + }, + "time": "2015-09-19T14:15:08+00:00" + }, + { + "name": "padraic/humbug_get_contents", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/humbug/file_get_contents.git", + "reference": "dcb086060c9dd6b2f51d8f7a895500307110b7a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/file_get_contents/zipball/dcb086060c9dd6b2f51d8f7a895500307110b7a7", + "reference": "dcb086060c9dd6b2f51d8f7a895500307110b7a7", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "ext-openssl": "*", + "php": "^5.3 || ^7.0 || ^7.1 || ^7.2" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.1", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false + }, + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Humbug\\": "src/" + }, + "files": [ + "src/function.php", + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Secure wrapper for accessing HTTPS resources with file_get_contents for PHP 5.3+", + "homepage": "https://github.com/padraic/file_get_contents", + "keywords": [ + "download", + "file_get_contents", + "http", + "https", + "ssl", + "tls" + ], + "support": { + "issues": "https://github.com/humbug/file_get_contents/issues", + "source": "https://github.com/humbug/file_get_contents/tree/master" + }, + "time": "2018-02-12T18:47:17+00:00" + }, + { + "name": "padraic/phar-updater", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/humbug/phar-updater.git", + "reference": "d01d3b8f26e541ac9b9eeba1e18d005d852f7ff1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/phar-updater/zipball/d01d3b8f26e541ac9b9eeba1e18d005d852f7ff1", + "reference": "d01d3b8f26e541ac9b9eeba1e18d005d852f7ff1", + "shasum": "" + }, + "require": { + "padraic/humbug_get_contents": "^1.0", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Humbug\\SelfUpdate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "http://blog.astrumfutura.com" + } + ], + "description": "A thing to make PHAR self-updating easy and secure.", + "keywords": [ + "humbug", + "phar", + "self-update", + "update" + ], + "support": { + "issues": "https://github.com/humbug/phar-updater/issues", + "source": "https://github.com/humbug/phar-updater/tree/1.0" + }, + "abandoned": true, + "time": "2018-03-30T12:52:15+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "c64472f8e76ca858c79ad9a4cf1e2734b3f8cc38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/c64472f8e76ca858c79ad9a4cf1e2734b3f8cc38", + "reference": "c64472f8e76ca858c79ad9a4cf1e2734b3f8cc38", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5", + "symfony/filesystem": "^2.3.0|^3|^4|^5" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.2.3", + "gregwar/rst": "^1.0", + "phpunit/phpunit": "^4.8.35|^5.7", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/master" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2020-06-20T10:53:13+00:00" + }, + { + "name": "phpcollection/phpcollection", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-collection.git", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "shasum": "" + }, + "require": { + "phpoption/phpoption": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4-dev" + } + }, + "autoload": { + "psr-0": { + "PhpCollection": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "General-Purpose Collection Library for PHP", + "keywords": [ + "collection", + "list", + "map", + "sequence", + "set" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-collection/issues", + "source": "https://github.com/schmittjoh/php-collection/tree/master" + }, + "time": "2015-05-17T12:39:23+00:00" + }, + { + "name": "phpdocumentor/fileset", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/Fileset.git", + "reference": "bfa78d8fa9763dfce6d0e5d3730c1d8ab25d34b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/Fileset/zipball/bfa78d8fa9763dfce6d0e5d3730c1d8ab25d34b0", + "reference": "bfa78d8fa9763dfce6d0e5d3730c1d8ab25d34b0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/finder": "~2.1" + }, + "require-dev": { + "phpunit/phpunit": "~3.7" + }, + "type": "library", + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/", + "tests/unit/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Fileset component for collecting a set of files given directories and file paths", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "files", + "fileset", + "phpdoc" + ], + "support": { + "issues": "https://github.com/phpDocumentor/Fileset/issues", + "source": "https://github.com/phpDocumentor/Fileset/tree/master" + }, + "time": "2013-08-06T21:07:42+00:00" + }, + { + "name": "phpdocumentor/graphviz", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/GraphViz.git", + "reference": "a906a90a9f230535f25ea31caf81b2323956283f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/GraphViz/zipball/a906a90a9f230535f25ea31caf81b2323956283f", + "reference": "a906a90a9f230535f25ea31caf81b2323956283f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/", + "tests/unit" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "issues": "https://github.com/phpDocumentor/GraphViz/issues", + "source": "https://github.com/phpDocumentor/GraphViz/tree/master" + }, + "time": "2016-02-02T13:00:08+00:00" + }, + { + "name": "phpdocumentor/phpdocumentor", + "version": "v2.9.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/phpDocumentor.git", + "reference": "2e4f981a55ebe6f5db592d7da892d13d5b3c7816" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/phpDocumentor/zipball/2e4f981a55ebe6f5db592d7da892d13d5b3c7816", + "reference": "2e4f981a55ebe6f5db592d7da892d13d5b3c7816", + "shasum": "" + }, + "require": { + "cilex/cilex": "~1.0", + "erusev/parsedown": "~1.0", + "jms/serializer": ">=0.12 < 1.8.0", + "monolog/monolog": "~1.6", + "padraic/phar-updater": "^1.0", + "php": ">=5.3.3", + "phpdocumentor/fileset": "~1.0", + "phpdocumentor/graphviz": "~1.0", + "phpdocumentor/reflection": "^3.0", + "phpdocumentor/reflection-docblock": "~2.0", + "symfony/config": "~2.3", + "symfony/console": "~2.3", + "symfony/event-dispatcher": "~2.1", + "symfony/process": "~2.0", + "symfony/stopwatch": "~2.3", + "symfony/validator": "~2.2", + "twig/twig": "~1.3", + "webmozart/assert": "^1.2", + "zendframework/zend-cache": "~2.1", + "zendframework/zend-config": "~2.1", + "zendframework/zend-filter": "~2.1", + "zendframework/zend-i18n": "~2.1", + "zendframework/zend-serializer": "~2.1", + "zendframework/zend-servicemanager": "~2.1", + "zendframework/zend-stdlib": "~2.1", + "zetacomponents/document": ">=1.3.1" + }, + "require-dev": { + "behat/behat": "^3.0", + "mikey179/vfsstream": "~1.2", + "mockery/mockery": "^0.9@dev", + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^1.4", + "symfony/expression-language": "^2.4" + }, + "suggest": { + "ext-twig": "Enabling the twig extension improves the generation of twig based templates.", + "ext-xslcache": "Enabling the XSLCache extension improves the generation of xml based templates." + }, + "bin": [ + "bin/phpdoc.php", + "bin/phpdoc" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "2.9-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/", + "tests/unit/" + ], + "Cilex\\Provider": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Documentation Generator for PHP", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "api", + "application", + "dga", + "documentation", + "phpdoc" + ], + "support": { + "issues": "https://github.com/phpDocumentor/phpDocumentor/issues", + "source": "https://github.com/phpDocumentor/phpDocumentor/tree/v2.9.1" + }, + "time": "2020-01-12T19:44:16+00:00" + }, + { + "name": "phpdocumentor/reflection", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/Reflection.git", + "reference": "793bfd92d9a0fc96ae9608fb3e947c3f59fb3a0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/793bfd92d9a0fc96ae9608fb3e947c3f59fb3a0d", + "reference": "793bfd92d9a0fc96ae9608fb3e947c3f59fb3a0d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^1.0", + "php": ">=5.3.3", + "phpdocumentor/reflection-docblock": "~2.0", + "psr/log": "~1.0" + }, + "require-dev": { + "behat/behat": "~2.4", + "mockery/mockery": "~0.8", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/", + "tests/unit/", + "tests/mocks/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Reflection library to do Static Analysis for PHP Projects", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/Reflection/issues", + "source": "https://github.com/phpDocumentor/Reflection/tree/master" + }, + "time": "2016-05-21T08:42:32+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e6a969a640b00d8daa3c66518b0405fb41ae0c4b", + "reference": "e6a969a640b00d8daa3c66518b0405fb41ae0c4b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/2.x" + }, + "time": "2016-01-25T08:17:30+00:00" + }, + { + "name": "phpmd/phpmd", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "ce10831d4ddc2686c1348a98069771dd314534a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/ce10831d4ddc2686c1348a98069771dd314534a8", + "reference": "ce10831d4ddc2686c1348a98069771dd314534a8", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.7.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.4", + "phpunit/phpunit": "^4.8.36 || ^5.7.27", + "squizlabs/php_codesniffer": "^2.0" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.9.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2020-09-23T22:06:32+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.7.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.7.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2020-07-20T17:29:33+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.10.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "451c3cd1418cf640de218914901e51b064abb093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, + "time": "2020-03-05T15:02:03+00:00" + }, + { + "name": "phpunit/dbunit", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/dbunit.git", + "reference": "9aaee6447663ff1b0cd50c23637e04af74c5e2ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/9aaee6447663ff1b0cd50c23637e04af74c5e2ae", + "reference": "9aaee6447663ff1b0cd50c23637e04af74c5e2ae", + "shasum": "" + }, + "require": { + "ext-pdo": "*", + "ext-simplexml": "*", + "php": ">=5.3.3", + "phpunit/phpunit": "~4|~5", + "symfony/yaml": "~2.1|~3.0" + }, + "bin": [ + "composer/bin/dbunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "", + "../../symfony/yaml/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "DbUnit port for PHP/PHPUnit to support database interaction testing.", + "homepage": "https://github.com/sebastianbergmann/dbunit/", + "keywords": [ + "database", + "testing", + "xunit" + ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/dbunit/issues", + "source": "https://github.com/sebastianbergmann/dbunit/tree/master" + }, + "abandoned": true, + "time": "2015-08-07T04:57:38+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", + "phpunit/php-token-stream": "^1.4.2 || ^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "^1.0 || ^2.0" + }, + "require-dev": { + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" + }, + "suggest": { + "ext-xdebug": "^2.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/4.0" + }, + "time": "2017-04-02T07:44:40+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" + }, + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + }, + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" + }, + "abandoned": true, + "time": "2017-11-27T05:48:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "5.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "3f67cee782c9abfaee5e32fd2f57cdd54bc257ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3f67cee782c9abfaee5e32fd2f57cdd54bc257ba", + "reference": "3f67cee782c9abfaee5e32fd2f57cdd54bc257ba", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "^4.0.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "^1.3 || ^2.0", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/object-enumerator": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", + "symfony/yaml": "~2.1|~3.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-tidy": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/5.5.7" + }, + "time": "2016-10-03T13:04:15+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", + "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/3.4" + }, + "abandoned": true, + "time": "2017-06-30T09:13:00+00:00" + }, + { + "name": "pimple/pimple", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "2019c145fe393923f3441b23f29bbdfaa5c58c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/2019c145fe393923f3441b23f29bbdfaa5c58c4d", + "reference": "2019c145fe393923f3441b23f29bbdfaa5c58c4d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple is a simple Dependency Injection Container for PHP 5.3", + "homepage": "http://pimple.sensiolabs.org", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "issues": "https://github.com/silexphp/Pimple/issues", + "source": "https://github.com/silexphp/Pimple/tree/v1.1.1" + }, + "time": "2013-11-22T08:30:29+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-30T08:15:22+00:00" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/1.2" + }, + "time": "2017-01-29T09:50:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/1.4" + }, + "time": "2017-05-22T07:24:03+00:00" + }, + { + "name": "sebastian/environment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/master" + }, + "time": "2016-11-26T07:53:53+00:00" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/master" + }, + "time": "2016-06-17T09:04:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/1.1.1" + }, + "time": "2015-10-12T03:26:01+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master" + }, + "time": "2016-01-28T13:25:10+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", + "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + }, + "time": "2016-10-03T07:41:43+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" + }, + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v4.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "5f11947e9ec072ac32c605c07cb22522c30f4b28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/5f11947e9ec072ac32c605c07cb22522c30f4b28", + "reference": "5f11947e9ec072ac32c605c07cb22522c30f4b28", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/dom-crawler": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/mime": "^4.3|^5.0", + "symfony/process": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v4.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-28T20:42:29+00:00" + }, + { + "name": "symfony/class-loader", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "a22265a9f3511c0212bf79f54910ca5a77c0e92c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/a22265a9f3511c0212bf79f54910ca5a77c0e92c", + "reference": "a22265a9f3511c0212bf79f54910ca5a77c0e92c", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "require-dev": { + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/class-loader/tree/v3.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/config", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "7dd5f5040dc04c118d057fb5886563963eb70011" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/7dd5f5040dc04c118d057fb5886563963eb70011", + "reference": "7dd5f5040dc04c118d057fb5886563963eb70011", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/filesystem": "~2.3|~3.0.0", + "symfony/polyfill-ctype": "~1.8" + }, + "require-dev": { + "symfony/yaml": "~2.7|~3.0.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v2.8.50" + }, + "time": "2018-11-26T09:38:12+00:00" + }, + { + "name": "symfony/console", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/debug": "^2.7.2|~3.0.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1|~3.0.0", + "symfony/process": "~2.1|~3.0.0" + }, + "suggest": { + "psr/log-implementation": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v2.8.52" + }, + "time": "2018-11-20T15:55:20+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b8d8eb06b0942e84a69e7acebc3e9c1e6e6e7256" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b8d8eb06b0942e84a69e7acebc3e9c1e6e6e7256", + "reference": "b8d8eb06b0942e84a69e7acebc3e9c1e6e6e7256", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v5.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-28T21:31:18+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/3.0" + }, + "time": "2016-07-30T07:22:48+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.2.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "d9f2e62e1a93d52ad4e4f6faaf66f6eef723d761" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d9f2e62e1a93d52ad4e4f6faaf66f6eef723d761", + "reference": "d9f2e62e1a93d52ad4e4f6faaf66f6eef723d761", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "symfony/yaml": "<3.2" + }, + "require-dev": { + "symfony/config": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/yaml": "~3.2" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/3.2" + }, + "time": "2017-07-28T15:22:55+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v4.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "30ad9ac96a01913195bf0328d48e29d54fa53e6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/30ad9ac96a01913195bf0328d48e29d54fa53e6e", + "reference": "30ad9ac96a01913195bf0328d48e29d54fa53e6e", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "masterminds/html5": "<2.6" + }, + "require-dev": { + "masterminds/html5": "^2.6", + "symfony/css-selector": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v4.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T11:50:19+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a77e974a5fecb4398833b0709210e3d5e334ffb0", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^2.0.5|~3.0.0", + "symfony/dependency-injection": "~2.6|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/stopwatch": "~2.3|~3.0.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v2.8.50" + }, + "time": "2018-11-21T14:20:20+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v3.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2da5009d9bacbd91d83486aa1f44c793a8c380d", + "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/3.0" + }, + "time": "2016-07-20T05:43:46+00:00" + }, + { + "name": "symfony/finder", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "1444eac52273e345d9b95129bf914639305a9ba4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/1444eac52273e345d9b95129bf914639305a9ba4", + "reference": "1444eac52273e345d9b95129bf914639305a9ba4", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v2.8.50" + }, + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "727d1096295d807c309fb01a851577302394c897" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/process", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v2.8.50" + }, + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "752586c80af8a85aeb74d1ae8202411c68836663" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/752586c80af8a85aeb74d1ae8202411c68836663", + "reference": "752586c80af8a85aeb74d1ae8202411c68836663", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Stopwatch Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" + }, + { + "name": "symfony/translation", + "version": "v3.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/eee6c664853fd0576f21ae25725cfffeafe83f26", + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/intl": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/3.0" + }, + "time": "2016-07-30T07:22:48+00:00" + }, + { + "name": "symfony/validator", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "d5d2090bba3139d8ddb79959fbf516e87238fe3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/d5d2090bba3139d8ddb79959fbf516e87238fe3a", + "reference": "d5d2090bba3139d8ddb79959fbf516e87238fe3a", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation": "~2.4|~3.0.0" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.0", + "egulias/email-validator": "^1.2.1", + "symfony/config": "~2.2|~3.0.0", + "symfony/expression-language": "~2.4|~3.0.0", + "symfony/http-foundation": "~2.3|~3.0.0", + "symfony/intl": "~2.7.25|^2.8.18|~3.2.5", + "symfony/property-access": "~2.3|~3.0.0", + "symfony/yaml": "^2.0.5|~3.0.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader and metadata cache.", + "egulias/email-validator": "Strict (RFC compliant) email validation", + "symfony/config": "", + "symfony/expression-language": "For using the 2.4 Expression validator", + "symfony/http-foundation": "", + "symfony/intl": "", + "symfony/property-access": "For using the 2.4 Validator API", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Validator Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v2.8.50" + }, + "time": "2018-11-14T14:06:48+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.3.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "af615970e265543a26ee712c958404eb9b7ac93d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/af615970e265543a26ee712c958404eb9b7ac93d", + "reference": "af615970e265543a26ee712c958404eb9b7ac93d", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/3.3" + }, + "time": "2018-01-20T15:04:53+00:00" + }, + { + "name": "twig/twig", + "version": "v1.44.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "04b15d4c0bb18ddbf82626320ac07f6a73f199c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/04b15d4c0bb18ddbf82626320ac07f6a73f199c9", + "reference": "04b15d4c0bb18ddbf82626320ac07f6a73f199c9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.44-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v1.44.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2020-10-27T19:22:48+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozart/assert/issues", + "source": "https://github.com/webmozart/assert/tree/master" + }, + "time": "2020-07-08T17:02:28+00:00" + }, + { + "name": "zendframework/zend-cache", + "version": "2.8.3", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-cache.git", + "reference": "edde41f1ee5c28e01701a032f434d03751b65df4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/edde41f1ee5c28e01701a032f434d03751b65df4", + "reference": "edde41f1ee5c28e01701a032f434d03751b65df4", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "zendframework/zend-eventmanager": "^2.6.3 || ^3.2", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "provide": { + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpbench/phpbench": "^0.13", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-serializer": "^2.6", + "zendframework/zend-session": "^2.7.4" + }, + "suggest": { + "ext-apc": "APC or compatible extension, to use the APC storage adapter", + "ext-apcu": "APCU >= 5.1.0, to use the APCu storage adapter", + "ext-dba": "DBA, to use the DBA storage adapter", + "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", + "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", + "ext-mongo": "Mongo, to use MongoDb storage adapter", + "ext-mongodb": "MongoDB, to use the ExtMongoDb storage adapter", + "ext-redis": "Redis, to use Redis storage adapter", + "ext-wincache": "WinCache, to use the WinCache storage adapter", + "ext-xcache": "XCache, to use the XCache storage adapter", + "mongodb/mongodb": "Required for use with the ext-mongodb adapter", + "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", + "zendframework/zend-serializer": "Zend\\Serializer component", + "zendframework/zend-session": "Zend\\Session component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8.x-dev", + "dev-develop": "2.9.x-dev" + }, + "zf": { + "component": "Zend\\Cache", + "config-provider": "Zend\\Cache\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "autoload/patternPluginManagerPolyfill.php" + ], + "psr-4": { + "Zend\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Caching implementation with a variety of storage options, as well as codified caching strategies for callbacks, classes, and output", + "keywords": [ + "ZendFramework", + "cache", + "psr-16", + "psr-6", + "zf" + ], + "support": { + "chat": "https://zendframework-slack.herokuapp.com", + "docs": "https://docs.zendframework.com/zend-cache/", + "forum": "https://discourse.zendframework.com/c/questions/components", + "issues": "https://github.com/zendframework/zend-cache/issues", + "rss": "https://github.com/zendframework/zend-cache/releases.atom", + "source": "https://github.com/zendframework/zend-cache" + }, + "abandoned": "laminas/laminas-cache", + "time": "2019-08-28T21:34:32+00:00" + }, + { + "name": "zendframework/zend-config", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-config.git", + "reference": "2920e877a9f6dca9fa8f6bd3b1ffc2e19bb1e30d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-config/zipball/2920e877a9f6dca9fa8f6bd3b1ffc2e19bb1e30d", + "reference": "2920e877a9f6dca9fa8f6bd3b1ffc2e19bb1e30d", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "require-dev": { + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", + "zendframework/zend-filter": "^2.6", + "zendframework/zend-i18n": "^2.5", + "zendframework/zend-json": "^2.6.1", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" + }, + "suggest": { + "zendframework/zend-filter": "Zend\\Filter component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-json": "Zend\\Json to use the Json reader or writer classes", + "zendframework/zend-servicemanager": "Zend\\ServiceManager for use with the Config Factory to retrieve reader and writer instances" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev", + "dev-develop": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Config\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides a nested object property based user interface for accessing this configuration data within application code", + "homepage": "https://github.com/zendframework/zend-config", + "keywords": [ + "config", + "zf2" + ], + "support": { + "issues": "https://github.com/zendframework/zend-config/issues", + "source": "https://github.com/zendframework/zend-config/tree/release-2.6.0" + }, + "abandoned": "laminas/laminas-config", + "time": "2016-02-04T23:01:10+00:00" + }, + { + "name": "zendframework/zend-eventmanager", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-eventmanager.git", + "reference": "a5e2583a211f73604691586b8406ff7296a946dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/a5e2583a211f73604691586b8406ff7296a946dd", + "reference": "a5e2583a211f73604691586b8406ff7296a946dd", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "athletic/athletic": "^0.1", + "container-interop/container-interop": "^1.1.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0" + }, + "suggest": { + "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev", + "dev-develop": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\EventManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Trigger and listen to events within a PHP application", + "homepage": "https://github.com/zendframework/zend-eventmanager", + "keywords": [ + "event", + "eventmanager", + "events", + "zf2" + ], + "support": { + "issues": "https://github.com/zendframework/zend-eventmanager/issues", + "source": "https://github.com/zendframework/zend-eventmanager/tree/master" + }, + "abandoned": "laminas/laminas-eventmanager", + "time": "2018-04-25T15:33:34+00:00" + }, + { + "name": "zendframework/zend-filter", + "version": "2.9.2", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-filter.git", + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/d78f2cdde1c31975e18b2a0753381ed7b61118ef", + "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "conflict": { + "zendframework/zend-validator": "<2.10.1" + }, + "require-dev": { + "pear/archive_tar": "^1.4.3", + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-factory": "^1.0", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-crypt": "^3.2.1", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", + "zendframework/zend-uri": "^2.6" + }, + "suggest": { + "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters", + "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", + "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality", + "zendframework/zend-uri": "Zend\\Uri component, for the UriNormalize filter" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" + }, + "zf": { + "component": "Zend\\Filter", + "config-provider": "Zend\\Filter\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\Filter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Programmatically filter and normalize data and files", + "keywords": [ + "ZendFramework", + "filter", + "zf" + ], + "support": { + "chat": "https://zendframework-slack.herokuapp.com", + "docs": "https://docs.zendframework.com/zend-filter/", + "forum": "https://discourse.zendframework.com/c/questions/components", + "issues": "https://github.com/zendframework/zend-filter/issues", + "rss": "https://github.com/zendframework/zend-filter/releases.atom", + "source": "https://github.com/zendframework/zend-filter" + }, + "abandoned": "laminas/laminas-filter", + "time": "2019-08-19T07:08:04+00:00" + }, + { + "name": "zendframework/zend-hydrator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-hydrator.git", + "reference": "22652e1661a5a10b3f564cf7824a2206cf5a4a65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/22652e1661a5a10b3f564cf7824a2206cf5a4a65", + "reference": "22652e1661a5a10b3f564cf7824a2206cf5a4a65", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "^2.0@dev", + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-filter": "^2.6", + "zendframework/zend-inputfilter": "^2.6", + "zendframework/zend-serializer": "^2.6.1", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" + }, + "suggest": { + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0, to support aggregate hydrator usage", + "zendframework/zend-filter": "^2.6, to support naming strategy hydrator usage", + "zendframework/zend-serializer": "^2.6.1, to use the SerializableStrategy", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3, to support hydrator plugin manager usage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-release-1.0": "1.0-dev", + "dev-release-1.1": "1.1-dev", + "dev-master": "2.0-dev", + "dev-develop": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Hydrator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-hydrator", + "keywords": [ + "hydrator", + "zf2" + ], + "support": { + "issues": "https://github.com/zendframework/zend-hydrator/issues", + "source": "https://github.com/zendframework/zend-hydrator/tree/release-1.1" + }, + "abandoned": "laminas/laminas-hydrator", + "time": "2016-02-18T22:38:26+00:00" + }, + { + "name": "zendframework/zend-i18n", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-i18n.git", + "reference": "84038e6a1838b611dcc491b1c40321fa4c3a123c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/84038e6a1838b611dcc491b1c40321fa4c3a123c", + "reference": "84038e6a1838b611dcc491b1c40321fa4c3a123c", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "php": "^5.6 || ^7.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", + "zendframework/zend-cache": "^2.6.1", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-config": "^2.6", + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-filter": "^2.6.1", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", + "zendframework/zend-validator": "^2.6", + "zendframework/zend-view": "^2.6.3" + }, + "suggest": { + "zendframework/zend-cache": "Zend\\Cache component", + "zendframework/zend-config": "Zend\\Config component", + "zendframework/zend-eventmanager": "You should install this package to use the events in the translator", + "zendframework/zend-filter": "You should install this package to use the provided filters", + "zendframework/zend-i18n-resources": "Translation resources", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component", + "zendframework/zend-validator": "You should install this package to use the provided validators", + "zendframework/zend-view": "You should install this package to use the provided view helpers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" + }, + "zf": { + "component": "Zend\\I18n", + "config-provider": "Zend\\I18n\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\I18n\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Provide translations for your application, and filter and validate internationalized values", + "keywords": [ + "ZendFramework", + "i18n", + "zf" + ], + "support": { + "chat": "https://zendframework-slack.herokuapp.com", + "docs": "https://docs.zendframework.com/zend-i18n/", + "forum": "https://discourse.zendframework.com/c/questions/components", + "issues": "https://github.com/zendframework/zend-i18n/issues", + "rss": "https://github.com/zendframework/zend-i18n/releases.atom", + "source": "https://github.com/zendframework/zend-i18n" + }, + "abandoned": "laminas/laminas-i18n", + "time": "2019-12-12T14:08:22+00:00" + }, + { + "name": "zendframework/zend-json", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-json.git", + "reference": "e9ddb1192d93fe7fff846ac895249c39db75132b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-json/zipball/e9ddb1192d93fe7fff846ac895249c39db75132b", + "reference": "e9ddb1192d93fe7fff846ac895249c39db75132b", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "suggest": { + "zendframework/zend-json-server": "For implementing JSON-RPC servers", + "zendframework/zend-xml2json": "For converting XML documents to JSON" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev", + "dev-develop": "3.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", + "keywords": [ + "ZendFramework", + "json", + "zf" + ], + "support": { + "chat": "https://zendframework-slack.herokuapp.com", + "docs": "https://docs.zendframework.com/zend-json/", + "forum": "https://discourse.zendframework.com/c/questions/components", + "issues": "https://github.com/zendframework/zend-json/issues", + "rss": "https://github.com/zendframework/zend-json/releases.atom", + "source": "https://github.com/zendframework/zend-json" + }, + "abandoned": "laminas/laminas-json", + "time": "2019-10-09T13:56:13+00:00" + }, + { + "name": "zendframework/zend-serializer", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-serializer.git", + "reference": "6fb7ae016cfdf0cfcdfa2b989e6a65f351170e21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/6fb7ae016cfdf0cfcdfa2b989e6a65f351170e21", + "reference": "6fb7ae016cfdf0cfcdfa2b989e6a65f351170e21", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "zendframework/zend-json": "^2.5 || ^3.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-math": "^2.6 || ^3.0", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" + }, + "suggest": { + "zendframework/zend-math": "(^2.6 || ^3.0) To support Python Pickle serialization", + "zendframework/zend-servicemanager": "(^2.7.5 || ^3.0.3) To support plugin manager support" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" + }, + "zf": { + "component": "Zend\\Serializer", + "config-provider": "Zend\\Serializer\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\Serializer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Serialize and deserialize PHP structures to a variety of representations", + "keywords": [ + "ZendFramework", + "serializer", + "zf" + ], + "support": { + "chat": "https://zendframework-slack.herokuapp.com", + "docs": "https://docs.zendframework.com/zend-serializer/", + "forum": "https://discourse.zendframework.com/c/questions/components", + "issues": "https://github.com/zendframework/zend-serializer/issues", + "rss": "https://github.com/zendframework/zend-serializer/releases.atom", + "source": "https://github.com/zendframework/zend-serializer" + }, + "abandoned": "laminas/laminas-serializer", + "time": "2019-10-19T08:06:30+00:00" + }, + { + "name": "zendframework/zend-servicemanager", + "version": "2.7.11", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-servicemanager.git", + "reference": "99ec9ed5d0f15aed9876433c74c2709eb933d4c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/99ec9ed5d0f15aed9876433c74c2709eb933d4c7", + "reference": "99ec9ed5d0f15aed9876433c74c2709eb933d4c7", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "~1.0", + "php": "^5.5 || ^7.0" + }, + "require-dev": { + "athletic/athletic": "dev-master", + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", + "zendframework/zend-di": "~2.5", + "zendframework/zend-mvc": "~2.5" + }, + "suggest": { + "ocramius/proxy-manager": "ProxyManager 0.5.* to handle lazy initialization of services", + "zendframework/zend-di": "Zend\\Di component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev", + "dev-develop": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-servicemanager", + "keywords": [ + "servicemanager", + "zf2" + ], + "support": { + "issues": "https://github.com/zendframework/zend-servicemanager/issues", + "source": "https://github.com/zendframework/zend-servicemanager/tree/release-2.7.11" + }, + "abandoned": "laminas/laminas-servicemanager", + "time": "2018-06-22T14:49:54+00:00" + }, + { + "name": "zendframework/zend-stdlib", + "version": "2.7.7", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-stdlib.git", + "reference": "0e44eb46788f65e09e077eb7f44d2659143bcc1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/0e44eb46788f65e09e077eb7f44d2659143bcc1f", + "reference": "0e44eb46788f65e09e077eb7f44d2659143bcc1f", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "zendframework/zend-hydrator": "~1.1" + }, + "require-dev": { + "athletic/athletic": "~0.1", + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", + "zendframework/zend-config": "~2.5", + "zendframework/zend-eventmanager": "~2.5", + "zendframework/zend-filter": "~2.5", + "zendframework/zend-inputfilter": "~2.5", + "zendframework/zend-serializer": "~2.5", + "zendframework/zend-servicemanager": "~2.5" + }, + "suggest": { + "zendframework/zend-eventmanager": "To support aggregate hydrator usage", + "zendframework/zend-filter": "To support naming strategy hydrator usage", + "zendframework/zend-serializer": "Zend\\Serializer component", + "zendframework/zend-servicemanager": "To support hydrator plugin manager usage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-release-2.7": "2.7-dev", + "dev-master": "3.0-dev", + "dev-develop": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-stdlib", + "keywords": [ + "stdlib", + "zf2" + ], + "support": { + "issues": "https://github.com/zendframework/zend-stdlib/issues", + "source": "https://github.com/zendframework/zend-stdlib/tree/release-2.7" + }, + "abandoned": "laminas/laminas-stdlib", + "time": "2016-04-12T21:17:31+00:00" + }, + { + "name": "zetacomponents/base", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/zetacomponents/Base.git", + "reference": "489e20235989ddc97fdd793af31ac803972454f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zetacomponents/Base/zipball/489e20235989ddc97fdd793af31ac803972454f1", + "reference": "489e20235989ddc97fdd793af31ac803972454f1", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "~5.7", + "zetacomponents/unit-test": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sergey Alexeev" + }, + { + "name": "Sebastian Bergmann" + }, + { + "name": "Jan Borsodi" + }, + { + "name": "Raymond Bosman" + }, + { + "name": "Frederik Holljen" + }, + { + "name": "Kore Nordmann" + }, + { + "name": "Derick Rethans" + }, + { + "name": "Vadym Savchuk" + }, + { + "name": "Tobias Schlitt" + }, + { + "name": "Alexandru Stanoi" + } + ], + "description": "The Base package provides the basic infrastructure that all packages rely on. Therefore every component relies on this package.", + "homepage": "https://github.com/zetacomponents", + "support": { + "issues": "https://github.com/zetacomponents/Base/issues", + "source": "https://github.com/zetacomponents/Base/tree/1.9.1" + }, + "time": "2017-11-28T11:30:00+00:00" + }, + { + "name": "zetacomponents/document", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/zetacomponents/Document.git", + "reference": "688abfde573cf3fe0730f82538fbd7aa9fc95bc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zetacomponents/Document/zipball/688abfde573cf3fe0730f82538fbd7aa9fc95bc8", + "reference": "688abfde573cf3fe0730f82538fbd7aa9fc95bc8", + "shasum": "" + }, + "require": { + "zetacomponents/base": "*" + }, + "require-dev": { + "zetacomponents/unit-test": "dev-master" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sebastian Bergmann" + }, + { + "name": "Kore Nordmann" + }, + { + "name": "Derick Rethans" + }, + { + "name": "Tobias Schlitt" + }, + { + "name": "Alexandru Stanoi" + } + ], + "description": "The Document components provides a general conversion framework for different semantic document markup languages like XHTML, Docbook, RST and similar.", + "homepage": "https://github.com/zetacomponents", + "support": { + "issues": "https://github.com/zetacomponents/Document/issues", + "source": "https://github.com/zetacomponents/Document/tree/1.3.1" + }, + "time": "2013-12-19T11:40:00+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "phpmd/phpmd": 0 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/mod/bigbluebuttonbn/config-dist.php b/mod/bigbluebuttonbn/config-dist.php new file mode 100644 index 0000000..ac9c480 --- /dev/null +++ b/mod/bigbluebuttonbn/config-dist.php @@ -0,0 +1,525 @@ +<?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/>. + +/** + * Configuration file for bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/* + * This file should be renamed to "config.php" in the plugin directory + * + * It is intended to be used for setting configuration by default and + * also for enable/diable configuration options in the admin setting UI + * for those multitenancy deployments where the admin account is given + * to the tenant owner and some shared information like the + * bigbluebutton_server_url and bigbluebutton_shared_secret must been + * kept private. And also when some of the features are going to be + * disabled for all the tenants in that server + **/ + +/* + * Any parameter included in this fill will not be shown in the admin UI + * If there was a previous configuration, the parameters here included + * will override the parameters already configured (if they were + * configured already) +** ------------------------------------------------------------------- **/ + +/* + * 1. GENERAL CONFIGURATION + ** ------------------------------------------------------------------ ** + **/ + +/* + * 1.1. BIGBLUEBUTTON SERVER CONFIGURATION + * + * First, you need to configure the credentials for accessing the + * bigbluebutton server. + * The URL of your BigBlueButton server must end with /bigbluebutton/. + * This default URL is for a BigBlueButton server provided by Blindside + * Networks that you can use for testing. + **/ + +$CFG->bigbluebuttonbn['server_url'] = 'http://test-install.blindsidenetworks.com/bigbluebutton/'; +$CFG->bigbluebuttonbn['shared_secret'] = '8cd8ef52e8e101574e400365b55e11a6'; + +/* + * 1.2. CONFIGURATION FOR "RECORDING" FEATURE + * + * Same as for the General Configuration, you need first to set the + * parameter values. + * As these are checkboxes in the moodle admin ui, the expected values + * are 1=checked, 0=unchecked. + **/ + +/* When the value is set to 0 (unchecked) the all the features for recordings + * are ignored. Recording features are enabled by default. + * $CFG->bigbluebuttonbn['recordings_enabled'] = 1; + */ + +/* When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have the recording capability enabled by default. + * $CFG->bigbluebuttonbn['recording_default'] = 1; + */ + +/* When the value is set to 1 (checked) the recording capability can be + * enabled/disabled by the user creating or editing the room or activity. + * $CFG->bigbluebuttonbn['recording_editable'] = 0; + */ + +/* When the value is set to 1 (checked) the list of recordings in both + * bigbluebuttonbn and recordingbn are generated using icons. + * $CFG->bigbluebuttonbn['recording_icons_enabled'] = 1; + */ + +/* + * 1.3. CONFIGURATION FOR "IMPORT RECORDINGS" FEATURE + * + * The "Import recordings" feature should only be used by Administrators + * or Teachers (or anyone with edition capabilities in the + * course). When this feature is enabled and the meeting can be recorded, + * a button will be shown in the intermediate page that will allow importing + * recordings from a different activity even from a different course. + **/ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have the 'import recordings' capability enabled. + * $CFG->bigbluebuttonbn['importrecordings_enabled'] = 0; + */ + +/* + * When the value is set to 1 (checked) the import recordings capability + * can import recordings from deleted activities. + * $CFG->bigbluebuttonbn['importrecordings_from_deleted_enabled'] = 0; + */ + +/* + * 1.4. CONFIGURATION FOR "WAIT FOR MODERATOR" FEATURE + * + * This feature makes the rooms or activity work as a traditional classroom + * cloed until the moderator (teacher) comes to unlock the room. The students + * or other viewers must wait until a moderators join to have the + * 'Join session' button enabled + **/ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have the 'wait for moderator' capability enabled by + * default. + * $CFG->bigbluebuttonbn['waitformoderator_default'] = 0; + */ + +/* + * When the value is set to 1 (checked) the 'wait for moderator' + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['waitformoderator_editable'] = 1; + */ + +/* + * When the 'wait for moderator' capability is enabled, the ping interval + * is used for pooling the status of the server. Its value is expresed + * in seconds. The default values is 15 secs. + * $CFG->bigbluebuttonbn['waitformoderator_ping_interval'] = 15; + */ + +/* + * When the 'wait for moderator' capability is enabled, the ping interval + * is used for pooling the status of the server. But for reducing the + * load to the BigBluebutton server, the information retrieved from it is + * cached. The value is expresed in seconds and is also used for other + * information gathering. The default value is 60 secs. + * $CFG->bigbluebuttonbn['waitformoderator_cache_ttl'] = 60; + */ + +/* + * 1.5. CONFIGURATION FOR "STATIC VOICE BRIDGE" FEATURE + * + **/ +/* + * A conference voice bridge number can be permanently assigned to a room + * or activity. + * $CFG->bigbluebuttonbn['voicebridge_editable'] = 0; + */ + +/* + * 1.6. CONFIGURATION FOR "PRE-UPLOAD PRESENTATION" FEATURE + * + **/ +/* + * Since version 0.8, BigBluebutton has an implementation for allowing + * preuploading presentation. When this feature is enabled, users creating or + * editing a room or activity can upload a PDF or Office document to the + * Moodle file repository and vinculate it to the BigBlueButtonBN room or + * activity in one step. This file will be pulled by the BigBluebutton server + * when the meeting session is accessed for the first time. + * $CFG->bigbluebuttonbn['preuploadpresentation_enabled'] = 1; + */ + +/* + * 1.7. CONFIGURATION FOR "USER LIMIT" FEATURE + * + * It is possible to establish a limit of users per session. This limit can be + * applied to each room or activity, or globally. + **/ + +/* + * The number of users allowed in a session by default when a new room or + * conference is added. If the number is set to 0, no limit is established. + * $CFG->bigbluebuttonbn['userlimit_default'] = 0; + */ + +/* + * When the value is set to 1 (checked) the 'wait for moderator' + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['userlimit_editable'] = 0; + */ + +/* + * 1.8. CONFIGURATION FOR "PERMISSIONS" FEATURE + * + * Defines a rule applied by default to all the new rooms or activities created + * for defining the users who will have access to the meeting session as Moderators. + * By default only the owner is assigned. + **/ + +/* + * The values for this parameter can be '0' (which identifies the owner) and/or any of the role IDs defined in + * Moodle (including the custom parameters). The value used will be the key for the role. + * [owner=0|manager=1|coursecreator=2|editingteacher=3|teacher=4|student=5|guest=6|user=7|frontpage=8|ANY_CUSTOM_ROLE=xx] + * $CFG->bigbluebuttonbn['participant_moderator_default'] = '0'; + */ + +/* + * 1.9. CONFIGURATION FOR "NOTIFICATION SENDING" FEATURE + * + **/ +/* + * When the value is set to 1 (checked) the 'notification sending' + * capability can be used by the user creating or editing the room or + * activity. + * $CFG->bigbluebuttonbn['sendnotifications_enabled'] = 0; + */ + +/* + * 1.10. GENERAL CONFIGURATION FOR RECORDINGS UI + * + **/ +/* + * When the value is set to 1 (checked) the bigbluebuttonbn resources + * will show the recodings in an html table by default. + * $CFG->bigbluebuttonbn['recordings_html_default'] = 0; + */ + +/* + * When the value is set to 1 (checked) the 'html ui' capability can be + * enabled/disabled by the user creating or editing the resource. + * $CFG->bigbluebuttonbn['recordings_html_editable'] = 0; + */ + +/* + * When the value is set to 1 (checked) the bigbluebuttonbn resources + * will show the recodings belonging to deleted activities as part of the list. + * $CFG->bigbluebuttonbn['recordings_deleted_default'] = 1; + */ + +/* + * When the value is set to 1 (checked) the 'include recordings from deleted activities' + * capability can be enabled/disabled by the user creating or editing the resource. + * $CFG->bigbluebuttonbn['recordings_deleted_editable'] = 0; + */ + +/* + * When the value is set to 1 (checked) the bigbluebuttonbn resources for recordings + * will show only the imported links as part of the list. + * $CFG->bigbluebuttonbn['recordings_imported_default'] = 0; + */ + +/* + * When the value is set to 1 (checked) the 'show only imported links' + * capability can be enabled/disabled by the user creating or editing the resource for recordings. + * $CFG->bigbluebuttonbn['recordings_imported_editable'] = 1; + */ + +/* + * When the value is set to 1 (checked) the bigbluebuttonbn resources + * will show the recodings with thumbnails. + * $CFG->bigbluebuttonbn['recordings_preview_default'] = 1; + */ + +/* + * When the value is set to 1 (checked) the 'preview ui' capability can be + * enabled/disabled by the user creating or editing the resource. + * $CFG->bigbluebuttonbn['recordings_preview_editable'] = 0; + */ + + /* When the value is set to 1 (checked) the playback URLs will be validated + * before the user access it. + * $CFG->bigbluebuttonbn['recordings_validate_url'] = 1; + */ + +/* + * 1.11. GENERAL CONFIGURATION FOR CLIENT TYPE SELECTION + * + **/ + +/* + * When the value is set to 1 (checked) the 'clienttype' capability is enabled, + * meaning that the administrator may be able to choose the default web client type + * and wheter it can be editable in each room through the plugin configuration + * $CFG->bigbluebuttonbn['clienttype_enabled'] = 0; + */ + +/* + * The WebClient selected by default is Flash (value = 0) + * [flash=0|html5=1] + * $CFG->bigbluebuttonbn['clienttype_default'] = 0; + */ + +/* + * When the value is set to 1 (checked) the WebClient can be chosen by + * the user creating or editing the resource. + * $CFG->bigbluebuttonbn['clienttype_editable'] = 0; + */ + +/* + * 1.12. CONFIGURATION FOR "MUTE ON START" FEATURE + * + * This feature makes the rooms muted on start. When the users joins to the session, + * they will be muted. + * + **/ + +/* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have the 'mute on start' capability enabled by + * default. + * $CFG->bigbluebuttonbn['muteonstart_default'] = 0; + */ + +/* + * When the value is set to 1 (checked) the 'mute on start' + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['muteonstart_editable'] = 0; + */ + +/* + * 1.12. CONFIGURATION FOR LOCKING SETTINGS + * + * These features are locking API options added in BBB v2.2. When the session is created, + * it will be created with these parameters. + **/ + +/* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have user webcams disabled. + * default. + * $CFG->bigbluebuttonbn['disablecam_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the disable webcam + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['disablecam_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have user microphones disabled. + * default. + * $CFG->bigbluebuttonbn['disablemic_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the disable microphone + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['disablemic_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have private chat disabled. + * default. + * $CFG->bigbluebuttonbn['disableprivatechat_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the private chat + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['disableprivatechat_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have public chat disabled. + * default. + * $CFG->bigbluebuttonbn['disablepublicchat_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the disable public chat + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['disablepublicchat_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have shared notes disabled. + * default. + * $CFG->bigbluebuttonbn['disablenote_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the disable shared notes + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['disablenote_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have the user list hidden. + * default. + * $CFG->bigbluebuttonbn['hideuserlist_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the hidden user list + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['hideuserlist_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will have a locked layout. + * default. + * $CFG->bigbluebuttonbn['lockedlayout_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the locked layout + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['lockedlayout_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the bigbluebuttonbn rooms or + * activities will ignore the locking settings. + * default. + * $CFG->bigbluebuttonbn['lockonjoin_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the ignore locking + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['lockonjoin_editable'] = 0; + */ + + /* + * When the value is set to 1 (checked) the capability to ignore + * locking settings is enabled. + * $CFG->bigbluebuttonbn['lockonjoinconfigurable_default'] = 0; + */ +/* + * When the value is set to 1 (checked) the ignore locking + * capability can be enabled/disabled by the user creating or editing + * the room or activity. + * $CFG->bigbluebuttonbn['lockonjoinconfigurable_editable'] = 0; + */ + +/* + * 2. CONFIGURATION FOR FEATURES OFFERED BY BN SERVERS + ** ------------------------------------------------------------------ ** + **/ + +/* + * 2.1. CONFIGURATION FOR "RECORDING READY" FEATURE + * + **/ +/* + * When the value is set to 1 (checked) the 'notify users when recording ready' + * capability is enabled, meaning that a message will be sent to all enrolled + * users in a course when a recording is ready + * $CFG->bigbluebuttonbn['recordingready_enabled'] = 0; + * $CFG->bigbluebuttonbn['recordingstatus_enabled'] = 0; + */ + +/* + * 2.2. CONFIGURATION FOR "REGISTER MEETING EVENTS" FEATURE + * + **/ +/* + * When the value is set to 1 (checked) the 'register meeting events' + * capability is enabled, meaning that once a recording is processed by BigBlueButton + * a message containing the events from the live session will be sent to Moodle. + * These avents are added to the logging system and used for reports + * + * This setting is required for Activity Completion, but it will work only if the + * BigBlueButton server is enabled to process events though the script post_process_analytics.rb. + * + * By default, the fueature only works if the session is recorded, but in order to make it + * work for every session, the property keepEvents must be st to true in BigBlueButton. + * Edit the file /usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties and set + * keepEvents=true + * + * $CFG->bigbluebuttonbn['meetingevents_enabled'] = 0; + */ + +/* + * 2.3. CONFIGURATION FOR "GENERAL WARNING MESSAGE" FEATURE + * + **/ +/* + * When general_warning_message value is different than "", the string is shown + * as a warning message to privileged users (administrators and Teachers or users allowed to edit). + * $CFG->bigbluebuttonbn['general_warning_message'] = "Would you like to record your BigBlueButton sessions for later viewing? "; + */ + + /* + * The warning box is always shown to administrators, but it is also possible to define other roles + * to whom the it will be shown. The roles are based on the shortnames defined by Moodle: + * 'manager,coursecreator,editingteacher,teacher,student,guest,user,frontpage' + * $CFG->bigbluebuttonbn['general_warning_roles'] = 'editingteacher,teacher'; + */ + + /* + * As the general_warning_message is shown in a box, its type can be defined with general_warning_type + * The default type is 'info' which is normaly rendered in blue when using a bootstrap theme. + * All the modifiers for boxed in bootstrap can be used [info|success|warning|danger]. + * $CFG->bigbluebuttonbn['general_warning_box_type'] = 'info'; + */ + + /* + * Additionally, when general_warning_button_href value is different than "", a button + * can also be shown right after the message. + * $CFG->bigbluebuttonbn['general_warning_button_href'] = "http://blindsidenetworks.com/"; + */ + + /* + * Finally, the text and class for the button can be modified + * $CFG->bigbluebuttonbn['general_warning_button_text'] = "Upgrade your site"; + * $CFG->bigbluebuttonbn['general_warning_button_class'] = "btn btn-primary"; + */ diff --git a/mod/bigbluebuttonbn/db/access.php b/mod/bigbluebuttonbn/db/access.php new file mode 100644 index 0000000..eac6cd7 --- /dev/null +++ b/mod/bigbluebuttonbn/db/access.php @@ -0,0 +1,99 @@ +<?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/>. + +/** + * Capabilities for BigBlueButton. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic@gmail.com) + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + // Ability to add a new bigbluebuttonbn instance. + 'mod/bigbluebuttonbn:addinstance' => array( + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ), + + // Ability to join a meeting. + 'mod/bigbluebuttonbn:join' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'student' => CAP_ALLOW, + 'guest' => CAP_ALLOW, + ), + ), + + // Ability to manage recordings. + 'mod/bigbluebuttonbn:managerecordings' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + ), + ), + + // Ability to access instances, regardless of the type. + 'mod/bigbluebuttonbn:view' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + // Ability to create instances with live meeting capabilities. + 'mod/bigbluebuttonbn:meeting' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ), + ), + + // Ability to create instances with recording capabilities. + 'mod/bigbluebuttonbn:recording' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ), + ), +); diff --git a/mod/bigbluebuttonbn/db/install.xml b/mod/bigbluebuttonbn/db/install.xml new file mode 100644 index 0000000..359845c --- /dev/null +++ b/mod/bigbluebuttonbn/db/install.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="mod/bigbluebuttonbn/db" VERSION="20200505" COMMENT="XMLDB file for Moodle mod/bigbluebuttonbn" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="bigbluebuttonbn" COMMENT="The bigbluebuttonbn table to store information about a meeting activities."> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="type" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="intro" TYPE="text" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="meetingid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="moderatorpass" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="viewerpass" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="wait" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="record" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="recordallfromstart" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="recordhidebutton" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="welcome" TYPE="text" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="voicebridge" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="openingtime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="closingtime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="presentation" TYPE="text" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="participants" TYPE="text" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="userlimit" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="recordings_html" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="recordings_deleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="recordings_imported" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="recordings_preview" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="clienttype" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="muteonstart" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="disablecam" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="disablemic" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="disableprivatechat" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="disablepublicchat" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="disablenote" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="hideuserlist" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="lockedlayout" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="lockonjoin" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="lockonjoinconfigurable" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="completionattendance" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if a certain number of minutes in the meeting are required to mark an activity completed for a user."/> + <FIELD NAME="completionengagementchats" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if chat during the meeting is required to mark an activity completed for a user."/> + <FIELD NAME="completionengagementtalks" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if talking during the meeting is required to mark an activity completed for a user."/> + <FIELD NAME="completionengagementraisehand" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if raising hand during the meeting is required to mark an activity completed for a user."/> + <FIELD NAME="completionengagementpollvotes" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if poll voting during the meeting is required to mark an activity completed for a user."/> + <FIELD NAME="completionengagementemojis" TYPE="int" LENGTH="9" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Nonzero if the use of emojis during the meeting is required to mark an activity completed for a user."/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + <TABLE NAME="bigbluebuttonbn_logs" COMMENT="The bigbluebuttonbn table to store meeting activity events"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="bigbluebuttonbnid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="meetingid" TYPE="char" LENGTH="256" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="log" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="meta" TYPE="text" NOTNULL="false" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/> + <INDEX NAME="log" UNIQUE="false" FIELDS="log"/> + <INDEX NAME="logrow" UNIQUE="false" FIELDS="courseid, bigbluebuttonbnid, userid, log"/> + <INDEX NAME="userlog" UNIQUE="false" FIELDS="userid, log"/> + </INDEXES> + </TABLE> + </TABLES> +</XMLDB> diff --git a/mod/bigbluebuttonbn/db/log.php b/mod/bigbluebuttonbn/db/log.php new file mode 100644 index 0000000..835ce37 --- /dev/null +++ b/mod/bigbluebuttonbn/db/log.php @@ -0,0 +1,43 @@ +<?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/>. + +/** + * Definition of log events. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +global $DB; + +$logs = array( + array('module' => 'bigbluebuttonbn', 'action' => 'add', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'update', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'view', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'view all', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'create', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'end', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'join', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'left', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'publish', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'unpublish', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), + array('module' => 'bigbluebuttonbn', 'action' => 'delete', 'mtable' => 'bigbluebuttonbn', 'field' => 'name'), +); diff --git a/mod/bigbluebuttonbn/db/mobile.php b/mod/bigbluebuttonbn/db/mobile.php new file mode 100644 index 0000000..46df23f --- /dev/null +++ b/mod/bigbluebuttonbn/db/mobile.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Mobile app definition for BigBlueButton. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +$addons = array( + "mod_bigbluebuttonbn" => array( + "handlers" => array( // Different places where the add-on will display content. + 'coursebigbluebuttonbn' => array( // Handler unique name (can be anything). + 'displaydata' => array( + 'title' => 'pluginname', + 'icon' => $CFG->wwwroot . '/mod/bigbluebuttonbn/pix/icon.gif', + 'class' => '', + ), + 'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the add-on). + 'method' => 'mobile_course_view' // Main function in \mod_bigbluebuttonbn\output\mobile. + ) + ), + 'lang' => array( + array('pluginname', 'bigbluebuttonbn'), + array('view_conference_action_join', 'bigbluebuttonbn'), + array('view_message_conference_room_ready', 'bigbluebuttonbn'), + array('view_mobile_message_reload_page_creation_time_meeting', 'bigbluebuttonbn') + ) + ) +); diff --git a/mod/bigbluebuttonbn/db/services.php b/mod/bigbluebuttonbn/db/services.php new file mode 100644 index 0000000..8b718b9 --- /dev/null +++ b/mod/bigbluebuttonbn/db/services.php @@ -0,0 +1,55 @@ +<?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/>. + +/** + * URL external functions and service definitions. + * + * @package mod_bigbluebuttonbn + * @category external + * @copyright 2018 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = array( + 'mod_bigbluebuttonbn_view_bigbluebuttonbn' => array( + 'classname' => 'mod_bigbluebuttonbn_external', + 'methodname' => 'view_bigbluebuttonbn', + 'description' => 'Trigger the course module viewed event and update the module completion status.', + 'type' => 'write', + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), + 'mod_bigbluebuttonbn_get_bigbluebuttonbns_by_courses' => array( + 'classname' => 'mod_bigbluebuttonbn_external', + 'methodname' => 'get_bigbluebuttonbns_by_courses', + 'description' => 'Returns a list of bigbluebuttonbns in a provided list of courses, if no list is provided + all bigbluebuttonbns that the user can view will be returned.', + 'type' => 'read', + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), + 'mod_bigbluebuttonbn_can_join' => array( + 'classname' => 'mod_bigbluebuttonbn_external', + 'methodname' => 'can_join', + 'description' => 'Returns information if the current user can join or not.', + 'type' => 'read', + 'capabilities' => 'mod/bigbluebuttonbn:view', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), +); diff --git a/mod/bigbluebuttonbn/db/uninstall.php b/mod/bigbluebuttonbn/db/uninstall.php new file mode 100644 index 0000000..0034a80 --- /dev/null +++ b/mod/bigbluebuttonbn/db/uninstall.php @@ -0,0 +1,34 @@ +<?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 and administrate BigBlueButton playback recordings. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Custom uninstallation procedure. + */ +function xmldb_bigbluebuttonbn_uninstall() { + return true; +} diff --git a/mod/bigbluebuttonbn/db/upgrade.php b/mod/bigbluebuttonbn/db/upgrade.php new file mode 100644 index 0000000..265d851 --- /dev/null +++ b/mod/bigbluebuttonbn/db/upgrade.php @@ -0,0 +1,381 @@ +<?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/>. + +/** + * Upgrade logic. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(dirname(__FILE__)).'/locallib.php'); + +/** + * Performs data migrations and updates on upgrade. + * + * @param integer $oldversion + * @return boolean + */ +function xmldb_bigbluebuttonbn_upgrade($oldversion = 0) { + global $DB; + $dbman = $DB->get_manager(); + if ($oldversion < 2015080605) { + // Drop field description. + xmldb_bigbluebuttonbn_drop_field($dbman, 'bigbluebuttonbn', 'description'); + // Change welcome, allow null. + $fielddefinition = array('type' => XMLDB_TYPE_TEXT, 'precision' => null, 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => null, 'previous' => 'type'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'welcome', + $fielddefinition); + // Change userid definition in bigbluebuttonbn_log. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '10', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => null, + 'previous' => 'bigbluebuttonbnid'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn_log', 'userid', + $fielddefinition); + // No settings to migrate. + // Update db version tag. + upgrade_mod_savepoint(true, 2015080605, 'bigbluebuttonbn'); + } + if ($oldversion < 2016011305) { + // Define field type to be droped from bigbluebuttonbn. + xmldb_bigbluebuttonbn_drop_field($dbman, 'bigbluebuttonbn', 'type'); + // Rename table bigbluebuttonbn_log to bigbluebuttonbn_logs. + xmldb_bigbluebuttonbn_rename_table($dbman, 'bigbluebuttonbn_log', 'bigbluebuttonbn_logs'); + // Rename field event to log in table bigbluebuttonbn_logs. + xmldb_bigbluebuttonbn_rename_field($dbman, 'bigbluebuttonbn_logs', 'event', 'log'); + // No settings to migrate. + // Update db version tag. + upgrade_mod_savepoint(true, 2016011305, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101000) { + // Drop field newwindow. + xmldb_bigbluebuttonbn_drop_field($dbman, 'bigbluebuttonbn', 'newwindow'); + // Add field type. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '2', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'id'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'type', + $fielddefinition); + // Add field recordings_html. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_html', + $fielddefinition); + // Add field recordings_deleted. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 1, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_deleted', + $fielddefinition); + // Add field recordings_imported. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_imported', + $fielddefinition); + // Drop field newwindow. + xmldb_bigbluebuttonbn_drop_field($dbman, 'bigbluebuttonbn', 'tagging'); + // Migrate settings. + unset_config('bigbluebuttonbn_recordingtagging_default', ''); + unset_config('bigbluebuttonbn_recordingtagging_editable', ''); + $cfgvalue = get_config('', 'bigbluebuttonbn_importrecordings_from_deleted_activities_enabled'); + set_config('bigbluebuttonbn_importrecordings_from_deleted_enabled', $cfgvalue, ''); + unset_config('bigbluebuttonbn_importrecordings_from_deleted_activities_enabled', ''); + $cfgvalue = get_config('', 'bigbluebuttonbn_moderator_default'); + set_config('bigbluebuttonbn_participant_moderator_default', $cfgvalue, ''); + unset_config('bigbluebuttonbn_moderator_default', ''); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101000, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101009) { + // Add field recordings_preview. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_preview', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101009, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101010) { + // Fix for CONTRIB-7221. + if ($oldversion == 2017101003) { + // A bug intorduced in 2017101003 causes new instances to be created without BBB passwords. + // A workaround was put in place in version 2017101004 that was relabeled to 2017101005. + // However, as the code was relocated to upgrade.php in version 2017101010, a new issue came up. + // There is now a timeout error when the plugin is upgraded in large Moodle sites. + // The script should only be considered when migrating from this version. + $sql = "SELECT * FROM {bigbluebuttonbn} "; + $sql .= "WHERE moderatorpass = ? OR viewerpass = ?"; + $instances = $DB->get_records_sql($sql, array('', '')); + foreach ($instances as $instance) { + $instance->moderatorpass = bigbluebuttonbn_random_password(12); + $instance->viewerpass = bigbluebuttonbn_random_password(12, $instance->moderatorpass); + // Store passwords in the database. + $DB->update_record('bigbluebuttonbn', $instance); + } + } + // Update db version tag. + upgrade_mod_savepoint(true, 2017101010, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101012) { + // Update field type (Fix for CONTRIB-7302). + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '2', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'id'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'type', + $fielddefinition); + // Update field meetingid (Fix for CONTRIB-7302). + $fielddefinition = array('type' => XMLDB_TYPE_CHAR, 'precision' => '255', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => null, 'previous' => 'introformat'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'meetingid', + $fielddefinition); + // Update field recordings_imported (Fix for CONTRIB-7302). + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_imported', + $fielddefinition); + // Add field recordings_preview.(Fix for CONTRIB-7302). + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordings_preview', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101012, 'bigbluebuttonbn'); + } + if ($oldversion < 2017101015) { + // Add field for client technology choice. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'clienttype', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2017101015, 'bigbluebuttonbn'); + } + if ($oldversion < 2019042000) { + // Add field for Mute on start feature. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'muteonstart', + $fielddefinition); + // Add field for record all from start. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordallfromstart', + $fielddefinition); + // Add field for record hide button. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'recordhidebutton', + $fielddefinition); + // Update db version tag. + upgrade_mod_savepoint(true, 2019042000, 'bigbluebuttonbn'); + } + if ($oldversion < 2019101001) { + // Add field for Completion with attendance. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionattendance', + $fielddefinition); + // Add field for Completion with engagement through chats. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementchats', + $fielddefinition); + // Add field for Completion with engagement through talks. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementtalks', + $fielddefinition); + // Add field for Completion with engagement through raisehand. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementraisehand', + $fielddefinition); + // Add field for Completion with engagement through pollvotes. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementpollvotes', + $fielddefinition); + // Add field for Completion with engagement through emojis. + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '9', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => null); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'completionengagementemojis', + $fielddefinition); + // Add index to bigbluebuttonbn_logs (Fix for CONTRIB-8157). + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'courseid', + ['courseid']); + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'log', + ['log']); + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'logrow', + ['courseid', 'bigbluebuttonbnid', 'userid', 'log']); + // Update db version tag. + upgrade_mod_savepoint(true, 2019101001, 'bigbluebuttonbn'); + } + + if ($oldversion < 2019101002) { + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'muteonstart'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablecam', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablecam'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablemic', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablemic'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disableprivatechat', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disableprivatechat'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablepublicchat', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablepublicchat'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'disablenote', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'disablenote'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'hideuserlist', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'hideuserlist'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'lockedlayout', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'lockedlayout'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'lockonjoin', + $fielddefinition); + + $fielddefinition = array('type' => XMLDB_TYPE_INTEGER, 'precision' => '1', 'unsigned' => null, + 'notnull' => XMLDB_NOTNULL, 'sequence' => null, 'default' => 0, 'previous' => 'lockonjoin'); + xmldb_bigbluebuttonbn_add_change_field($dbman, 'bigbluebuttonbn', 'lockonjoinconfigurable', + $fielddefinition); + + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2019101002, 'bigbluebuttonbn'); + } + + if ($oldversion < 2019101004) { + // Add index to bigbluebuttonbn_logs (Leftover for CONTRIB-8157). + xmldb_bigbluebuttonbn_index_table($dbman, 'bigbluebuttonbn_logs', 'userlog', + ['userid', 'log']); + // Bigbluebuttonbn savepoint reached. + upgrade_mod_savepoint(true, 2019101004, 'bigbluebuttonbn'); + } + + return true; +} + +/** + * Generic helper function for adding or changing a field in a table. + * + * @param object $dbman + * @param string $tablename + * @param string $fieldname + * @param array $fielddefinition + */ +function xmldb_bigbluebuttonbn_add_change_field($dbman, $tablename, $fieldname, $fielddefinition) { + $table = new xmldb_table($tablename); + $field = new xmldb_field($fieldname); + $field->set_attributes($fielddefinition['type'], $fielddefinition['precision'], $fielddefinition['unsigned'], + $fielddefinition['notnull'], $fielddefinition['sequence'], $fielddefinition['default'], + $fielddefinition['previous']); + if ($dbman->field_exists($table, $field)) { + $dbman->change_field_type($table, $field, true, true); + $dbman->change_field_precision($table, $field, true, true); + $dbman->change_field_notnull($table, $field, true, true); + $dbman->change_field_default($table, $field, true, true); + return; + } + $dbman->add_field($table, $field, true, true); +} + +/** + * Generic helper function for dropping a field from a table. + * + * @param object $dbman + * @param string $tablename + * @param string $fieldname + */ +function xmldb_bigbluebuttonbn_drop_field($dbman, $tablename, $fieldname) { + $table = new xmldb_table($tablename); + $field = new xmldb_field($fieldname); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field, true, true); + } +} + +/** + * Generic helper function for renaming a field in a table. + * + * @param object $dbman + * @param string $tablename + * @param string $fieldnameold + * @param string $fieldnamenew + */ +function xmldb_bigbluebuttonbn_rename_field($dbman, $tablename, $fieldnameold, $fieldnamenew) { + $table = new xmldb_table($tablename); + $field = new xmldb_field($fieldnameold); + if ($dbman->field_exists($table, $field)) { + $dbman->rename_field($table, $field, $fieldnamenew, true, true); + } +} + +/** + * Generic helper function for renaming a table. + * + * @param object $dbman + * @param string $tablenameold + * @param string $tablenamenew + */ +function xmldb_bigbluebuttonbn_rename_table($dbman, $tablenameold, $tablenamenew) { + $table = new xmldb_table($tablenameold); + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, $tablenamenew, true, true); + } +} + +/** + * Generic helper function for adding index to a table. + * + * @param object $dbman + * @param string $tablename + * @param string $indexname + * @param array $indexfields + * @param string $indextype + */ +function xmldb_bigbluebuttonbn_index_table($dbman, $tablename, $indexname, $indexfields, + $indextype = XMLDB_INDEX_NOTUNIQUE) { + $table = new xmldb_table($tablename); + if (!$dbman->table_exists($table)) { + return; + } + $index = new xmldb_index($indexname, $indextype, $indexfields); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $dbman->add_index($table, $index, true, true); +} diff --git a/mod/bigbluebuttonbn/fonts/bigbluebutton-font.eot b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.eot new file mode 100644 index 0000000000000000000000000000000000000000..f88c77847da7a407aff0eec01620b66b9de61dc1 GIT binary patch literal 1746 zcmcb_#=yYA#=yYHz{CIoObo1?AQ~hxff2;=2>_7{3=9HLngJ}#z`$_tt<4FT7=l(| zNMgujNM}f5$YCgDNM%T3C}k*NC}GHF$YaoDNCVR)3=9k$NNh2NFoslyB8FmyOt8EH zgCT<+g8>5r1F@ztfE>)gz{}9Uz|0Wf<`&{xbI|+<0|V<D1_lNhcNbST215om1_stM z3=9kk{=xc22I@YS7#J8M7#JABl5-Oa4mvBVGB7Y6U|?YMN>3~<`2U}QnSp`z2*|1F zIhAR#Hg@kA7?>p(7#IpNQWH}I7he6yz`$^afq}s+BO^7DMVyJ1fq_v3<mimtk_t8k z1|9|m#top5%gIkpWUv5*5i<`10|QrXVnqRi2ZJ^P10zUOAulmEb%(U$GzJFd4h9A$ zmV*4^k`OP)Ees5-Jq!#C7T_RfWMDX9^Y&gmzs*+$Zb)q1a6DB7G8IhoFz_&bVf+Ge zFEld2VFDJ>gQ#cx0yYVx1%_F`A}kCVP(2`pj9@Vbs1imd2F3{ttPC8CK8$G$3=Hm2 z`YVGZLkj}~BE^8z!xISu14BTXYYKzIgP9LzGCuhKfI*M(%YTsTpvebh5l9D!W&(#5 zD7?W~25dhAg9`&Qm?gsiavuW&8w1F-ObiSUX8!*V(hH&Gv%soA0t}4cv<zl4FhY%n z@Sr5rb|`PSQ=rtv!~jbf><sD*3_>DeMrI~zO2Q&++KftSwv0yNVzP`P){I8Bj3(NQ zYU*mH#zx{|qCEYl`uk7a(UFUk*O3$u6pR!Q5K<BoPm~oGV@w4}_5V90r=ufxPh4DK zf}o&_Ad>*Ugs6lp11K)1L1ToGL5M*O%4T9<VQ_}BnHlsMRzTS-3_=Y5plntKZbnTg zn~gz;F&dNtSU4FN8B`b;pll`v4hBmoo0%bw!2`->VNhYX0cEo?h%pL6*=!6djHbjD zN=cdNNjasdNu?zv`FXl&`FSM_h*Ao|bpsbrB@7_Ch#{RJl|g~Q2wYexFjz4t5M>WU zr4fSxgD!(1gAs!+1Bh)1Dk0qR^Ge+Ei_%jSjPwi?tQ0U^q+q3BWT0zkq-$hgXu&{~ zeGHhUFyJb2!%~ZiGxPHl4D}2sFdUTg5P1tT&#{2ZYbHh}UKFQ+MF0P1U;?KAJ_ZH` kP-zP)Vi*~iL9xjI(g!gcY~~r;Mdv}*fXhDy1_lrf02^iNlK=n! literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg new file mode 100644 index 0000000..846eedb --- /dev/null +++ b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata>Generated by Fontastic.me</metadata> +<defs> +<font id="bigbluebutton-font" horiz-adv-x="512"> +<font-face font-family="bigbluebutton-font" units-per-em="512" ascent="480" descent="-32"/> +<missing-glyph horiz-adv-x="512" /> + +<glyph glyph-name="bigbluebutton" unicode="" d="M12 256c0-135 109-244 244-244 135 0 244 109 244 244 0 135-109 244-244 244-135 0-244-109-244-244m363-74c0-21-8-38-22-52-15-15-32-22-52-22l-89 0c-21 0-38 7-53 22-14 14-22 31-22 52l0 222c16 0 30-7 41-23 11-15 17-34 17-55l0-144c0-12 5-17 17-17l89 0c11 0 16 5 16 17l0 68c0 11-5 17-16 17l-18 0c-22 1-41 7-56 18-15 10-23 24-23 39l97 0c20 0 37-7 52-21 14-15 22-32 22-53z"/> +</font></defs></svg> diff --git a/mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c291be5bac36707ddeb5e20a39f979c0eb176ebb GIT binary patch literal 1544 zcmZQzWME+6WoTevW(aU|3-PTvXnur&f%Odo1A~mai>n)hAp;u&1M3+E1_lNHV0|M4 zb)QQN42%&B3=Cn(xrqe_ot0G?7#I&QFfe+hCl(j{|Ifh8z`%Nhfq{V|J*P4)*2eA~ z0|T=J0|P@rMrvY;;KHjP85kJuFfcHfWn`o#vWPRWGB7ZTFfcHvWaO4qurV<3FfcG~ zU|?Vn$jMJmWUydhU|?Y8VPIh3%1x{&VDMnjW?*0hi7MnJ=BDnDcAUn*z}&&Wz{FCJ zUtAL6<+z1`fwhN$fx!aoH%11A6E<(}#q-;IW#DE48OCtK@l+Lv0j7Bvco@GhegV0c z0V2%+(gzmNg9tEw0f~Tgz%dJ0goQx^st2Ty5iI5aRl~@{z&L?{m4SoNhcS(Tfx#V0 ze`SzlXklPrVqoQjsswoz!DI+Xb4_7zcrf$9OvVTQA28@Ke)$h_9Vng{K>8p`K^8KB z!wMAMU@QZ+pMk-Jff>w_VF0<0fq{(y<XR>Mh6gkM{|D)Y(DGSeRUiQdMmDggA#AA8 zU=ECc+79Cma~hPom>6IwgPlR0fk8+_%*f0{O-WdUO`B0k&6d$fTuhcx#G28_meE9; zQB7UV)YwQ|Oq8epRDb`eJ34Za@;Z_Nf`X9(0zyh+;)$~2VvMOEss4Y5<aBi8?um;l zOb`@w5o8kJmk^bZWdOzHG-!-4G6*rKLD@_UEDX+2HZy}h!wM*yg+YknAC%3?z|E)$ zWwS8|F-C(@01GDrBZCSk^Dr>7a56A4a4=Xx#hDr67(AeC76uiD8&EbYgBYVAl+DJV z!f48n#E{95&XB~A!%)hQ%8<lR%22{k!jR99$DqrQ2Bu3G7?Lv6lX6N^lS)fU^7C}l z^7Bd<5RwqC8&oZbE@DV$NM%r9Fk;YSFknz%uwqak${vVHBL)KoT?Ru2BL-at5Ze$` zLb&DUmAK^>rKc(w=@}?kDPX!t!Ail%K-bVn*T}%of`KUeu=*>EA(f$sp_m~P>>&jP zL$D_p!cvQhGxPHl4D}2sFdUTg5LpW|&#^FovMCcI6EBJfL8AZvGcbWu03QPb1E{nG h6%~vO%%Iq00O^C64L0+P?V|G_Yry3n0|U4`1OS*g(k1`^ literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/fonts/bigbluebutton-font.woff b/mod/bigbluebuttonbn/fonts/bigbluebutton-font.woff new file mode 100644 index 0000000000000000000000000000000000000000..fe53a2606a3cedbff4444d38a8bdafb7f5c254ef GIT binary patch literal 1064 zcmXT-cXRU(3GruOV9{XUW?*1oU@Ku@1ksq-+09LXfq{_&itS7vTz%r^7UIjmz$C!H zz#zrIz#vm|(ENzIi>n(01CtH|1A{aJ1A_vCJ_Cz?u)Yxk1Cs{>1A`X>14Dv=r_bl) z+{6L~2Bs+t3=CEb42)g}ot5M=QWH}c7?>_FFfiydFff=2F1-3VBO^7Dfq@z1AQ=V* z1{D@@CgzOXk_rX}W)Po)fq{X8g@J)5H?g9Cfq_{C<Owil3Sh`f%uQurU<SG64Fdxc z%MNMBX$ASkB@7HK5)2FsAa^S;Ff%Y#%t=m3NK8yfNfGfo<>P-!)7Mu^SM%ig%f6@2 zYbPe8BqgL2{82po#d+G#b{21b{$z9hVsrj(8S&<n4Hy2u%d=>1ul@B!Ue?|!B2_}{ z!)XHpjuQqtVp9?yOh_~lPD)^tJbZk?frOSM_GPURa}FFhaO2#8BPUK=Id1;<x5&l! zm+nh821vR5Py3_G+fpE<qWAsp{>3jUzrABUTbz}C^~{=CA@|NMJbEqGcxPGI<27xS zYq@t@Jlo_u`@$`S_vhm7U%RG|ekAO^S>mog<)RDbU905XER}ZMZ`HeJKVpyEi#6Ms zrFLP0r7rg_%Xu5lzMp5Dd2P*1WA*bd;|?7Bp4{cJJ;R(|oM9JBNo~cP<b;HT6ow|_ zZQbXZUr6^YUzK3O;48^)#=roQOG!w0z?{U%CXkSn(BdR!(9g)g63So#mP<=Ym~-I3 zsRIXJ9G-FPz<~#ROZOXj7;P~yFfiOuWOSgBq3NW+glDr3J@}CDta)Y+|3CYL3(0>z z{pSvtA-K+gkwcQv{|DbXh&>4jX$=XIY;J4<1&p604%UdV@hY5UTOuhrLqbCC%%A`A zOBkF^r#oC_W>l7DZZdXaWDsA#VGq%pl90ggXioda{6{}?_Zbx!8#Wx^Jsy0uU{-r$ z`$z7_;tJE78FhFWoW;3$!R}5;NceN;z<~wFk1#Mfm?<-BB``6Vx-iKzF)%PNF);8j zFa&@Z6?5kHAM|226lmMO<@TZ%N9Amu2e9x*Zf10>7VYxC(ET7$F;?sp+qT{79~uAf zu<NzB$rrcBLwM5v(@*BlWZChI*;%elwEUDYSKO56HXl~TbUcr)JUDe{c-$tV{6{87 zm%3aF4tpjkZlis^@_Fb^f%DTpHANRM-jk9mw=y#`#oj>3)tzl-C*Se9ON_6!t>jN$ zb3D;w?(OA%N&BZSe;n2yS-nzXBa7Fb7jD<;f6bZWSR^xb#q1rAJWqK~&V4mI!};ag zWB+O=vNV^6790$pRPb$oS<-t;uPU?1%=z{Q^}Zc9E8V8`_vg`i29JQKWKdkEIk5Hc MWH2x=^f9mk06%n%9smFU literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/import_view.php b/mod/bigbluebuttonbn/import_view.php new file mode 100644 index 0000000..0e0618b --- /dev/null +++ b/mod/bigbluebuttonbn/import_view.php @@ -0,0 +1,70 @@ +<?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 for importing BigBlueButtonBN recordings. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +use mod_bigbluebuttonbn\plugin; +use mod_bigbluebuttonbn\output\import_view; +use mod_bigbluebuttonbn\output\renderer; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/locallib.php'); + +$bn = required_param('bn', PARAM_INT); +$tc = optional_param('tc', 0, PARAM_INT); + +if (!$bn) { + print_error('view_error_url_missing_parameters', plugin::COMPONENT); +} + +$bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', ['id' => $bn], '*', MUST_EXIST); +$course = $DB->get_record('course', ['id' => $bigbluebuttonbn->course], '*', MUST_EXIST); +$cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $course->id, false, MUST_EXIST); + +require_login($course, true, $cm); + +if (!isset($SESSION) || !isset($SESSION->bigbluebuttonbn_bbbsession)) { + print_error('view_error_invalid_session', plugin::COMPONENT); +} + +if (!(boolean)\mod_bigbluebuttonbn\locallib\config::importrecordings_enabled()) { + print_error('view_message_importrecordings_disabled', plugin::COMPONENT); +} + +// Print the page header. +$PAGE->set_url('/mod/bigbluebuttonbn/import_view.php', ['id' => $cm->id, 'bigbluebuttonbn' => $bigbluebuttonbn->id]); +$PAGE->set_title($bigbluebuttonbn->name); +$PAGE->set_cacheable(false); +$PAGE->set_heading($course->fullname); + +// View widget must be initialized here in order to properly load javascript. +$view = new import_view($course, $bigbluebuttonbn, $tc); + +/** @var renderer $renderer */ +$renderer = $PAGE->get_renderer(plugin::COMPONENT); + +echo $OUTPUT->header(); + +echo $renderer->render($view); + +echo $OUTPUT->footer(); diff --git a/mod/bigbluebuttonbn/index.php b/mod/bigbluebuttonbn/index.php new file mode 100644 index 0000000..3f3b45a --- /dev/null +++ b/mod/bigbluebuttonbn/index.php @@ -0,0 +1,85 @@ +<?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 all BigBlueButton instances in this course. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +use mod_bigbluebuttonbn\plugin; +use mod_bigbluebuttonbn\output\renderer; +use mod_bigbluebuttonbn\output\index; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/locallib.php'); + +$id = required_param('id', PARAM_INT); +$a = optional_param('a', 0, PARAM_INT); +$g = optional_param('g', 0, PARAM_INT); + +$course = $DB->get_record('course', ['id' => $id]); +if (!$course) { + print_error('invalidcourseid'); +} + +require_login($course, true); + +$PAGE->set_url('/mod/bigbluebuttonbn/index.php', ['id' => $id]); +$PAGE->set_title(get_string('modulename', plugin::COMPONENT)); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(false); +$PAGE->set_pagelayout('incourse'); + +$PAGE->navbar->add($PAGE->title, $PAGE->url); + +$action = optional_param('action', '', PARAM_TEXT); +if ($action === 'end') { + // A request to end the meeting. + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', ['id' => $a]); + if (!$bigbluebuttonbn) { + print_error('index_error_bbtn', plugin::COMPONENT, '', $a); + } + $course = $DB->get_record('course', array('id' => $bigbluebuttonbn->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $course->id, false, MUST_EXIST); + // User roles. + $participantlist = bigbluebuttonbn_get_participant_list($bigbluebuttonbn, $PAGE->context); + $moderator = bigbluebuttonbn_is_moderator($PAGE->context, $participantlist); + $administrator = is_siteadmin(); + if ($moderator || $administrator) { + bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['meeting_end'], $bigbluebuttonbn); + echo get_string('index_ending', plugin::COMPONENT); + $meetingid = sprintf('%s-%d-%d', $bigbluebuttonbn->meetingid, $course->id, $bigbluebuttonbn->id); + if ($g != 0) { + $meetingid .= sprintf('[%d]', $g); + } + + bigbluebuttonbn_end_meeting($meetingid, $bigbluebuttonbn->moderatorpass); + redirect($PAGE->url); + } +} + +/** @var renderer $renderer */ +$renderer = $PAGE->get_renderer(plugin::COMPONENT); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('index_heading', plugin::COMPONENT)); +echo $renderer->render(new index($course)); +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php b/mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php new file mode 100644 index 0000000..26bd802 --- /dev/null +++ b/mod/bigbluebuttonbn/lang/en/bigbluebuttonbn.php @@ -0,0 +1,568 @@ +<?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/>. + +/** + * Language File. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ +defined('MOODLE_INTERNAL') || die(); + +$string['activityoverview'] = 'You have upcoming bigbluebuttonbn sessions'; +$string['bbbduetimeoverstartingtime'] = 'The due time for this activity must be greater than the starting time'; +$string['bbbdurationwarning'] = 'The maximum duration for this session is %duration% minutes.'; +$string['bbbrecordwarning'] = 'This session may be recorded.'; +$string['bbbrecordallfromstartwarning'] = 'This session is being recorded from start.'; +$string['bigbluebuttonbn:addinstance'] = 'Add a new bigbluebuttonbn room/activity'; +$string['bigbluebuttonbn:join'] = 'Join a bigbluebuttonbn meeting'; +$string['bigbluebuttonbn:view'] = 'View a room/activity'; +$string['bigbluebuttonbn:managerecordings'] = 'Manage bigbluebuttonbn recordings'; +$string['bigbluebuttonbn:meeting'] = 'Create instances with live meeting capabilities'; +$string['bigbluebuttonbn:recording'] = 'Create instances with recording capabilities'; +$string['bigbluebuttonbn'] = 'BigBlueButton'; +$string['indicator:cognitivedepth'] = 'BigBlueButtonBN cognitive'; +$string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a BigBlueButtonBN activity.'; +$string['indicator:socialbreadth'] = 'BigBlueButtonBN social'; +$string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a BigBlueButtonBN activity.'; +$string['modulename'] = 'BigBlueButtonBN'; +$string['modulenameplural'] = 'BigBlueButtonBN'; +$string['modulename_help'] = 'BigBlueButtonBN lets you create from within Moodle links to real-time on-line classrooms using BigBlueButton, an open source web conferencing system for distance education. + +Using BigBlueButtonBN you can specify for the title, description, calendar entry (which gives a date range for joining the session), groups, and details about the recording of the on-line session.'; +$string['modulename_link'] = 'BigBlueButtonBN/view'; +$string['pluginadministration'] = 'BigBlueButton administration'; +$string['pluginname'] = 'BigBlueButtonBN'; + +$string['removedevents'] = 'Deleted events'; +$string['removedtags'] = 'Deleted tags'; +$string['removedlogs'] = 'Deleted custom logs'; +$string['removedrecordings'] = 'Deleted recordings'; +$string['resetevents'] = 'Delete events'; +$string['resettags'] = 'Delete tags'; +$string['resetlogs'] = 'Delete custom logs'; +$string['resetrecordings'] = 'Delete recordings'; +$string['resetlogs_help'] = 'Deleting the logs will cause the lost of references to recordings'; +$string['resetrecordings_help'] = 'Deleting the recordings will make them inaccessible from anywhere and it can not be undone'; + +$string['search:activity'] = 'BigBlueButtonBN - activity information'; +$string['search:tags'] = 'BigBlueButtonBN - tags information'; + +$string['privacy:metadata:bigbluebuttonbn'] = 'Stores the configuration for the room or activity that defines the features and general behaviour of the BigBlueButton session.'; +$string['privacy:metadata:bigbluebuttonbn:participants'] = 'A list of rules that define the role users will in the live meeting. A user ID may be stored as permissions can be granted per role or user.'; +$string['privacy:metadata:bigbluebuttonbn_logs'] = 'Stores events triggered when using the plugin.'; +$string['privacy:metadata:bigbluebuttonbn_logs:userid'] = 'The user ID of the user who triggered the event.'; +$string['privacy:metadata:bigbluebuttonbn_logs:timecreated'] = 'The time at which the log was created.'; +$string['privacy:metadata:bigbluebuttonbn_logs:meetingid'] = 'The meeting ID the user had access to.'; +$string['privacy:metadata:bigbluebuttonbn_logs:log'] = 'The type of event triggered by the user.'; +$string['privacy:metadata:bigbluebuttonbn_logs:meta'] = 'May include extra information related to the meeting or the recording afected by the event.'; +$string['privacy:metadata:bigbluebutton'] = 'In order to create and join BigBlueButton sessions, user data needs to be exchanged with the server.'; +$string['privacy:metadata:bigbluebutton:userid'] = 'The userid of the user accessing the BigBlueButton server.'; +$string['privacy:metadata:bigbluebutton:fullname'] = 'The fullname of the user accessing the BigBlueButton server.'; + +$string['completionattendance'] = 'Student must attend the meeting for:'; +$string['completionattendancedesc'] = 'Student must attend the meeting and remain in the session for at least {$a} minute(s)'; +$string['completionattendancegroup'] = 'Require attendance'; +$string['completionattendancegroup_help'] = 'Attending the meeting for (n) minutes is required for completion'; + +$string['completionengagementchats'] = 'Chat events'; +$string['completionengagementtalks'] = 'Talk events'; +$string['completionengagementtalktime'] = 'Talk time'; +$string['completionengagementraisehand'] = 'Raise hand events'; +$string['completionengagementpollvotes'] = 'Poll votes'; +$string['completionengagementemojis'] = 'Emojis'; + +$string['completionengagementdesc'] = 'Student must engage in activities during the meeting'; +$string['completionengagementgroup'] = 'Require engagement'; +$string['completionengagementgroup_help'] = 'Active participation during the session is required for completion'; + +$string['completionupdatestate'] = "Completion update state"; +$string['completionvalidatestate'] = "Validate completion"; +$string['completionvalidatestatetriggered'] = "Validate completion has been triggered."; + +$string['sendnotification'] = "Send notification"; + +$string['minute'] = 'minute'; +$string['minutes'] = 'minutes'; + +$string['config_general'] = 'General configuration'; +$string['config_general_description'] = 'These settings are <b>always</b> used'; +$string['config_server_url'] = 'BigBlueButton Server URL'; +$string['config_server_url_description'] = 'The URL of your BigBlueButton server must end with /bigbluebutton/. (This default URL is for a BigBlueButton server provided by Blindside Networks that you can use for testing.)'; +$string['config_shared_secret'] = 'BigBlueButton Shared Secret'; +$string['config_shared_secret_description'] = 'The security salt of your BigBlueButton server. (This default salt is for a BigBlueButton server provided by Blindside Networks that you can use for testing.)'; + +$string['config_recording'] = 'Configuration for "Record meeting" feature'; +$string['config_recording_description'] = 'These settings are feature specific'; +$string['config_recording_default'] = 'Recording feature enabled by default'; +$string['config_recording_default_description'] = 'If enabled the sessions created in BigBlueButton will have recording capabilities.'; +$string['config_recording_editable'] = 'Recording feature can be edited'; +$string['config_recording_editable_description'] = 'If checked the interface includes an option for enable and disable the recording feature.'; +$string['config_recording_icons_enabled'] = 'Icons for recording management'; +$string['config_recording_icons_enabled_description'] = 'When enabled, the recording management panel shows icons for the publish/unpublish and delete actions.'; +$string['config_recording_all_from_start_default'] = 'Record all from start'; +$string['config_recording_all_from_start_default_description'] = 'If checked the meeting will record to start'; +$string['config_recording_all_from_start_editable'] = 'Record all from start can be edited'; +$string['config_recording_all_from_start_editable_description'] = 'If checked the interface includes an option for enable and disable the record all from start feature.'; +$string['config_recording_hide_button_default'] = 'Hide recording button'; +$string['config_recording_hide_button_default_description'] = 'If checked the button for record will be hide'; +$string['config_recording_hide_button_editable'] = 'Hide recording button can be edited'; +$string['config_recording_hide_button_editable_description'] = 'If checked the interface includes an option for enable and disable the hide recording button feature.'; + +$string['config_recordings'] = 'Configuration for "Show recordings" feature'; +$string['config_recordings_description'] = 'These settings are feature specific'; +$string['config_recordings_general'] = 'Show recording settings'; +$string['config_recordings_general_description'] = 'These settings are used only when showing recordings'; +$string['config_recordings_html_default'] = 'UI as html is enabled by default'; +$string['config_recordings_html_default_description'] = 'If enabled the recording table is shown in plain HTML by default.'; +$string['config_recordings_html_editable'] = 'UI as html feature can be edited'; +$string['config_recordings_html_editable_description'] = 'UI as html value by default can be edited when the instance is added or updated.'; +$string['config_recordings_deleted_default'] = 'Include recordings from deleted activities enabled by default'; +$string['config_recordings_deleted_default_description'] = 'If enabled the recording table will include the recordings belonging to deleted activities if there is any.'; +$string['config_recordings_deleted_editable'] = 'Include recordings from deleted activities feature can be edited'; +$string['config_recordings_deleted_editable_description'] = 'Include recordings from deleted activities by default can be edited when the instance is added or updated.'; +$string['config_recordings_imported_default'] = 'Show only imported links enabled by default'; +$string['config_recordings_imported_default_description'] = 'If enabled the recording table will include only the imported links to recordings.'; +$string['config_recordings_imported_editable'] = 'Show only imported links feature can be edited'; +$string['config_recordings_imported_editable_description'] = 'Show only imported links by default can be edited when the instance is added or updated.'; +$string['config_recordings_preview_default'] = 'Preview is enabled by default'; +$string['config_recordings_preview_default_description'] = 'If enabled the table includes a preview of the presentation.'; +$string['config_recordings_preview_editable'] = 'Preview feature can be edited'; +$string['config_recordings_preview_editable_description'] = 'Preview feature can be edited when the instance is added or updated.'; +$string['config_recordings_sortorder'] = 'Order the recordings in ascending order.'; +$string['config_recordings_sortorder_description'] = 'By default recordings are displayed in descending order. When checked they will be sorted in ascending order.'; +$string['config_recordings_validate_url'] = 'Validate URL'; +$string['config_recordings_validate_url_description'] = 'If checked the playback URL will be validated before the user access it.'; + +$string['config_importrecordings'] = 'Configuration for "Import recordings" feature'; +$string['config_importrecordings_description'] = 'These settings are feature specific'; +$string['config_importrecordings_enabled'] = 'Import recordings enabled'; +$string['config_importrecordings_enabled_description'] = 'When this and the recording feature are enabled, it is possible to import recordings from different courses into an activity.'; +$string['config_importrecordings_from_deleted_enabled'] = 'Import recordings from deleted activities enabled'; +$string['config_importrecordings_from_deleted_enabled_description'] = 'When this and the import recording feature are enabled, it is possible to import recordings from activities that are no longer in the course.'; + +$string['config_waitformoderator'] = 'Configuration for "Wait for moderator" feature'; +$string['config_waitformoderator_description'] = 'These settings are feature specific'; +$string['config_waitformoderator_default'] = 'Wait for moderator enabled by default'; +$string['config_waitformoderator_default_description'] = 'Wait for moderator feature is enabled by default when a new room or conference is added.'; +$string['config_waitformoderator_editable'] = 'Wait for moderator feature can be edited'; +$string['config_waitformoderator_editable_description'] = 'Wait for moderator value by default can be edited when the room or conference is added or updated.'; +$string['config_waitformoderator_ping_interval'] = 'Wait for moderator ping (seconds)'; +$string['config_waitformoderator_ping_interval_description'] = 'When the wait for moderator feature is enabled, the client pings for the status of the session each [number] seconds. This parameter defines the interval for requests made to the Moodle server'; +$string['config_waitformoderator_cache_ttl'] = 'Wait for moderator cache TTL (seconds)'; +$string['config_waitformoderator_cache_ttl_description'] = 'To support a heavy load of clients this plugin makes use of a cache. This parameter defines the time the cache will be kept before the next request is sent to the BigBlueButton server.'; + +$string['config_voicebridge'] = 'Configuration for "Voice bridge" feature'; +$string['config_voicebridge_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_voicebridge_editable'] = 'Conference voice bridge can be edited'; +$string['config_voicebridge_editable_description'] = 'Conference voice bridge number can be permanently assigned to a room conference. When assigned, the number can not be used by any other room or conference'; + +$string['config_preuploadpresentation'] = 'Configuration for "Pre-upload presentation" feature'; +$string['config_preuploadpresentation_description'] = 'These settings enable or disable options in the UI and also define default values for these options. The feature works only if the Moodle server is accessible to BigBlueButton..'; +$string['config_preuploadpresentation_enabled'] = 'Pre-uploading presentation enabled'; +$string['config_preuploadpresentation_enabled_description'] = 'Preupload presentation feature is enabled in the UI when the room or conference is added or updated.'; + +$string['config_presentation_default'] = 'Default file for "Pre-upload presentation" feature'; +$string['config_presentation_default_description'] = 'This setting allow to select a file to use as default in all BBB instances if "Pre-upload presentation" is enabled.'; + +$string['config_participant'] = 'Participant configuration'; +$string['config_participant_description'] = 'These settings define the role by default for participants in a conference.'; +$string['config_participant_moderator_default'] = 'Moderator by default'; +$string['config_participant_moderator_default_description'] = 'This rule is used by default when a new room is added.'; + +$string['config_userlimit'] = 'Configuration for "User limit" feature'; +$string['config_userlimit_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_userlimit_default'] = 'User limit enabled by default'; +$string['config_userlimit_default_description'] = 'The number of users allowed in a session by default when a new room or conference is added. If the number is set to 0, no limit is established'; +$string['config_userlimit_editable'] = 'User limit feature can be edited'; +$string['config_userlimit_editable_description'] = 'User limit value by default can be edited when the room or conference is added or updated.'; + +$string['config_scheduled'] = 'Configuration for "Scheduled sessions"'; +$string['config_scheduled_description'] = 'These settings define some of the behaviour by default for scheduled sessions.'; +$string['config_scheduled_duration_enabled'] = 'Calculate duration enabled'; +$string['config_scheduled_duration_enabled_description'] = 'The duration of an scheduled session is calculated based on the opening and closing times.'; +$string['config_scheduled_duration_compensation'] = 'Compensatory time (minutes)'; +$string['config_scheduled_duration_compensation_description'] = 'Minutes added to the scheduled closing when calculating the duration.'; +$string['config_scheduled_pre_opening'] = 'Accessible before opening time (minutes)'; +$string['config_scheduled_pre_opening_description'] = 'The time in minutes for the session to be acceessible before the schedules opening time is due.'; + +$string['config_sendnotifications'] = 'Configuration for "Send notifications" feature'; +$string['config_sendnotifications_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_sendnotifications_enabled'] = 'Send notifications enabled'; +$string['config_sendnotifications_enabled_description'] = 'If enabled the UI for editing the activity includes an option for sending a notification to enrolled user when the activity is added or updated.'; + +$string['config_extended_capabilities'] = 'Configuration for extended capabilities'; +$string['config_extended_capabilities_description'] = 'Configuration for extended capabilities when the BigBlueButton server offers them.'; +$string['config_uidelegation_enabled'] = 'UI delegation is enabled'; +$string['config_uidelegation_enabled_description'] = 'These settings enable or disable the UI delegation to the BigBlueButton server.'; +$string['config_recordingready_enabled'] = 'Send notifications when a recording is ready'; +$string['config_recordingready_enabled_description'] = 'Enable the plugin for sending notifications when the recording is ready. (It will only work if the script post_publish_recording_ready_callback is enabled in the BigBlueButton server)'; +$string['config_meetingevents_enabled'] = 'Register live events'; +$string['config_meetingevents_enabled_description'] = 'Enable the plugin for accepting and processing live events after the session ends. (It must be enabled for "Activity completion" and will only work if the BigBlueButton server is capable of processing post_events scripts)'; + +$string['config_warning_curl_not_installed'] = 'This feature requires the CURL extension for php installed and enabled. The settings will be accessible only if this condition is fulfilled.'; +$string['config_warning_bigbluebuttonbn_cfg_deprecated'] = 'BigBlueButtonBN is making use of config.php with a global variable that has been deprecated. Please convert the file as it will not be supported in future versions'; + +$string['config_muteonstart'] = 'Configuration for "Mute on Start" feature'; +$string['config_muteonstart_description'] = 'These settings enable or disable options in the UI and also define default values for these options.'; +$string['config_muteonstart_default'] = 'Mute on start enabled by default'; +$string['config_muteonstart_default_description'] = 'If enabled the session will be muted on start.'; +$string['config_muteonstart_editable'] = 'Mute on start can be edited'; +$string['config_muteonstart_editable_description'] = 'Mute on start by default can be edited when the instance is added or updated.'; +$string['config_welcome_default'] = 'Default welcome message'; +$string['config_welcome_default_description'] = 'Replaces the default message setted up for the BigBlueButton server. The message can includes keywords (%%CONFNAME%%, %%DIALNUM%%, %%CONFNUM%%) which will be substituted automatically, and also html tags like <b>...</b> or <i></i> '; +$string['config_default_messages'] = 'Default messages'; +$string['config_default_messages_description'] = 'Set message defaults for activities'; + +$string['config_locksettings'] = 'Configuration for locking settings'; +$string['config_locksettings_description'] = 'These setttings enable or disable options in the UI for locking settings, and also define default values for these options.'; + +$string['config_disablecam_default'] = 'Disable cam enabled by default'; +$string['config_disablecam_default_description'] = 'If enabled the webcams will be disabled.'; +$string['config_disablecam_editable'] = 'Disable cam can be edited'; +$string['config_disablecam_editable_description'] = 'Disable cam by default can be edited when the instance is added or updated.'; + +$string['config_disablemic_default'] = 'Disable mic enabled by default'; +$string['config_disablemic_default_description'] = 'If enabled the microphones will be disabled.'; +$string['config_disablemic_editable'] = 'Disable mic can be edited'; +$string['config_disablemic_editable_description'] = 'Disable mic by default can be edited when the instance is added or updated.'; + +$string['config_disableprivatechat_default'] = 'Disable private chat enabled by default'; +$string['config_disableprivatechat_default_description'] = 'If enabled the private chat will be disabled.'; +$string['config_disableprivatechat_editable'] = 'Disable private chat can be edited'; +$string['config_disableprivatechat_editable_description'] = 'Disable private chat by default can be edited when the instance is added or updated.'; + +$string['config_disablepublicchat_default'] = 'Disable public chat enabled by default'; +$string['config_disablepublicchat_default_description'] = 'If enabled the public chat will be disabled.'; +$string['config_disablepublicchat_editable'] = 'Disable public chat can be edited'; +$string['config_disablepublicchat_editable_description'] = 'Disable public chat by default can be edited when the instance is added or updated.'; + +$string['config_disablenote_default'] = 'Disable shared notes enabled by default'; +$string['config_disablenote_default_description'] = 'If enabled the shared notes will be disabled.'; +$string['config_disablenote_editable'] = 'Disable shared notes can be edited'; +$string['config_disablenote_editable_description'] = 'Disable shared notes by default can be edited when the instance is added or updated.'; + +$string['config_hideuserlist_default'] = 'Hide user list enabled by default'; +$string['config_hideuserlist_default_description'] = 'If enabled the session user list will be hidden.'; +$string['config_hideuserlist_editable'] = 'Hide user list can be edited'; +$string['config_hideuserlist_editable_description'] = 'Hide user list by default can be edited when the instance is added or updated.'; + +$string['config_lockedlayout_default'] = 'Locked layout enabled by default'; +$string['config_lockedlayout_default_description'] = 'If enabled the session layout will be locked.'; +$string['config_lockedlayout_editable'] = 'Locked layout can be edited'; +$string['config_lockedlayout_editable_description'] = 'Locked layout by default can be edited when the instance is added or updated.'; + +$string['config_lockonjoin_default'] = 'Ignore lock on join enabled by default'; +$string['config_lockonjoin_default_description'] = 'If enabled the lock settings will be ignored. Lock configuration must be enabled for this to apply.'; +$string['config_lockonjoin_editable'] = 'Ignore lock on join can be edited'; +$string['config_lockonjoin_editable_description'] = 'Ignore lock on join by default can be edited when the instance is added or updated.'; + +$string['config_lockonjoinconfigurable_default'] = 'Lock configuration enabled by default'; +$string['config_lockonjoinconfigurable_default_description'] = 'If enabled the session lock settings can be enabled or disabled from the above control.'; +$string['config_lockonjoinconfigurable_editable'] = 'Lock configuration can be edited'; +$string['config_lockonjoinconfigurable_editable_description'] = 'Lock configuration by default can be edited when the instance is added or updated.'; + +$string['config_experimental_features'] = 'Configuration for experimental features'; +$string['config_experimental_features_description'] = 'Configuration for experimental features.'; + +$string['general_error_unable_connect'] = 'Unable to connect. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; +$string['general_error_not_allowed_to_create_instances'] = 'User is not allowed to create any type of instances.'; + +$string['index_confirm_end'] = 'Do you wish to end the virtual class?'; +$string['index_disabled'] = 'disabled'; +$string['index_enabled'] = 'enabled'; +$string['index_ending'] = 'Ending the virtual classroom ... please wait'; +$string['index_error_checksum'] = 'A checksum error occurred. Make sure you entered the correct salt.'; +$string['index_error_forciblyended'] = 'Unable to join this meeting because it has been manually ended.'; +$string['index_error_unable_display'] = 'Unable to display the meetings. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; +$string['index_heading_actions'] = 'Actions'; +$string['index_heading_group'] = 'Group'; +$string['index_heading_moderator'] = 'Moderators'; +$string['index_heading_name'] = 'Room'; +$string['index_heading_recording'] = 'Recording'; +$string['index_heading_users'] = 'Users'; +$string['index_heading_viewer'] = 'Viewers'; +$string['index_heading'] = 'BigBlueButton Rooms'; +$string['mod_form_block_general'] = 'General settings'; +$string['mod_form_block_room'] = 'Activity/Room settings'; +$string['mod_form_block_recordings'] = 'View for recording'; +$string['mod_form_block_presentation'] = 'Presentation content'; +$string['mod_form_block_presentation_default'] = 'Presentation default content'; +$string['mod_form_block_participants'] = 'Role assigned during live session'; +$string['mod_form_block_schedule'] = 'Schedule for session'; +$string['mod_form_block_record'] = 'Record settings'; +$string['mod_form_field_openingtime'] = 'Join open'; +$string['mod_form_field_closingtime'] = 'Join closed'; +$string['mod_form_field_intro'] = 'Description'; +$string['mod_form_field_intro_help'] = 'A short description for the room or conference.'; +$string['mod_form_field_duration_help'] = 'Setting the duration for a meeting will establish the maximum time for a meeting to keep alive before the recording finish'; +$string['mod_form_field_duration'] = 'Duration'; +$string['mod_form_field_userlimit'] = 'User limit'; +$string['mod_form_field_userlimit_help'] = 'Maximum limit of users allowed in a meeting. If the limit is set to 0 the number of users will be unlimited.'; +$string['mod_form_field_name'] = 'Virtual classroom name'; +$string['mod_form_field_room_name'] = 'Room name'; +$string['mod_form_field_conference_name'] = 'Conference name'; +$string['mod_form_field_record'] = 'Session can be recorded'; +$string['mod_form_field_voicebridge'] = 'Voice bridge [####]'; +$string['mod_form_field_voicebridge_help'] = 'Voice conference number that participants enter to join the voice conference when using dial-in. A number between 1 and 9999 must be typed. If the value is 0 the static voicebridge number will be ignored and a random number will be generated by BigBlueButton. A number 7 will preced to the four digits typed'; +$string['mod_form_field_voicebridge_format_error'] = 'Format error. You should input a number between 1 and 9999.'; +$string['mod_form_field_voicebridge_notunique_error'] = 'Not a unique value. This number is being used by another room or conference.'; +$string['mod_form_field_wait'] = 'Wait for moderator'; +$string['mod_form_field_wait_help'] = 'Viewers must wait until a moderator enters the session before they can do so'; +$string['mod_form_field_welcome'] = 'Welcome message'; +$string['mod_form_field_welcome_help'] = 'Replaces the default message setted up for the BigBlueButton server. The message can includes keywords (%%CONFNAME%%, %%DIALNUM%%, %%CONFNUM%%) which will be substituted automatically, and also html tags like <b>...</b>, <br />, <u></u> or <i></i> '; +$string['mod_form_field_welcome_default'] = '<br>Welcome to <b>%%CONFNAME%%</b>!<br><br>For help on using BigBlueButton see these (short) <a href="event:http://www.bigbluebutton.org/content/videos"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the phone icon (top center). <b>Please use a headset to avoid causing background noise for others.</b>'; +$string['mod_form_field_participant_add'] = 'Add assignation'; +$string['mod_form_field_participant_list'] = 'Assignation list'; +$string['mod_form_field_participant_list_type_all'] = 'All users enrolled'; +$string['mod_form_field_participant_list_type_role'] = 'Role'; +$string['mod_form_field_participant_list_type_user'] = 'User'; +$string['mod_form_field_participant_list_type_owner'] = 'Owner'; +$string['mod_form_field_participant_list_text_as'] = 'joins session as'; +$string['mod_form_field_participant_list_action_add'] = 'Add'; +$string['mod_form_field_participant_list_action_remove'] = 'Remove'; +$string['mod_form_field_participant_bbb_role_moderator'] = 'Moderator'; +$string['mod_form_field_participant_bbb_role_viewer'] = 'Viewer'; +$string['mod_form_field_instanceprofiles'] = 'Instance type'; +$string['mod_form_field_instanceprofiles_help'] = 'Select the type for this BigBlueButtonBN instance.'; +$string['mod_form_field_muteonstart'] = 'Mute on start'; +$string['mod_form_field_notification'] = 'Notify this change to users enrolled'; +$string['mod_form_field_notification_help'] = 'Send a notification to all users enrolled to let them know that this activity has been added or updated'; +$string['mod_form_field_notification_created_help'] = 'Send a notification to all users enrolled to let them know that this activity has been created'; +$string['mod_form_field_notification_modified_help'] = 'Send a notification to all users enrolled to let them know that this activity has been updated'; +$string['mod_form_field_notification_msg_created'] = 'added'; +$string['mod_form_field_notification_msg_modified'] = 'updated'; +$string['mod_form_field_notification_msg_at'] = 'at'; +$string['mod_form_field_recordings_html'] = 'Show the table in plain html'; +$string['mod_form_field_recordings_deleted'] = 'Include recordings from deleted activities'; +$string['mod_form_field_recordings_imported'] = 'Show only imported links'; +$string['mod_form_field_recordings_preview'] = 'Show recording preview'; +$string['mod_form_field_recordallfromstart'] = 'Record all from start'; +$string['mod_form_field_recordhidebutton'] = 'Hide recording button'; +$string['mod_form_field_nosettings'] = 'No settings can be edited'; +$string['mod_form_field_disablecam'] = 'Disable webcams'; +$string['mod_form_field_disablemic'] = 'Disable microphones'; +$string['mod_form_field_disableprivatechat'] = 'Disable private chat'; +$string['mod_form_field_disablepublicchat'] = 'Disable public chat'; +$string['mod_form_field_disablenote'] = 'Disable shared notes'; +$string['mod_form_field_hideuserlist'] = 'Hide user list'; +$string['mod_form_field_lockedlayout'] = 'Lock room layout'; +$string['mod_form_field_lockonjoin'] = 'Ignore lock settings'; +$string['mod_form_field_lockonjoinconfigurable'] = 'Allow ignore locking settings'; +$string['mod_form_locksettings'] = 'Lock settings'; + + +$string['starts_at'] = 'Starts'; +$string['started_at'] = 'Started'; +$string['ends_at'] = 'Ends'; +$string['calendarstarts'] = '{$a} is scheduled for'; +$string['view_error_no_group_student'] = 'You have not been enrolled in a group. Please contact your Teacher or the Administrator.'; +$string['view_error_no_group_teacher'] = 'There are no groups configured yet. Please set up groups or contact the Administrator.'; +$string['view_error_no_group'] = 'There are no groups configured yet. Please set up groups before trying to join the meeting.'; +$string['view_error_unable_join_student'] = 'Unable to connect to the BigBlueButton server. Please contact your Teacher or the Administrator.'; +$string['view_error_unable_join_teacher'] = 'Unable to connect to the BigBlueButton server. Please contact the Administrator.'; +$string['view_error_unable_join'] = 'Unable to join the meeting. Please check the url of the BigBlueButton server AND check to see if the BigBlueButton server is running.'; +$string['view_error_bigbluebutton'] = 'BigBlueButton responded with errors. {$a}'; +$string['view_error_create'] = 'The BigBlueButton server responded with an error message, the meeting could not be created.'; +$string['view_error_max_concurrent'] = 'Number of concurrent meetings allowed has been reached.'; +$string['view_error_userlimit_reached'] = 'The number of users allowed in a meeting has been reached.'; +$string['view_error_url_missing_parameters'] = 'There are parameters missing in this URL'; +$string['view_error_import_no_courses'] = 'No courses to look up for recordings'; +$string['view_error_import_no_recordings'] = 'No recordings in this course for importing'; +$string['view_error_invalid_session'] = 'The session is expired. Go back to the activity main page.'; +$string['view_groups_selection_join'] = 'Join'; +$string['view_groups_selection'] = 'Select the group you want to join and confirm the action'; +$string['view_login_moderator'] = 'Logging in as moderator ...'; +$string['view_login_viewer'] = 'Logging in as viewer ...'; +$string['view_noguests'] = 'The BigBlueButtonBN is not open to guests'; +$string['view_nojoin'] = 'You are not in a role allowed to join this session.'; +$string['view_recording_list_actionbar_edit'] = 'Edit'; +$string['view_recording_list_actionbar_delete'] = 'Delete'; +$string['view_recording_list_actionbar_import'] = 'Import'; +$string['view_recording_list_actionbar_hide'] = 'Hide'; +$string['view_recording_list_actionbar_show'] = 'Show'; +$string['view_recording_list_actionbar_publish'] = 'Publish'; +$string['view_recording_list_actionbar_unpublish'] = 'Unpublish'; +$string['view_recording_list_actionbar_protect'] = 'Make it private'; +$string['view_recording_list_actionbar_unprotect'] = 'Make it public'; +$string['view_recording_list_action_publish'] = 'Publishing'; +$string['view_recording_list_action_unpublish'] = 'Unpublishing'; +$string['view_recording_list_action_process'] = 'Processing'; +$string['view_recording_list_action_delete'] = 'Deleting'; +$string['view_recording_list_action_protect'] = 'Protecting'; +$string['view_recording_list_action_unprotect'] = 'Unprotecting'; +$string['view_recording_list_action_update'] = 'Updating'; +$string['view_recording_list_action_edit'] = 'Updating'; +$string['view_recording_list_action_play'] = 'Play'; +$string['view_recording_list_actionbar'] = 'Toolbar'; +$string['view_recording_list_activity'] = 'Activity'; +$string['view_recording_list_course'] = 'Course'; +$string['view_recording_list_date'] = 'Date'; +$string['view_recording_list_description'] = 'Description'; +$string['view_recording_list_duration'] = 'Duration'; +$string['view_recording_list_recording'] = 'Recording'; +$string['view_recording_button_import'] = 'Import recording links'; +$string['view_recording_button_return'] = 'Go back'; +$string['view_recording_format_notes'] = 'Notes'; +$string['view_recording_format_podcast'] = 'Podcast'; +$string['view_recording_format_presentation'] = 'Presentation'; +$string['view_recording_format_screenshare'] = 'Screenshare'; +$string['view_recording_format_statistics'] = 'Statistics'; +$string['view_recording_format_video'] = 'Video'; +$string['view_recording_format_errror_unreachable'] = 'The URL for this recording format is unreachable.'; +$string['view_section_title_presentation'] = 'Presentation file'; +$string['view_section_title_recordings'] = 'Recordings'; +$string['view_message_norecordings'] = 'There are no recording to show.'; +$string['view_message_finished'] = 'This activity is over.'; +$string['view_message_notavailableyet'] = 'This session is not yet available.'; + +$string['view_message_session_started_at'] = 'This session started at'; +$string['view_message_session_running_for'] = 'This session has been running for'; +$string['view_message_hour'] = 'hour'; +$string['view_message_hours'] = 'hours'; +$string['view_message_minute'] = 'minute'; +$string['view_message_minutes'] = 'minutes'; +$string['view_message_moderator'] = 'moderator'; +$string['view_message_moderators'] = 'moderators'; +$string['view_message_viewer'] = 'viewer'; +$string['view_message_viewers'] = 'viewers'; +$string['view_message_user'] = 'user'; +$string['view_message_users'] = 'users'; +$string['view_message_has_joined'] = 'has joined'; +$string['view_message_have_joined'] = 'have joined'; +$string['view_message_session_no_users'] = 'There are no users in this session'; +$string['view_message_session_has_user'] = 'There is'; +$string['view_message_session_has_users'] = 'There are'; +$string['view_message_session_for'] = 'the session for'; +$string['view_message_times'] = 'times'; +$string['view_message_and'] = 'and'; + +$string['view_message_room_closed'] = 'This room is closed.'; +$string['view_message_room_ready'] = 'This room is ready.'; +$string['view_message_room_open'] = 'This room is open.'; +$string['view_message_conference_room_ready'] = 'This conference room is ready. You can join the session now.'; +$string['view_message_conference_not_started'] = 'This conference has not started yet.'; +$string['view_message_conference_wait_for_moderator'] = 'Waiting for a moderator to join.'; +$string['view_message_conference_in_progress'] = 'This conference is in progress.'; +$string['view_message_conference_has_ended'] = 'This conference has ended.'; +$string['view_message_tab_close'] = 'This tab/window must be closed manually'; +$string['view_message_recordings_disabled'] = 'Recordings were disabled on this server. BigBlueButtonBN instances for recordings only can not be used.'; +$string['view_message_importrecordings_disabled'] = 'Feature for import recording links is disabled on this server.'; + +$string['view_groups_selection_warning'] = 'There is a conference room for each group and you have access to more than one. Be sure to select the correct one.'; +$string['view_groups_nogroups_warning'] = 'The room was configured for using groups but the course does not have groups defined.'; +$string['view_groups_notenrolled_warning'] = 'The room was configured for using groups but you are not enrolled in any of them.'; +$string['view_conference_action_join'] = 'Join session'; +$string['view_conference_action_end'] = 'End session'; + +$string['view_recording'] = 'recording'; +$string['view_recording_link'] = 'imported link'; +$string['view_recording_link_warning'] = 'This is a link pointing to a recording that was created in a different course or activity'; +$string['view_recording_delete_confirmation'] = 'Are you sure to delete this {$a}?'; +$string['view_recording_delete_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is deleted that link will also be removed'; +$string['view_recording_delete_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is deleted those links will also be removed'; +$string['view_recording_publish_link_deleted'] = 'This link can not be re-published because the actual recording does not exist in the current BigBlueButton server. The link should be removed.'; +$string['view_recording_publish_link_not_published'] = 'This link can not be re-published because the actual recording is unpublished'; +$string['view_recording_unpublish_confirmation'] = 'Are you sure to unpublish this {$a}?'; +$string['view_recording_unpublish_confirmation_warning_s'] = 'This recording has {$a} link associated that was imported in a different course or activity. If the recording is unpublished that link will also be unpublished'; +$string['view_recording_unpublish_confirmation_warning_p'] = 'This recording has {$a} links associated that were imported in different courses or activities. If the recording is unpublished those links will also be unpublished'; +$string['view_recording_import_confirmation'] = 'Are you sure to import this recording?'; +$string['view_recording_unprotect_link_deleted'] = 'This link can not be un-protected because the actual recording does not exist in the current BigBlueButton server. The link should be removed.'; +$string['view_recording_unprotect_link_not_unprotected'] = 'This link can not be un-protected because the actual recording is protected'; +$string['view_recording_actionbar'] = 'Toolbar'; +$string['view_recording_activity'] = 'Activity'; +$string['view_recording_course'] = 'Course'; +$string['view_recording_date'] = 'Date'; +$string['view_recording_description'] = 'Description'; +$string['view_recording_length'] = 'Length'; +$string['view_recording_meeting'] = 'Meeting'; +$string['view_recording_duration'] = 'Duration'; +$string['view_recording_recording'] = 'Recording'; +$string['view_recording_duration_min'] = 'min'; +$string['view_recording_name'] = 'Name'; +$string['view_recording_tags'] = 'Tags'; +$string['view_recording_playback'] = 'Playback'; +$string['view_recording_preview'] = 'Preview'; +$string['view_recording_preview_help'] = 'Hover over an image to view it in full size'; +$string['view_recording_modal_button'] = 'Apply'; +$string['view_recording_modal_title'] = 'Set values for recording'; +$string['view_recording_yui_first'] = 'First'; +$string['view_recording_yui_prev'] = 'Previous'; +$string['view_recording_yui_next'] = 'Next'; +$string['view_recording_yui_last'] = 'Last'; +$string['view_recording_yui_page'] = 'Page'; +$string['view_recording_yui_go'] = 'Go'; +$string['view_recording_yui_rows'] = 'Rows'; +$string['view_recording_yui_show_all'] = 'Show all'; + +$string['event_activity_created'] = 'Activity created'; +$string['event_activity_viewed'] = 'Activity viewed'; +$string['event_activity_deleted'] = 'Activity deleted'; +$string['event_activity_updated'] = 'Activity updated'; +$string['event_meeting_created'] = 'Meeting created'; +$string['event_meeting_ended'] = 'Meeting forcibly ended'; +$string['event_meeting_joined'] = 'Meeting joined'; +$string['event_meeting_left'] = 'Meeting left'; +$string['event_recording_viewed'] = 'Recording viewed'; +$string['event_recording_edited'] = 'Recording edited'; +$string['event_recording_deleted'] = 'Recording deleted'; +$string['event_recording_imported'] = 'Recording imported'; +$string['event_recording_published'] = 'Recording published'; +$string['event_recording_unpublished'] = 'Recording unpublished'; +$string['event_recording_protected'] = 'Recording protected'; +$string['event_recording_unprotected'] = 'Recording unprotected'; +$string['event_live_session'] = 'Live session event'; + +$string['instance_type_default'] = 'Room/Activity with recordings'; +$string['instance_type_room_only'] = 'Room/Activity only'; +$string['instance_type_recording_only'] = 'Recordings only'; + +$string['email_body_notification_meeting_has_been'] = 'has been'; +$string['email_body_notification_meeting_details'] = 'Details'; +$string['email_body_notification_meeting_title'] = 'Title'; +$string['email_body_notification_meeting_description'] = 'Description'; +$string['email_body_notification_meeting_start_date'] = 'Start date'; +$string['email_body_notification_meeting_end_date'] = 'End date'; +$string['email_body_notification_meeting_by'] = 'by'; +$string['email_body_recording_ready_for'] = 'Recording for'; +$string['email_body_recording_ready_is_ready'] = 'is ready'; +$string['email_footer_sent_by'] = 'This automatic notification message was sent by'; +$string['email_footer_sent_from'] = 'from the course'; + +$string['view_error_meeting_not_running'] = 'Something went wrong, the meeting is not running.'; +$string['view_error_current_state_not_found'] = 'Current state was not found. The recording may have been deleted or the BigBlueButton server is not compatible with the action performed.'; +$string['view_error_action_not_completed'] = 'Action could not be completed'; +$string['view_warning_default_server'] = 'This Moodle server is making use of the BigBlueButton testing server that comes pre-configured by default. It should be replaced for production.'; + +$string['view_room'] = 'View room'; +$string['mod_form_block_clienttype'] = 'Web Client Technology'; +$string['mod_form_block_clienttype_flash'] = 'Client based on Adobe Flash technology'; +$string['mod_form_block_clienttype_html5'] = 'Client based on HTML5 technology'; +$string['mod_form_field_block_clienttype'] = 'Web Client Technology'; +$string['config_clienttype'] = 'Configuration for "Web Client" type'; +$string['config_clienttype_default'] = 'Default Web Client type'; +$string['config_clienttype_default_description'] = 'Choose between the classical Adobe Flash client or the new HTML5 one.'; +$string['config_clienttype_description'] = 'This setting enable/disable the Web Client choice for each room.'; +$string['config_clienttype_editable'] = 'The Web Client choice can be edited'; +$string['config_clienttype_editable_description'] = 'This option enable the choice of the Web Client (AdobeFlash/HTML5) from the room editing form.'; +$string['index_error_noinstances'] = 'There are no instances of bigbluebuttonbn'; +$string['index_error_bbtn'] = 'BigBlueButton ID {$a} is incorrect'; + +$string['view_mobile_message_reload_page_creation_time_meeting'] = 'You exceeded the 45 seconds in this page, please reload the page to join correctly to the meeting.'; +$string['view_mobile_message_groups_not_supported'] = 'This instance is enable to work with groups but the mobile app has not support for this. Please open in desktop if you want to use the group support.'; diff --git a/mod/bigbluebuttonbn/lib.php b/mod/bigbluebuttonbn/lib.php new file mode 100644 index 0000000..c71abbb --- /dev/null +++ b/mod/bigbluebuttonbn/lib.php @@ -0,0 +1,1257 @@ +<?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/>. + +/** + * Library calls for Moodle and BigBlueButton. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +global $CFG; + +// JWT is included in Moodle 3.7 core, but a local package is still needed for backward compatibility. +if (!class_exists('\Firebase\JWT\JWT')) { + if (file_exists($CFG->libdir.'/php-jwt/src/JWT.php')) { + require_once($CFG->libdir.'/php-jwt/src/JWT.php'); + } else { + require_once($CFG->dirroot.'/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/JWT.php'); + } +} + +// Do not declare new $CFG variables if unit tests are running +// as it can cause "unexpected new $CFG->xxx value" warnings. +if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { + if (!isset($CFG->bigbluebuttonbn)) { + $CFG->bigbluebuttonbn = array(); + } + + if (file_exists(dirname(__FILE__).'/config.php')) { + require_once(dirname(__FILE__).'/config.php'); + } + + /* + * DURATIONCOMPENSATION: Feature removed by configuration + */ + $CFG->bigbluebuttonbn['scheduled_duration_enabled'] = 0; + /* + * Remove this block when restored + */ +} + +/** @var BIGBLUEBUTTONBN_DEFAULT_SERVER_URL string of default bigbluebutton server url */ +const BIGBLUEBUTTONBN_DEFAULT_SERVER_URL = 'http://test-install.blindsidenetworks.com/bigbluebutton/'; +/** @var BIGBLUEBUTTONBN_DEFAULT_SHARED_SECRET string of default bigbluebutton server shared secret */ +const BIGBLUEBUTTONBN_DEFAULT_SHARED_SECRET = '8cd8ef52e8e101574e400365b55e11a6'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_ADD string defines the bigbluebuttonbn Add event */ +const BIGBLUEBUTTONBN_LOG_EVENT_ADD = 'Add'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_EDIT string defines the bigbluebuttonbn Edit event */ +const BIGBLUEBUTTONBN_LOG_EVENT_EDIT = 'Edit'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_CREATE string defines the bigbluebuttonbn Create event */ +const BIGBLUEBUTTONBN_LOG_EVENT_CREATE = 'Create'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_JOIN string defines the bigbluebuttonbn Join event */ +const BIGBLUEBUTTONBN_LOG_EVENT_JOIN = 'Join'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_PLAYED string defines the bigbluebuttonbn Playback event */ +const BIGBLUEBUTTONBN_LOG_EVENT_PLAYED = 'Played'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_LOGOUT string defines the bigbluebuttonbn Logout event */ +const BIGBLUEBUTTONBN_LOG_EVENT_LOGOUT = 'Logout'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_IMPORT string defines the bigbluebuttonbn Import event */ +const BIGBLUEBUTTONBN_LOG_EVENT_IMPORT = 'Import'; +/** @var BIGBLUEBUTTONBN_LOG_EVENT_DELETE string defines the bigbluebuttonbn Delete event */ +const BIGBLUEBUTTONBN_LOG_EVENT_DELETE = 'Delete'; +/** @var BIGBLUEBUTTON_LOG_EVENT_CALLBACK string defines the bigbluebuttonbn Callback event */ +const BIGBLUEBUTTON_LOG_EVENT_CALLBACK = 'Callback'; +/** @var BIGBLUEBUTTON_LOG_EVENT_SUMMARY string defines the bigbluebuttonbn Summary event */ +const BIGBLUEBUTTON_LOG_EVENT_SUMMARY = 'Summary'; +/** + * Indicates API features that the bigbluebuttonbn supports. + * + * @uses FEATURE_IDNUMBER + * @uses FEATURE_GROUPS + * @uses FEATURE_GROUPINGS + * @uses FEATURE_GROUPMEMBERSONLY + * @uses FEATURE_MOD_INTRO + * @uses FEATURE_BACKUP_MOODLE2 + * @uses FEATURE_COMPLETION_TRACKS_VIEWS + * @uses FEATURE_COMPLETION_HAS_RULES + * @uses FEATURE_GRADE_HAS_GRADE + * @uses FEATURE_GRADE_OUTCOMES + * @uses FEATURE_SHOW_DESCRIPTION + * @param string $feature + * @return mixed True if yes (some features may use other values) + */ +function bigbluebuttonbn_supports($feature) { + if (!$feature) { + return null; + } + $features = array( + (string) FEATURE_IDNUMBER => true, + (string) FEATURE_GROUPS => true, + (string) FEATURE_GROUPINGS => true, + (string) FEATURE_GROUPMEMBERSONLY => true, + (string) FEATURE_MOD_INTRO => true, + (string) FEATURE_BACKUP_MOODLE2 => true, + (string) FEATURE_COMPLETION_TRACKS_VIEWS => true, + (string) FEATURE_COMPLETION_HAS_RULES => true, + (string) FEATURE_GRADE_HAS_GRADE => false, + (string) FEATURE_GRADE_OUTCOMES => false, + (string) FEATURE_SHOW_DESCRIPTION => true, + ); + if (isset($features[(string) $feature])) { + return $features[$feature]; + } + return null; +} + +/** + * Obtains the automatic completion state for this bigbluebuttonbn based on any conditions + * in bigbluebuttonbn settings. + * + * @param object $course Course + * @param object $cm Course-module + * @param int $userid User ID + * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) + * + * @return bool True if completed, false if not. (If no conditions, then return + * value depends on comparison type) + */ +function bigbluebuttonbn_get_completion_state($course, $cm, $userid, $type) { + global $DB; + + // Get bigbluebuttonbn details. + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $cm->instance), '*', + MUST_EXIST); + if (!$bigbluebuttonbn) { + throw new Exception("Can't find bigbluebuttonbn {$cm->instance}"); + } + + // Default return value. + $result = $type; + + $sql = "SELECT * FROM {bigbluebuttonbn_logs} "; + $sql .= "WHERE bigbluebuttonbnid = ? AND userid = ? AND log = ?"; + $logs = $DB->get_records_sql($sql, array($bigbluebuttonbn->id, $userid, BIGBLUEBUTTON_LOG_EVENT_SUMMARY)); + + if ($bigbluebuttonbn->completionattendance) { + if (!$logs) { + // As completion by attendance was required, the activity hasn't been completed. + return false; + } + $attendancecount = 0; + foreach ($logs as $log) { + $summary = json_decode($log->meta); + $attendancecount += $summary->data->duration; + } + $attendancecount /= 60; + $value = $bigbluebuttonbn->completionattendance <= $attendancecount; + if ($type == COMPLETION_AND) { + $result = $result && $value; + } else { + $result = $result || $value; + } + } + + if ($bigbluebuttonbn->completionengagementchats) { + if (!$logs) { + // As completion by engagement with chat was required, the activity hasn't been completed. + return false; + } + $engagementchatscount = 0; + foreach ($logs as $log) { + $summary = json_decode($log->meta); + $engagementchatscount += $summary->data->engagement->chats; + } + $value = $bigbluebuttonbn->completionengagementchats <= $engagementchatscount; + if ($type == COMPLETION_AND) { + $result = $result && $value; + } else { + $result = $result || $value; + } + } + + if ($bigbluebuttonbn->completionengagementtalks) { + if (!$logs) { + // As completion by engagement with talk was required, the activity hasn't been completed. + return false; + } + $engagementtalkscount = 0; + foreach ($logs as $log) { + $summary = json_decode($log->meta); + $engagementtalkscount += $summary->data->engagement->talks; + } + $value = $bigbluebuttonbn->completionengagementtalks <= $engagementtalkscount; + if ($type == COMPLETION_AND) { + $result = $result && $value; + } else { + $result = $result || $value; + } + } + + return $result; +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will create a new instance and return the id number + * of the new instance. + * + * @param object $bigbluebuttonbn An object from the form in mod_form.php + * @return int The id of the newly inserted bigbluebuttonbn record + */ +function bigbluebuttonbn_add_instance($bigbluebuttonbn) { + global $DB; + // Excecute preprocess. + bigbluebuttonbn_process_pre_save($bigbluebuttonbn); + // Pre-set initial values. + $bigbluebuttonbn->presentation = bigbluebuttonbn_get_media_file($bigbluebuttonbn); + // Insert a record. + $bigbluebuttonbn->id = $DB->insert_record('bigbluebuttonbn', $bigbluebuttonbn); + // Encode meetingid. + $bigbluebuttonbn->meetingid = bigbluebuttonbn_unique_meetingid_seed(); + // Set the meetingid column in the bigbluebuttonbn table. + $DB->set_field('bigbluebuttonbn', 'meetingid', $bigbluebuttonbn->meetingid, array('id' => $bigbluebuttonbn->id)); + // Log insert action. + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTONBN_LOG_EVENT_ADD); + // Complete the process. + bigbluebuttonbn_process_post_save($bigbluebuttonbn); + return $bigbluebuttonbn->id; +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will update an existing instance with new data. + * + * @param object $bigbluebuttonbn An object from the form in mod_form.php + * @return bool Success/Fail + */ +function bigbluebuttonbn_update_instance($bigbluebuttonbn) { + global $DB; + // Excecute preprocess. + bigbluebuttonbn_process_pre_save($bigbluebuttonbn); + // Pre-set initial values. + $bigbluebuttonbn->id = $bigbluebuttonbn->instance; + $bigbluebuttonbn->presentation = bigbluebuttonbn_get_media_file($bigbluebuttonbn); + // Update a record. + $DB->update_record('bigbluebuttonbn', $bigbluebuttonbn); + // Get the meetingid column in the bigbluebuttonbn table. + $bigbluebuttonbn->meetingid = (string)$DB->get_field('bigbluebuttonbn', 'meetingid', array('id' => $bigbluebuttonbn->id)); + // Log update action. + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTONBN_LOG_EVENT_EDIT); + // Complete the process. + bigbluebuttonbn_process_post_save($bigbluebuttonbn); + return true; +} + +/** + * Given an ID of an instance of this module, + * this function will permanently delete the instance + * and any data that depends on it. + * + * @param int $id Id of the module instance + * + * @return bool Success/Failure + */ +function bigbluebuttonbn_delete_instance($id) { + global $DB; + + if (!$bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $id))) { + return false; + } + + // TODO: End the meeting if it is running. + + $result = true; + + // Delete any dependent records here. + if (!$DB->delete_records('bigbluebuttonbn', array('id' => $bigbluebuttonbn->id))) { + $result = false; + } + + if (!$DB->delete_records('event', array('modulename' => 'bigbluebuttonbn', 'instance' => $bigbluebuttonbn->id))) { + $result = false; + } + + // Log action performed. + bigbluebuttonbn_delete_instance_log($bigbluebuttonbn); + + return $result; +} + +/** + * Given an ID of an instance of this module, + * this function will permanently delete the data that depends on it. + * + * @param object $bigbluebuttonbn Id of the module instance + * + * @return bool Success/Failure + */ +function bigbluebuttonbn_delete_instance_log($bigbluebuttonbn) { + global $DB; + $sql = "SELECT * FROM {bigbluebuttonbn_logs} "; + $sql .= "WHERE bigbluebuttonbnid = ? AND log = ? AND ". $DB->sql_compare_text('meta') . " = ?"; + $logs = $DB->get_records_sql($sql, array($bigbluebuttonbn->id, BIGBLUEBUTTONBN_LOG_EVENT_CREATE, "{\"record\":true}")); + $meta = "{\"has_recordings\":" . empty($logs) ? "true" : "false" . "}"; + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTONBN_LOG_EVENT_DELETE, [], $meta); +} + +/** + * Return a small object with summary information about what a + * user has done with a given particular instance of this module + * Used for user activity reports. + * + * @param object $course + * @param object $user + * @param object $mod + * @param object $bigbluebuttonbn + * + * @return bool + */ +function bigbluebuttonbn_user_outline($course, $user, $mod, $bigbluebuttonbn) { + if ($completed = bigbluebuttonbn_user_complete($course, $user, $bigbluebuttonbn)) { + return fullname($user) . ' ' . get_string('view_message_has_joined', 'bigbluebuttonbn') . ' ' . + get_string('view_message_session_for', 'bigbluebuttonbn') . ' ' . (string) $completed . ' ' . + get_string('view_message_times', 'bigbluebuttonbn'); + } + return ''; +} + +/** + * Print a detailed representation of what a user has done with + * a given particular instance of this module, for user activity reports. + * + * @param object|int $courseorid + * @param object|int $userorid + * @param object $bigbluebuttonbn + * + * @return bool + */ +function bigbluebuttonbn_user_complete($courseorid, $userorid, $bigbluebuttonbn) { + global $DB; + if (is_object($courseorid)) { + $course = $courseorid; + } else { + $course = (object)array('id' => $courseorid); + } + if (is_object($userorid)) { + $user = $userorid; + } else { + $user = (object)array('id' => $userorid); + } + $sql = "SELECT COUNT(*) FROM {bigbluebuttonbn_logs} "; + $sql .= "WHERE courseid = ? AND bigbluebuttonbnid = ? AND userid = ? AND (log = ? OR log = ?)"; + $result = $DB->count_records_sql($sql, array($course->id, $bigbluebuttonbn->id, $user->id, + BIGBLUEBUTTONBN_LOG_EVENT_JOIN, BIGBLUEBUTTONBN_LOG_EVENT_PLAYED)); + return $result; +} + +/** + * Returns all other caps used in module. + * + * @return string[] + */ +function bigbluebuttonbn_get_extra_capabilities() { + return array('moodle/site:accessallgroups'); +} + +/** + * Define items to be reset by course/reset.php + * + * @return array + */ +function bigbluebuttonbn_reset_course_items() { + $items = array("events" => 0, "tags" => 0, "logs" => 0); + // Include recordings only if enabled. + if ((boolean)\mod_bigbluebuttonbn\locallib\config::recordings_enabled()) { + $items["recordings"] = 0; + } + return $items; +} + +/** + * Called by course/reset.php + * + * @param object $mform + * @return void + */ +function bigbluebuttonbn_reset_course_form_definition(&$mform) { + $items = bigbluebuttonbn_reset_course_items(); + $mform->addElement('header', 'bigbluebuttonbnheader', get_string('modulenameplural', 'bigbluebuttonbn')); + foreach ($items as $item => $default) { + $mform->addElement( + 'advcheckbox', + "reset_bigbluebuttonbn_{$item}", + get_string("reset{$item}", 'bigbluebuttonbn') + ); + if ($item == 'logs' || $item == 'recordings') { + $mform->addHelpButton("reset_bigbluebuttonbn_{$item}", "reset{$item}", 'bigbluebuttonbn'); + } + } +} + +/** + * Course reset form defaults. + * + * @param object $course + * @return array + */ +function bigbluebuttonbn_reset_course_form_defaults($course) { + $formdefaults = array(); + $items = bigbluebuttonbn_reset_course_items(); + // All unchecked by default. + foreach ($items as $item => $default) { + $formdefaults["reset_bigbluebuttonbn_{$item}"] = $default; + } + return $formdefaults; +} + +/** + * This function is used by the reset_course_userdata function in moodlelib. + * + * @param array $data the data submitted from the reset course. + * @return array status array + */ +function bigbluebuttonbn_reset_userdata($data) { + $items = bigbluebuttonbn_reset_course_items(); + $status = array(); + // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. + // See MDL-9367. + if (array_key_exists('recordings', $items) && !empty($data->reset_bigbluebuttonbn_recordings)) { + // Remove all the recordings from a BBB server that are linked to the room/activities in this course. + bigbluebuttonbn_reset_recordings($data->courseid); + unset($items['recordings']); + $status[] = bigbluebuttonbn_reset_getstatus('recordings'); + } + if (!empty($data->reset_bigbluebuttonbn_tags)) { + // Remove all the tags linked to the room/activities in this course. + bigbluebuttonbn_reset_tags($data->courseid); + unset($items['tags']); + $status[] = bigbluebuttonbn_reset_getstatus('tags'); + } + // TODO : seems to be duplicated code unless we just want to force reset tags. + foreach ($items as $item => $default) { + // Remove instances or elements linked to this course, others than recordings or tags. + if (!empty($data->{"reset_bigbluebuttonbn_{$item}"})) { + call_user_func("bigbluebuttonbn_reset_{$item}", $data->courseid); + $status[] = bigbluebuttonbn_reset_getstatus($item); + } + } + return $status; +} + +/** + * Returns status used on every defined reset action. + * + * @param string $item + * @return array status array + */ +function bigbluebuttonbn_reset_getstatus($item) { + return array('component' => get_string('modulenameplural', 'bigbluebuttonbn') + , 'item' => get_string("removed{$item}", 'bigbluebuttonbn') + , 'error' => false); +} + +/** + * Used by the reset_course_userdata for deleting events linked to bigbluebuttonbn instances in the course. + * + * @param string $courseid + * @return array status array + */ +function bigbluebuttonbn_reset_events($courseid) { + global $DB; + // Remove all the events. + return $DB->delete_records('event', array('modulename' => 'bigbluebuttonbn', 'courseid' => $courseid)); +} + +/** + * Used by the reset_course_userdata for deleting tags linked to bigbluebuttonbn instances in the course. + * + * @param array $courseid + * @return array status array + */ +function bigbluebuttonbn_reset_tags($courseid) { + global $DB; + // Remove all the tags linked to the room/activities in this course. + if ($bigbluebuttonbns = $DB->get_records('bigbluebuttonbn', array('course' => $courseid))) { + foreach ($bigbluebuttonbns as $bigbluebuttonbn) { + if (!$cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $courseid)) { + continue; + } + $context = context_module::instance($cm->id); + core_tag_tag::delete_instances('mod_bigbluebuttonbn', null, $context->id); + } + } +} + +/** + * Used by the reset_course_userdata for deleting bigbluebuttonbn_logs linked to bigbluebuttonbn instances in the course. + * + * @param string $courseid + * @return array status array + */ +function bigbluebuttonbn_reset_logs($courseid) { + global $DB; + // Remove all the logs. + return $DB->delete_records('bigbluebuttonbn_logs', array('courseid' => $courseid)); +} + +/** + * Used by the reset_course_userdata for deleting recordings in a BBB server linked to bigbluebuttonbn instances in the course. + * + * @param string $courseid + * @return array status array + */ +function bigbluebuttonbn_reset_recordings($courseid) { + require_once(__DIR__.'/locallib.php'); + // Criteria for search [courseid | bigbluebuttonbn=null | subset=false | includedeleted=true]. + $recordings = bigbluebuttonbn_get_recordings($courseid, null, false, true); + // Remove all the recordings. + bigbluebuttonbn_delete_recordings(implode(",", array_keys($recordings))); +} + +/** + * List of view style log actions. + * + * @return string[] + */ +function bigbluebuttonbn_get_view_actions() { + return array('view', 'view all'); +} + +/** + * List of update style log actions. + * + * @return string[] + */ +function bigbluebuttonbn_get_post_actions() { + return array('update', 'add', 'delete'); +} + +/** + * Print an overview of all bigbluebuttonbn instances for the courses. + * + * @param array $courses + * @param array $htmlarray Passed by reference + * + * @return void + */ +function bigbluebuttonbn_print_overview($courses, &$htmlarray) { + if (empty($courses) || !is_array($courses)) { + return array(); + } + $bns = get_all_instances_in_courses('bigbluebuttonbn', $courses); + foreach ($bns as $bn) { + $now = time(); + if ($bn->openingtime and (!$bn->closingtime or $bn->closingtime > $now)) { + // A bigbluebuttonbn is scheduled. + if (empty($htmlarray[$bn->course]['bigbluebuttonbn'])) { + $htmlarray[$bn->course]['bigbluebuttonbn'] = ''; + } + // Make sure we print all bigbluebutton instances. + $htmlarray[$bn->course]['bigbluebuttonbn'] .= bigbluebuttonbn_print_overview_element($bn, $now); + } + } +} + +/** + * Print an overview of a bigbluebuttonbn instance. + * + * @param array $bigbluebuttonbn + * @param int $now + * + * @return string + */ +function bigbluebuttonbn_print_overview_element($bigbluebuttonbn, $now) { + global $CFG; + $start = 'started_at'; + if ($bigbluebuttonbn->openingtime > $now) { + $start = 'starts_at'; + } + $classes = ''; + if ($bigbluebuttonbn->visible) { + $classes = 'class="dimmed" '; + } + $str = '<div class="bigbluebuttonbn overview">'."\n"; + $str .= ' <div class="name">'.get_string('modulename', 'bigbluebuttonbn').': '."\n"; + $str .= ' <a '.$classes.'href="'.$CFG->wwwroot.'/mod/bigbluebuttonbn/view.php?id='.$bigbluebuttonbn->coursemodule. + '">'.$bigbluebuttonbn->name.'</a>'."\n"; + $str .= ' </div>'."\n"; + $str .= ' <div class="info">'.get_string($start, 'bigbluebuttonbn').': '.userdate($bigbluebuttonbn->openingtime). + '</div>'."\n"; + if (!empty($bigbluebuttonbn->closingtime)) { + $str .= ' <div class="info">'.get_string('ends_at', 'bigbluebuttonbn').': '.userdate($bigbluebuttonbn->closingtime) + .'</div>'."\n"; + } + $str .= '</div>'."\n"; + return $str; +} + +/** + * Given a course_module object, this function returns any + * "extra" information that may be needed when printing + * this activity in a course listing. + * See get_array_of_activities() in course/lib.php. + * + * @param object $coursemodule + * + * @return null|cached_cm_info + */ +function bigbluebuttonbn_get_coursemodule_info($coursemodule) { + global $DB; + + $dbparams = ['id' => $coursemodule->instance]; + $fields = 'id, name, intro, introformat, completionattendance'; + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', $dbparams, $fields); + if (!$bigbluebuttonbn) { + return false; + } + $info = new cached_cm_info(); + $info->name = $bigbluebuttonbn->name; + if ($coursemodule->showdescription) { + // Convert intro to html. Do not filter cached version, filters run at display time. + $info->content = format_module_intro('bigbluebuttonbn', $bigbluebuttonbn, $coursemodule->id, false); + } + // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. + if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { + $info->customdata['customcompletionrules']['completionattendance'] = $bigbluebuttonbn->completionattendance; + } + + return $info; +} + +/** + * Callback which returns human-readable strings describing the active completion custom rules for the module instance. + * + * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] + * @return array $descriptions the array of descriptions for the custom rules. + */ +function mod_bigbluebuttonbn_get_completion_active_rule_descriptions($cm) { + // Values will be present in cm_info, and we assume these are up to date. + if (empty($cm->customdata['customcompletionrules']) + || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { + return []; + } + + $descriptions = []; + foreach ($cm->customdata['customcompletionrules'] as $key => $val) { + switch ($key) { + case 'completionattendance': + if (!empty($val)) { + $descriptions[] = get_string('completionattendancedesc', 'bigbluebuttonbn', $val); + $descriptions[] = get_string('completionengagementdesc', 'bigbluebuttonbn', $val); + } + break; + default: + break; + } + } + return $descriptions; +} + +/** + * Runs any processes that must run before a bigbluebuttonbn insert/update. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_pre_save(&$bigbluebuttonbn) { + bigbluebuttonbn_process_pre_save_instance($bigbluebuttonbn); + bigbluebuttonbn_process_pre_save_checkboxes($bigbluebuttonbn); + bigbluebuttonbn_process_pre_save_common($bigbluebuttonbn); + $bigbluebuttonbn->participants = htmlspecialchars_decode($bigbluebuttonbn->participants); +} + +/** + * Runs process for defining the instance (insert/update). + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_pre_save_instance(&$bigbluebuttonbn) { + require_once(__DIR__.'/locallib.php'); + $bigbluebuttonbn->timemodified = time(); + if ((integer)$bigbluebuttonbn->instance == 0) { + $bigbluebuttonbn->meetingid = 0; + $bigbluebuttonbn->timecreated = time(); + $bigbluebuttonbn->timemodified = 0; + // As it is a new activity, assign passwords. + $bigbluebuttonbn->moderatorpass = bigbluebuttonbn_random_password(12); + $bigbluebuttonbn->viewerpass = bigbluebuttonbn_random_password(12, $bigbluebuttonbn->moderatorpass); + } +} + +/** + * Runs process for assigning default value to checkboxes. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_pre_save_checkboxes(&$bigbluebuttonbn) { + if (!isset($bigbluebuttonbn->wait)) { + $bigbluebuttonbn->wait = 0; + } + if (!isset($bigbluebuttonbn->record)) { + $bigbluebuttonbn->record = 0; + } + if (!isset($bigbluebuttonbn->recordallfromstart)) { + $bigbluebuttonbn->recordallfromstart = 0; + } + if (!isset($bigbluebuttonbn->recordhidebutton)) { + $bigbluebuttonbn->recordhidebutton = 0; + } + if (!isset($bigbluebuttonbn->recordings_html)) { + $bigbluebuttonbn->recordings_html = 0; + } + if (!isset($bigbluebuttonbn->recordings_deleted)) { + $bigbluebuttonbn->recordings_deleted = 0; + } + if (!isset($bigbluebuttonbn->recordings_imported)) { + $bigbluebuttonbn->recordings_imported = 0; + } + if (!isset($bigbluebuttonbn->recordings_preview)) { + $bigbluebuttonbn->recordings_preview = 0; + } + if (!isset($bigbluebuttonbn->muteonstart)) { + $bigbluebuttonbn->muteonstart = 0; + } + if (!isset($bigbluebuttonbn->disablecam)) { + $bigbluebuttonbn->disablecam = 0; + } + if (!isset($bigbluebuttonbn->disablemic)) { + $bigbluebuttonbn->disablemic = 0; + } + if (!isset($bigbluebuttonbn->disableprivatechat)) { + $bigbluebuttonbn->disableprivatechat = 0; + } + if (!isset($bigbluebuttonbn->disablepublicchat)) { + $bigbluebuttonbn->disablepublicchat = 0; + } + if (!isset($bigbluebuttonbn->disablenote)) { + $bigbluebuttonbn->disablenote = 0; + } + if (!isset($bigbluebuttonbn->hideuserlist)) { + $bigbluebuttonbn->hideuserlist = 0; + } + if (!isset($bigbluebuttonbn->lockedlayout)) { + $bigbluebuttonbn->lockedlayout = 0; + } + if (!isset($bigbluebuttonbn->lockonjoin)) { + $bigbluebuttonbn->lockonjoin = 0; + } + if (!isset($bigbluebuttonbn->lockonjoinconfigurable)) { + $bigbluebuttonbn->lockonjoinconfigurable = 0; + } + if (!isset($bigbluebuttonbn->recordings_validate_url)) { + $bigbluebuttonbn->recordings_validate_url = 1; + } +} + +/** + * Runs process for wipping common settings when 'recordings only'. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_pre_save_common(&$bigbluebuttonbn) { + // Make sure common settings are removed when 'recordings only'. + if ($bigbluebuttonbn->type == BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY) { + $bigbluebuttonbn->groupmode = 0; + $bigbluebuttonbn->groupingid = 0; + } +} + +/** + * Runs any processes that must be run after a bigbluebuttonbn insert/update. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_post_save(&$bigbluebuttonbn) { + if (isset($bigbluebuttonbn->notification) && $bigbluebuttonbn->notification) { + bigbluebuttonbn_process_post_save_notification($bigbluebuttonbn); + } + bigbluebuttonbn_process_post_save_event($bigbluebuttonbn); + bigbluebuttonbn_process_post_save_completion($bigbluebuttonbn); +} + +/** + * Generates a message on insert/update which is sent to all users enrolled. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_post_save_notification(&$bigbluebuttonbn) { + $action = get_string('mod_form_field_notification_msg_modified', 'bigbluebuttonbn'); + if (isset($bigbluebuttonbn->add) && !empty($bigbluebuttonbn->add)) { + $action = get_string('mod_form_field_notification_msg_created', 'bigbluebuttonbn'); + } + \mod_bigbluebuttonbn\locallib\notifier::notify_instance_updated($bigbluebuttonbn, $action); +} + +/** + * Generates an event after a bigbluebuttonbn insert/update. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_post_save_event(&$bigbluebuttonbn) { + global $CFG, $DB; + require_once($CFG->dirroot.'/calendar/lib.php'); + $eventid = $DB->get_field('event', 'id', array('modulename' => 'bigbluebuttonbn', + 'instance' => $bigbluebuttonbn->id)); + // Delete the event from calendar when/if openingtime is NOT set. + if (!isset($bigbluebuttonbn->openingtime) || !$bigbluebuttonbn->openingtime) { + if ($eventid) { + $calendarevent = calendar_event::load($eventid); + $calendarevent->delete(); + } + return; + } + // Add evento to the calendar as openingtime is set. + $event = new stdClass(); + $event->eventtype = BIGBLUEBUTTON_EVENT_MEETING_START; + $event->type = CALENDAR_EVENT_TYPE_ACTION; + $event->name = get_string('calendarstarts', 'bigbluebuttonbn', $bigbluebuttonbn->name); + $event->description = format_module_intro('bigbluebuttonbn', $bigbluebuttonbn, $bigbluebuttonbn->coursemodule, false); + $event->format = FORMAT_HTML; + $event->courseid = $bigbluebuttonbn->course; + $event->groupid = 0; + $event->userid = 0; + $event->modulename = 'bigbluebuttonbn'; + $event->instance = $bigbluebuttonbn->id; + $event->timestart = $bigbluebuttonbn->openingtime; + $event->timeduration = 0; + $event->timesort = $event->timestart; + $event->visible = instance_is_visible('bigbluebuttonbn', $bigbluebuttonbn); + $event->priority = null; + // Update the event in calendar when/if eventid was found. + if ($eventid) { + $event->id = $eventid; + $calendarevent = calendar_event::load($eventid); + $calendarevent->update($event); + return; + } + calendar_event::create($event); +} + +/** + * Generates an event after a bigbluebuttonbn activity is completed. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return void + **/ +function bigbluebuttonbn_process_post_save_completion($bigbluebuttonbn) { + if (!empty($bigbluebuttonbn->completionexpected)) { + \core_completion\api::update_completion_date_event( + $bigbluebuttonbn->coursemodule, + 'bigbluebuttonbn', + $bigbluebuttonbn->id, + $bigbluebuttonbn->completionexpected + ); + } +} + +/** + * Get a full path to the file attached as a preuploaded presentation + * or if there is none, set the presentation field will be set to blank. + * + * @param object $bigbluebuttonbn BigBlueButtonBN form data + * + * @return string + */ +function bigbluebuttonbn_get_media_file(&$bigbluebuttonbn) { + if (!isset($bigbluebuttonbn->presentation) || $bigbluebuttonbn->presentation == '') { + return ''; + } + $context = context_module::instance($bigbluebuttonbn->coursemodule); + // Set the filestorage object. + $fs = get_file_storage(); + // Save the file if it exists that is currently in the draft area. + file_save_draft_area_files($bigbluebuttonbn->presentation, $context->id, 'mod_bigbluebuttonbn', 'presentation', 0); + // Get the file if it exists. + $files = $fs->get_area_files( + $context->id, + 'mod_bigbluebuttonbn', + 'presentation', + 0, + 'itemid, filepath, filename', + false + ); + // Check that there is a file to process. + $filesrc = ''; + if (count($files) == 1) { + // Get the first (and only) file. + $file = reset($files); + $filesrc = '/'.$file->get_filename(); + } + return $filesrc; +} + +/** + * Serves the bigbluebuttonbn attachments. Implements needed access control ;-). + * + * @category files + * + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param string $filearea file area + * @param array $args extra arguments + * @param bool $forcedownload whether or not force download + * @param array $options additional options affecting the file serving + * + * @return false|null false if file not found, does not return if found - justsend the file + */ +function bigbluebuttonbn_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + if (!bigbluebuttonbn_pluginfile_valid($context, $filearea)) { + return false; + } + $file = bigbluebuttonbn_pluginfile_file($course, $cm, $context, $filearea, $args); + if (empty($file)) { + return false; + } + // Finally send the file. + send_stored_file($file, 0, 0, $forcedownload, $options); // download MUST be forced - security! +} + +/** + * Helper for validating pluginfile. + * @param stdClass $context context object + * @param string $filearea file area + * + * @return false|null false if file not valid + */ +function bigbluebuttonbn_pluginfile_valid($context, $filearea) { + + // Can be in context module or in context_system (if is the presentation by default). + if (!in_array($context->contextlevel, array(CONTEXT_MODULE, CONTEXT_SYSTEM))) { + return false; + } + + if (!array_key_exists($filearea, bigbluebuttonbn_get_file_areas())) { + return false; + } + + return true; +} + +/** + * Helper for getting pluginfile. + * + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param string $filearea file area + * @param array $args extra arguments + * + * @return object + */ +function bigbluebuttonbn_pluginfile_file($course, $cm, $context, $filearea, $args) { + $filename = bigbluebuttonbn_pluginfile_filename($course, $cm, $context, $args); + if (!$filename) { + return false; + } + $fullpath = "/$context->id/mod_bigbluebuttonbn/$filearea/0/".$filename; + $fs = get_file_storage(); + $file = $fs->get_file_by_hash(sha1($fullpath)); + if (!$file || $file->is_directory()) { + return false; + } + return $file; +} + +/** + * Helper for give access to the file configured in setting as default presentation. + * + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param array $args extra arguments + * + * @return array + */ +function bigbluebuttonbn_default_presentation_get_file($course, $cm, $context, $args) { + + // The difference with the standard bigbluebuttonbn_pluginfile_filename() are. + // - Context is system, so we don't need to check the cmid in this case. + // - The area is "presentationdefault_cache". + if (count($args) > 1) { + $cache = cache::make_from_params( + cache_store::MODE_APPLICATION, + 'mod_bigbluebuttonbn', + 'presentationdefault_cache' + ); + + $noncekey = sha1($context->id); + $presentationnonce = $cache->get($noncekey); + $noncevalue = $presentationnonce['value']; + $noncecounter = $presentationnonce['counter']; + if ($args['0'] != $noncevalue) { + return; + } + + // The nonce value is actually used twice because BigBlueButton reads the file two times. + $noncecounter += 1; + $cache->set($noncekey, array('value' => $noncevalue, 'counter' => $noncecounter)); + if ($noncecounter == 2) { + $cache->delete($noncekey); + } + return($args['1']); + } + require_course_login($course, true, $cm); + if (!has_capability('mod/bigbluebuttonbn:join', $context)) { + return; + } + return implode('/', $args); +} + +/** + * Helper for getting pluginfile name. + * + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param array $args extra arguments + * + * @return array + */ +function bigbluebuttonbn_pluginfile_filename($course, $cm, $context, $args) { + global $DB; + + if ($context->contextlevel == CONTEXT_SYSTEM) { + // Plugin has a file to use as default in general setting. + return(bigbluebuttonbn_default_presentation_get_file($course, $cm, $context, $args)); + } + + if (count($args) > 1) { + if (!$bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $cm->instance))) { + return; + } + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'presentation_cache'); + $noncekey = sha1($bigbluebuttonbn->id); + $presentationnonce = $cache->get($noncekey); + if (!empty($presentationnonce)) { + $noncevalue = $presentationnonce['value']; + $noncecounter = $presentationnonce['counter']; + } else { + $noncevalue = null; + $noncecounter = 0; + } + + if ($args['0'] != $noncevalue) { + return; + } + // The nonce value is actually used twice because BigBlueButton reads the file two times. + $noncecounter += 1; + $cache->set($noncekey, array('value' => $noncevalue, 'counter' => $noncecounter)); + if ($noncecounter == 2) { + $cache->delete($noncekey); + } + return $args['1']; + } + require_course_login($course, true, $cm); + if (!has_capability('mod/bigbluebuttonbn:join', $context)) { + return; + } + return implode('/', $args); +} + +/** + * Returns an array of file areas. + * + * @category files + * + * @return array a list of available file areas + */ +function bigbluebuttonbn_get_file_areas() { + $areas = array(); + $areas['presentation'] = get_string('mod_form_block_presentation', 'bigbluebuttonbn'); + $areas['presentationdefault'] = get_string('mod_form_block_presentation_default', 'bigbluebuttonbn'); + return $areas; +} + +/** + * Mark the activity completed (if required) and trigger the course_module_viewed event. + * + * @param stdClass $bigbluebuttonbn bigbluebuttonbn object + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @since Moodle 3.0 + */ +function bigbluebuttonbn_view($bigbluebuttonbn, $course, $cm, $context) { + + // Trigger course_module_viewed event. + $params = array( + 'context' => $context, + 'objectid' => $bigbluebuttonbn->id + ); + + $event = \mod_bigbluebuttonbn\event\activity_viewed::create($params); // Fix event name. + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('bigbluebuttonbn', $bigbluebuttonbn); + $event->trigger(); + + // Completion. + $completion = new completion_info($course); + $completion->set_module_viewed($cm); +} + +/** + * Check if the module has any update that affects the current user since a given time. + * + * @param cm_info $cm course module data + * @param int $from the time to check updates from + * @param array $filter if we need to check only specific updates + * @return stdClass an object with the different type of areas indicating if they were updated or not + * @since Moodle 3.2 + */ +function bigbluebuttonbn_check_updates_since(cm_info $cm, $from, $filter = array()) { + $updates = course_check_module_updates_since($cm, $from, array('content'), $filter); + return $updates; +} + + +/** + * Get icon mapping for font-awesome. + */ +function mod_bigbluebuttonbn_get_fontawesome_icon_map() { + return [ + 'mod_bigbluebuttonbn:icon' => 'icon-bigbluebutton', + ]; +} + +/** + * This function receives a calendar event and returns the action associated with it, or null if there is none. + * + * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event + * is not displayed on the block. + * + * @param calendar_event $event + * @param \core_calendar\action_factory $factory + * @return \core_calendar\local\event\entities\action_interface|null + */ +function mod_bigbluebuttonbn_core_calendar_provide_event_action( + calendar_event $event, + \core_calendar\action_factory $factory +) { + global $CFG, $DB; + + require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + + // Get mod info. + $cm = get_fast_modinfo($event->courseid)->instances['bigbluebuttonbn'][$event->instance]; + + // Get bigbluebuttonbn activity. + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $event->instance), '*', MUST_EXIST); + + // Get if the user has joined in live session or viewed the recorded. + $usercomplete = bigbluebuttonbn_user_complete($event->courseid, $event->userid, $bigbluebuttonbn); + // Get if the room is available. + list($roomavailable) = bigbluebuttonbn_room_is_available($bigbluebuttonbn); + // Get if the user can join. + list($usercanjoin) = bigbluebuttonbn_user_can_join_meeting($bigbluebuttonbn); + // Get if the time has already passed. + $haspassed = $bigbluebuttonbn->openingtime < time(); + + // Check if the room is closed and the user has already joined this session or played the record. + if ($haspassed && !$roomavailable && $usercomplete) { + return null; + } + + // Check if the user can join this session. + $actionable = ($roomavailable && $usercanjoin) || $haspassed; + + // Action data. + $string = get_string('view_room', 'bigbluebuttonbn'); + $url = new \moodle_url('/mod/bigbluebuttonbn/view.php', array('id' => $cm->id)); + if (groups_get_activity_groupmode($cm) == NOGROUPS) { + // No groups mode. + $string = get_string('view_conference_action_join', 'bigbluebuttonbn'); + $url = new \moodle_url('/mod/bigbluebuttonbn/bbb_view.php', array('action' => 'join', + 'id' => $cm->id, 'bn' => $bigbluebuttonbn->id, 'timeline' => 1)); + } + + return $factory->create_instance($string, $url, 1, $actionable); +} + +/** + * Register a bigbluebuttonbn event + * + * @param object $bigbluebuttonbn + * @param string $event + * @param array $overrides + * @param string $meta + * + * @return bool Success/Failure + */ +function bigbluebuttonbn_log($bigbluebuttonbn, $event, array $overrides = [], $meta = null) { + global $DB, $USER; + $log = new stdClass(); + // Default values. + $log->courseid = $bigbluebuttonbn->course; + $log->bigbluebuttonbnid = $bigbluebuttonbn->id; + $log->userid = $USER->id; + $log->meetingid = $bigbluebuttonbn->meetingid; + $log->timecreated = time(); + $log->log = $event; + $log->meta = $meta; + // Overrides. + foreach ($overrides as $key => $value) { + $log->$key = $value; + } + if (!$DB->insert_record('bigbluebuttonbn_logs', $log)) { + return false; + } + return true; +} + +/** + * Adds module specific settings to the settings block + * + * @param settings_navigation $settingsnav The settings navigation object + * @param navigation_node $nodenav The node to add module settings to + */ +function bigbluebuttonbn_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $nodenav) { + global $PAGE, $USER; + // Don't add validate completion if the callback for meetingevents is NOT enabled. + if (!(boolean)\mod_bigbluebuttonbn\locallib\config::get('meetingevents_enabled')) { + return; + } + // Don't add validate completion if user is not allowed to edit the activity. + $context = context_module::instance($PAGE->cm->id); + if (!has_capability('moodle/course:manageactivities', $context, $USER->id)) { + return; + } + $completionvalidate = '#action=completion_validate&bigbluebuttonbn=' . $PAGE->cm->instance; + $nodenav->add(get_string('completionvalidatestate', 'bigbluebuttonbn'), + $completionvalidate, navigation_node::TYPE_CONTAINER); +} diff --git a/mod/bigbluebuttonbn/locallib.php b/mod/bigbluebuttonbn/locallib.php new file mode 100644 index 0000000..679a903 --- /dev/null +++ b/mod/bigbluebuttonbn/locallib.php @@ -0,0 +1,3664 @@ +<?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/>. + +/** + * Internal library of functions for module BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +use mod_bigbluebuttonbn\locallib; +use mod_bigbluebuttonbn\plugin; +use mod_bigbluebuttonbn\task; + +defined('MOODLE_INTERNAL') || die; + +global $CFG; + +require_once(__DIR__ . '/lib.php'); + +/** @var BIGBLUEBUTTONBN_UPDATE_CACHE boolean set to true indicates that cache has to be updated */ +const BIGBLUEBUTTONBN_UPDATE_CACHE = true; +/** @var BIGBLUEBUTTONBN_TYPE_ALL integer set to 0 defines an instance type that inclueds room and recordings */ +const BIGBLUEBUTTONBN_TYPE_ALL = 0; +/** @var BIGBLUEBUTTONBN_TYPE_ROOM_ONLY integer set to 1 defines an instance type that inclueds only room */ +const BIGBLUEBUTTONBN_TYPE_ROOM_ONLY = 1; +/** @var BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY integer set to 2 defines an instance type that inclueds only recordings */ +const BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY = 2; +/** @var BIGBLUEBUTTONBN_ROLE_VIEWER string defines the bigbluebutton viewer role */ +const BIGBLUEBUTTONBN_ROLE_VIEWER = 'viewer'; +/** @var BIGBLUEBUTTONBN_ROLE_MODERATOR string defines the bigbluebutton moderator role */ +const BIGBLUEBUTTONBN_ROLE_MODERATOR = 'moderator'; +/** @var BIGBLUEBUTTON_EVENT_ACTIVITY_VIEWED string defines the bigbluebuttonbn activity_viewed event */ +const BIGBLUEBUTTON_EVENT_ACTIVITY_VIEWED = 'activity_viewed'; +/** @var BIGBLUEBUTTON_EVENT_ACTIVITY_MANAGEMENT_VIEWED string defines the bigbluebuttonbn activity_management_viewed event */ +const BIGBLUEBUTTON_EVENT_ACTIVITY_MANAGEMENT_VIEWED = 'activity_management_viewed'; +/** @var BIGBLUEBUTTON_EVENT_LIVE_SESSION string defines the bigbluebuttonbn live_session event */ +const BIGBLUEBUTTON_EVENT_LIVE_SESSION = 'live_session'; +/** @var BIGBLUEBUTTON_EVENT_MEETING_CREATED string defines the bigbluebuttonbn meeting_created event */ +const BIGBLUEBUTTON_EVENT_MEETING_CREATED = 'meeting_created'; +/** @var BIGBLUEBUTTON_EVENT_MEETING_ENDED string defines the bigbluebuttonbn meeting_ended event */ +const BIGBLUEBUTTON_EVENT_MEETING_ENDED = 'meeting_ended'; +/** @var BIGBLUEBUTTON_EVENT_MEETING_JOINED string defines the bigbluebuttonbn meeting_joined event */ +const BIGBLUEBUTTON_EVENT_MEETING_JOINED = 'meeting_joined'; +/** @var BIGBLUEBUTTON_EVENT_MEETING_LEFT string defines the bigbluebuttonbn meeting_left event */ +const BIGBLUEBUTTON_EVENT_MEETING_LEFT = 'meeting_left'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_DELETED string defines the bigbluebuttonbn recording_deleted event */ +const BIGBLUEBUTTON_EVENT_RECORDING_DELETED = 'recording_deleted'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_IMPORTED string defines the bigbluebuttonbn recording_imported event */ +const BIGBLUEBUTTON_EVENT_RECORDING_IMPORTED = 'recording_imported'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_PROTECTED string defines the bigbluebuttonbn recording_protected event */ +const BIGBLUEBUTTON_EVENT_RECORDING_PROTECTED = 'recording_protected'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_PUBLISHED string defines the bigbluebuttonbn recording_published event */ +const BIGBLUEBUTTON_EVENT_RECORDING_PUBLISHED = 'recording_published'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_UNPROTECTED string defines the bigbluebuttonbn recording_unprotected event */ +const BIGBLUEBUTTON_EVENT_RECORDING_UNPROTECTED = 'recording_unprotected'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_UNPUBLISHED string defines the bigbluebuttonbn recording_unpublished event */ +const BIGBLUEBUTTON_EVENT_RECORDING_UNPUBLISHED = 'recording_unpublished'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_EDITED string defines the bigbluebuttonbn recording_edited event */ +const BIGBLUEBUTTON_EVENT_RECORDING_EDITED = 'recording_edited'; +/** @var BIGBLUEBUTTON_EVENT_RECORDING_VIEWED string defines the bigbluebuttonbn recording_viewed event */ +const BIGBLUEBUTTON_EVENT_RECORDING_VIEWED = 'recording_viewed'; +/** @var BIGBLUEBUTTON_EVENT_MEETING_START string defines the bigbluebuttonbn meeting_start event */ +const BIGBLUEBUTTON_EVENT_MEETING_START = 'meeting_start'; +/** @var BIGBLUEBUTTON_CLIENTTYPE_FLASH integer that defines the bigbluebuttonbn default web client based on Adobe FLASH */ +const BIGBLUEBUTTON_CLIENTTYPE_FLASH = 0; +/** @var BIGBLUEBUTTON_CLIENTTYPE_HTML5 integer that defines the bigbluebuttonbn default web client based on HTML5 */ +const BIGBLUEBUTTON_CLIENTTYPE_HTML5 = 1; +/** @var BIGBLUEBUTTON_ORIGIN_BASE integer set to 0 defines that the user acceded the session from activity page */ +const BIGBLUEBUTTON_ORIGIN_BASE = 0; +/** @var BIGBLUEBUTTON_ORIGIN_TIMELINE integer set to 1 defines that the user acceded the session from Timeline */ +const BIGBLUEBUTTON_ORIGIN_TIMELINE = 1; +/** @var BIGBLUEBUTTON_ORIGIN_INDEX integer set to 2 defines that the user acceded the session from Index */ +const BIGBLUEBUTTON_ORIGIN_INDEX = 2; + +/** + * Builds and retunrs a url for joining a bigbluebutton meeting. + * + * @param string $meetingid + * @param string $username + * @param string $pw + * @param string $logouturl + * @param string $configtoken + * @param string $userid + * @param string $clienttype + * @param string $createtime + * + * @return string + */ +function bigbluebuttonbn_get_join_url( + $meetingid, + $username, + $pw, + $logouturl, + $configtoken = null, + $userid = null, + $clienttype = BIGBLUEBUTTON_CLIENTTYPE_FLASH, + $createtime = null +) { + $data = ['meetingID' => $meetingid, + 'fullName' => $username, + 'password' => $pw, + 'logoutURL' => $logouturl, + ]; + // Choose between Adobe Flash or HTML5 Client. + if ($clienttype == BIGBLUEBUTTON_CLIENTTYPE_HTML5) { + $data['joinViaHtml5'] = 'true'; + } + if (!is_null($configtoken)) { + $data['configToken'] = $configtoken; + } + if (!is_null($userid)) { + $data['userID'] = $userid; + } + if (!is_null($createtime)) { + $data['createTime'] = $createtime; + } + return \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('join', $data); +} + +/** + * Creates a bigbluebutton meeting and returns the response in an array. + * + * @param array $data + * @param array $metadata + * @param string $pname + * @param string $purl + * + * @return array + */ +function bigbluebuttonbn_get_create_meeting_array($data, $metadata = array(), $pname = null, $purl = null) { + $createmeetingurl = \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('create', $data, $metadata); + $method = 'GET'; + $payload = null; + if (!is_null($pname) && !is_null($purl)) { + $method = 'POST'; + $payload = "<?xml version='1.0' encoding='UTF-8'?><modules><module name='presentation'><document url='". + $purl."' /></module></modules>"; + } + $xml = bigbluebuttonbn_wrap_xml_load_file($createmeetingurl, $method, $payload); + if ($xml) { + $response = array('returncode' => $xml->returncode, 'message' => $xml->message, 'messageKey' => $xml->messageKey); + if ($xml->meetingID) { + $response += array('meetingID' => $xml->meetingID, 'attendeePW' => $xml->attendeePW, + 'moderatorPW' => $xml->moderatorPW, 'hasBeenForciblyEnded' => $xml->hasBeenForciblyEnded); + } + return $response; + } + return array('returncode' => 'FAILED', 'message' => 'unreachable', 'messageKey' => 'Server is unreachable'); +} + +/** + * Fetch meeting info and wrap response in array. + * + * @param string $meetingid + * + * @return array + */ +function bigbluebuttonbn_get_meeting_info_array($meetingid) { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('getMeetingInfo', ['meetingID' => $meetingid]) + ); + if ($xml && $xml->returncode == 'SUCCESS' && empty($xml->messageKey)) { + // Meeting info was returned. + return array('returncode' => $xml->returncode, + 'meetingID' => $xml->meetingID, + 'moderatorPW' => $xml->moderatorPW, + 'attendeePW' => $xml->attendeePW, + 'hasBeenForciblyEnded' => $xml->hasBeenForciblyEnded, + 'running' => $xml->running, + 'recording' => $xml->recording, + 'startTime' => $xml->startTime, + 'endTime' => $xml->endTime, + 'participantCount' => $xml->participantCount, + 'moderatorCount' => $xml->moderatorCount, + 'attendees' => $xml->attendees, + 'metadata' => $xml->metadata, + ); + } + if ($xml) { + // Either failure or success without meeting info. + return (array) $xml; + } + // If the server is unreachable, then prompts the user of the necessary action. + return array('returncode' => 'FAILED', 'message' => 'unreachable', 'messageKey' => 'Server is unreachable'); +} + +/** + * Helper function to retrieve recordings from a BigBlueButton server. + * + * @param string|array $meetingids list of meetingIDs "mid1,mid2,mid3" or array("mid1","mid2","mid3") + * @param string|array $recordingids list of $recordingids "rid1,rid2,rid3" or array("rid1","rid2","rid3") for filtering + * + * @return associative array with recordings indexed by recordID, each recording is a non sequential associative array + */ +function bigbluebuttonbn_get_recordings_array($meetingids, $recordingids = []) { + $meetingidsarray = $meetingids; + if (!is_array($meetingids)) { + $meetingidsarray = explode(',', $meetingids); + } + // If $meetingidsarray is empty there is no need to go further. + if (empty($meetingidsarray)) { + return array(); + } + $recordings = bigbluebuttonbn_get_recordings_array_fetch($meetingidsarray); + // Sort recordings. + uasort($recordings, 'bigbluebuttonbn_recording_build_sorter'); + // Filter recordings based on recordingIDs. + $recordingidsarray = $recordingids; + if (!is_array($recordingids)) { + $recordingidsarray = explode(',', $recordingids); + } + if (empty($recordingidsarray)) { + // No recording ids, no need to filter. + return $recordings; + } + return bigbluebuttonbn_get_recordings_array_filter($recordingidsarray, $recordings); +} + +/** + * Helper function to fetch recordings from a BigBlueButton server. + * + * @param array $meetingidsarray array with meeting ids in the form array("mid1","mid2","mid3") + * + * @return array (associative) with recordings indexed by recordID, each recording is a non sequential associative array + */ +function bigbluebuttonbn_get_recordings_array_fetch($meetingidsarray) { + if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST) + || defined('BEHAT_SITE_RUNNING') + || defined('BEHAT_TEST') + || defined('BEHAT_UTIL')) { + // Just return the fake recording. + global $CFG; + require_once($CFG->libdir . '/testing/generator/lib.php'); + require_once(__DIR__ . '/tests/generator/lib.php'); + return mod_bigbluebuttonbn_generator::bigbluebuttonbn_get_recordings_array_fetch($meetingidsarray); + } + $recordings = array(); + // Execute a paginated getRecordings request. + $pagecount = 25; + $pages = floor(count($meetingidsarray) / $pagecount) + 1; + if (count($meetingidsarray) > 0 && count($meetingidsarray) % $pagecount == 0) { + $pages--; + } + for ($page = 1; $page <= $pages; ++$page) { + $mids = array_slice($meetingidsarray, ($page - 1) * $pagecount, $pagecount); + $recordings += bigbluebuttonbn_get_recordings_array_fetch_page($mids); + } + return $recordings; +} + +/** + * Helper function to fetch one page of upto 25 recordings from a BigBlueButton server. + * + * @param array $mids + * + * @return array + */ +function bigbluebuttonbn_get_recordings_array_fetch_page($mids) { + $recordings = array(); + // Do getRecordings is executed using a method GET (supported by all versions of BBB). + $url = \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('getRecordings', ['meetingID' => implode(',', $mids)]); + $xml = bigbluebuttonbn_wrap_xml_load_file($url); + if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) { + // If there were meetings already created. + foreach ($xml->recordings->recording as $recordingxml) { + $recording = bigbluebuttonbn_get_recording_array_value($recordingxml); + $recordings[$recording['recordID']] = $recording; + + // Check if there is childs. + if (isset($recordingxml->breakoutRooms->breakoutRoom)) { + foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) { + $url = \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url( + 'getRecordings', + ['recordID' => implode(',', (array) $breakoutroom)] + ); + $xml = bigbluebuttonbn_wrap_xml_load_file($url); + if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) { + // If there were meetings already created. + foreach ($xml->recordings->recording as $recordingxml) { + $recording = bigbluebuttonbn_get_recording_array_value($recordingxml); + $recordings[$recording['recordID']] = $recording; + } + } + } + } + } + } + return $recordings; +} + +/** + * Helper function to remove a set of recordings from an array. + * + * @param array $rids + * @param array $recordings + * + * @return array + */ +function bigbluebuttonbn_get_recordings_array_filter($rids, &$recordings) { + foreach ($recordings as $key => $recording) { + if (!in_array($recording['recordID'], $rids)) { + unset($recordings[$key]); + } + } + return $recordings; +} + +/** + * Helper function to retrieve imported recordings from the Moodle database. + * The references are stored as events in bigbluebuttonbn_logs. + * + * @param string $courseid + * @param string $bigbluebuttonbnid + * @param bool $subset + * + * @return associative array with imported recordings indexed by recordID, each recording + * is a non sequential associative array that corresponds to the actual recording in BBB + */ +function bigbluebuttonbn_get_recordings_imported_array($courseid = 0, $bigbluebuttonbnid = null, $subset = true) { + global $DB; + $select = bigbluebuttonbn_get_recordings_imported_sql_select($courseid, $bigbluebuttonbnid, $subset); + $recordsimported = $DB->get_records_select('bigbluebuttonbn_logs', $select); + $recordsimportedarray = array(); + foreach ($recordsimported as $recordimported) { + $meta = json_decode($recordimported->meta, true); + $recording = $meta['recording']; + // Override imported flag with actual ID. + $recording['imported'] = $recordimported->id; + if (isset($recordimported->protected)) { + $recording['protected'] = (string) $recordimported->protected; + } + $recordsimportedarray[$recording['recordID']] = $recording; + } + return $recordsimportedarray; +} + +/** + * Helper function to retrive the default config.xml file. + * + * @return string + */ +function bigbluebuttonbn_get_default_config_xml() { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('getDefaultConfigXML') + ); + return $xml; +} + +/** + * Helper function to convert an xml recording object to an array in the format used by the plugin. + * + * @param object $recording + * + * @return array + */ +function bigbluebuttonbn_get_recording_array_value($recording) { + // Add formats. + $playbackarray = array(); + foreach ($recording->playback->format as $format) { + $playbackarray[(string) $format->type] = array('type' => (string) $format->type, + 'url' => trim((string) $format->url), 'length' => (string) $format->length); + // Add preview per format when existing. + if ($format->preview) { + $playbackarray[(string) $format->type]['preview'] = bigbluebuttonbn_get_recording_preview_images($format->preview); + } + } + // Add the metadata to the recordings array. + $metadataarray = bigbluebuttonbn_get_recording_array_meta(get_object_vars($recording->metadata)); + $recordingarray = array('recordID' => (string) $recording->recordID, + 'meetingID' => (string) $recording->meetingID, 'meetingName' => (string) $recording->name, + 'published' => (string) $recording->published, 'startTime' => (string) $recording->startTime, + 'endTime' => (string) $recording->endTime, 'playbacks' => $playbackarray); + if (isset($recording->protected)) { + $recordingarray['protected'] = (string) $recording->protected; + } + return $recordingarray + $metadataarray; +} + +/** + * Helper function to convert an xml recording preview images to an array in the format used by the plugin. + * + * @param object $preview + * + * @return array + */ +function bigbluebuttonbn_get_recording_preview_images($preview) { + $imagesarray = array(); + foreach ($preview->images->image as $image) { + $imagearray = array('url' => trim((string) $image)); + foreach ($image->attributes() as $attkey => $attvalue) { + $imagearray[$attkey] = (string) $attvalue; + } + array_push($imagesarray, $imagearray); + } + return $imagesarray; +} + +/** + * Helper function to convert an xml recording metadata object to an array in the format used by the plugin. + * + * @param array $metadata + * + * @return array + */ +function bigbluebuttonbn_get_recording_array_meta($metadata) { + $metadataarray = array(); + foreach ($metadata as $key => $value) { + if (is_object($value)) { + $value = ''; + } + $metadataarray['meta_' . $key] = $value; + } + return $metadataarray; +} + +/** + * Helper function to sort an array of recordings. It compares the startTime in two recording objecs. + * + * @param object $a + * @param object $b + * + * @return array + */ +function bigbluebuttonbn_recording_build_sorter($a, $b) { + global $CFG; + $resultless = !empty($CFG->bigbluebuttonbn_recordings_sortorder) ? -1 : 1; + $resultmore = !empty($CFG->bigbluebuttonbn_recordings_sortorder) ? 1 : -1; + if ($a['startTime'] < $b['startTime']) { + return $resultless; + } + if ($a['startTime'] == $b['startTime']) { + return 0; + } + return $resultmore; +} + +/** + * Perform deleteRecordings on BBB. + * + * @param string $recordids + * + * @return boolean + */ +function bigbluebuttonbn_delete_recordings($recordids) { + $ids = explode(',', $recordids); + foreach ($ids as $id) { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('deleteRecordings', ['recordID' => $id]) + ); + if ($xml && $xml->returncode != 'SUCCESS') { + return false; + } + } + return true; +} + +/** + * Perform publishRecordings on BBB. + * + * @param string $recordids + * @param string $publish + */ +function bigbluebuttonbn_publish_recordings($recordids, $publish = 'true') { + $ids = explode(',', $recordids); + foreach ($ids as $id) { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('publishRecordings', ['recordID' => $id, 'publish' => $publish]) + ); + if ($xml && $xml->returncode != 'SUCCESS') { + return false; + } + } + return true; +} + +/** + * Perform updateRecordings on BBB. + * + * @param string $recordids + * @param array $params ['key'=>param_key, 'value'] + */ +function bigbluebuttonbn_update_recordings($recordids, $params) { + $ids = explode(',', $recordids); + foreach ($ids as $id) { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('updateRecordings', ['recordID' => $id] + (array) $params) + ); + if ($xml && $xml->returncode != 'SUCCESS') { + return false; + } + } + return true; +} + +/** + * Perform end on BBB. + * + * @param string $meetingid + * @param string $modpw + */ +function bigbluebuttonbn_end_meeting($meetingid, $modpw) { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('end', ['meetingID' => $meetingid, 'password' => $modpw]) + ); + if ($xml) { + // If the xml packet returned failure it displays the message to the user. + return array('returncode' => $xml->returncode, 'message' => $xml->message, 'messageKey' => $xml->messageKey); + } + // If the server is unreachable, then prompts the user of the necessary action. + return null; +} + +/** + * Perform api request on BBB. + * + * @return string + */ +function bigbluebuttonbn_get_server_version() { + $xml = bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url() + ); + if ($xml && $xml->returncode == 'SUCCESS') { + return $xml->version; + } + return null; +} + +/** + * Perform api request on BBB and wraps the response in an XML object + * + * @param string $url + * @param string $method + * @param string $data + * @param string $contenttype + * + * @return object + */ +function bigbluebuttonbn_wrap_xml_load_file($url, $method = 'GET', $data = null, $contenttype = 'text/xml') { + if (extension_loaded('curl')) { + $response = bigbluebuttonbn_wrap_xml_load_file_curl_request($url, $method, $data, $contenttype); + if (!$response) { + debugging('No response on wrap_simplexml_load_file', DEBUG_DEVELOPER); + return null; + } + $previous = libxml_use_internal_errors(true); + try { + $xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); + return $xml; + } catch (Exception $e) { + libxml_use_internal_errors($previous); + $error = 'Caught exception: ' . $e->getMessage(); + debugging($error, DEBUG_DEVELOPER); + return null; + } + } + // Alternative request non CURL based. + $previous = libxml_use_internal_errors(true); + try { + $response = simplexml_load_file($url, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS); + return $response; + } catch (Exception $e) { + $error = 'Caught exception: ' . $e->getMessage(); + debugging($error, DEBUG_DEVELOPER); + libxml_use_internal_errors($previous); + return null; + } +} + +/** + * Perform api request on BBB using CURL and wraps the response in an XML object + * + * @param string $url + * @param string $method + * @param string $data + * @param string $contenttype + * + * @return object + */ +function bigbluebuttonbn_wrap_xml_load_file_curl_request($url, $method = 'GET', $data = null, $contenttype = 'text/xml') { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + $c = new curl(); + $c->setopt(array('SSL_VERIFYPEER' => true)); + if ($method == 'POST') { + if (is_null($data) || is_array($data)) { + return $c->post($url); + } + $options = array(); + $options['CURLOPT_HTTPHEADER'] = array( + 'Content-Type: ' . $contenttype, + 'Content-Length: ' . strlen($data), + 'Content-Language: en-US', + ); + + return $c->post($url, $data, $options); + } + if ($method == 'HEAD') { + $c->head($url, array('followlocation' => true, 'timeout' => 1)); + return $c->get_info(); + } + return $c->get($url); +} + +/** + * End the session associated with this instance (if it's running). + * + * @param object $bigbluebuttonbn + * + * @return void + */ +function bigbluebuttonbn_end_meeting_if_running($bigbluebuttonbn) { + $meetingid = $bigbluebuttonbn->meetingid . '-' . $bigbluebuttonbn->course . '-' . $bigbluebuttonbn->id; + if (bigbluebuttonbn_is_meeting_running($meetingid)) { + bigbluebuttonbn_end_meeting($meetingid, $bigbluebuttonbn->moderatorpass); + } +} + +/** + * Returns user roles in a context. + * + * @param object $context + * @param integer $userid + * + * @return array $userroles + */ +function bigbluebuttonbn_get_user_roles($context, $userid) { + global $DB; + $userroles = get_user_roles($context, $userid); + if ($userroles) { + $where = ''; + foreach ($userroles as $userrole) { + $where .= (empty($where) ? ' WHERE' : ' OR') . ' id=' . $userrole->roleid; + } + $userroles = $DB->get_records_sql('SELECT * FROM {role}' . $where); + } + return $userroles; +} + +/** + * Returns guest role wrapped in an array. + * + * @return array + */ +function bigbluebuttonbn_get_guest_role() { + $guestrole = get_guest_role(); + return array($guestrole->id => $guestrole); +} + +/** + * Returns an array containing all the users in a context. + * + * @param context $context + * + * @return array $users + */ +function bigbluebuttonbn_get_users(context $context = null) { + $users = (array) get_enrolled_users($context, '', 0, 'u.*', null, 0, 0, true); + foreach ($users as $key => $value) { + $users[$key] = fullname($value); + } + return $users; +} + +/** + * Returns an array containing all the users in a context wrapped for html select element. + * + * @param context_course $context + * @param null $bbactivity + * @return array $users + * @throws coding_exception + * @throws moodle_exception + */ +function bigbluebuttonbn_get_users_select(context_course $context, $bbactivity = null) { + // CONTRIB-7972, check the group of current user and course group mode. + $groups = null; + $users = (array) get_enrolled_users($context, '', 0, 'u.*', null, 0, 0, true); + $course = get_course($context->instanceid); + $groupmode = groups_get_course_groupmode($course); + if ($bbactivity) { + list($bbcourse, $cm) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); + $groupmode = groups_get_activity_groupmode($cm); + + } + if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { + global $USER; + $groups = groups_get_all_groups($course->id, $USER->id); + $users = []; + foreach ($groups as $g) { + $users += (array) get_enrolled_users($context, '', $g->id, 'u.*', null, 0, 0, true); + } + } + return array_map( + function($u) { + return array('id' => $u->id, 'name' => fullname($u)); + }, + $users); +} + +/** + * Returns an array containing all the roles in a context. + * + * @param context $context + * @param bool $onlyviewableroles + * + * @return array $roles + */ +function bigbluebuttonbn_get_roles(context $context = null, bool $onlyviewableroles = true) { + global $CFG; + + if ($onlyviewableroles == true && $CFG->branch >= 35) { + $roles = (array) get_viewable_roles($context); + foreach ($roles as $key => $value) { + $roles[$key] = $value; + } + } else { + $roles = (array) role_get_names($context); + foreach ($roles as $key => $value) { + $roles[$key] = $value->localname; + } + } + + return $roles; +} + +/** + * Returns an array containing all the roles in a context wrapped for html select element. + * + * @param context $context + * @param bool $onlyviewableroles + * + * @return array $users + */ +function bigbluebuttonbn_get_roles_select(context $context = null, bool $onlyviewableroles = true) { + global $CFG; + + if ($onlyviewableroles == true && $CFG->branch >= 35) { + $roles = (array) get_viewable_roles($context); + foreach ($roles as $key => $value) { + $roles[$key] = array('id' => $key, 'name' => $value); + } + } else { + $roles = (array) role_get_names($context); + foreach ($roles as $key => $value) { + $roles[$key] = array('id' => $value->id, 'name' => $value->localname); + } + } + + return $roles; +} + +/** + * Returns role that corresponds to an id. + * + * @param string|integer $id + * + * @return object $role + */ +function bigbluebuttonbn_get_role($id) { + $roles = (array) role_get_names(); + if (is_numeric($id) && isset($roles[$id])) { + return (object) $roles[$id]; + } + foreach ($roles as $role) { + if ($role->shortname == $id) { + return $role; + } + } +} + +/** + * Returns an array to populate a list of participants used in mod_form.js. + * + * @param context $context + * @param null|object $bbactivity + * @return array $data + */ +function bigbluebuttonbn_get_participant_data($context, $bbactivity = null) { + $data = array( + 'all' => array( + 'name' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'), + 'children' => [] + ), + ); + $data['role'] = array( + 'name' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'), + 'children' => bigbluebuttonbn_get_roles_select($context, true) + ); + $data['user'] = array( + 'name' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'), + 'children' => bigbluebuttonbn_get_users_select($context, $bbactivity), + ); + return $data; +} + +/** + * Returns an array to populate a list of participants used in mod_form.php. + * + * @param object $bigbluebuttonbn + * @param context $context + * + * @return array + */ +function bigbluebuttonbn_get_participant_list($bigbluebuttonbn, $context) { + global $USER; + if ($bigbluebuttonbn == null) { + return bigbluebuttonbn_get_participant_rules_encoded( + bigbluebuttonbn_get_participant_list_default($context, $USER->id) + ); + } + if (empty($bigbluebuttonbn->participants)) { + $bigbluebuttonbn->participants = "[]"; + } + $rules = json_decode($bigbluebuttonbn->participants, true); + if (empty($rules)) { + $rules = bigbluebuttonbn_get_participant_list_default($context, bigbluebuttonbn_instance_ownerid($bigbluebuttonbn)); + } + return bigbluebuttonbn_get_participant_rules_encoded($rules); +} + +/** + * Returns an array to populate a list of participants used in mod_form.php with default values. + * + * @param context $context + * @param integer $ownerid + * + * @return array + */ +function bigbluebuttonbn_get_participant_list_default($context, $ownerid = null) { + $participantlist = array(); + $participantlist[] = array( + 'selectiontype' => 'all', + 'selectionid' => 'all', + 'role' => BIGBLUEBUTTONBN_ROLE_VIEWER, + ); + $defaultrules = explode(',', \mod_bigbluebuttonbn\locallib\config::get('participant_moderator_default')); + foreach ($defaultrules as $defaultrule) { + if ($defaultrule == '0') { + if (!empty($ownerid) && is_enrolled($context, $ownerid)) { + $participantlist[] = array( + 'selectiontype' => 'user', + 'selectionid' => (string) $ownerid, + 'role' => BIGBLUEBUTTONBN_ROLE_MODERATOR); + } + continue; + } + $participantlist[] = array( + 'selectiontype' => 'role', + 'selectionid' => $defaultrule, + 'role' => BIGBLUEBUTTONBN_ROLE_MODERATOR); + } + return $participantlist; +} + +/** + * Returns an array to populate a list of participants used in mod_form.php with bigbluebuttonbn values. + * + * @param array $rules + * + * @return array + */ +function bigbluebuttonbn_get_participant_rules_encoded($rules) { + foreach ($rules as $key => $rule) { + if ($rule['selectiontype'] !== 'role' || is_numeric($rule['selectionid'])) { + continue; + } + $role = bigbluebuttonbn_get_role($rule['selectionid']); + if ($role == null) { + unset($rules[$key]); + continue; + } + $rule['selectionid'] = $role->id; + $rules[$key] = $rule; + } + return $rules; +} + +/** + * Returns an array to populate a list of participant_selection used in mod_form.php. + * + * @return array + */ +function bigbluebuttonbn_get_participant_selection_data() { + return [ + 'type_options' => [ + 'all' => get_string('mod_form_field_participant_list_type_all', 'bigbluebuttonbn'), + 'role' => get_string('mod_form_field_participant_list_type_role', 'bigbluebuttonbn'), + 'user' => get_string('mod_form_field_participant_list_type_user', 'bigbluebuttonbn'), + ], + 'type_selected' => 'all', + 'options' => ['all' => '---------------'], + 'selected' => 'all', + ]; +} + +/** + * Evaluate if a user in a context is moderator based on roles and participation rules. + * + * @param context $context + * @param array $participantlist + * @param integer $userid + * + * @return boolean + */ +function bigbluebuttonbn_is_moderator($context, $participantlist, $userid = null) { + global $USER; + if (!is_array($participantlist)) { + return false; + } + if (empty($userid)) { + $userid = $USER->id; + } + $userroles = bigbluebuttonbn_get_guest_role(); + if (!isguestuser()) { + $userroles = bigbluebuttonbn_get_user_roles($context, $userid); + } + return bigbluebuttonbn_is_moderator_validator($participantlist, $userid, $userroles); +} + +/** + * Iterates participant list rules to evaluate if a user is moderator. + * + * @param array $participantlist + * @param integer $userid + * @param array $userroles + * + * @return boolean + */ +function bigbluebuttonbn_is_moderator_validator($participantlist, $userid, $userroles) { + // Iterate participant rules. + foreach ($participantlist as $participant) { + if (bigbluebuttonbn_is_moderator_validate_rule($participant, $userid, $userroles)) { + return true; + } + } + return false; +} + +/** + * Evaluate if a user is moderator based on roles and a particular participation rule. + * + * @param object $participant + * @param integer $userid + * @param array $userroles + * + * @return boolean + */ +function bigbluebuttonbn_is_moderator_validate_rule($participant, $userid, $userroles) { + if ($participant['role'] == BIGBLUEBUTTONBN_ROLE_VIEWER) { + return false; + } + // Validation for the 'all' rule. + if ($participant['selectiontype'] == 'all') { + return true; + } + // Validation for a 'user' rule. + if ($participant['selectiontype'] == 'user') { + if ($participant['selectionid'] == $userid) { + return true; + } + return false; + } + // Validation for a 'role' rule. + $role = bigbluebuttonbn_get_role($participant['selectionid']); + if ($role != null && array_key_exists($role->id, $userroles)) { + return true; + } + return false; +} + +/** + * Helper returns error message key for the language file that corresponds to a bigbluebutton error key. + * + * @param string $messagekey + * @param string $defaultkey + * + * @return string + */ +function bigbluebuttonbn_get_error_key($messagekey, $defaultkey = null) { + if ($messagekey == 'checksumError') { + return 'index_error_checksum'; + } + if ($messagekey == 'maxConcurrent') { + return 'view_error_max_concurrent'; + } + return $defaultkey; +} + +/** + * Helper evaluates if a voicebridge number is unique. + * + * @param integer $instance + * @param integer $voicebridge + * + * @return string + */ +function bigbluebuttonbn_voicebridge_unique($instance, $voicebridge) { + global $DB; + if ($voicebridge == 0) { + return true; + } + $select = 'voicebridge = ' . $voicebridge; + if ($instance != 0) { + $select .= ' AND id <>' . $instance; + } + if (!$DB->get_records_select('bigbluebuttonbn', $select)) { + return true; + } + return false; +} + +/** + * Helper estimate a duration for the meeting based on the closingtime. + * + * @param integer $closingtime + * + * @return integer + */ +function bigbluebuttonbn_get_duration($closingtime) { + $duration = 0; + $now = time(); + if ($closingtime > 0 && $now < $closingtime) { + $duration = ceil(($closingtime - $now) / 60); + $compensationtime = intval((int) \mod_bigbluebuttonbn\locallib\config::get('scheduled_duration_compensation')); + $duration = intval($duration) + $compensationtime; + } + return $duration; +} + +/** + * Helper return array containing the file descriptor for a preuploaded presentation. + * + * @param context $context + * @param string $presentation + * @param integer $id + * + * @return array + */ +function bigbluebuttonbn_get_presentation_array($context, $presentation, $id = null) { + global $CFG; + if (empty($presentation)) { + if ($CFG->bigbluebuttonbn_preuploadpresentation_enabled) { + // Item has not presentation but presentation is enabled.. + // Check if exist some file by default in general mod setting ("presentationdefault"). + $fs = get_file_storage(); + $files = $fs->get_area_files( + context_system::instance()->id, + 'mod_bigbluebuttonbn', + 'presentationdefault', + 0, + "filename", + false + ); + + if (count($files) == 0) { + // Not exist file by default in "presentationbydefault" setting. + return array('url' => null, 'name' => null, 'icon' => null, 'mimetype_description' => null); + } + + // Exists file in general setting to use as default for presentation. Cache image for temp public access. + $file = reset($files); + unset($files); + $pnoncevalue = null; + if (!is_null($id)) { + // Create the nonce component for granting a temporary public access. + $cache = cache::make_from_params( + cache_store::MODE_APPLICATION, + 'mod_bigbluebuttonbn', + 'presentationdefault_cache' + ); + $pnoncekey = sha1(context_system::instance()->id); + /* The item id was adapted for granting public access to the presentation once in order + * to allow BigBlueButton to gather the file. */ + $pnoncevalue = bigbluebuttonbn_generate_nonce(); + $cache->set($pnoncekey, array('value' => $pnoncevalue, 'counter' => 0)); + } + + $url = moodle_url::make_pluginfile_url( + $file->get_contextid(), + $file->get_component(), + $file->get_filearea(), + $pnoncevalue, + $file->get_filepath(), + $file->get_filename() + ); + return (array('name' => $file->get_filename(), 'icon' => file_file_icon($file, 24), + 'url' => $url->out(false), 'mimetype_description' => get_mimetype_description($file))); + } + + return array('url' => null, 'name' => null, 'icon' => null, 'mimetype_description' => null); + } + $fs = get_file_storage(); + $files = $fs->get_area_files( + $context->id, + 'mod_bigbluebuttonbn', + 'presentation', + 0, + 'itemid, filepath, filename', + false + ); + if (count($files) == 0) { + return array('url' => null, 'name' => null, 'icon' => null, 'mimetype_description' => null); + } + $file = reset($files); + unset($files); + $pnoncevalue = null; + if (!is_null($id)) { + // Create the nonce component for granting a temporary public access. + $cache = cache::make_from_params( + cache_store::MODE_APPLICATION, + 'mod_bigbluebuttonbn', + 'presentation_cache' + ); + $pnoncekey = sha1($id); + /* The item id was adapted for granting public access to the presentation once in order + * to allow BigBlueButton to gather the file. */ + $pnoncevalue = bigbluebuttonbn_generate_nonce(); + $cache->set($pnoncekey, array('value' => $pnoncevalue, 'counter' => 0)); + } + $url = moodle_url::make_pluginfile_url( + $file->get_contextid(), + $file->get_component(), + $file->get_filearea(), + $pnoncevalue, + $file->get_filepath(), + $file->get_filename() + ); + return array('name' => $file->get_filename(), 'icon' => file_file_icon($file, 24), + 'url' => $url->out(false), 'mimetype_description' => get_mimetype_description($file)); +} + +/** + * Helper generates a nonce used for the preuploaded presentation callback url. + * + * @return string + */ +function bigbluebuttonbn_generate_nonce() { + $mt = microtime(); + $rand = mt_rand(); + return md5($mt . $rand); +} + +/** + * Helper generates a random password. + * + * @param integer $length + * @param string $unique + * + * @return string + */ +function bigbluebuttonbn_random_password($length = 8, $unique = "") { + $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + do { + $password = substr(str_shuffle($chars), 0, $length); + } while ($unique == $password); + return $password; +} + +/** + * Helper register a bigbluebuttonbn event. + * + * @param string $type + * @param object $bigbluebuttonbn + * @param array $options [timecreated, userid, other] + * + * @return void + */ +function bigbluebuttonbn_event_log($type, $bigbluebuttonbn, $options = []) { + global $DB; + if (!in_array($type, \mod_bigbluebuttonbn\event\events::$events)) { + // No log will be created. + return; + } + $course = $DB->get_record('course', array('id' => $bigbluebuttonbn->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $course->id, false, MUST_EXIST); + $context = context_module::instance($cm->id); + $params = array('context' => $context, 'objectid' => $bigbluebuttonbn->id); + if (array_key_exists('timecreated', $options)) { + $params['timecreated'] = $options['timecreated']; + } + if (array_key_exists('userid', $options)) { + $params['userid'] = $options['userid']; + } + if (array_key_exists('other', $options)) { + $params['other'] = $options['other']; + } + $event = call_user_func_array( + '\mod_bigbluebuttonbn\event\\' . $type . '::create', + array($params) + ); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('bigbluebuttonbn', $bigbluebuttonbn); + $event->trigger(); +} + +/** + * Updates the meeting info cached object when a participant has joined. + * + * @param string $meetingid + * @param bool $ismoderator + * + * @return void + */ +function bigbluebuttonbn_participant_joined($meetingid, $ismoderator) { + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache'); + $result = $cache->get($meetingid); + $meetinginfo = json_decode($result['meeting_info']); + $meetinginfo->participantCount += 1; + if ($ismoderator) { + $meetinginfo->moderatorCount += 1; + } + $cache->set($meetingid, array('creation_time' => $result['creation_time'], + 'meeting_info' => json_encode($meetinginfo))); +} + +/** + * Gets a meeting info object cached or fetched from the live session. + * + * @param string $meetingid + * @param boolean $updatecache + * + * @return array + */ +function bigbluebuttonbn_get_meeting_info($meetingid, $updatecache = false) { + $cachettl = (int) \mod_bigbluebuttonbn\locallib\config::get('waitformoderator_cache_ttl'); + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache'); + $result = $cache->get($meetingid); + $now = time(); + if (!$updatecache && !empty($result) && $now < ($result['creation_time'] + $cachettl)) { + // Use the value in the cache. + return (array) json_decode($result['meeting_info']); + } + // Ping again and refresh the cache. + $meetinginfo = (array) bigbluebuttonbn_wrap_xml_load_file( + \mod_bigbluebuttonbn\locallib\bigbluebutton::action_url('getMeetingInfo', ['meetingID' => $meetingid]) + ); + $cache->set($meetingid, array('creation_time' => time(), 'meeting_info' => json_encode($meetinginfo))); + return $meetinginfo; +} + +/** + * Perform isMeetingRunning on BBB. + * + * @param string $meetingid + * @param boolean $updatecache + * + * @return boolean + */ +function bigbluebuttonbn_is_meeting_running($meetingid, $updatecache = false) { + /* As a workaround to isMeetingRunning that always return SUCCESS but only returns true + * when at least one user is in the session, we use getMeetingInfo instead. + */ + $meetinginfo = bigbluebuttonbn_get_meeting_info($meetingid, $updatecache); + return ($meetinginfo['returncode'] === 'SUCCESS'); +} + +/** + * Publish an imported recording. + * + * @param string $id + * @param boolean $publish + * + * @return boolean + */ +function bigbluebuttonbn_publish_recording_imported($id, $publish = true) { + global $DB; + // Locate the record to be updated. + $record = $DB->get_record('bigbluebuttonbn_logs', array('id' => $id)); + $meta = json_decode($record->meta, true); + // Prepare data for the update. + $meta['recording']['published'] = ($publish) ? 'true' : 'false'; + $record->meta = json_encode($meta); + // Proceed with the update. + $DB->update_record('bigbluebuttonbn_logs', $record); + return true; +} + +/** + * Delete an imported recording. + * + * @param string $id + * + * @return boolean + */ +function bigbluebuttonbn_delete_recording_imported($id) { + global $DB; + // Execute delete. + $DB->delete_records('bigbluebuttonbn_logs', array('id' => $id)); + return true; +} + +/** + * Update an imported recording. + * + * @param string $id + * @param array $params ['key'=>param_key, 'value'] + * + * @return boolean + */ +function bigbluebuttonbn_update_recording_imported($id, $params) { + global $DB; + // Locate the record to be updated. + $record = $DB->get_record('bigbluebuttonbn_logs', array('id' => $id)); + $meta = json_decode($record->meta, true); + // Prepare data for the update. + $meta['recording'] = $params + $meta['recording']; + $record->meta = json_encode($meta); + // Proceed with the update. + if (!$DB->update_record('bigbluebuttonbn_logs', $record)) { + return false; + } + return true; +} + +/** + * Protect/Unprotect an imported recording. + * + * @param string $id + * @param boolean $protect + * + * @return boolean + */ +function bigbluebuttonbn_protect_recording_imported($id, $protect = true) { + global $DB; + // Locate the record to be updated. + $record = $DB->get_record('bigbluebuttonbn_logs', array('id' => $id)); + $meta = json_decode($record->meta, true); + // Prepare data for the update. + $meta['recording']['protected'] = ($protect) ? 'true' : 'false'; + $record->meta = json_encode($meta); + // Proceed with the update. + $DB->update_record('bigbluebuttonbn_logs', $record); + return true; +} + +/** + * Sets a custom config.xml file for being used on create. + * + * @param string $meetingid + * @param string $configxml + * + * @return object + */ +function bigbluebuttonbn_set_config_xml($meetingid, $configxml) { + $urldefaultconfig = \mod_bigbluebuttonbn\locallib\config::get('server_url') . 'api/setConfigXML?'; + $configxmlparams = bigbluebuttonbn_set_config_xml_params($meetingid, $configxml); + $xml = bigbluebuttonbn_wrap_xml_load_file( + $urldefaultconfig, + 'POST', + $configxmlparams, + 'application/x-www-form-urlencoded' + ); + return $xml; +} + +/** + * Sets qs used with a custom config.xml file request. + * + * @param string $meetingid + * @param string $configxml + * + * @return string + */ +function bigbluebuttonbn_set_config_xml_params($meetingid, $configxml) { + $params = 'configXML=' . urlencode($configxml) . '&meetingID=' . urlencode($meetingid); + $sharedsecret = \mod_bigbluebuttonbn\locallib\config::get('shared_secret'); + $configxmlparams = $params . '&checksum=' . sha1('setConfigXML' . $params . $sharedsecret); + return $configxmlparams; +} + +/** + * Sets a custom config.xml file for being used on create. + * + * @param string $meetingid + * @param string $configxml + * + * @return array + */ +function bigbluebuttonbn_set_config_xml_array($meetingid, $configxml) { + $configxml = bigbluebuttonbn_set_config_xml($meetingid, $configxml); + $configxmlarray = (array) $configxml; + if ($configxmlarray['returncode'] != 'SUCCESS') { + debugging('BigBlueButton was not able to set the custom config.xml file', DEBUG_DEVELOPER); + return ''; + } + return $configxmlarray['configToken']; +} + +/** + * Helper function builds a row for the data used by the recording table. + * + * @param array $bbbsession + * @param array $recording + * @param array $tools + * + * @return array + */ +function bigbluebuttonbn_get_recording_data_row($bbbsession, $recording, $tools = ['protect', 'publish', 'delete']) { + if (!bigbluebuttonbn_include_recording_table_row($bbbsession, $recording)) { + return; + } + $rowdata = new stdClass(); + // Set recording_types. + $rowdata->playback = bigbluebuttonbn_get_recording_data_row_types($recording, $bbbsession); + // Set activity name. + $rowdata->recording = bigbluebuttonbn_get_recording_data_row_meta_activity($recording, $bbbsession); + // Set activity description. + $rowdata->description = bigbluebuttonbn_get_recording_data_row_meta_description($recording, $bbbsession); + if (bigbluebuttonbn_get_recording_data_preview_enabled($bbbsession)) { + // Set recording_preview. + $rowdata->preview = bigbluebuttonbn_get_recording_data_row_preview($recording); + } + // Set date. + $rowdata->date = bigbluebuttonbn_get_recording_data_row_date($recording); + // Set formatted date. + $rowdata->date_formatted = bigbluebuttonbn_get_recording_data_row_date_formatted($rowdata->date); + // Set formatted duration. + $rowdata->duration_formatted = $rowdata->duration = bigbluebuttonbn_get_recording_data_row_duration($recording); + // Set actionbar, if user is allowed to manage recordings. + if ($bbbsession['managerecordings']) { + $rowdata->actionbar = bigbluebuttonbn_get_recording_data_row_actionbar($recording, $tools); + } + return $rowdata; +} + +/** + * Helper function evaluates if a row for the data used by the recording table is editable. + * + * @param array $bbbsession + * + * @return boolean + */ +function bigbluebuttonbn_get_recording_data_row_editable($bbbsession) { + return ($bbbsession['managerecordings'] && ((double) $bbbsession['serverversion'] >= 1.0 || $bbbsession['bnserver'])); +} + +/** + * Helper function evaluates if recording preview should be included. + * + * @param array $bbbsession + * + * @return boolean + */ +function bigbluebuttonbn_get_recording_data_preview_enabled($bbbsession) { + return ((double) $bbbsession['serverversion'] >= 1.0 && $bbbsession['bigbluebuttonbn']->recordings_preview == '1'); +} + +/** + * Helper function converts recording date used in row for the data used by the recording table. + * + * @param array $recording + * + * @return integer + */ +function bigbluebuttonbn_get_recording_data_row_date($recording) { + if (!isset($recording['startTime'])) { + return 0; + } + return floatval($recording['startTime']); +} + +/** + * Helper function format recording date used in row for the data used by the recording table. + * + * @param integer $starttime + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_date_formatted($starttime) { + global $USER; + $starttime = $starttime - ($starttime % 1000); + // Set formatted date. + $dateformat = get_string('strftimerecentfull', 'langconfig') . ' %Z'; + return userdate($starttime / 1000, $dateformat, usertimezone($USER->timezone)); +} + +/** + * Helper function converts recording duration used in row for the data used by the recording table. + * + * @param array $recording + * + * @return integer + */ +function bigbluebuttonbn_get_recording_data_row_duration($recording) { + foreach (array_values($recording['playbacks']) as $playback) { + // Ignore restricted playbacks. + if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'true') { + continue; + } + // Take the lenght form the fist playback with an actual value. + if (!empty($playback['length'])) { + return intval($playback['length']); + } + } + return 0; +} + +/** + * Helper function builds recording actionbar used in row for the data used by the recording table. + * + * @param array $recording + * @param array $tools + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_actionbar($recording, $tools) { + $actionbar = ''; + foreach ($tools as $tool) { + $buttonpayload = bigbluebuttonbn_get_recording_data_row_actionbar_payload($recording, $tool); + if ($tool == 'protect') { + if (isset($recording['imported'])) { + $buttonpayload['disabled'] = 'disabled'; + } + if (!isset($recording['protected'])) { + $buttonpayload['disabled'] = 'invisible'; + } + } + $actionbar .= bigbluebuttonbn_actionbar_render_button($recording, $buttonpayload); + } + $head = html_writer::start_tag('div', array( + 'id' => 'recording-actionbar-' . $recording['recordID'], + 'data-recordingid' => $recording['recordID'], + 'data-meetingid' => $recording['meetingID'])); + $tail = html_writer::end_tag('div'); + return $head . $actionbar . $tail; +} + +/** + * Helper function returns the corresponding payload for an actionbar button used in row + * for the data used by the recording table. + * + * @param array $recording + * @param array $tool + * + * @return array + */ +function bigbluebuttonbn_get_recording_data_row_actionbar_payload($recording, $tool) { + if ($tool == 'protect') { + $protected = 'false'; + if (isset($recording['protected'])) { + $protected = $recording['protected']; + } + return bigbluebuttonbn_get_recording_data_row_action_protect($protected); + } + if ($tool == 'publish') { + return bigbluebuttonbn_get_recording_data_row_action_publish($recording['published']); + } + return array('action' => $tool, 'tag' => $tool); +} + +/** + * Helper function returns the payload for protect action button used in row + * for the data used by the recording table. + * + * @param string $protected + * + * @return array + */ +function bigbluebuttonbn_get_recording_data_row_action_protect($protected) { + if ($protected == 'true') { + return array('action' => 'unprotect', 'tag' => 'lock'); + } + return array('action' => 'protect', 'tag' => 'unlock'); +} + +/** + * Helper function returns the payload for publish action button used in row + * for the data used by the recording table. + * + * @param string $published + * + * @return array + */ +function bigbluebuttonbn_get_recording_data_row_action_publish($published) { + if ($published == 'true') { + return array('action' => 'unpublish', 'tag' => 'hide'); + } + return array('action' => 'publish', 'tag' => 'show'); +} + +/** + * Helper function builds recording preview used in row for the data used by the recording table. + * + * @param array $recording + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_preview($recording) { + $options = array('id' => 'preview-' . $recording['recordID']); + if ($recording['published'] === 'false') { + $options['hidden'] = 'hidden'; + } + $recordingpreview = html_writer::start_tag('div', $options); + foreach ($recording['playbacks'] as $playback) { + if (isset($playback['preview'])) { + $recordingpreview .= bigbluebuttonbn_get_recording_data_row_preview_images($playback); + break; + } + } + $recordingpreview .= html_writer::end_tag('div'); + return $recordingpreview; +} + +/** + * Helper function builds element with actual images used in recording preview row based on a selected playback. + * + * @param array $playback + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_preview_images($playback) { + global $CFG; + $recordingpreview = html_writer::start_tag('div', array('class' => 'container-fluid')); + $recordingpreview .= html_writer::start_tag('div', array('class' => 'row')); + foreach ($playback['preview'] as $image) { + if ($CFG->bigbluebuttonbn_recordings_validate_url && !bigbluebuttonbn_is_valid_resource(trim($image['url']))) { + return ''; + } + $recordingpreview .= html_writer::start_tag('div', array('class' => '')); + $recordingpreview .= html_writer::empty_tag( + 'img', + array('src' => trim($image['url']) . '?' . time(), 'class' => 'recording-thumbnail pull-left') + ); + $recordingpreview .= html_writer::end_tag('div'); + } + $recordingpreview .= html_writer::end_tag('div'); + $recordingpreview .= html_writer::start_tag('div', array('class' => 'row')); + $recordingpreview .= html_writer::tag( + 'div', + get_string('view_recording_preview_help', 'bigbluebuttonbn'), + array('class' => 'text-center text-muted small') + ); + $recordingpreview .= html_writer::end_tag('div'); + $recordingpreview .= html_writer::end_tag('div'); + return $recordingpreview; +} + +/** + * Helper function renders recording types to be used in row for the data used by the recording table. + * + * @param array $recording + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_types($recording, $bbbsession) { + $dataimported = 'false'; + $title = ''; + if (isset($recording['imported'])) { + $dataimported = 'true'; + $title = get_string('view_recording_link_warning', 'bigbluebuttonbn'); + } + $visibility = ''; + if ($recording['published'] === 'false') { + $visibility = 'hidden '; + } + $id = 'playbacks-' . $recording['recordID']; + $recordingtypes = html_writer::start_tag('div', array('id' => $id, 'data-imported' => $dataimported, + 'data-meetingid' => $recording['meetingID'], 'data-recordingid' => $recording['recordID'], + 'title' => $title, $visibility => $visibility)); + foreach ($recording['playbacks'] as $playback) { + $recordingtypes .= bigbluebuttonbn_get_recording_data_row_type($recording, $bbbsession, $playback); + } + $recordingtypes .= html_writer::end_tag('div'); + return $recordingtypes; +} + +/** + * Helper function renders the link used for recording type in row for the data used by the recording table. + * + * @param array $recording + * @param array $bbbsession + * @param array $playback + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_type($recording, $bbbsession, $playback) { + global $CFG, $OUTPUT; + if (!bigbluebuttonbn_include_recording_data_row_type($recording, $bbbsession, $playback)) { + return ''; + } + $text = bigbluebuttonbn_get_recording_type_text($playback['type']); + $href = $CFG->wwwroot . '/mod/bigbluebuttonbn/bbb_view.php?action=play&bn=' . $bbbsession['bigbluebuttonbn']->id . + '&mid=' . $recording['meetingID'] . '&rid=' . $recording['recordID'] . '&rtype=' . $playback['type']; + if (!isset($recording['imported']) || !isset($recording['protected']) || $recording['protected'] === 'false') { + $href .= '&href=' . urlencode(trim($playback['url'])); + } + $linkattributes = array( + 'id' => 'recording-play-' . $playback['type'] . '-' . $recording['recordID'], + 'class' => 'btn btn-sm btn-default', + 'onclick' => 'M.mod_bigbluebuttonbn.recordings.recordingPlay(this);', + 'data-action' => 'play', + 'data-target' => $playback['type'], + 'data-href' => $href, + ); + if ($CFG->bigbluebuttonbn_recordings_validate_url && !bigbluebuttonbn_is_bn_server() + && !bigbluebuttonbn_is_valid_resource(trim($playback['url']))) { + $linkattributes['class'] = 'btn btn-sm btn-warning'; + $linkattributes['title'] = get_string('view_recording_format_errror_unreachable', 'bigbluebuttonbn'); + unset($linkattributes['data-href']); + } + return $OUTPUT->action_link('#', $text, null, $linkattributes) . ' '; +} + +/** + * Helper function to handle yet unknown recording types + * + * @param string $playbacktype : for now presentation, video, statistics, capture, notes, podcast + * + * @return string the matching language string or a capitalised version of the provided string + */ +function bigbluebuttonbn_get_recording_type_text($playbacktype) { + // Check first if string exists, and if it does'nt just default to the capitalised version of the string. + $text = ucwords($playbacktype); + $typestringid = 'view_recording_format_' . $playbacktype; + if (get_string_manager()->string_exists($typestringid, 'bigbluebuttonbn')) { + $text = get_string($typestringid, 'bigbluebuttonbn'); + } + return $text; +} + +/** + * Helper function validates a remote resource. + * + * @param string $url + * + * @return boolean + */ +function bigbluebuttonbn_is_valid_resource($url) { + $urlhost = parse_url($url, PHP_URL_HOST); + $serverurlhost = parse_url(\mod_bigbluebuttonbn\locallib\config::get('server_url'), PHP_URL_HOST); + // Skip validation when the recording URL host is the same as the configured BBB server. + if ($urlhost == $serverurlhost) { + return true; + } + // Skip validation when the recording URL was already validated. + $validatedurls = bigbluebuttonbn_cache_get('recordings_cache', 'validated_urls', array()); + if (array_key_exists($urlhost, $validatedurls)) { + return $validatedurls[$urlhost]; + } + // Validate the recording URL. + $validatedurls[$urlhost] = true; + $curlinfo = bigbluebuttonbn_wrap_xml_load_file_curl_request($url, 'HEAD'); + if (!isset($curlinfo['http_code']) || $curlinfo['http_code'] != 200) { + $error = "Resources hosted by " . $urlhost . " are unreachable. Server responded with code " . $curlinfo['http_code']; + debugging($error, DEBUG_DEVELOPER); + $validatedurls[$urlhost] = false; + } + bigbluebuttonbn_cache_set('recordings_cache', 'validated_urls', $validatedurls); + return $validatedurls[$urlhost]; +} + +/** + * Helper function renders the name for meeting used in row for the data used by the recording table. + * + * @param array $recording + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_meeting($recording, $bbbsession) { + $payload = array(); + $source = 'meetingName'; + $metaname = trim($recording['meetingName']); + return bigbluebuttonbn_get_recording_data_row_text($recording, $metaname, $source, $payload); +} + +/** + * Helper function renders the name for recording used in row for the data used by the recording table. + * + * @param array $recording + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_meta_activity($recording, $bbbsession) { + $payload = array(); + if (bigbluebuttonbn_get_recording_data_row_editable($bbbsession)) { + $payload = array('recordingid' => $recording['recordID'], 'meetingid' => $recording['meetingID'], + 'action' => 'edit', 'tag' => 'edit', + 'target' => 'name'); + } + $oldsource = 'meta_contextactivity'; + if (isset($recording[$oldsource])) { + $metaname = trim($recording[$oldsource]); + return bigbluebuttonbn_get_recording_data_row_text($recording, $metaname, $oldsource, $payload); + } + $newsource = 'meta_bbb-recording-name'; + if (isset($recording[$newsource])) { + $metaname = trim($recording[$newsource]); + return bigbluebuttonbn_get_recording_data_row_text($recording, $metaname, $newsource, $payload); + } + $metaname = trim($recording['meetingName']); + return bigbluebuttonbn_get_recording_data_row_text($recording, $metaname, $newsource, $payload); +} + +/** + * Helper function renders the description for recording used in row for the data used by the recording table. + * + * @param array $recording + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_meta_description($recording, $bbbsession) { + $payload = array(); + if (bigbluebuttonbn_get_recording_data_row_editable($bbbsession)) { + $payload = array('recordingid' => $recording['recordID'], 'meetingid' => $recording['meetingID'], + 'action' => 'edit', 'tag' => 'edit', + 'target' => 'description'); + } + $oldsource = 'meta_contextactivitydescription'; + if (isset($recording[$oldsource])) { + $metadescription = trim($recording[$oldsource]); + return bigbluebuttonbn_get_recording_data_row_text($recording, $metadescription, $oldsource, $payload); + } + $newsource = 'meta_bbb-recording-description'; + if (isset($recording[$newsource])) { + $metadescription = trim($recording[$newsource]); + return bigbluebuttonbn_get_recording_data_row_text($recording, $metadescription, $newsource, $payload); + } + return bigbluebuttonbn_get_recording_data_row_text($recording, '', $newsource, $payload); +} + +/** + * Helper function renders text element for recording used in row for the data used by the recording table. + * + * @param array $recording + * @param string $text + * @param string $source + * @param array $data + * + * @return string + */ +function bigbluebuttonbn_get_recording_data_row_text($recording, $text, $source, $data) { + $htmltext = '<span>' . htmlentities($text) . '</span>'; + if (empty($data)) { + return $htmltext; + } + $target = $data['action'] . '-' . $data['target']; + $id = 'recording-' . $target . '-' . $data['recordingid']; + $attributes = array('id' => $id, 'class' => 'quickeditlink col-md-20', + 'data-recordingid' => $data['recordingid'], 'data-meetingid' => $data['meetingid'], + 'data-target' => $data['target'], 'data-source' => $source); + $head = html_writer::start_tag('div', $attributes); + $tail = html_writer::end_tag('div'); + $payload = array('action' => $data['action'], 'tag' => $data['tag'], 'target' => $data['target']); + $htmllink = bigbluebuttonbn_actionbar_render_button($recording, $payload); + return $head . $htmltext . $htmllink . $tail; +} + +/** + * Helper function render a button for the recording action bar + * + * @param array $recording + * @param array $data + * + * @return string + */ +function bigbluebuttonbn_actionbar_render_button($recording, $data) { + global $OUTPUT; + if (empty($data)) { + return ''; + } + $target = $data['action']; + if (isset($data['target'])) { + $target .= '-' . $data['target']; + } + $id = 'recording-' . $target . '-' . $recording['recordID']; + $onclick = 'M.mod_bigbluebuttonbn.recordings.recording' . ucfirst($data['action']) . '(this); return false;'; + if ((boolean) \mod_bigbluebuttonbn\locallib\config::get('recording_icons_enabled')) { + // With icon for $manageaction. + $iconattributes = array('id' => $id, 'class' => 'iconsmall'); + $linkattributes = array( + 'id' => $id, + 'onclick' => $onclick, + 'data-action' => $data['action'], + ); + if (!isset($recording['imported'])) { + $linkattributes['data-links'] = bigbluebuttonbn_count_recording_imported_instances( + $recording['recordID'] + ); + } + if (isset($data['disabled'])) { + $iconattributes['class'] .= ' fa-' . $data['disabled']; + $linkattributes['class'] = 'disabled'; + unset($linkattributes['onclick']); + } + $icon = new pix_icon( + 'i/' . $data['tag'], + get_string('view_recording_list_actionbar_' . $data['action'], 'bigbluebuttonbn'), + 'moodle', + $iconattributes + ); + return $OUTPUT->action_icon('#', $icon, null, $linkattributes, false); + } + // With text for $manageaction. + $linkattributes = array('title' => get_string($data['tag']), 'class' => 'btn btn-xs btn-danger', + 'onclick' => $onclick); + return $OUTPUT->action_link('#', get_string($data['action']), null, $linkattributes); +} + +/** + * Helper function builds the recording table. + * + * @param array $bbbsession + * @param array $recordings + * @param array $tools + * + * @return object + */ +function bigbluebuttonbn_get_recording_table($bbbsession, $recordings, $tools = ['protect', 'publish', 'delete']) { + global $DB; + // Declare the table. + $table = new html_table(); + $table->data = array(); + // Initialize table headers. + $table->head[] = get_string('view_recording_playback', 'bigbluebuttonbn'); + $table->head[] = get_string('view_recording_name', 'bigbluebuttonbn'); + $table->head[] = get_string('view_recording_description', 'bigbluebuttonbn'); + if (bigbluebuttonbn_get_recording_data_preview_enabled($bbbsession)) { + $table->head[] = get_string('view_recording_preview', 'bigbluebuttonbn'); + } + $table->head[] = get_string('view_recording_date', 'bigbluebuttonbn'); + $table->head[] = get_string('view_recording_duration', 'bigbluebuttonbn'); + $table->align = array('left', 'left', 'left', 'left', 'left', 'center'); + $table->size = array('', '', '', '', '', ''); + if ($bbbsession['managerecordings']) { + $table->head[] = get_string('view_recording_actionbar', 'bigbluebuttonbn'); + $table->align[] = 'left'; + $table->size[] = (count($tools) * 40) . 'px'; + } + // Get the groups of the user. + $usergroups = groups_get_all_groups($bbbsession['course']->id, $bbbsession['userID']); + + // Build table content. + foreach ($recordings as $recording) { + $meetingid = $recording['meetingID']; + $shortmeetingid = explode('-', $recording['meetingID']); + if (isset($shortmeetingid[0])) { + $meetingid = $shortmeetingid[0]; + } + // Check if the record belongs to a Visible Group type. + list($course, $cm) = get_course_and_cm_from_cmid($bbbsession['cm']->id); + $groupmode = groups_get_activity_groupmode($cm); + $displayrow = true; + if (($groupmode != VISIBLEGROUPS) + && !$bbbsession['administrator'] && !$bbbsession['moderator']) { + $groupid = explode('[', $recording['meetingID']); + if (isset($groupid[1])) { + // It is a group recording and the user is not moderator/administrator. Recording should not be included by default. + $displayrow = false; + $groupid = explode(']', $groupid[1]); + if (isset($groupid[0])) { + foreach ($usergroups as $usergroup) { + if ($usergroup->id == $groupid[0]) { + // Include recording if the user is in the same group. + $displayrow = true; + } + } + } + } + } + if ($displayrow) { + $rowdata = bigbluebuttonbn_get_recording_data_row($bbbsession, $recording, $tools); + if (!empty($rowdata)) { + $row = bigbluebuttonbn_get_recording_table_row($bbbsession, $recording, $rowdata); + array_push($table->data, $row); + } + } + } + return $table; +} + +/** + * Helper function builds the recording table row and insert into table. + * + * @param array $bbbsession + * @param array $recording + * @param object $rowdata + * + * @return object + */ +function bigbluebuttonbn_get_recording_table_row($bbbsession, $recording, $rowdata) { + $row = new html_table_row(); + $row->id = 'recording-tr-' . $recording['recordID']; + $row->attributes['data-imported'] = 'false'; + $texthead = ''; + $texttail = ''; + if (isset($recording['imported'])) { + $row->attributes['title'] = get_string('view_recording_link_warning', 'bigbluebuttonbn'); + $row->attributes['data-imported'] = 'true'; + $texthead = '<em>'; + $texttail = '</em>'; + } + $rowdata->date_formatted = str_replace(' ', ' ', $rowdata->date_formatted); + $row->cells = array(); + $row->cells[] = $texthead . $rowdata->playback . $texttail; + $row->cells[] = $texthead . $rowdata->recording . $texttail; + $row->cells[] = $texthead . $rowdata->description . $texttail; + if (bigbluebuttonbn_get_recording_data_preview_enabled($bbbsession)) { + $row->cells[] = $rowdata->preview; + } + $row->cells[] = $texthead . $rowdata->date_formatted . $texttail; + $row->cells[] = $rowdata->duration_formatted; + if ($bbbsession['managerecordings']) { + $row->cells[] = $rowdata->actionbar; + } + return $row; +} + +/** + * Get the basic data to display in the table view + * + * @param array $bbbsession the current session + * @param array $enabledfeatures feature enabled for this activity + * @return associative array containing the recordings indexed by recordID, each recording is also a + * non sequential associative array itself that corresponds to the actual recording in BBB + */ +function bigbluebutton_get_recordings_for_table_view($bbbsession, $enabledfeatures) { + $bigbluebuttonbnid = null; + if ($enabledfeatures['showroom']) { + $bigbluebuttonbnid = $bbbsession['bigbluebuttonbn']->id; + } + // Get recordings. + $recordings = bigbluebuttonbn_get_recordings( + $bbbsession['course']->id, $bigbluebuttonbnid, $enabledfeatures['showroom'], + $bbbsession['bigbluebuttonbn']->recordings_deleted + ); + if ($enabledfeatures['importrecordings']) { + // Get recording links. + $bigbluebuttonbnid = $bbbsession['bigbluebuttonbn']->id; + $recordingsimported = bigbluebuttonbn_get_recordings_imported_array( + $bbbsession['course']->id, $bigbluebuttonbnid, true + ); + /* Perform aritmetic addition instead of merge so the imported recordings corresponding to existent + * recordings are not included. */ + if ($bbbsession['bigbluebuttonbn']->recordings_imported) { + $recordings = $recordingsimported; + } else { + $recordings += $recordingsimported; + } + } + return $recordings; +} + +/** + * Helper function evaluates if recording row should be included in the table. + * + * @param array $bbbsession + * @param array $recording + * + * @return boolean + */ +function bigbluebuttonbn_include_recording_table_row($bbbsession, $recording) { + // Exclude unpublished recordings, only if user has no rights to manage them. + if ($recording['published'] != 'true' && !$bbbsession['managerecordings']) { + return false; + } + // Imported recordings are always shown as long as they are published. + if (isset($recording['imported'])) { + return true; + } + // Administrators and moderators are always allowed. + if ($bbbsession['administrator'] || $bbbsession['moderator']) { + return true; + } + // When groups are enabled, exclude those to which the user doesn't have access to. + if (isset($bbbsession['group']) && $recording['meetingID'] != $bbbsession['meetingid']) { + return false; + } + return true; +} + +/** + * Helper function triggers a send notification when the recording is ready. + * + * @param object $bigbluebuttonbn + * + * @return void + */ +function bigbluebuttonbn_send_notification_recording_ready($bigbluebuttonbn) { + \mod_bigbluebuttonbn\locallib\notifier::notify_recording_ready($bigbluebuttonbn); +} + +/** + * Helper function enqueues list of meeting events to be stored and processed as for completion. + * + * @param object $bigbluebuttonbn + * @param object $jsonobj + * + * @return void + */ +function bigbluebuttonbn_process_meeting_events($bigbluebuttonbn, $jsonobj) { + $meetingid = $jsonobj->{'meeting_id'}; + $recordid = $jsonobj->{'internal_meeting_id'}; + $attendees = $jsonobj->{'data'}->{'attendees'}; + foreach ($attendees as $attendee) { + $userid = $attendee->{'ext_user_id'}; + $overrides['meetingid'] = $meetingid; + $overrides['userid'] = $userid; + $meta['recordid'] = $recordid; + $meta['data'] = $attendee; + // Stores the log. + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTON_LOG_EVENT_SUMMARY, $overrides, json_encode($meta)); + // Enqueue a task for processing the completion. + bigbluebuttonbn_enqueue_completion_update($bigbluebuttonbn, $userid); + } +} + +/** + * Helper function enqueues one user for being validated as for completion. + * + * @param object $bigbluebuttonbn + * @param string $userid + * + * @return void + */ +function bigbluebuttonbn_enqueue_completion_update($bigbluebuttonbn, $userid) { + try { + // Create the instance of completion_update_state task. + $task = new \mod_bigbluebuttonbn\task\completion_update_state(); + // Add custom data. + $data = array( + 'bigbluebuttonbn' => $bigbluebuttonbn, + 'userid' => $userid, + ); + $task->set_custom_data($data); + // CONTRIB-7457: Task should be executed by a user, maybe Teacher as Student won't have rights for overriding. + // $ task -> set_userid ( $ user -> id );. + // Enqueue it. + \core\task\manager::queue_adhoc_task($task); + } catch (Exception $e) { + mtrace("Error while enqueuing completion_update_state task. " . (string) $e); + } +} + +/** + * Helper function enqueues completion trigger. + * + * @param object $bigbluebuttonbn + * @param string $userid + * + * @return void + */ +function bigbluebuttonbn_completion_update_state($bigbluebuttonbn, $userid) { + global $CFG; + require_once($CFG->libdir.'/completionlib.php'); + list($course, $cm) = get_course_and_cm_from_instance($bigbluebuttonbn, 'bigbluebuttonbn'); + $completion = new completion_info($course); + if (!$completion->is_enabled($cm)) { + mtrace("Completion not enabled"); + return; + } + if (bigbluebuttonbn_get_completion_state($course, $cm, $userid, COMPLETION_AND)) { + mtrace("Completion succeeded for user $userid"); + $completion->update_state($cm, COMPLETION_COMPLETE, $userid, true); + } else { + mtrace("Completion did not succeed for user $userid"); + } +} + +/** + * Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain. + * + * @return boolean + */ +function bigbluebuttonbn_is_bn_server() { + if (\mod_bigbluebuttonbn\locallib\config::get('bn_server')) { + return true; + } + $parsedurl = parse_url(\mod_bigbluebuttonbn\locallib\config::get('server_url')); + if (!isset($parsedurl['host'])) { + return false; + } + $h = $parsedurl['host']; + $hends = explode('.', $h); + $hendslength = count($hends); + return ($hends[$hendslength - 1] == 'com' && $hends[$hendslength - 2] == 'blindsidenetworks'); +} + +/** + * Helper function returns a list of courses a user has access to, wrapped in an array that can be used + * by a html select. + * + * @param array $bbbsession + * + * @return array + */ +function bigbluebuttonbn_import_get_courses_for_select(array $bbbsession) { + if ($bbbsession['administrator']) { + $courses = get_courses('all', 'c.fullname ASC'); + // It includes the name of the site as a course (category 0), so remove the first one. + unset($courses['1']); + } else { + $courses = enrol_get_users_courses($bbbsession['userID'], false, 'id,shortname,fullname'); + } + $coursesforselect = []; + foreach ($courses as $course) { + $coursesforselect[$course->id] = $course->fullname . " (" . $course->shortname . ")"; + } + return $coursesforselect; +} + +/** + * Helper function renders recording table. + * + * @param array $bbbsession + * @param array $recordings + * @param array $tools + * + * @return array + */ +function bigbluebuttonbn_output_recording_table($bbbsession, $recordings, $tools = ['protect', 'publish', 'delete']) { + if (isset($recordings) && !empty($recordings)) { + // There are recordings for this meeting. + $table = bigbluebuttonbn_get_recording_table($bbbsession, $recordings, $tools); + } + if (!isset($table) || !isset($table->data)) { + // Render a table with "No recordings". + return html_writer::div( + get_string('view_message_norecordings', 'bigbluebuttonbn'), + '', + array('id' => 'bigbluebuttonbn_recordings_table') + ); + } + // Render the table. + return html_writer::div(html_writer::table($table), '', array('id' => 'bigbluebuttonbn_recordings_table')); +} + +/** + * Helper function to convert an html string to plain text. + * + * @param string $html + * @param integer $len + * + * @return string + */ +function bigbluebuttonbn_html2text($html, $len = 0) { + $text = strip_tags($html); + $text = str_replace(' ', ' ', $text); + $textlen = strlen($text); + $text = mb_substr($text, 0, $len); + if ($textlen > $len) { + $text .= '...'; + } + return $text; +} + +/** + * Helper function to obtain the tags linked to a bigbluebuttonbn activity + * + * @param string $id + * + * @return string containing the tags separated by commas + */ +function bigbluebuttonbn_get_tags($id) { + if (class_exists('core_tag_tag')) { + return implode(',', core_tag_tag::get_item_tags_array('core', 'course_modules', $id)); + } + return implode(',', tag_get_tags('bigbluebuttonbn', $id)); +} + +/** + * Helper function to define the sql used for gattering the bigbluebuttonbnids whose meetingids should be included + * in the getRecordings request + * + * @param string $courseid + * @param string $bigbluebuttonbnid + * @param bool $subset + * + * @return string containing the sql used for getting the target bigbluebuttonbn instances + */ +function bigbluebuttonbn_get_recordings_sql_select($courseid, $bigbluebuttonbnid = null, $subset = true) { + if (empty($courseid)) { + $courseid = 0; + } + if (empty($bigbluebuttonbnid)) { + return "course = '{$courseid}'"; + } + if ($subset) { + return "id = '{$bigbluebuttonbnid}'"; + } + return "id <> '{$bigbluebuttonbnid}' AND course = '{$courseid}'"; +} + +/** + * Helper function to define the sql used for gattering the bigbluebuttonbnids whose meetingids should be included + * in the getRecordings request considering only those that belong to deleted activities. + * + * @param string $courseid + * @param string $bigbluebuttonbnid + * @param bool $subset + * + * @return string containing the sql used for getting the target bigbluebuttonbn instances + */ +function bigbluebuttonbn_get_recordings_deleted_sql_select($courseid = 0, $bigbluebuttonbnid = null, $subset = true) { + $sql = "log = '" . BIGBLUEBUTTONBN_LOG_EVENT_DELETE . "' AND meta like '%has_recordings%' AND meta like '%true%'"; + if (empty($courseid)) { + $courseid = 0; + } + if (empty($bigbluebuttonbnid)) { + return $sql . " AND courseid = {$courseid}"; + } + if ($subset) { + return $sql . " AND bigbluebuttonbnid = '{$bigbluebuttonbnid}'"; + } + return $sql . " AND courseid = {$courseid} AND bigbluebuttonbnid <> '{$bigbluebuttonbnid}'"; +} + +/** + * Helper function to define the sql used for gattering the bigbluebuttonbnids whose meetingids should be included + * in the getRecordings request considering only those that belong to imported recordings. + * + * @param string $courseid + * @param string $bigbluebuttonbnid + * @param bool $subset + * + * @return string containing the sql used for getting the target bigbluebuttonbn instances + */ +function bigbluebuttonbn_get_recordings_imported_sql_select($courseid = 0, $bigbluebuttonbnid = null, $subset = true) { + $sql = "log = '" . BIGBLUEBUTTONBN_LOG_EVENT_IMPORT . "'"; + if (empty($courseid)) { + $courseid = 0; + } + if (empty($bigbluebuttonbnid)) { + return $sql . " AND courseid = '{$courseid}'"; + } + if ($subset) { + return $sql . " AND bigbluebuttonbnid = '{$bigbluebuttonbnid}'"; + } + return $sql . " AND courseid = '{$courseid}' AND bigbluebuttonbnid <> '{$bigbluebuttonbnid}'"; +} + +/** + * Helper function to get recordings and imported recordings together. + * + * @param string $courseid + * @param string $bigbluebuttonbnid + * @param bool $subset + * @param bool $includedeleted + * + * @return associative array containing the recordings indexed by recordID, each recording is also a + * non sequential associative array itself that corresponds to the actual recording in BBB + */ +function bigbluebuttonbn_get_allrecordings($courseid = 0, $bigbluebuttonbnid = null, $subset = true, $includedeleted = false) { + $recordings = bigbluebuttonbn_get_recordings($courseid, $bigbluebuttonbnid, $subset, $includedeleted); + $recordingsimported = bigbluebuttonbn_get_recordings_imported_array($courseid, $bigbluebuttonbnid, $subset); + return ($recordings + $recordingsimported); +} + +/** + * Helper function to retrieve recordings from the BigBlueButton. The references are stored as events + * in bigbluebuttonbn_logs. + * + * @param string $courseid + * @param string $bigbluebuttonbnid + * @param bool $subset + * @param bool $includedeleted + * + * @return associative array containing the recordings indexed by recordID, each recording is also a + * non sequential associative array itself that corresponds to the actual recording in BBB + */ +function bigbluebuttonbn_get_recordings($courseid = 0, $bigbluebuttonbnid = null, $subset = true, $includedeleted = false) { + global $DB; + $select = bigbluebuttonbn_get_recordings_sql_select($courseid, $bigbluebuttonbnid, $subset); + $bigbluebuttonbns = $DB->get_records_select_menu('bigbluebuttonbn', $select, null, 'id', 'id, meetingid'); + /* Consider logs from deleted bigbluebuttonbn instances whose meetingids should be included in + * the getRecordings request. */ + if ($includedeleted) { + $selectdeleted = bigbluebuttonbn_get_recordings_deleted_sql_select($courseid, $bigbluebuttonbnid, $subset); + $bigbluebuttonbnsdel = $DB->get_records_select_menu( + 'bigbluebuttonbn_logs', + $selectdeleted, + null, + 'bigbluebuttonbnid', + 'bigbluebuttonbnid, meetingid' + ); + if (!empty($bigbluebuttonbnsdel)) { + // Merge bigbluebuttonbnis from deleted instances, only keys are relevant. + // Artimetic merge is used in order to keep the keys. + $bigbluebuttonbns += $bigbluebuttonbnsdel; + } + } + // Gather the meetingids from bigbluebuttonbn logs that include a create with record=true. + if (empty($bigbluebuttonbns)) { + return array(); + } + // Prepare select for loading records based on existent bigbluebuttonbns. + $sql = 'SELECT DISTINCT meetingid, bigbluebuttonbnid FROM {bigbluebuttonbn_logs} WHERE '; + $sql .= '(bigbluebuttonbnid=' . implode(' OR bigbluebuttonbnid=', array_keys($bigbluebuttonbns)) . ')'; + // Include only Create events and exclude those with record not true. + $sql .= ' AND log = ? AND meta LIKE ? AND meta LIKE ?'; + // Execute select for loading records based on existent bigbluebuttonbns. + $records = $DB->get_records_sql_menu($sql, array(BIGBLUEBUTTONBN_LOG_EVENT_CREATE, '%record%', '%true%')); + // Get actual recordings. + return bigbluebuttonbn_get_recordings_array(array_keys($records)); +} + +/** + * Helper function iterates an array with recordings and unset those already imported. + * + * @param array $recordings + * @param integer $courseid + * @param integer $bigbluebuttonbnid + * + * @return array + */ +function bigbluebuttonbn_unset_existent_recordings_already_imported($recordings, $courseid, $bigbluebuttonbnid) { + $recordingsimported = bigbluebuttonbn_get_recordings_imported_array($courseid, $bigbluebuttonbnid, true); + foreach ($recordings as $key => $recording) { + if (isset($recordingsimported[$recording['recordID']])) { + unset($recordings[$key]); + } + } + return $recordings; +} + +/** + * Helper function to count the imported recordings for a recordingid. + * + * @param string $recordid + * + * @return integer + */ +function bigbluebuttonbn_count_recording_imported_instances($recordid) { + global $DB; + $sql = 'SELECT COUNT(DISTINCT id) FROM {bigbluebuttonbn_logs} WHERE log = ? AND meta LIKE ? AND meta LIKE ?'; + return $DB->count_records_sql($sql, array(BIGBLUEBUTTONBN_LOG_EVENT_IMPORT, '%recordID%', "%{$recordid}%")); +} + +/** + * Helper function returns an array with all the instances of imported recordings for a recordingid. + * + * @param string $recordid + * + * @return array + */ +function bigbluebuttonbn_get_recording_imported_instances($recordid) { + global $DB; + $sql = 'SELECT * FROM {bigbluebuttonbn_logs} WHERE log = ? AND meta LIKE ? AND meta LIKE ?'; + $recordingsimported = $DB->get_records_sql($sql, array(BIGBLUEBUTTONBN_LOG_EVENT_IMPORT, '%recordID%', + "%{$recordid}%")); + return $recordingsimported; +} + +/** + * Helper function to get how much callback events are logged. + * + * @param string $recordid + * @param string $callbacktype + * + * @return integer + */ +function bigbluebuttonbn_get_count_callback_event_log($recordid, $callbacktype = 'recording_ready') { + global $DB; + $sql = 'SELECT count(DISTINCT id) FROM {bigbluebuttonbn_logs} WHERE log = ? AND meta LIKE ? AND meta LIKE ?'; + // Callback type added on version 2.4, validate recording_ready first or assume it on records with no callback. + if ($callbacktype == 'recording_ready') { + $sql .= ' AND (meta LIKE ? OR meta NOT LIKE ? )'; + $count = $DB->count_records_sql($sql, array(BIGBLUEBUTTON_LOG_EVENT_CALLBACK, '%recordid%', "%$recordid%", + $callbacktype, 'callback')); + return $count; + } + $sql .= ' AND meta LIKE ?;'; + $count = $DB->count_records_sql($sql, array(BIGBLUEBUTTON_LOG_EVENT_CALLBACK, '%recordid%', "%$recordid%", "%$callbacktype%")); + return $count; +} + +/** + * Helper function returns an array with the profiles (with features per profile) for the different types + * of bigbluebuttonbn instances. + * + * @return array + */ +function bigbluebuttonbn_get_instance_type_profiles() { + $instanceprofiles = array( + BIGBLUEBUTTONBN_TYPE_ALL => array('id' => BIGBLUEBUTTONBN_TYPE_ALL, + 'name' => get_string('instance_type_default', 'bigbluebuttonbn'), + 'features' => array('all')), + BIGBLUEBUTTONBN_TYPE_ROOM_ONLY => array('id' => BIGBLUEBUTTONBN_TYPE_ROOM_ONLY, + 'name' => get_string('instance_type_room_only', 'bigbluebuttonbn'), + 'features' => array('showroom', 'welcomemessage', 'voicebridge', 'waitformoderator', 'userlimit', + 'recording', 'sendnotifications', 'preuploadpresentation', 'permissions', 'schedule', 'groups', + 'modstandardelshdr', 'availabilityconditionsheader', 'tagshdr', 'competenciessection', + 'clienttype', 'completionattendance', 'completionengagement', 'availabilityconditionsheader')), + BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY => array('id' => BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY, + 'name' => get_string('instance_type_recording_only', 'bigbluebuttonbn'), + 'features' => array('showrecordings', 'importrecordings', 'availabilityconditionsheader')), + ); + return $instanceprofiles; +} + +/** + * Helper function returns an array with enabled features for an specific profile type. + * + * @param array $typeprofiles + * @param string $type + * + * @return array + */ +function bigbluebuttonbn_get_enabled_features($typeprofiles, $type = null) { + $enabledfeatures = array(); + $features = $typeprofiles[BIGBLUEBUTTONBN_TYPE_ALL]['features']; + if (!is_null($type) && key_exists($type, $typeprofiles)) { + $features = $typeprofiles[$type]['features']; + } + $enabledfeatures['showroom'] = (in_array('all', $features) || in_array('showroom', $features)); + // Evaluates if recordings are enabled for the Moodle site. + $enabledfeatures['showrecordings'] = false; + if (\mod_bigbluebuttonbn\locallib\config::recordings_enabled()) { + $enabledfeatures['showrecordings'] = (in_array('all', $features) || in_array('showrecordings', $features)); + } + $enabledfeatures['importrecordings'] = false; + if (\mod_bigbluebuttonbn\locallib\config::importrecordings_enabled()) { + $enabledfeatures['importrecordings'] = (in_array('all', $features) || in_array('importrecordings', $features)); + } + // Evaluates if clienttype is enabled for the Moodle site. + $enabledfeatures['clienttype'] = false; + if (\mod_bigbluebuttonbn\locallib\config::clienttype_enabled()) { + $enabledfeatures['clienttype'] = (in_array('all', $features) || in_array('clienttype', $features)); + } + return $enabledfeatures; +} + +/** + * Helper function returns an array with the profiles (with features per profile) for the different types + * of bigbluebuttonbn instances that the user is allowed to create. + * + * @param boolean $room + * @param boolean $recording + * + * @return array + */ +function bigbluebuttonbn_get_instance_type_profiles_create_allowed($room, $recording) { + $profiles = bigbluebuttonbn_get_instance_type_profiles(); + if (!$room) { + unset($profiles[BIGBLUEBUTTONBN_TYPE_ROOM_ONLY]); + unset($profiles[BIGBLUEBUTTONBN_TYPE_ALL]); + } + if (!$recording) { + unset($profiles[BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY]); + unset($profiles[BIGBLUEBUTTONBN_TYPE_ALL]); + } + return $profiles; +} + +/** + * Helper function returns an array with the profiles (with features per profile) for the different types + * of bigbluebuttonbn instances. + * + * @param array $profiles + * + * @return array + */ +function bigbluebuttonbn_get_instance_profiles_array($profiles = []) { + $profilesarray = array(); + foreach ($profiles as $key => $profile) { + $profilesarray[$profile['id']] = $profile['name']; + } + return $profilesarray; +} + +/** + * Helper function returns time in a formatted string. + * + * @param integer $time + * + * @return string + */ +function bigbluebuttonbn_format_activity_time($time) { + global $CFG; + require_once($CFG->dirroot.'/calendar/lib.php'); + $activitytime = ''; + if ($time) { + $activitytime = calendar_day_representation($time) . ' ' . + get_string('mod_form_field_notification_msg_at', 'bigbluebuttonbn') . ' ' . + calendar_time_representation($time); + } + return $activitytime; +} + +/** + * Helper function returns array with all the strings to be used in javascript. + * + * @return array + */ +function bigbluebuttonbn_get_strings_for_js() { + $locale = bigbluebuttonbn_get_locale(); + $stringman = get_string_manager(); + $strings = $stringman->load_component_strings('bigbluebuttonbn', $locale); + return $strings; +} + +/** + * Helper function returns the locale set by moodle. + * + * @return string + */ +function bigbluebuttonbn_get_locale() { + $lang = get_string('locale', 'core_langconfig'); + return substr($lang, 0, strpos($lang, '.')); +} + +/** + * Helper function returns the locale code based on the locale set by moodle. + * + * @return string + */ +function bigbluebuttonbn_get_localcode() { + $locale = bigbluebuttonbn_get_locale(); + return substr($locale, 0, strpos($locale, '_')); +} + +/** + * Helper function returns array with the instance settings used in views. + * + * @param string $id + * @param object $bigbluebuttonbnid + * + * @return array + */ +function bigbluebuttonbn_view_validator($id, $bigbluebuttonbnid) { + if ($id) { + return bigbluebuttonbn_view_instance_id($id); + } + if ($bigbluebuttonbnid) { + return bigbluebuttonbn_view_instance_bigbluebuttonbn($bigbluebuttonbnid); + } +} + +/** + * Helper function returns array with the instance settings used in views based on id. + * + * @param string $id + * + * @return array + */ +function bigbluebuttonbn_view_instance_id($id) { + global $DB; + $cm = get_coursemodule_from_id('bigbluebuttonbn', $id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $cm->instance), '*', MUST_EXIST); + return array('cm' => $cm, 'course' => $course, 'bigbluebuttonbn' => $bigbluebuttonbn); +} + +/** + * Helper function returns array with the instance settings used in views based on bigbluebuttonbnid. + * + * @param object $bigbluebuttonbnid + * + * @return array + */ +function bigbluebuttonbn_view_instance_bigbluebuttonbn($bigbluebuttonbnid) { + global $DB; + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $bigbluebuttonbnid), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $bigbluebuttonbn->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('bigbluebuttonbn', $bigbluebuttonbn->id, $course->id, false, MUST_EXIST); + return array('cm' => $cm, 'course' => $course, 'bigbluebuttonbn' => $bigbluebuttonbn); +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_general(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_general_shown()) { + $renderer->render_group_header('general'); + $renderer->render_group_element( + 'server_url', + $renderer->render_group_element_text('server_url', BIGBLUEBUTTONBN_DEFAULT_SERVER_URL) + ); + $renderer->render_group_element( + 'shared_secret', + $renderer->render_group_element_text('shared_secret', BIGBLUEBUTTONBN_DEFAULT_SHARED_SECRET) + ); + } +} + +/** + * Helper function renders record settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_record(&$renderer) { + // Configuration for 'recording' feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_record_meeting_shown()) { + $renderer->render_group_header('recording'); + $renderer->render_group_element( + 'recording_default', + $renderer->render_group_element_checkbox('recording_default', 1) + ); + $renderer->render_group_element( + 'recording_editable', + $renderer->render_group_element_checkbox('recording_editable', 1) + ); + $renderer->render_group_element( + 'recording_icons_enabled', + $renderer->render_group_element_checkbox('recording_icons_enabled', 1) + ); + + // Add recording start to load and allow/hide stop/pause. + $renderer->render_group_element( + 'recording_all_from_start_default', + $renderer->render_group_element_checkbox('recording_all_from_start_default', 0) + ); + $renderer->render_group_element( + 'recording_all_from_start_editable', + $renderer->render_group_element_checkbox('recording_all_from_start_editable', 0) + ); + $renderer->render_group_element( + 'recording_hide_button_default', + $renderer->render_group_element_checkbox('recording_hide_button_default', 0) + ); + $renderer->render_group_element( + 'recording_hide_button_editable', + $renderer->render_group_element_checkbox('recording_hide_button_editable', 0) + ); + } +} + +/** + * Helper function renders import recording settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_importrecordings(&$renderer) { + // Configuration for 'import recordings' feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_import_recordings_shown()) { + $renderer->render_group_header('importrecordings'); + $renderer->render_group_element( + 'importrecordings_enabled', + $renderer->render_group_element_checkbox('importrecordings_enabled', 0) + ); + $renderer->render_group_element( + 'importrecordings_from_deleted_enabled', + $renderer->render_group_element_checkbox('importrecordings_from_deleted_enabled', 0) + ); + } +} + +/** + * Helper function renders show recording settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_showrecordings(&$renderer) { + // Configuration for 'show recordings' feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_show_recordings_shown()) { + $renderer->render_group_header('recordings'); + $renderer->render_group_element( + 'recordings_html_default', + $renderer->render_group_element_checkbox('recordings_html_default', 1) + ); + $renderer->render_group_element( + 'recordings_html_editable', + $renderer->render_group_element_checkbox('recordings_html_editable', 0) + ); + $renderer->render_group_element( + 'recordings_deleted_default', + $renderer->render_group_element_checkbox('recordings_deleted_default', 1) + ); + $renderer->render_group_element( + 'recordings_deleted_editable', + $renderer->render_group_element_checkbox('recordings_deleted_editable', 0) + ); + $renderer->render_group_element( + 'recordings_imported_default', + $renderer->render_group_element_checkbox('recordings_imported_default', 0) + ); + $renderer->render_group_element( + 'recordings_imported_editable', + $renderer->render_group_element_checkbox('recordings_imported_editable', 1) + ); + $renderer->render_group_element( + 'recordings_preview_default', + $renderer->render_group_element_checkbox('recordings_preview_default', 1) + ); + $renderer->render_group_element( + 'recordings_preview_editable', + $renderer->render_group_element_checkbox('recordings_preview_editable', 0) + ); + $renderer->render_group_element( + 'recordings_sortorder', + $renderer->render_group_element_checkbox('recordings_sortorder', 0) + ); + $renderer->render_group_element( + 'recordings_validate_url', + $renderer->render_group_element_checkbox('recordings_validate_url', 1) + ); + } +} + +/** + * Helper function renders wait for moderator settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_waitmoderator(&$renderer) { + // Configuration for wait for moderator feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_wait_moderator_shown()) { + $renderer->render_group_header('waitformoderator'); + $renderer->render_group_element( + 'waitformoderator_default', + $renderer->render_group_element_checkbox('waitformoderator_default', 0) + ); + $renderer->render_group_element( + 'waitformoderator_editable', + $renderer->render_group_element_checkbox('waitformoderator_editable', 1) + ); + $renderer->render_group_element( + 'waitformoderator_ping_interval', + $renderer->render_group_element_text('waitformoderator_ping_interval', 10, PARAM_INT) + ); + $renderer->render_group_element( + 'waitformoderator_cache_ttl', + $renderer->render_group_element_text('waitformoderator_cache_ttl', 60, PARAM_INT) + ); + } +} + +/** + * Helper function renders static voice bridge settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_voicebridge(&$renderer) { + // Configuration for "static voice bridge" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_static_voice_bridge_shown()) { + $renderer->render_group_header('voicebridge'); + $renderer->render_group_element( + 'voicebridge_editable', + $renderer->render_group_element_checkbox('voicebridge_editable', 0) + ); + } +} + +/** + * Helper function renders preuploaded presentation settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_preupload(&$renderer) { + // Configuration for "preupload presentation" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_preupload_presentation_shown()) { + // This feature only works if curl is installed. + $preuploaddescripion = get_string('config_preuploadpresentation_description', 'bigbluebuttonbn'); + if (!extension_loaded('curl')) { + $preuploaddescripion .= '<div class="form-defaultinfo">'; + $preuploaddescripion .= get_string('config_warning_curl_not_installed', 'bigbluebuttonbn'); + $preuploaddescripion .= '</div><br>'; + } + $renderer->render_group_header('preuploadpresentation', null, $preuploaddescripion); + if (extension_loaded('curl')) { + $renderer->render_group_element( + 'preuploadpresentation_enabled', + $renderer->render_group_element_checkbox('preuploadpresentation_enabled', 0) + ); + } + } +} + +/** + * Helper function renders preuploaded presentation manage file if the feature is enabled. + * This allow to select a file for use as default in all BBB instances if preuploaded presetantion is enable. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_preupload_manage_default_file(&$renderer) { + // Configuration for "preupload presentation" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_preupload_presentation_shown()) { + if (extension_loaded('curl')) { + // This feature only works if curl is installed. + $renderer->render_filemanager_default_file_presentation("presentation_default"); + } + } +} + +/** + * Helper function renders userlimit settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_userlimit(&$renderer) { + // Configuration for "user limit" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_user_limit_shown()) { + $renderer->render_group_header('userlimit'); + $renderer->render_group_element( + 'userlimit_default', + $renderer->render_group_element_text('userlimit_default', 0, PARAM_INT) + ); + $renderer->render_group_element( + 'userlimit_editable', + $renderer->render_group_element_checkbox('userlimit_editable', 0) + ); + } +} + +/** + * Helper function renders duration settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_duration(&$renderer) { + // Configuration for "scheduled duration" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_scheduled_duration_shown()) { + $renderer->render_group_header('scheduled'); + $renderer->render_group_element( + 'scheduled_duration_enabled', + $renderer->render_group_element_checkbox('scheduled_duration_enabled', 1) + ); + $renderer->render_group_element( + 'scheduled_duration_compensation', + $renderer->render_group_element_text('scheduled_duration_compensation', 10, PARAM_INT) + ); + $renderer->render_group_element( + 'scheduled_pre_opening', + $renderer->render_group_element_text('scheduled_pre_opening', 10, PARAM_INT) + ); + } +} + +/** + * Helper function renders participant settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_participants(&$renderer) { + // Configuration for defining the default role/user that will be moderator on new activities. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_moderator_default_shown()) { + $renderer->render_group_header('participant'); + // UI for 'participants' feature. + $roles = bigbluebuttonbn_get_roles(null, false); + $owner = array('0' => get_string('mod_form_field_participant_list_type_owner', 'bigbluebuttonbn')); + $renderer->render_group_element( + 'participant_moderator_default', + $renderer->render_group_element_configmultiselect( + 'participant_moderator_default', + array_keys($owner), + $owner + $roles // CONTRIB-7966: don't use array_merge here so it does not reindex the array. + ) + ); + } +} + +/** + * Helper function renders notification settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_notifications(&$renderer) { + // Configuration for "send notifications" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_send_notifications_shown()) { + $renderer->render_group_header('sendnotifications'); + $renderer->render_group_element( + 'sendnotifications_enabled', + $renderer->render_group_element_checkbox('sendnotifications_enabled', 1) + ); + } +} + +/** + * Helper function renders client type settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_clienttype(&$renderer) { + // Configuration for "clienttype" feature. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_clienttype_shown()) { + $renderer->render_group_header('clienttype'); + $renderer->render_group_element( + 'clienttype_editable', + $renderer->render_group_element_checkbox('clienttype_editable', 0) + ); + // Web Client default. + $default = intval((int) \mod_bigbluebuttonbn\locallib\config::get('clienttype_default')); + $choices = array(BIGBLUEBUTTON_CLIENTTYPE_FLASH => get_string('mod_form_block_clienttype_flash', 'bigbluebuttonbn'), + BIGBLUEBUTTON_CLIENTTYPE_HTML5 => get_string('mod_form_block_clienttype_html5', 'bigbluebuttonbn')); + $renderer->render_group_element( + 'clienttype_default', + $renderer->render_group_element_configselect( + 'clienttype_default', + $default, + $choices + ) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_muteonstart(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_muteonstart_shown()) { + $renderer->render_group_header('muteonstart'); + $renderer->render_group_element( + 'muteonstart_default', + $renderer->render_group_element_checkbox('muteonstart_default', 0) + ); + $renderer->render_group_element( + 'muteonstart_editable', + $renderer->render_group_element_checkbox('muteonstart_editable', 0) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_locksettings(&$renderer) { + $renderer->render_group_header('locksettings'); + // Configuration for various lock settings for meetings. + bigbluebuttonbn_settings_disablecam($renderer); + bigbluebuttonbn_settings_disablemic($renderer); + bigbluebuttonbn_settings_disableprivatechat($renderer); + bigbluebuttonbn_settings_disablepublicchat($renderer); + bigbluebuttonbn_settings_disablenote($renderer); + bigbluebuttonbn_settings_hideuserlist($renderer); + bigbluebuttonbn_settings_lockedlayout($renderer); + bigbluebuttonbn_settings_lockonjoin($renderer); + bigbluebuttonbn_settings_lockonjoinconfigurable($renderer); +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_disablecam(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_disablecam_shown()) { + $renderer->render_group_element( + 'disablecam_default', + $renderer->render_group_element_checkbox('disablecam_default', 0) + ); + $renderer->render_group_element( + 'disablecam_editable', + $renderer->render_group_element_checkbox('disablecam_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_disablemic(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_disablemic_shown()) { + $renderer->render_group_element( + 'disablemic_default', + $renderer->render_group_element_checkbox('disablemic_default', 0) + ); + $renderer->render_group_element( + 'disablecam_editable', + $renderer->render_group_element_checkbox('disablemic_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_disableprivatechat(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_disableprivatechat_shown()) { + $renderer->render_group_element( + 'disableprivatechat_default', + $renderer->render_group_element_checkbox('disableprivatechat_default', 0) + ); + $renderer->render_group_element( + 'disableprivatechat_editable', + $renderer->render_group_element_checkbox('disableprivatechat_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_disablepublicchat(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_disablepublicchat_shown()) { + $renderer->render_group_element( + 'disablepublicchat_default', + $renderer->render_group_element_checkbox('disablepublicchat_default', 0) + ); + $renderer->render_group_element( + 'disablepublicchat_editable', + $renderer->render_group_element_checkbox('disablepublicchat_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_disablenote(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_disablenote_shown()) { + $renderer->render_group_element( + 'disablenote_default', + $renderer->render_group_element_checkbox('disablenote_default', 0) + ); + $renderer->render_group_element( + 'disablenote_editable', + $renderer->render_group_element_checkbox('disablenote_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_hideuserlist(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_hideuserlist_shown()) { + $renderer->render_group_element( + 'hideuserlist_default', + $renderer->render_group_element_checkbox('hideuserlist_default', 0) + ); + $renderer->render_group_element( + 'hideuserlist_editable', + $renderer->render_group_element_checkbox('hideuserlist_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_lockedlayout(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_lockedlayout_shown()) { + $renderer->render_group_element( + 'lockedlayout_default', + $renderer->render_group_element_checkbox('lockedlayout_default', 0) + ); + $renderer->render_group_element( + 'lockedlayout_editable', + $renderer->render_group_element_checkbox('lockedlayout_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_lockonjoin(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_lockonjoin_shown()) { + $renderer->render_group_element( + 'lockonjoin_default', + $renderer->render_group_element_checkbox('lockonjoin_default', 0) + ); + $renderer->render_group_element( + 'lockonjoin_editable', + $renderer->render_group_element_checkbox('lockonjoin_editable', 1) + ); + } +} + +/** + * Helper function renders general settings if the feature is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_lockonjoinconfigurable(&$renderer) { + // Configuration for BigBlueButton. + if ((boolean) \mod_bigbluebuttonbn\settings\validator::section_lockonjoinconfigurable_shown()) { + $renderer->render_group_element( + 'lockonjoinconfigurable_default', + $renderer->render_group_element_checkbox('lockonjoinconfigurable_default', 0) + ); + $renderer->render_group_element( + 'lockonjoinconfigurable_editable', + $renderer->render_group_element_checkbox('lockonjoinconfigurable_editable', 1) + ); + } +} + +/** + * Helper function renders default messages settings. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_default_messages(&$renderer) { + $renderer->render_group_header('default_messages'); + $renderer->render_group_element( + 'welcome_default', + $renderer->render_group_element_textarea('welcome_default', '', PARAM_TEXT) + ); +} + +/** + * Helper function renders extended settings if any of the features there is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_extended(&$renderer) { + // Configuration for extended capabilities. + if (!(boolean) \mod_bigbluebuttonbn\settings\validator::section_settings_extended_shown()) { + return; + } + $renderer->render_group_header('extended_capabilities'); + // UI for 'notify users when recording ready' feature. + $renderer->render_group_element( + 'recordingready_enabled', + $renderer->render_group_element_checkbox('recordingready_enabled', 0) + ); + // Configuration for extended BN capabilities should go here. +} + +/** + * Helper function renders experimental settings if any of the features there is enabled. + * + * @param object $renderer + * + * @return void + */ +function bigbluebuttonbn_settings_experimental(&$renderer) { + // Configuration for experimental features should go here. + $renderer->render_group_header('experimental_features'); + // UI for 'register meeting events' feature. + $renderer->render_group_element( + 'meetingevents_enabled', + $renderer->render_group_element_checkbox('meetingevents_enabled', 0) + ); +} + +/** + * Helper function returns a sha1 encoded string that is unique and will be used as a seed for meetingid. + * + * @return string + */ +function bigbluebuttonbn_unique_meetingid_seed() { + global $DB; + do { + $encodedseed = sha1(bigbluebuttonbn_random_password(12)); + $meetingid = (string) $DB->get_field('bigbluebuttonbn', 'meetingid', array('meetingid' => $encodedseed)); + } while ($meetingid == $encodedseed); + return $encodedseed; +} + +/** + * Helper function renders the link used for recording type in row for the data used by the recording table. + * + * @param array $recording + * @param array $bbbsession + * @param array $playback + * + * @return boolean + */ +function bigbluebuttonbn_include_recording_data_row_type($recording, $bbbsession, $playback) { + // All types that are not restricted are included. + if (array_key_exists('restricted', $playback) && strtolower($playback['restricted']) == 'false') { + return true; + } + // All types that are not statistics are included. + if ($playback['type'] != 'statistics') { + return true; + } + // Exclude imported recordings. + if (isset($recording['imported'])) { + return false; + } + // Exclude non moderators. + if (!$bbbsession['administrator'] && !$bbbsession['moderator']) { + return false; + } + return true; +} + +/** + * Renders the general warning message. + * + * @param string $message + * @param string $type + * @param string $href + * @param string $text + * @param string $class + * + * @return string + */ +function bigbluebuttonbn_render_warning($message, $type = 'info', $href = '', $text = '', $class = '') { + global $OUTPUT; + $output = "\n"; + // Evaluates if config_warning is enabled. + if (empty($message)) { + return $output; + } + $output .= $OUTPUT->box_start( + 'box boxalignleft adminerror alert alert-' . $type . ' alert-block fade in', + 'bigbluebuttonbn_view_general_warning' + ) . "\n"; + $output .= ' ' . $message . "\n"; + $output .= ' <div class="singlebutton pull-right">' . "\n"; + if (!empty($href)) { + $output .= bigbluebuttonbn_render_warning_button($href, $text, $class); + } + $output .= ' </div>' . "\n"; + $output .= $OUTPUT->box_end() . "\n"; + return $output; +} + +/** + * Renders the general warning button. + * + * @param string $href + * @param string $text + * @param string $class + * @param string $title + * + * @return string + */ +function bigbluebuttonbn_render_warning_button($href, $text = '', $class = '', $title = '') { + if ($text == '') { + $text = get_string('ok', 'moodle'); + } + if ($title == '') { + $title = $text; + } + if ($class == '') { + $class = 'btn btn-secondary'; + } + $output = ' <form method="post" action="' . $href . '" class="form-inline">' . "\n"; + $output .= ' <button type="submit" class="' . $class . '"' . "\n"; + $output .= ' title="' . $title . '"' . "\n"; + $output .= ' >' . $text . '</button>' . "\n"; + $output .= ' </form>' . "\n"; + return $output; +} + +/** + * Check if a BigBlueButtonBN is available to be used by the current user. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance + * + * @return boolean status if room available and current user allowed to join + */ +function bigbluebuttonbn_get_availability_status($bigbluebuttonbn) { + list($roomavailable) = bigbluebuttonbn_room_is_available($bigbluebuttonbn); + list($usercanjoin) = bigbluebuttonbn_user_can_join_meeting($bigbluebuttonbn); + return ($roomavailable && $usercanjoin); +} + +/** + * Helper for evaluating if scheduled activity is avaiable. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance + * + * @return array status (room available or not and possible warnings) + */ +function bigbluebuttonbn_room_is_available($bigbluebuttonbn) { + $open = true; + $closed = false; + $warnings = array(); + + $timenow = time(); + $timeopen = $bigbluebuttonbn->openingtime; + $timeclose = $bigbluebuttonbn->closingtime; + if (!empty($timeopen) && $timeopen > $timenow) { + $open = false; + } + if (!empty($timeclose) && $timenow > $timeclose) { + $closed = true; + } + + if (!$open || $closed) { + if (!$open) { + $warnings['notopenyet'] = userdate($timeopen); + } + if ($closed) { + $warnings['expired'] = userdate($timeclose); + } + return array(false, $warnings); + } + + return array(true, $warnings); +} + +/** + * Helper for evaluating if meeting can be joined. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance + * @param string $mid + * @param integer $userid + * + * @return array status (user allowed to join or not and possible message) + */ +function bigbluebuttonbn_user_can_join_meeting($bigbluebuttonbn, $mid = null, $userid = null) { + // By default, use a meetingid without groups. + if (empty($mid)) { + $mid = $bigbluebuttonbn->meetingid . '-' . $bigbluebuttonbn->course . '-' . $bigbluebuttonbn->id; + } + // When meeting is running, all authorized users can join right in. + if (bigbluebuttonbn_is_meeting_running($mid)) { + return array(true, get_string('view_message_conference_in_progress', 'bigbluebuttonbn')); + } + // When meeting is not running, see if the user can join. + $context = context_course::instance($bigbluebuttonbn->course); + $participantlist = bigbluebuttonbn_get_participant_list($bigbluebuttonbn, $context); + $isadmin = is_siteadmin($userid); + $ismoderator = bigbluebuttonbn_is_moderator($context, $participantlist, $userid); + // If user is administrator, moderator or if is viewer and no waiting is required, join allowed. + if ($isadmin || $ismoderator || !$bigbluebuttonbn->wait) { + return array(true, get_string('view_message_conference_room_ready', 'bigbluebuttonbn')); + } + // Otherwise, no join allowed. + return array(false, get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn')); +} + +/** + * Helper for getting a value from a bigbluebuttonbn cache. + * + * @param string $name BigBlueButtonBN cache + * @param string $key Key to be retrieved + * @param integer $default Default value in case key is not found or it is empty + * + * @return variable key value + */ +function bigbluebuttonbn_cache_get($name, $key, $default = null) { + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', $name); + $result = $cache->get($key); + if (!empty($result)) { + return $result; + } + return $default; +} + +/** + * Helper for setting a value in a bigbluebuttonbn cache. + * + * @param string $name BigBlueButtonBN cache + * @param string $key Key to be created/updated + * @param variable $value Default value to be set + */ +function bigbluebuttonbn_cache_set($name, $key, $value) { + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', $name); + $cache->set($key, $value); +} + +/** + * Helper for getting the owner userid of a bigbluebuttonbn instance. + * + * @param stdClass $bigbluebuttonbn BigBlueButtonBN instance + * + * @return integer ownerid (a valid user id or null if not registered/found) + */ +function bigbluebuttonbn_instance_ownerid($bigbluebuttonbn) { + global $DB; + $filters = array('bigbluebuttonbnid' => $bigbluebuttonbn->id, 'log' => 'Add'); + $ownerid = (integer) $DB->get_field('bigbluebuttonbn_logs', 'userid', $filters); + return $ownerid; +} + +/** + * Helper evaluates if the bigbluebutton server used belongs to blindsidenetworks domain. + * + * @return boolean + */ +function bigbluebuttonbn_has_html5_client() { + $checkurl = \mod_bigbluebuttonbn\locallib\bigbluebutton::root() . "html5client/check"; + $curlinfo = bigbluebuttonbn_wrap_xml_load_file_curl_request($checkurl, 'HEAD'); + return (isset($curlinfo['http_code']) && $curlinfo['http_code'] == 200); +} + +/** + * Return the status of an activity [open|not_started|ended]. + * + * @param array $bbbsession + * @return string + */ +function bigbluebuttonbn_view_get_activity_status(&$bbbsession) { + $now = time(); + if (!empty($bbbsession['bigbluebuttonbn']->openingtime) && $now < $bbbsession['bigbluebuttonbn']->openingtime) { + // The activity has not been opened. + return 'not_started'; + } + if (!empty($bbbsession['bigbluebuttonbn']->closingtime) && $now > $bbbsession['bigbluebuttonbn']->closingtime) { + // The activity has been closed. + return 'ended'; + } + // The activity is open. + return 'open'; +} + +/** + * Set session URLs. + * + * @param array $bbbsession + * @param int $id + * @return string + */ +function bigbluebuttonbn_view_session_config(&$bbbsession, $id) { + // Operation URLs. + $bbbsession['bigbluebuttonbnURL'] = plugin::necurl( + '/mod/bigbluebuttonbn/view.php', + ['id' => $bbbsession['cm']->id] + ); + $bbbsession['logoutURL'] = plugin::necurl( + '/mod/bigbluebuttonbn/bbb_view.php', + ['action' => 'logout', 'id' => $id, 'bn' => $bbbsession['bigbluebuttonbn']->id] + ); + $bbbsession['recordingReadyURL'] = plugin::necurl( + '/mod/bigbluebuttonbn/bbb_broker.php', + ['action' => 'recording_ready', 'bigbluebuttonbn' => $bbbsession['bigbluebuttonbn']->id] + ); + $bbbsession['meetingEventsURL'] = plugin::necurl( + '/mod/bigbluebuttonbn/bbb_broker.php', + ['action' => 'meeting_events', 'bigbluebuttonbn' => $bbbsession['bigbluebuttonbn']->id] + ); + $bbbsession['joinURL'] = plugin::necurl( + '/mod/bigbluebuttonbn/bbb_view.php', + ['action' => 'join', 'id' => $id, 'bn' => $bbbsession['bigbluebuttonbn']->id] + ); + + // Check status and set extra values. + $activitystatus = bigbluebuttonbn_view_get_activity_status($bbbsession); // In locallib. + if ($activitystatus == 'ended') { + $bbbsession['presentation'] = bigbluebuttonbn_get_presentation_array( + $bbbsession['context'], + $bbbsession['bigbluebuttonbn']->presentation + ); + } else if ($activitystatus == 'open') { + $bbbsession['presentation'] = bigbluebuttonbn_get_presentation_array( + $bbbsession['context'], + $bbbsession['bigbluebuttonbn']->presentation, + $bbbsession['bigbluebuttonbn']->id + ); + } + + return $activitystatus; +} + +/** + * Helper for preparing metadata used while creating the meeting. + * + * @param array $bbbsession + * @return array + */ +function bigbluebuttonbn_create_meeting_metadata(&$bbbsession) { + global $USER; + // Create standard metadata. + $metadata = [ + 'bbb-origin' => $bbbsession['origin'], + 'bbb-origin-version' => $bbbsession['originVersion'], + 'bbb-origin-server-name' => $bbbsession['originServerName'], + 'bbb-origin-server-common-name' => $bbbsession['originServerCommonName'], + 'bbb-origin-tag' => $bbbsession['originTag'], + 'bbb-context' => $bbbsession['course']->fullname, + 'bbb-context-id' => $bbbsession['course']->id, + 'bbb-context-name' => trim(html_to_text($bbbsession['course']->fullname, 0)), + 'bbb-context-label' => trim(html_to_text($bbbsession['course']->shortname, 0)), + 'bbb-recording-name' => bigbluebuttonbn_html2text($bbbsession['meetingname'], 64), + 'bbb-recording-description' => bigbluebuttonbn_html2text($bbbsession['meetingdescription'], 64), + 'bbb-recording-tags' => bigbluebuttonbn_get_tags($bbbsession['cm']->id), // Same as $id. + ]; + // Special metadata for recording processing. + if ((boolean) \mod_bigbluebuttonbn\locallib\config::get('recordingstatus_enabled')) { + $metadata["bn-recording-status"] = json_encode( + array( + 'email' => array('"' . fullname($USER) . '" <' . $USER->email . '>'), + 'context' => $bbbsession['bigbluebuttonbnURL'], + ) + ); + } + if ((boolean) \mod_bigbluebuttonbn\locallib\config::get('recordingready_enabled')) { + $metadata['bn-recording-ready-url'] = $bbbsession['recordingReadyURL']; + } + if ((boolean) \mod_bigbluebuttonbn\locallib\config::get('meetingevents_enabled')) { + $metadata['analytics-callback-url'] = $bbbsession['meetingEventsURL']; + } + return $metadata; +} diff --git a/mod/bigbluebuttonbn/mobile.notification.js b/mod/bigbluebuttonbn/mobile.notification.js new file mode 100644 index 0000000..4ca9173 --- /dev/null +++ b/mod/bigbluebuttonbn/mobile.notification.js @@ -0,0 +1,39 @@ +// 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/>. + +/** + * This file is part of the Moodle apps support for the choicegroup plugin. + * Defines the function to be used from the mobile course view template. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent [at] call-learning [dt] fr) + */ + +// That will be the global this (ionic convention). +var that = this; + +var TIMEOUTCHECK = 20000; + +that.onCanJoinReturns = function (data) { + if (data && data.can_join) { + that.openContent('', {'cmid': data.cmid}, 'mod_bigbluebuttonbn', 'mobile_course_view'); + } else { + setTimeout(function () { + that.refreshContent(true); + }, TIMEOUTCHECK); + } +}; \ No newline at end of file diff --git a/mod/bigbluebuttonbn/mod_form.php b/mod/bigbluebuttonbn/mod_form.php new file mode 100644 index 0000000..b4a38c9 --- /dev/null +++ b/mod/bigbluebuttonbn/mod_form.php @@ -0,0 +1,705 @@ +<?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/>. + +/** + * Config all BigBlueButtonBN instances in this course. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/locallib.php'); +require_once($CFG->dirroot.'/course/moodleform_mod.php'); + +/** + * Moodle class for mod_form. + * + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_bigbluebuttonbn_mod_form extends moodleform_mod { + + /** + * Define (add) particular settings this activity can have. + * + * @return void + */ + public function definition() { + global $CFG, $DB, $OUTPUT, $PAGE; + $mform = &$this->_form; + + // Validates if the BigBlueButton server is running. + $serverversion = bigbluebuttonbn_get_server_version(); + if (is_null($serverversion)) { + print_error('general_error_unable_connect', 'bigbluebuttonbn', + $CFG->wwwroot.'/admin/settings.php?section=modsettingbigbluebuttonbn'); + return; + } + $bigbluebuttonbn = null; + if ($this->current->id) { + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $this->current->id), '*', MUST_EXIST); + } + // UI configuration options. + $cfg = \mod_bigbluebuttonbn\locallib\config::get_options(); + + $jsvars = array(); + + // Get only those that are allowed. + $course = get_course($this->current->course); + $context = context_course::instance($course->id); + $jsvars['instanceTypeProfiles'] = bigbluebuttonbn_get_instance_type_profiles_create_allowed( + has_capability('mod/bigbluebuttonbn:meeting', $context), + has_capability('mod/bigbluebuttonbn:recording', $context) + ); + // If none is allowed, fail and return. + if (empty($jsvars['instanceTypeProfiles'])) { + // Also check module context for those that are allowed + $contextm = context_module::instance($this->_cm->id); + $jsvars['instanceTypeProfiles'] = bigbluebuttonbn_get_instance_type_profiles_create_allowed( + has_capability('mod/bigbluebuttonbn:meeting', $contextm), + has_capability('mod/bigbluebuttonbn:recording', $contextm) + ); + // If still none is allowed, fail and return. + if (empty($jsvars['instanceTypeProfiles'])) { + print_error('general_error_not_allowed_to_create_instances)', 'bigbluebuttonbn', + $CFG->wwwroot.'/admin/settings.php?section=modsettingbigbluebuttonbn'); + return; + } + } + $jsvars['instanceTypeDefault'] = array_keys($jsvars['instanceTypeProfiles'])[0]; + $this->bigbluebuttonbn_mform_add_block_profiles($mform, $jsvars['instanceTypeProfiles']); + // Data for participant selection. + $participantlist = bigbluebuttonbn_get_participant_list($bigbluebuttonbn, $context); + // Add block 'General'. + $this->bigbluebuttonbn_mform_add_block_general($mform, $cfg); + // Add block 'Room'. + $this->bigbluebuttonbn_mform_add_block_room($mform, $cfg); + // Add block 'Lock'. + $this->bigbluebuttonbn_mform_add_block_locksettings($mform, $cfg); + // Add block 'Preuploads'. + $this->bigbluebuttonbn_mform_add_block_preuploads($mform, $cfg); + // Add block 'Participant List'. + $this->bigbluebuttonbn_mform_add_block_user_role_mapping($mform, $participantlist); + // Add block 'Schedule'. + $this->bigbluebuttonbn_mform_add_block_schedule($mform, $this->current); + // Add block 'client Type'. + $this->bigbluebuttonbn_mform_add_block_clienttype($mform, $cfg); + // Add standard elements, common to all modules. + $this->standard_coursemodule_elements(); + // Add standard buttons, common to all modules. + $this->add_action_buttons(); + // JavaScript for locales. + $PAGE->requires->strings_for_js(array_keys(bigbluebuttonbn_get_strings_for_js()), 'bigbluebuttonbn'); + $jsvars['participantData'] = bigbluebuttonbn_get_participant_data($context, $bigbluebuttonbn); + $jsvars['participantList'] = $participantlist; + $jsvars['iconsEnabled'] = (boolean)$cfg['recording_icons_enabled']; + $jsvars['pixIconDelete'] = (string)$OUTPUT->pix_icon('t/delete', get_string('delete'), 'moodle'); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-modform', + 'M.mod_bigbluebuttonbn.modform.init', array($jsvars)); + } + + /** + * Prepare the attachment for being stored. + * + * @param array $defaultvalues + * @return void + */ + public function data_preprocessing(&$defaultvalues) { + parent::data_preprocessing($defaultvalues); + + // Completion: tick by default if completion attendance settings is set to 1 or more. + $defaultvalues['completionattendanceenabled'] = 0; + if (!empty($defaultvalues['completionattendance'])) { + $defaultvalues['completionattendanceenabled'] = 1; + } + // Check if we are Editing an existing instance. + if ($this->current->instance) { + // Pre-uploaded presentation: copy existing files into draft area. + try { + $draftitemid = file_get_submitted_draft_itemid('presentation'); + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_bigbluebuttonbn', 'presentation', 0, + array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1, 'mainfile' => true) + ); + $defaultvalues['presentation'] = $draftitemid; + } catch (Exception $e) { + debugging('Presentation could not be loaded: '.$e->getMessage(), DEBUG_DEVELOPER); + return; + } + // Completion: tick if completion attendance settings is set to 1 or more. + $defaultvalues['completionattendanceenabled'] = 0; + if (!empty($this->current->completionattendance)) { + $defaultvalues['completionattendanceenabled'] = 1; + } + } + } + + /** + * Validates the data processed by the form. + * + * @param array $data + * @param array $files + * @return void + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + if (isset($data['openingtime']) && isset($data['closingtime'])) { + if ($data['openingtime'] != 0 && $data['closingtime'] != 0 && + $data['closingtime'] < $data['openingtime']) { + $errors['closingtime'] = get_string('bbbduetimeoverstartingtime', 'bigbluebuttonbn'); + } + } + if (isset($data['voicebridge'])) { + if (!bigbluebuttonbn_voicebridge_unique($data['instance'], $data['voicebridge'])) { + $errors['voicebridge'] = get_string('mod_form_field_voicebridge_notunique_error', 'bigbluebuttonbn'); + } + } + return $errors; + } + + /** + * Add elements for setting the custom completion rules. + * + * @category completion + * @return array List of added element names, or names of wrapping group elements. + */ + public function add_completion_rules() { + $mform = $this->_form; + if (!(boolean)\mod_bigbluebuttonbn\locallib\config::get('meetingevents_enabled')) { + return []; + } + + // Elements for completion by Attendance. + $attendance['grouplabel'] = get_string('completionattendancegroup', 'bigbluebuttonbn'); + $attendance['rulelabel'] = get_string('completionattendance', 'bigbluebuttonbn'); + $attendance['group'] = [ + $mform->createElement('advcheckbox', 'completionattendanceenabled', '', $attendance['rulelabel'] . ' '), + $mform->createElement('text', 'completionattendance', '', ['size' => 3]), + $mform->createElement('static', 'completionattendanceunit', ' ', get_string('minutes', 'bigbluebuttonbn')) + ]; + $mform->setType('completionattendance', PARAM_INT); + $mform->addGroup($attendance['group'], 'completionattendancegroup', $attendance['grouplabel'], [' '], false); + $mform->addHelpButton('completionattendancegroup', 'completionattendancegroup', 'bigbluebuttonbn'); + $mform->disabledIf('completionattendancegroup', 'completionview', 'notchecked'); + $mform->disabledIf('completionattendance', 'completionattendanceenabled', 'notchecked'); + + // Elements for completion by Engagement. + $engagement['grouplabel'] = get_string('completionengagementgroup', 'bigbluebuttonbn'); + $engagement['chatlabel'] = get_string('completionengagementchats', 'bigbluebuttonbn'); + $engagement['talklabel'] = get_string('completionengagementtalks', 'bigbluebuttonbn'); + $engagement['raisehand'] = get_string('completionengagementraisehand', 'bigbluebuttonbn'); + $engagement['pollvotes'] = get_string('completionengagementpollvotes', 'bigbluebuttonbn'); + $engagement['emojis'] = get_string('completionengagementemojis', 'bigbluebuttonbn'); + $engagement['group'] = [ + $mform->createElement('advcheckbox', 'completionengagementchats', '', $engagement['chatlabel'] . ' '), + $mform->createElement('advcheckbox', 'completionengagementtalks', '', $engagement['talklabel'] . ' '), + $mform->createElement('advcheckbox', 'completionengagementraisehand', '', $engagement['raisehand'] . ' '), + $mform->createElement('advcheckbox', 'completionengagementpollvotes', '', $engagement['pollvotes'] . ' '), + $mform->createElement('advcheckbox', 'completionengagementemojis', '', $engagement['emojis'] . ' '), + ]; + $mform->addGroup($engagement['group'], 'completionengagementgroup', $engagement['grouplabel'], [' '], false); + $mform->addHelpButton('completionengagementgroup', 'completionengagementgroup', 'bigbluebuttonbn'); + $mform->disabledIf('completionengagementgroup', 'completionview', 'notchecked'); + + return ['completionattendancegroup', 'completionengagementgroup']; + } + + /** + * Called during validation to see whether some module-specific completion rules are selected. + * + * @param array $data Input data not yet validated. + * @return bool True if one or more rules is enabled, false if none are. + */ + public function completion_rule_enabled($data) { + return (!empty($data['completionattendanceenabled']) && $data['completionattendance'] != 0); + } + + /** + * Allows module to modify the data returned by form get_data(). + * This method is also called in the bulk activity completion form. + * + * Only available on moodleform_mod. + * + * @param stdClass $data the form data to be modified. + */ + public function data_postprocessing($data) { + parent::data_postprocessing($data); + // Turn off completion settings if the checkboxes aren't ticked. + if (!empty($data->completionunlocked)) { + $autocompletion = !empty($data->completion) && $data->completion == COMPLETION_TRACKING_AUTOMATIC; + if (empty($data->completionattendanceenabled) || !$autocompletion) { + $data->completionattendance = 0; + } + } + } + + + /** + * Function for showing the block for selecting profiles. + * + * @param object $mform + * @param array $profiles + * @return void + */ + private function bigbluebuttonbn_mform_add_block_profiles(&$mform, $profiles) { + if ((boolean)\mod_bigbluebuttonbn\locallib\config::recordings_enabled()) { + $mform->addElement('select', 'type', get_string('mod_form_field_instanceprofiles', 'bigbluebuttonbn'), + bigbluebuttonbn_get_instance_profiles_array($profiles), + array('onchange' => 'M.mod_bigbluebuttonbn.modform.updateInstanceTypeProfile(this);')); + $mform->addHelpButton('type', 'mod_form_field_instanceprofiles', 'bigbluebuttonbn'); + } + } + + /** + * Function for showing the block for general settings. + * + * @param object $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_general(&$mform, $cfg) { + global $CFG; + $mform->addElement('header', 'general', get_string('mod_form_block_general', 'bigbluebuttonbn')); + $mform->addElement('text', 'name', get_string('mod_form_field_name', 'bigbluebuttonbn'), + 'maxlength="64" size="32"'); + $mform->setType('name', empty($CFG->formatstringstriptags) ? PARAM_CLEANHTML : PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + $this->standard_intro_elements(get_string('mod_form_field_intro', 'bigbluebuttonbn')); + $mform->setAdvanced('introeditor'); + $mform->setAdvanced('showdescription'); + if ($cfg['sendnotifications_enabled']) { + $field = ['type' => 'checkbox', 'name' => 'notification', 'data_type' => PARAM_INT, + 'description_key' => 'mod_form_field_notification']; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], 0); + } + } + + /** + * Function for showing details of the room settings for the room. + * + * @param object $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_room_room(&$mform, $cfg) { + $field = ['type' => 'textarea', 'name' => 'welcome', 'data_type' => PARAM_CLEANHTML, + 'description_key' => 'mod_form_field_welcome']; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['welcome_default'], ['wrap' => 'virtual', 'rows' => 5, 'cols' => '60']); + $field = ['type' => 'hidden', 'name' => 'voicebridge', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['voicebridge_editable']) { + $field['type'] = 'text'; + $field['data_type'] = PARAM_TEXT; + $field['description_key'] = 'mod_form_field_voicebridge'; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], 0, ['maxlength' => 4, 'size' => 6], + ['message' => get_string('mod_form_field_voicebridge_format_error', 'bigbluebuttonbn'), + 'type' => 'numeric', 'rule' => '####', 'validator' => 'server'] + ); + } else { + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], 0, ['maxlength' => 4, 'size' => 6]); + } + $field = ['type' => 'hidden', 'name' => 'wait', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['waitformoderator_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_wait'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['waitformoderator_default']); + $field = ['type' => 'hidden', 'name' => 'userlimit', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['userlimit_editable']) { + $field['type'] = 'text'; + $field['data_type'] = PARAM_TEXT; + $field['description_key'] = 'mod_form_field_userlimit'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['userlimit_default']); + $field = ['type' => 'hidden', 'name' => 'record', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['recording_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_record'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recording_default']); + + // Record all from start and hide button. + $field = ['type' => 'hidden', 'name' => 'recordallfromstart', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['recording_all_from_start_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordallfromstart'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recording_all_from_start_default']); + + $field = ['type' => 'hidden', 'name' => 'recordhidebutton', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['recording_hide_button_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordhidebutton'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recording_hide_button_default']); + + $mform->disabledIf('recordallfromstart', 'record', $condition = 'notchecked', $value = '0'); + $mform->disabledIf('recordhidebutton', 'record', $condition = 'notchecked', $value = '0'); + $mform->disabledIf('recordhidebutton', 'recordallfromstart', $condition = 'notchecked', $value = '0'); + // End Record all from start and hide button. + + $field = ['type' => 'hidden', 'name' => 'muteonstart', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['muteonstart_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_muteonstart'; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['muteonstart_default']); + + } + + /** + * Function for showing details of the lock settings for the room. + * + * @param object $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_locksettings(&$mform, $cfg) { + $mform->addElement('header', 'lock', get_string('mod_form_locksettings', 'bigbluebuttonbn')); + + $locksettings = false; + + $field = ['type' => 'hidden', 'name' => 'disablecam', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablecam_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablecam'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablecam_default']); + + $field = ['type' => 'hidden', 'name' => 'disablemic', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablemic_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablemic'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablemic_default']); + + $field = ['type' => 'hidden', 'name' => 'disableprivatechat', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disableprivatechat_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disableprivatechat'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disableprivatechat_default']); + + $field = ['type' => 'hidden', 'name' => 'disablepublicchat', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablepublicchat_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablepublicchat'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablepublicchat_default']); + + $field = ['type' => 'hidden', 'name' => 'disablenote', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['disablenote_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_disablenote'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['disablenote_default']); + + $field = ['type' => 'hidden', 'name' => 'hideuserlist', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['hideuserlist_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_hideuserlist'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['hideuserlist_default']); + + $field = ['type' => 'hidden', 'name' => 'lockedlayout', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['lockedlayout_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_lockedlayout'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['lockedlayout_default']); + + $field = ['type' => 'hidden', 'name' => 'lockonjoin', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['lockonjoin_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_lockonjoin'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['lockonjoin_default']); + + $field = ['type' => 'hidden', 'name' => 'lockonjoinconfigurable', 'data_type' => PARAM_INT, 'description_key' => null]; + if ($cfg['lockonjoinconfigurable_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_lockonjoinconfigurable'; + $locksettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['lockonjoinconfigurable_default']); + + // Output message if no settings. + if (!$locksettings) { + $field = ['type' => 'static', 'name' => 'no_locksettings', + 'defaultvalue' => get_string('mod_form_field_nosettings', 'bigbluebuttonbn')]; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], null, null, + $field['defaultvalue']); + } + } + + /** + * Function for showing details of the recording settings for the room. + * + * @param object $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_room_recordings(&$mform, $cfg) { + $recordingsettings = false; + $field = ['type' => 'hidden', 'name' => 'recordings_html', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['recordings_html_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_html'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_html_default']); + $field = ['type' => 'hidden', 'name' => 'recordings_deleted', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['recordings_deleted_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_deleted'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_deleted_default']); + $field = ['type' => 'hidden', 'name' => 'recordings_imported', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['importrecordings_enabled'] && $cfg['recordings_imported_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_imported'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_imported_default']); + $field = ['type' => 'hidden', 'name' => 'recordings_preview', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['recordings_preview_editable']) { + $field['type'] = 'checkbox'; + $field['description_key'] = 'mod_form_field_recordings_preview'; + $recordingsettings = true; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['recordings_preview_default']); + + if (!$recordingsettings) { + $field = ['type' => 'static', 'name' => 'no_recordings', + 'defaultvalue' => get_string('mod_form_field_nosettings', 'bigbluebuttonbn')]; + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], null, null, + $field['defaultvalue']); + } + } + + /** + * Function for showing the block for room settings. + * + * @param object $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_room(&$mform, $cfg) { + if ($cfg['voicebridge_editable'] || $cfg['waitformoderator_editable'] || + $cfg['userlimit_editable'] || $cfg['recording_editable']) { + $mform->addElement('header', 'room', get_string('mod_form_block_room', 'bigbluebuttonbn')); + $this->bigbluebuttonbn_mform_add_block_room_room($mform, $cfg); + } + if ($cfg['recordings_html_editable'] || $cfg['recordings_deleted_editable'] || + $cfg['recordings_imported_editable'] || $cfg['recordings_preview_editable']) { + $mform->addElement('header', 'recordings', get_string('mod_form_block_recordings', 'bigbluebuttonbn')); + $this->bigbluebuttonbn_mform_add_block_room_recordings($mform, $cfg); + } + } + + /** + * Function for showing the block for preuploaded presentation. + * + * @param object $mform + * @param array $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_preuploads(&$mform, $cfg) { + if ($cfg['preuploadpresentation_enabled']) { + $mform->addElement('header', 'preuploadpresentation', + get_string('mod_form_block_presentation', 'bigbluebuttonbn')); + $mform->setExpanded('preuploadpresentation'); + $filemanageroptions = array(); + $filemanageroptions['accepted_types'] = '*'; + $filemanageroptions['maxbytes'] = 0; + $filemanageroptions['subdirs'] = 0; + $filemanageroptions['maxfiles'] = 1; + $filemanageroptions['mainfile'] = true; + $mform->addElement('filemanager', 'presentation', get_string('selectfiles'), + null, $filemanageroptions); + } + } + + /** + * Function for showing the block for setting participant roles. + * + * @param object $mform + * @param string $participantlist + * @return void + */ + private function bigbluebuttonbn_mform_add_block_user_role_mapping(&$mform, $participantlist) { + $participantselection = bigbluebuttonbn_get_participant_selection_data(); + $mform->addElement('header', 'permissions', get_string('mod_form_block_participants', 'bigbluebuttonbn')); + $mform->setExpanded('permissions'); + $mform->addElement('hidden', 'participants', json_encode($participantlist)); + $mform->setType('participants', PARAM_TEXT); + // Render elements for participant selection. + $htmlselectiontype = html_writer::select($participantselection['type_options'], + 'bigbluebuttonbn_participant_selection_type', $participantselection['type_selected'], array(), + array('id' => 'bigbluebuttonbn_participant_selection_type', + 'onchange' => 'M.mod_bigbluebuttonbn.modform.participantSelectionSet(); return 0;')); + $htmlselectionoptions = html_writer::select($participantselection['options'], 'bigbluebuttonbn_participant_selection', + $participantselection['selected'], array(), + array('id' => 'bigbluebuttonbn_participant_selection', 'disabled' => 'disabled')); + $htmlselectioninput = html_writer::tag('input', '', array('id' => 'id_addselectionid', + 'type' => 'button', 'class' => 'btn btn-secondary', + 'value' => get_string('mod_form_field_participant_list_action_add', 'bigbluebuttonbn'), + 'onclick' => 'M.mod_bigbluebuttonbn.modform.participantAdd(); return 0;' + )); + $htmladdparticipant = html_writer::tag('div', + $htmlselectiontype . ' ' . $htmlselectionoptions . ' ' . $htmlselectioninput, null); + $mform->addElement('html', "\n\n"); + $mform->addElement('static', 'static_add_participant', + get_string('mod_form_field_participant_add', 'bigbluebuttonbn'), $htmladdparticipant); + $mform->addElement('html', "\n\n"); + // Declare the table. + $htmltable = new html_table(); + $htmltable->align = array('left', 'left', 'left', 'left'); + $htmltable->id = 'participant_list_table'; + $htmltable->data = array(array()); + // Render elements for participant list. + $htmlparticipantlist = html_writer::table($htmltable); + $mform->addElement('html', "\n\n"); + $mform->addElement('static', 'static_participant_list', + get_string('mod_form_field_participant_list', 'bigbluebuttonbn'), $htmlparticipantlist); + $mform->addElement('html', "\n\n"); + } + + /** + * Function for showing the client type + * + * @param object $mform + * @param object $cfg + * @return void + */ + private function bigbluebuttonbn_mform_add_block_clienttype(&$mform, &$cfg) { + // Validates if clienttype capability is enabled. + if (!$cfg['clienttype_enabled']) { + return; + } + // Validates if the html5client is supported by the BigBlueButton Server. + if (!bigbluebuttonbn_has_html5_client()) { + return; + } + $field = ['type' => 'hidden', 'name' => 'clienttype', 'data_type' => PARAM_INT, + 'description_key' => null]; + if ($cfg['clienttype_editable']) { + $field['type'] = 'select'; + $field['data_type'] = PARAM_TEXT; + $field['description_key'] = 'mod_form_field_block_clienttype'; + $choices = array(BIGBLUEBUTTON_CLIENTTYPE_FLASH => get_string('mod_form_block_clienttype_flash', 'bigbluebuttonbn'), + BIGBLUEBUTTON_CLIENTTYPE_HTML5 => get_string('mod_form_block_clienttype_html5', 'bigbluebuttonbn')); + $mform->addElement('header', 'clienttypeselection', get_string('mod_form_block_clienttype', 'bigbluebuttonbn')); + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + $field['description_key'], $cfg['clienttype_default'], $choices); + return; + } + $this->bigbluebuttonbn_mform_add_element($mform, $field['type'], $field['name'], $field['data_type'], + null, $cfg['clienttype_default']); + } + + /** + * Function for showing the block for integration with the calendar. + * + * @param object $mform + * @param object $activity + * @return void + */ + private function bigbluebuttonbn_mform_add_block_schedule(&$mform, &$activity) { + $mform->addElement('header', 'schedule', get_string('mod_form_block_schedule', 'bigbluebuttonbn')); + if (isset($activity->openingtime) && $activity->openingtime != 0 || + isset($activity->closingtime) && $activity->closingtime != 0) { + $mform->setExpanded('schedule'); + } + $mform->addElement('date_time_selector', 'openingtime', + get_string('mod_form_field_openingtime', 'bigbluebuttonbn'), array('optional' => true)); + $mform->setDefault('openingtime', 0); + $mform->addElement('date_time_selector', 'closingtime', + get_string('mod_form_field_closingtime', 'bigbluebuttonbn'), array('optional' => true)); + $mform->setDefault('closingtime', 0); + } + + /** + * Function for showing an element. + * + * @param object $mform + * @param string $type + * @param string $name + * @param string $datatype + * @param string $descriptionkey + * @param string $defaultvalue + * @param array $options + * @param string $rule + * @return void + */ + private function bigbluebuttonbn_mform_add_element(&$mform, $type, $name, $datatype, + $descriptionkey, $defaultvalue = null, $options = null, $rule = null) { + if ($type === 'hidden' || $type === 'static') { + $mform->addElement($type, $name, $defaultvalue); + $mform->setType($name, $datatype); + return; + } + $mform->addElement($type, $name, get_string($descriptionkey, 'bigbluebuttonbn'), $options); + if (get_string_manager()->string_exists($descriptionkey.'_help', 'bigbluebuttonbn')) { + $mform->addHelpButton($name, $descriptionkey, 'bigbluebuttonbn'); + } + if (!empty($rule)) { + $mform->addRule($name, $rule['message'], $rule['type'], $rule['rule'], $rule['validator']); + } + $mform->setDefault($name, $defaultvalue); + $mform->setType($name, $datatype); + } +} diff --git a/mod/bigbluebuttonbn/phpunit.xml b/mod/bigbluebuttonbn/phpunit.xml new file mode 100644 index 0000000..891aeb8 --- /dev/null +++ b/mod/bigbluebuttonbn/phpunit.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit> + <testsuites> + <testsuite> + <directory suffix=".php">Test</directory> + </testsuite> + </testsuites> +</phpunit> diff --git a/mod/bigbluebuttonbn/pix/flex_icons.php b/mod/bigbluebuttonbn/pix/flex_icons.php new file mode 100644 index 0000000..db57ac9 --- /dev/null +++ b/mod/bigbluebuttonbn/pix/flex_icons.php @@ -0,0 +1,34 @@ +<?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/>. + +/** + * Pix icon handler for Totara + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +$icons = array( + 'mod_bigbluebuttonbn|icon' => array( + 'data' => array( + 'classes' => 'icon-bigbluebutton', + ), + ), +); diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-128.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-128.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d32bb37cf79e3c541425dd15911e420c6bba35 GIT binary patch literal 7752 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 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-24.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-24.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed24ea8d98aa0326a7d2561650696062b8e117a GIT binary patch literal 1386 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l<VJ?tf(zOL*K*hNIetQodWn8d)q^4-(LF~s9|>D06NBH?03 z>(|@eiZzL7JeDhC;GC4`>(auq$$+OvNZqls?aii+7LiPoz-w#%p6_v!bW#bJk}}c7 zNLx(Msri_Sn#AG-B@B`;{CDrV^={R>^7lHccI7xPTe7@v`>&VxiqBhrpThX!E8DuQ ze~p!Ts^*=3^yux9n>q8?m)w7!z5l*>tJB4q^XC0~dSc@BSIpWCO&SsbCj6mCB_6jp zKDf`*<@%a&#jVWjuj~osd*ha0%@TGM^qh3zty@puzC$a6)o<=D*Y`AHxU|GdXTdDJ zlU<4vAI}IrP!#6CeQbuKNa>F9_j>DIKlp8TE$Z7_>Eyh3v0aW05wq2<>u5G;EXri> zQd-2oEa|~`fceAUrY*Nk#l-U;|F%=|S{ZWaJNF#B+G!If3Lf!ltBEcOdJ;L|-FCUN z+;?`Bem<}{{d~>!(t=p=>t8)4wFs-HX^3@8YTfOu@$hc5xzFD{<I8pCjzuqewl)V% zijX<}z{1Yhc=MZEp-xZuGcMXMVGTWdup>|V!S8u5ORQKN9VYQul)uw)6i`q)Qh4Up zFR8a-lhys@lHWFL+*o+**fGuN#|v!ByM;rG_H8=O*zx$rmnz$u`{`xhbas2GhA8q} z-&y-xY;9U^o&B<qTdf@%Hf=gne!uqg_xtsy-O81n^{g|bjGitn-mvg#(ai1ls-`Cy zhI-3<kBHjkymL?g-QDGzckMcLdAUF5;R#>AmR`Gdt>VuQL#0!{<!d^gSFUL4NZ`@f zboJn~G^2{&Z@&j^_4wt!`>q;C*OuG4lT;G3vbqd-K5yN)v2p9P-R1Sm!m779yJm{t zwz8=F^rSaw<Bh=7^8vbBZxvh3<vW`;S%G85j2RCOwQ}Fwpc=Egtas^BRR%fRRYeaD z99-L2{gJOvW+oTo?$|DVxjW0_)(5Y?+Oep^qHkx>Q?Bs!ajB`Pt=`l1HtyQRb>MKk z{F9TeEFl3muQkg0z1XHG#iO$}Y_i*8$L+U|N;vP0TOPG`k_u;WadAX;VXXP`WRHm& zJ4<Xob>3O|*|}Y9XMO!ljjj#Xgjg7-dbL{gtqjq!WS#fi(6rPfd}7Y*H+v5BeLT-6 zYgHl<dbBiF&aURf(QfgX^XI#VYKm~h?5~@9@}wtMD--+X&65ICdJ-#yGuaP$-wRqP z^Wert^PR=%6SKCS`t!%;c)$E{31^8m9X-8G>F4G8<mRUR|7V!8yDMcicYelR{i^QQ zJNK0T%hxEV|0%aNth=?9HRk%!@3n63?v;PPYM%T1RaWLYvt(1S%he;EkC-GHzHX?t z@%v>i5LbCI`jN%LixuY<yYCPF`!?L@>c>|HJx@LTYq74qW0DGIf<WM-ip?LFpX@Z( znp(@NKfQlX&dya`@2h=$WKwGWgr=qWE$`ve7uoQa@#rG=)SNkI&&r)mv+mh7*I#>T za_=0az}k~Vq5tF8{A^t&do1Gi@vemzGp^PD`Z@jc{J)PM&o5iS;cH+2tLZ7<-qh1w zt%?eQjLcyzmpmrAEtb3&9d~o0&HR)e#$=y=l19<&o33YUo4%GonJsz2uiukDJnR4e z^U3@9HCvQ_ZeZTL;e&H=**E?Fq7OH8m?|Z?Tv@To^QFMzS*QGqtuF7lxZ=D0|3C9T zPyhdS{qDZUe<EFk7I2p|oK*~a-+em1uWfF|lAEil_nw`1xl`$SRQ8&!ZlN2(dR84& z(0Cx>&iJZ#*WYKa*H^95S~=g4VNwq7Klas^7EFCr&>73Xz`)??>gTe~DWM4fx3HfY literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-256.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-256.png new file mode 100644 index 0000000000000000000000000000000000000000..a4f79dfbecc3a490ea4995daaf8ef553c1de9e11 GIT binary patch literal 18500 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4mJh`hM1xiX$%YuY)RhkE)4%caKYZ?lNlHo zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_6O`DqC(2$ThAU~U{GN2ba4!+xb=2#Wr57~ zxzB&r&W!xt$>h+m!^1)4ptp^Sz!k^nDJikjbgge?$#33#dE@Q7eRKQf&YkhC?7h6L zAm96{#r*BEyWe)?ZM|N0y6p7^w`AG8^6o`GTk`^iRJ*5W-22Wb;IQzWw(<Sj_FOZ4 zM2uE%YOrejuA~*<9=0gp#QE>#d+z_XZQ!^2@t~RC?ngvr^O^(ur!xpJbTG&;#4z0P zxBvTPdsS7HoLbABDW{(*cnWc~GNnYaCG*Jjv->R<?lI(OVwirKmEpwGA_frSn8c@_ zHVg+|mgrT)R-ch*zWw%Ee|pc$_34`<85(ZCjVsvmvD9-C1H&SXq^vA1z3JR~)44ZB z@GSS6d+6%w@QG*B_!)j2Jzw{2^Kr%vJEi4o)_5=)9MWejU@G|kq22z^p=G|a|H*H^ zEqmcX+~zdil*nVt=ht!dA7?%$;Sw0buu$}h)R$w&mkLPBM?2<~*)en+O?vU}-LYf6 z(jQN%&wudb<m8C0S)zTi)@}Xv|0MX@nJ1|@3b1r6;&3{_dF;mi`Y#vV??0bzBd;CE zaOT1Hy8plL|Brva<8j~rW<xdC#eqCLJPUU3mR{~Z|JcjR%O95Q-dOfFO2A3XVYThI z3Y%pC)1yB5s2RWi;iJaPu<L-t`Q^5HwR3~G7$hVm9m~tj`D82_{O$jWNSoyx*qD4= zp#ONO|AB`E4kE4{i#Qww7!<rYe(ue$|9xBk)9HErQ&kw2e%Q@WQU7JJ{NMJ64<G)Y z9i*kz)pT~Yx$^YW5AT-WZ$13bAw6APLc8ir-1_T6NyldVtFoFqk9{rM&g-vPlX+g2 zR2}@-WB8-S?r+80(zg|F|4s=ybM9Qy=Vxa*+7JKuy1u^m^mP5?k`j^Wda+JgQ=6{8 zZax0E@$}QC%P+6EHcWW5f2P1B)(_S9|2#9_$?@T$1!F=_CxaZ@2abloV>kMCK7aMM z`k(~!QjxAj3iHn^Pd@qJ)z#G}o)%3=mE62}^NV-y-0bEbZ{-$O@D#GLwto2iHzUKz z6d{HOpMNqitPJsLzxLxs#h#C)o4*HpiOanI{`;WB>~gzfGyd&5{P4i7t=WaYzGTj^ zF6Zkp?CI-MntgWB=FP$n3nU&ENE}QMsM{~Ew_Td=rJKfrP6m0?otsuwT#I|=D!>vd zaN<3~9EJ}(4dNV%S+}_vl5<23oKKCE3>Ohyb?SfwYjSVv{s0Yy$tM-0<{ta{S~x(2 zb&^Wr?{9BA+!kBZ{4l7hu6|fzRhq%v$RLs2as2UTx$FQoCPoGarHM1tnL1q-z4^WM z@z(Fh9~T-N6X0QczE_Ti%~8|UM(+5%z151ILU9!jSu1ShIGPw7CkSw`T(~HB@!|KM ze+sH?3asW7zq4mlOz~tokX6vSS*y`N?uZy;$j!|gjy_nscSFdJnJ+K52yiqpcnUoz zvU*TveV_G%JcpwCtuO|O<a#g7-+6lBA6Bf2Du1DUu>S8amP;>1LP8yN#160Bey=M; ztJVBo1#^o)NlD2JrxOy+I%3QWe>bw_YKUACF240KtbgnG?0(Iqzka8ur+@t{SwH{& z`{TLW@3OfDez-jUpNQUcZu7fa_-6WuJuHxrl@<J3{QECs#h$q4r_VoZXa2LaUBKx< zK?euRp2$Fvf7*&q*mKnxdN%H#nELemr%K5wW_jOEa{Ze*Gc#sSh1{xD2c8xsUSAhG zW8S=D+Uxf;g{}@$6zZJPI>)f>=FLck8SI+LJP!*b7zBFVzFzMQXg{22aO}(V-hR!c zFJ8R3aDTn*`pAF3-|v^7r1Eg%aXDujx#Jfu2>kh{;Iwc;-cRR+0)PHB<o&e#^Ok#m zVaFnd`R94J-~MUqbYlMYUIv3>`<7|E3ci`+=)c7D_9t~ALyfK`7bV6(k&D-_w=28% zy=ce|nAUuUC+Tge@eA*T+mxA%jg5bOY;<5)r19wM_4xMFPY<0q!SSh5a_-!@9WGYS z51!Zk|EVdy+Dm|ig;{P#Oy7E8r@EV;m<~u3Gg-{`d;TkQ>b8cb^AG*}oW5|CmW>?$ zv17*yzP<{Tv8@uR+b>_YKfZm-;>CwWf_Ssuew{f}l3_;qo^QJZlQthudq02XOva@m z6FpqSr-J<3W4Pzfr_(d$&1<{<+STGtV}Qn)hw+LZvcJnr>hMshkUAjd<gswt9gfn3 zj}x67GdX{(kDu?LBi4HVJ^NA->+*LlHgd;*zu(UvIW6#vx5z=2mKWFJnZvm`4?X;_ z=eqL5j=Z;=iXARWpX1)Yd9$U)hr=;F{rR5n<z;0~0xYo}`#!YlGceSBnmj)tE9=yQ zZ^b{trT#E1SfRD)<{vAO^oti>F@2b@XoYXPz0}-eQ?;iH^tvgYHhQ<`vmdBf`Ty@P z2V1j*l+-2T4{k3^HB4X2bRTu#xENcd{KYMQp-JC$mqTwhswpjfUv43@<LkBPgA#{@ z{cRW{rwMYfEYO&;`>(dhzelEgPq>X3Y_9MzZSY<lkv;F}d>0!#?)~@INB^8TQ*zJe zbJia!Y$m99w$Bn+)ZK7#Quc)Gc@1h@Pj!9<*)TG^c=gIjN9^$Z|9|f{8@9<>mmPTi zSy_au@JG1DpIbAGS8d<Ahr!@jmGs2#o15y(%Gj>2i*?fxYt}h^V|%{5jT}Fpyj@R$ zMbE^Ef_Fb{Z{l!K66{#iaXW7}uld~);XXNAuH~0I`}+7!Z0ho8iT*d0&z>P*YGcr? z`)o`N0eg-Ahkfw(^;MeK@zU`Bx3{+^`luDYyR)-Wv~bq$dt6EfBn~(8+i@I|m{<2J zbMKw&k3T9&@D%;9{KIhg&ewBuas`-7*#F(UDe2=AbM(y{os`I9vgLOgU6dOC{{F5k z!j<^-)m4R-GtE0@DlLfoXIuS^fkCeRPhsscrRmxW7Aqfm`2555TE^`SW>VLt{kIBQ z$?~Vpo~xB<*REX?eAGUEyPbdd`R9XAi_SLh5p{706yf3LZ#Hamm#-CR5x7%yT6cnq zI#VM<`~1W4XXkJK!+L=0UxZHA%9WgF&YfEnppj5hBQsqu_R#C~`}-0MRviD+>9R=G zMM0?3L4c*Fx7Tr6Dk$A9n6ha1Zt0sje6q5#GIf0Q&JXgX88<Z5SN*cO^irk9PCg{` z=(gP33pBWbR*EQ1?6|kVdG?`fR>6Q!(eL-F^<4sw%+BA(x&3x;?CxU<972EUKzY2k zdA}~pf;ms;&)e5`^Je6e&y{z|Zs&F!e_Zi$>GTJmf0l4;omzQTjPdQ)jT;Ra4ovp9 z;|vX*cwT&h&oTkN&r0%q^~n$OD;YJ?e;UtBV(zK2>*tj=Q<&=YaBlfM&2_Ej8~>aY zV~m_;XlKXA@Z(~C9g7o3+1p#JP8@&g?AzxbzF+X8(P|BAK<Lz^K|6Ov{X2OwJ0Mha zlFGu}yQRa|#~poodiur7mz&?c%e%aEdszQju|E;I9GArYglsNr2<pl(>FSj>PfAK+ zdVX&1;VV}}+S=O{IfQEL<lE;T&VPOW^(&r34<}@u%JHsZcq;ZSWpif5-nbK+de%m7 zKlJ{)^7PXS*RMZ6?P>lJb%9>Dg&JJ%cD>dsDJ^|?q*M6C`}fD!#qM^{wD=?QPwU^F zUq4u!b{~-MKcAGA#<ekmXJ-sw^|v>UHgd-gHnV?~t7{R6zF_XeVv?VDyib;)A$?vY z+sPE6l_6XoE1IT%FZwZ?ul_V=L+t7Osi(zMCVTGrey^H?g=yDQezs;^MW$O*p6sjr z&A?#)_oe^WmG-WI4?lmLG5_%Wnjg_x>;dOKWgG6l9QmgBd4a`;kH_T~uV2p}8yow% z>O-bOc-HaI^>Z3lt(q}&=EdqSd;fmBeIjjhv8z2x-1>4yb$LGf_WS1?V`CRSoyoi* z`=@#Sy`Fpb;*LBnEGaKPzxmIBUcNnNKe6<9_q)5h7rwb+$iv5%bbVdygw&~r&p(Ve z`E%NgWrfJ+^XJaJGOSmccwqJVeO$*Re*O9-=5XIf@WQVjcWXYMWoURlzh3V0<;x5W zGiOG!G&}Oyx7T0%pnp0j!TR%`Sx^1dCRcpFTQ11M_OQT0XJLLD$B`t%C!Z^8o*B<) zWU%>mBYA=f=k_~A+?#I7Z2u$kZ(7U#l}rJ7^Wr!End!s!ut37t*!aiW{QB+!i=KxM zA68y4i)Pt!@9!oZ<(WQ@Zl=#?Rb;WXwVg3*mZQP}UHkU^rVsOHG2Gbrr)$xJXS4I$ zjz2zlv|C(|uU-7epR;0(5jt&73mdH8?O;|`Rt7amnhq+Iy}#G%qsHEU{P4r)AGYiK z@fBpS-un00jQ`F2b`6(b9+_pD?Xxg{8b^zhqDEJfZuB+=20l5PjtrBo0F9QDCtrUq zd-L!^@sDW1e{I|jOG9hr`q{e{CA_}2)?;aqMahc^_wF9ISbIH3@Pf;$Z@2T=8UFlv zZr`raRlKvy<W3$a8?NQAcV<1XYqGz{_4VS{KQF(W$-~dDJkjI9fktMIR;SO`K5uX0 zU}<&~;bOIz>lfhl@y+J*Obl%7?1gV{sX()m*aveThErcZcrSfawXOc~vECVT<{X)* z?EWRRcIgH4V3sX8`_q2%%UCchzuakC{Y^p4`@q|>;vbfOa+&L$Sr!EG@6Rw<wf?!j z2$x0qJDFX(cRx@49qq`{bWlOW_29F!vlTg*?0!BGwwUYp<?GkSr>3sH^Qrj9ZN7iI zxE+?h+iN%9Uw_`kYoB-D%}d!F$#8Ay-d=E6=a{WF|M2IiejTWZaNoY+=1qwhJ&>16 z+vab*nRTGZ`diH2D#?o(A~Sv1c9*@qWc@S4_K(I=(bvDFRhbSZ2>h&xTW|B{>iRlX zCk~K@e|{4EQDetv-@ae>A^WQ>5*jnlM7_H@-A{d?2G^sH7HsV7g55_A3=J6<+%Pp4 z;d-gMtw|w-<=UG<OGg7PFU!Wxqe%;<9C}!gkdeWm?l<Sa&(F^lyOg%&+-%yMe*WO= zryq)cNHg0rZ&+o0=T~>5*4wM*#>R`q&;O{gi`iMk`uFeOs_r)xHtW`J$zG-|z`^q6 zNQ#2MfhSK?+S=L-OiWt3#q|q+SpMN{-5*=TaD)d`qII|}|2m`o_qVqmDnb?|F9cW| z9n>aYT$s-)cp>TE-QDHQhHdeG9*Iv#m6Vkg^zl7=;AIKOh+O{qwX6*c3Te~+e|p+o z_3_NTz13f1tDF}KTw4>lnc-JjSHt<|&UW*U|N8oxk>R}UcNv$!BNr}6#q21UsQ>2S z2Yu-u*)y3Hip>@0`klU*k#z2pzrxHO$3PJkA<i8!dM-+Wb1Vv-)~~$&>eW^!>14hI zUkdjYGffHNXkyrZzrV(=|NFga{<{70(c5wkzPz0N;G@XH_b*;J7)U$;1rb-hZ4twg zX&;y;EV`IcmbGffwEu@%KL?0N%Js7+Cntl_p1XW)OJ5(~j~csL-CFGqP$4yI)}cOG zYX^>lyWj8Q_R`!Fr@!c8q=k$hC^}b8+aEf)!Rik~Ow=~}i5-WZe{KrunrmIIB-j7= z!a`>a5iSi8uFp|fnY!DzOpEE|V}JB<hn*@XgTMV>lSLObe*OAY@b{NV`~1WE*WCFR z#ofTRU$~*Xs7UDI3zJ0`leYaoXZ`-jn>RXfH6LABnjLMQ>AzilT}M!(_vifjf0Z7S zS{fS}=h#-q#a}x8@c9RQ^B>u&42#(Q$N#+=|Mz%^*3#=APH*a2xl(h_*K5%QyYC9L zJX*gwYu(KZ!H%Oz9?LI(`2DxzXi~+aPW1<6yA93E+B!R57g)&1)bah_)Ve>+vVno& zy#2q&_W#Z^Gc(^mo3>dgndkQQ{NpDls~3KI6Io#+*K#=V_TR5k6NLjpMK52zd}Cj& zb<s{4zxj5(ot>O(;`T}f2OERx{nH2U-<56K%v#KFZvUUt`~Q5OZI=6M{~W*N5gH;J za&Mcd2yx2T*Tv{_ty*@rY_Bcj*-!V2PU|u>h{shhZr;54#*RYeb+NNSkr>VQuPT&T zKN3`sFz+sZ|Ifa7=bR~i%Qx2kE<2KB_~!n8`9=qZ)#2-}EzDyTG&uIB$FO#uy-??a zFE2OSJbV6Oy~K}fB?kK+2l@YbYlwVG-F(xs!|(Cl@AsO|KW|p?wEA+Om)B&LD$~XY zo|Pe7v9YlMp`!k_Uq$ZZnIAiTeCN~WAGTZk(cL9=;rZuU)A{F<Gcq{t-o3k{?5)(V zU%zxV|2-?jc=l6&U!RkR>z{Y!`<pMnJo4ay!nrw?$93)7>yI9c7iQ#OVcNShhR@d4 z_Q#9G{SKP0dedjKuf6y;zl|fqaKG94|F2fBKlHLBDJQ2Vx{mLESL=Q?CI^m#vrMxK z{{5*u^0<(PmzS|&-bMY^T^E%ee60%95HYZ_;$o=yb~9bEONqttfuVhSz3bt4Va5~B zKNsx2Yf<(_;!mBuis!8KO2#wIdnPI^h^$+C;`ztp^6i&j9{KUZV*c~zAJ$iZLT089 zn|r@p>*bd%_uso)&1FmW5pN7yA+5@Et7+cBgN`wKt3>tp|M_%h)jq|zhsQxpOfQBl zmH$k$uf2Hvck;<6d)`!=Nae0jH?X!|z5e<4pEW<8O!hahvFW+HyF9t5s4Kb-lyAbA z9u!zSGyQbM#o_e6=}yu4f**eW<xcunW7l6|)%*Fme!=PU5A)-HL~?UAC~$DFFeUUT zPCm(?k#=8msnry>6R$OyZaq^<PEKa%x?3&F$A0Yi_cM`o`&RWIKL2q2N`{QXk3TjT zwuNXdbzZ6Z$m=!3D@CSRPrp~#>?nB1G_U%dB}Y@k!Gnr(?Cay+KLOXD%74Cc@l4ZU z@LMi?GiM|B2GDSc&-|v#n?9yZxE{o^=%U7^n>xpi9|w)^%&{nBQdU-$ke5Gx<;s+M zhe4J;|8PBmVNaYs!waPc-+z~Sn4ajJ=djjQL4adHfJVaOW4#PruYaber8#IG{rOq^ z>$|-&pm3;v81d=SA?J2JLodysl_DvTZ7QA<QYEjik8jsG{rORGuI7eomEsIa6FC^p zL~F(T+Eo~x_c$ntp<rjsjcvKoBCZF6{cT$v6dKOXHh+9)C-<*qvf8|LYo&q%xlTO& z6d)ow(?@D$2-nRVsZBcP_j9l?G6^bp9&K57{PE1_(+?X?KfNZrw@IPs$83)J^Q#Qz z__=3)WvTgkHGJmfmo2MSbsT>TN<UU}cX0%GJ&!DP6v$FyniAx)IM8CQUr$fZg9itj zPdxv8V?!eIq?1$j#a{Pnh!@~k7eD{V)YH=o-rb4Zd)7*kWy+<cu?ofa`4w66wzE4s zJ4?t!HHWUQ%8xlC^N%f}u5DhZ>{?BRTYOesixSe(+}87jh_GJ1e0jl?MVmJVe`lGd z#T0d^5HuKb{PB+uhxrdmILF7!>&5Kgn16o%nTN|kF}f~pZ_~^dJd-+EQX;P{j6Zch z^rw<u9b5Zh#{KvE*R9L@^5x5-{q+Kqo;>()o%7!!_6to+AI@37-{7Oh92%+!9>QF< zeEG!l&mVsNxnPx6T3VU|$3?~XE)IiZ0=;e<)6dIU$ne?x{c`zNRZEJ|+Wd=q?>spF zyx2n}>Bon|hj$b`<zjKX5WU8<I>5`3<KTe<4A<7*?>KiZ&mvZjub%f|ekS9EB8FBc zM!t6DpEYv9!ND3LT->V|t*x!ab(wDY96mSKdf_T9P~Yvx-@2SzTR5ld#a`V0BF^uF zzhB(p-|zQ#rx-OlEer^M9k=>niPgi;KR14!7W?Am%f}zCGyIz-+ED*{d;RR`%qN}} z6<EkrIR}J>ew|l8OMQccU_wt~S{mEJ01i-%SbW~LJ;$uO^z}8x>8B6Bjk6Q2Yju)K zKR@r-$H&LN*macIZ{KFFoXoRG<Iu|zr%5UnGJK$sP!1K(OU2Byy!|z=ZZEl9r`5$2 z8Y=#T`Ar?4m*%cry9{h<gs-iWZ9M(-;Rk)8f78Smyfi^N7<R^-Ghts)$#k`gAwb9Q z-qvh!zd06-#_8u?<nGmr-`C}!aN*kr@y5WyFE83bQLE?~boHdw+v+Q2k!4nMtvYU< z<=9=Q9KN_9qkrrCnLcbYeb{d1gx$ZKXqbGsE$~j!=^g*-AAGpozCV@q!P@Qjl5XaR zbuBt`n(;}IWo_(%FJHFIV^(MhQDutaJqQ|z>r$Fm^U1TtNs*=5@yX{;Ml0W@SNwi! zUhwmiYKqa!u8p=!MOLi>4Jq2%+H$Zp7wnALl`(~Rk4T<z;0CKU!)4WMU5gZ^pH`fG z_LU`rb(~}2`Buj}Rj*|gnod02Ua4!y_T}4_6$}}w2Y%MXi8o%dVo2sYu#O|6)ipUe zIY5Nf{`bxEUuOKjwe{q-?SEFSdhq6^amD++<qI^tZk}IxcFT{ybzi=HTeN;X{~`^i z(9jjDtj;&xX1vc;Vr}nzKVC!R*(*Mg=*xYx1m4`K<Zg4n<}9MhSI_#RQB1TUJg&0U za;~37*P`bPbNrUi<JfYFF+_;b*TgLEPRGB0bv7T52v?n1uaR{-|B7U<-{S*~;(;0> zT8ADLSZpYK>{e1<-Yl?*!72Gn>^WwGeR(x!c}lFE62H}XX@2_oXU`qSr5B#QtA6KN zUasH1|BYpI2&f;i;yUArO>3Sr#OQ?|m1&sDA>j1j`|pjFpVMS4i&&=X#WE%HEM6>a zXjv(HFkwUP!|6|L?$vxQjoDLi@Y83*V+&Sk{rdg;;{E&UuLnrz)@q(e6WjI0cIpFD z=6x|M0?t`qun~Uy(@V+Rz@X!H-feErqM{$Z5Aq`!D?Xn!fBuxAXy=_KtA--WhD*nz zHeT3Q`&*$)sqX9Qctr^wr%5U+w6b4^)&>Vpwy5~v5V=~PgUM;3K-rrc3)5epy;H6I zz%)R+G5_o4ISM+iGH?27f4_6ff6vf<|H;c~q72Eh4H{kCuNbtRGW7H}A5=JY{P;o* zF1hMApfU5DJ39ogulL`+_Nh2aQ!_jNgHKPrr(FvO4P6tpm20+HZWCy3V{_5n)0~Zx z9ml?BKT0?*y}>Ge=gj7!{kt!{VN+y@jg9?rXYc&&Vtt|<^?O+oPA*M};x5=+#~}5i z$8c{~ep=eGFNN;z>>;5?&zj%oVW@bua`}Q)T2)n52FAv>w|!WC+4Il$|Np#Wb`;zb zsY_l|oRj0zet%te-j*-7l8q&{KHoZj>GKVr*u3(~`nI0p&FG%uC9XWRPX7A(_~YE? z?VD4K&OKcJ=vH<Y`-j{2|4m!DYSnXwEn9!;P3PXEbNd`4OS9vbFJBU>s-)ceWEzh@ zKKSB=#<Jy$Ia-}^Isac>J^kY)@BdtZBD(tDOtY^gl$4aj<>WuTHhuQ)>yz@12`sqs zYP#M2DUyvv_ZshBx0UOETE4iF`FqVfyOOeXk3KNJc-y%pj^o2I>HG~oYK!$*^+dR4 z%$jxSHN%!i3yMk^Hb(IL{`R(6#S=7&qi}lDzCT-H#lO{jIJn!Q?oaUL9E)V0yzSD~ z<?pVPYnd_MKXcUVP`21>{nV=UL2L5M!w&dmEa;qMwly*@=iVMx^ZPZYySD%ND$p=% zjauREy9^r^sO}K^F6~|EeYf5<u;BgP?@a<t^8bGv?{HiE;_chkRhw8purnQ;);^yB zlq)}cJnruMaQT!)%a^m4->X#rQ&%7QK|1#5C7%6LPshCr`?c@VgabNrHdyt4jM)5H z!kIt$#iY|u4UCP0&2)bJc)Ymc_uK0ZCr-8QPmOMRT4Z^5F0;XU)g6XE!?l>A4xaz< z<Hw#E_ClQsa{fv(eEJRd-+tV(<7xVh{r}A^>i*1l`ux_mT<LAOx5Enc_Qu%cvz*X! zGw=S{`~LC*lYMbj`|rHhvc2|V=9GEYbyN)YEvxKY8FK1NwJ-~#xPIJ$gU!En_l5J- zUuJO#E_~otZIEo9!22smFhGQLq6bU(x|oCZe-7I>7`Dx|E@x8U4*R;*>Q9ehgQ1MY zhxIdN%-AsJ-tC>m>Y%o)ZtY4}bEi}_uCxiEUiYRl<xbVn*&FXUWz8P8#S$SxlWyLO z^zrqL{1>mTruN}eV|}B-gsJTHtIJrgZMqiaU7$P9_}dTTu7)sC3p=~Mix&gu*hb%( zzkc&(;gum$4-4F-qv{?!I5^$F&@l1WPM5$Vx!doyojZ4~Qm<aqW9iAAL9b&^=IWR4 zx%$&<?Y65w&-SfP+jiiM@sxSavt4%;JZ#GRp~AtmZ29sJ`ui*SdII_CS6491;+XN2 z!K6H1n9)n~-Twb|2FE@e5%z!Z>+9=?{dKl1j!pOFGn`NJzq$WkI%Z$Z%`XwlFL&;( z{{HInlC%TUs~FPORWH49`_x~3ajq%V`|f;MIMrZAZQADjCttjrQTxwnVFS0go<pF> z<(Z%%*zlf*4DI`OS~8sdo8F^1(}$_y-lpj8pNAhBxC9=VXIuScMtq=1g^k>j%aJ7| zTjoDiSFk8~;TyBxF7wyUrh^LK-reQ)oTL+5XYF08eRs`~Lu*vFPf9f}{{FTpU-a?p z&i2R^2{p0PQs-C}vu)CuUAKMm<jEDEd&9dP7#@3=zl=R#m%hWxK8MSfF9(E*?%lih z#M7dRFBjbpN*vynd;4L5h4`9R+2Y&(|NVadV5jhLU;F8|-yXYh<Hnw&)@cP(($22k z!?4;VLA8Iu9PWG{j%m>nA!jYMIeV@?-|(w;x!+tP-MM0}f}r8ui;LwM%GPo7)z`8( zoc@<x&7Ad{Nurp6ubsL2`#V>6clLW#uXSw|XPg&*b|ByW$NBm8$(;MIzdrhQ`~7u? z^VOyE_W1hxKK%0X@{BoiuISfkXPioX_txiqr0TOZLgi0|>dKaJzip8}*5&EMac-V% zww133$3gY^H9f`Y{zX5cRoN3t{%_Sh&@NZS(PQ}MnfZQ4v)M<lt&M(IvP*o*_ifyN zHb#i7-}kF4bM3CvPY;2DecGL+N3GMI?sl_aV`JNpeO)i-#s<c&MGqeL+aF_>uUYUs zcumsBqT=sUh4P<1D&Adid*h#}lO}{|vuiH3D1O$H>E|H8QuE>9Zx6L3nSZW4nJd*V zT>D>bBlo1pQf0EIg$!SdlcJBm|6|aYSiqeX!PB1ZRO#>WSYoTYUqN8cpHFp{*V#5H zB-GT@$o)xMx+iVpzOp%|pE~8|>;L-wyYP1I_Lt)SjnCUKdrkuN*#bqxW3Ps<?AdW^ zYD1W|z}qi;7w#pOsD3;STMMwg^TbT!^e?gVy;&bUogRPWx%@Aq7(Mg${gRdp3vO)p zFaDnzdF<QU+Xp2M^Vk1y4)6l?gDdvdu{1B-);#~>BiB9k|M$O`=eN>&SM~Qi3z<IF zYiD(C=ZBhxXECxm9{Bb3^~dG)zm`kL${xMA*j=#y_;i`(>0&=aW|eB_${2A=ubO$# zKrX#Jz^m}(CDqHXodQMn{J$4{TC6Tiy>x}AECWNWxzzmg><l+{m1?hx+1UgdRo$ec zEW+g&DrseHef!cUVW)zxuT*1pmz6etP47v(yv+AQ)!u2omRhQxyk7r3>(t#YUnlYS z@nehne|zqApVwNVVSYE<$~r(ZZ2Kxrjc}Kg(B%o&mTZ|d(L?3SKcS_MFD>=9`1dl9 zuU?teL6|X@YeRs>gLS*#HQj%Ie4?_u09WgV+TUeA{x-6%{&kD>-=kLX<J#;0lpRb@ z?nz8ZVcENPZ|d@EUW^xiJ)bEXo_DAE{oWsM((Sv8pPy5lemYX@)0BDpV@%^UXH31u zFr|w3X_~y(Ube^2W6mhFBoq||#h1ld85g{{aeTtlL!X-WhiWsLe!u_tqr<e+oZH)Y zdkk&<{dnxL{Bp<9qy-``-n_|*{=xWQZuvcfy%~S&>_J0d2iEFuJux-wb+75ysmJc^ ztp?3No}R8hJL;iVXjR(0=1@WZrA!K|qZ*4II_Vxe+AZ$8U*9$G!zpe3!XMHv8B;b# zrfiP<@wd*$*Z1La`@b*m*!(wRVLE6Qd!zXAyPKQU3*X){z5E9>4Ul7c+@SmKTCe=K zJ@!AE?He?@X3Uwh<_6CZt+4eW(FtN<A**Z+xb=I!PoI@<bkDxNE^)G`6UU>E7U$+$ zpM97g!ly9*eDdRCy%JJVhjtb}fAF>H`Kx=HN)x3G>or8Ow0}q@pLkwuQSw6e@|yqC zbfcf8{&{=k(bGo{vtl2E<}FXA2yN1t9(y%B@7?}8N^+|;T~^-@T{k0hqh9^aiRYhx zRok@m@rjB22WOmmxPCi>Mpx6ne{~*{TH^n{3ZF4^=0RWcyDaU8P4#cfbG1JB&VK9E zCt;@#FPF>fq4&AOz#e74y!6XqjXT4!_q-F*j2eTN<zJo>of<&~tJX0cN@{Y;bV zDqOWbNY>Hzpn2cw@byab&(H2U9G$=K*u(r522k&H>-DbqG7A|ut*MSGLNho2d&_Ab z@aFmVi|+fQKgfAb0wtr<JGKWEt-bUm*7Wh~c+eEulzp~8l3x8-E5>kh<|}RUr{Uf| z)}B-4U|JWqch>_eEj4a+h0~RM^|q`D6%`W4`{hAHWgAmZi*YnHBp7sfED?TKup{@O zzS_TgCUZYY%gE^Lj=8QSqQQFQ%-eNkBI1Fi3UmF!TQ<#_TKhEY`_!n)&5yGzwKzSs zrd~3iWXk8I8CU&v`)RhiYh6qQpPqQ``F1NCynIE@w(HzEzm~(UtM8xtAkJlZ`I(&G za$#p@=V`tAZ$+NYsATKrWHjO4k$HJpWpGoNoX1XAyQ|xTw4a-r2-PhXPf%N6Y;3&4 zuCn)VyguWCDM`1t<?bkXDfH>*p1g^-B<mO~Dn2y4WZeqVUi>EPW1Z^hY}4ucBpUMe z|NX|=>UdS{&l??4z3WjqhL1wC7aDkTa5XgutM?WD&^<QkIul1z!{K&*<+*-~ckixV z_qd;B{q^hHJ{(U9^zrl4EBJ3K*B|#bJ-vF3m-V}-t>@O2?TFDUDKB@PmdeqTu-m#| z>-nkH{(Bd!OY`yZc`<i+Z*ox)Q`emKCk2H+vfnZ;3DTTnQz@i3ojb3*uyy|V=lP&K zcV(q8_k-|IQE9WB4Ytqoug^7`T^@f%BX8EKkUw|BRQHrgHAI!F<gxzPdSrg&jbjpr zv)AvvbnnAvu~_bchlf}#WcV!R`t7)z_oHUtwzl|5E_3W^Pnq0&_OU`|`TI8_hi*g` zY`$^v`t|UQwh@|p(m3}P<?b<6%yIp(b)tugPdZD|#>kY-Gk+?qEdULvJwG=$Sk<%X z@WY0~4>PiVFh2P3uw2|^enQXLx1kHB`8G^#KBx68E=M)tB?k-Bw98dtr3_QA=|z+( zu1jCD|4MPTb<pg!H)qV+RkpZVpnbm?(}eTS<HcDRcX4>|bu79u?ZfI-U6+^p>;5>i zNCVWI+)!8c>;BZuhr$-<rDd!((iWAPdV9&?9RVw^Sg$C1nP>dq`kKE_o-BEpZd>(a zaZ<+dhwJAuF#L!VtYBQEaqe!*w&wjBX*XFzQ+o{8#O~gf^X$&5^0=jKbC=#b929oa zQid;<LF=me6zzw)v91|4F}p6Ty!ybLZ}U9fmnBk{gN0@OMR6UNX`C*n-ZDq5Md?e_ zr!{vUuuJ&OiP$lHzKSOY3sdU!1Jh=6Z3(Uo=e{0wJ7sfB<;HLJf-H>3{!f3l*K)3( z#azF?`%XW-lx|mJcfV<N*_t$lDAqcLWvz~jG#>r<m@HHG`0L&)!h1d*lRkBAZS;SM z19c4N%C`JrU{*ZPDSTXf&!V?wA$gO1R!@t%>ht$%mY44Q^N+RTe{!+0v%jwUm|^`B z)TmPT7n3eNwW%jUhb=TzJC6P98Py|6hCDnxIu&+Py_$lo)FZT||IOlHVEFc8asR*D z<@-L*)faS{$NnJ4{0Cz~)+?(|KmXWxyJ@<HhfiO7uVU)6GunH@RzI1wYE{ARyDwh6 zSn>Pl<%<^^Cr%WsUEf~({M^GE8<Q>S{#Y!!s1dYM<kQbRD+R9X&gc+mdigT5q_p&G zwcSjgrXb0t2RYh5qPGbJNZ<eWZF@Y+hoxmL@(g-$do~>C6?GL{7;qxYwsz;&PieMa zPu-nX^)P-{3Fldf!-o%7yja-&VQqdbclEb72VY%Xeevea%vlrHf7kH)d(@}(`H_$Q z7Js&iF);jiX1@PtlJ%kU3_kw;GovgvZ@TYwJ4ZM5_}!(4962uD{g~DVs*&rzxYxJ* z`v)47IP|c<WKT@Q?6Vqw>(0%w<P8+L#b0;r5z~crADE9aO}g~(`|sTEo7S5=&nrKE zcPUfQocHsLVwLZ|xOnwytNqW(_6MJxoxL&Zs@9H}b!}V?t5z+O?mDq)O@89`Ne}aP zF>K-a^D(lvJo#~f)!fjxd(+Fs?-rfT&n~>U{Bov@RSCyhBe}{af_F-<$G&*`_G<a0 ztRo673R1mMA#859dFr<9VObFRKwoYBxf$Oci?bXo3-;Y@{Xxq0@R{a_u2z$G&OhF4 zKA)78wJO!+L7(-z1KV<MU$}U2p?{=V(vio7mo-mS<dhvfeBXpIL-hyagEKRYKkEq{ zllW7&f9;NaG4^}CdL6UpGF-7?I3~g3c;I~fzt79`F6zqi+kO$q+x1fI%h#{EnX7ip za@4$9zQL^T=FJyf?fW&EE@XXJ-IY|;_@iduCfoO?b5}*a`|5S_VAK<aE$0jW{i&>Z zUwz+MyVqT=lEtC$^_Qxy$zlCY92cXnOS>D*jQIG$Jcc2Px6WY3A%2nU*V=D)WT#KR zS7iEq={!f>G)Awzou8kdfBgIY|9^Xygd4ZtetRr@|F6&%r-i#(ncUtt2_(%D@Ku{E zaN<<k{)J2zx;`+wGHSH?O;S1SYV$PgWTb5Hy32WMXJ^dfy!i6+@{i*6AH{bRJ@xYL zIN)o3x8>ctcdV{fTifEL(?D|$Pri0;Yn;DzGs70GKS#Th7cz7#%Ge#4X1e=B#99d* z(OFR~3`?7oP8;p}Joo*BcKbhyY3b>+wHTTd63)-FHT}wPYU+a1n{uLhrg$9<H~GUW z*bvqx&u}U8v+c7}M?aNazcBsv<@`|h4I%mq?i^AI6alZvJpb>_`43Mf`_Fn;c4fuJ z+}maqHgdI+#cS8rtz?+7SKZ2Z^P-3BS)0Wc<UV9S!`;B4xUBkac<}T5=cTJZ>t1DE z!P0&B;eqV+d)uzZ*UR#?Gk?ER%>QoR@4O#>|Fv};I5*dNqL13dvuWa{+6munmrC!> zct3ZhaKpBS`CDf)Y+?SxFyZ79-`Fcf-1TovE?<eMT>br8vg(7RBZ~qw9$er5&vah> zzsi!bvPIjsi%;}mnLBrG@ox(@pNB7By^8wOb-48PwTB-b9xmPZD=jfF)0RO|Tl(KF zPKVtG_!(N8Dw|HM$?`AWXd1BgR$k`fAoaUp0*tyZIYo>g-^ywHSUj^*d`b{Yv*Vwi z^Z#3d7A$<NW^QEQP!w?8xay9(>4)r{jWu@r7jOmg^M+~s`FfUd3*(=oDhc<lw643t zx&4k!-A>E@UbBm~cs=af7};^{rROA-zt2jnCZ2x!;qLpo>sy4^&wco@qJ%LbYti~U z8(Rb}8#shs73Hn}%d)`t0eeD}N>a()w`bTwvbKlrQrU6m+U>O=JJZtxyb8a(xahj~ z`qQGDZM&jY=d)G6{5<tq+!nDvUw7&)ITow6mVN(Jh6vHOAyql*=gi+J#Yly1k@{0+ ze?BQ)c*}*wVGm}@uD3P&9UT#wFx~zSuSmnSmiw=mjJEw;b>-G+Kkf})$ES0-?QV#F ze}8kx>isvqvaRK?&yHuzi2V^MxjoF==jO||;aNvli*}`{GtOqL-y2)USMSZ?@cQuk zuT|n-m^9X2)Zcn(mPg1etu<;oD^lYRl(p@Te6Zf-$7>yiw+TOIE@clmcj$KJhsfRl zozn6LQ`!C{Jy?HZVFl}<cz4DO`5#+(P0uuFq+iSy6pP^GXs<8q+*fqmds51-7prq0 z<Zt&nP`2?;-gfDa6*Z0Q0a9F3gd2`VRr{*+ByGNw^w%w-)@$X<o@R|MZK;3DI2^op z{^eO_`^qb}Ry#X4?d#h&q5<c$AAGoe?`e8ZB-a72w*3!51%$XW!}O&LQ#OAp^Q!iK z{;!wS@!0V@pRSu-47d8O&(U;m+8v86y-wj;e|SY3!dm1RoEA>dR0+FVeTJ*@jh@J+ zD*pC;WgF{W-p{_Xe$y9gPL`%zHv4RU#*EM(j1h4xW!pY#ZI`;>C$}K>V9wJc2`}AN zxayy`xhUOPvW@r8npp3~XvsQ;0;|}nuk$}98I?WP)V;m-OU%x*&O5KJ&tDV&YtFU* z3%>kTXgTnle_h6p*Ln<V`Ro}^_nAz+Q@MD0z`48T-14t@|9N?Q3f=T;sttpeCfl#y z3olLOu8(D1Ap3xwVfyL42bdYUB$HeNb4>qgm^)v{I$bl}n<GKI?%0f#C#v_xt(dYV z<Hu_?hPO=i3?6*!DxOXY17`f+qoO$ZWXRTz<#%`(<+pl%yUDtL()9G4`fFkzwm-es zpv$-a+oxMwv(GN?@S4<^HoGCh^y`BU`hFa16wLOeiPycg)w_OZ<vow|>s{^p_c2{i z`@no))>h?d;&UTkzbiUx<2AeP!^V>7_c`h`|D-KStkDzTnDI}ZsqsTs`~Eu12D2u4 zhN`Nt!w(B|GNe^GoNiYAyW|pYUG`;V`I@-ZpKovcmA3c)+HDHlZHKhw{{3oa$Yro+ z2skCc*Y2C_u5EsQ$JB7`t)H{5ESvrHUu<{bPw|*LO$o352|Hc5C>Z(igSjU|6nEX* z)^q1tZ--=x`)4-$C-*w6z5Dl4%96VezyH=XzW257YIRkWSgG9E$69mcV!KY;pWi!m z32SIWkKvm5{c@|kZgbUbvte0a`jB1X*Kc;Qn2sL9+UXB2-PpG!xnS+Fp2Y35vf2(m zJn-)BZb1%~2PIa%;wHLZmmN&2@tc<V=FZOI!1<Bcr`OiLmpZX@|M`l|vrSmKf@{;( z^Yir<{CGW!Im7q|<AVM50r!P3XfVXB7r&TsrD$%w?|#K&1s@+Bb>08Jc>ZshOD|2% z&9Q8*u;~lY3cY+jqs3FiwPJ5v&132N2Udly7AxJCW5yjh%~5l%`Rk<Pch}!|&}`_U zBp4j{q2f<2M}2fTYhLb;?ttJm(c9&g`^}9>h&0s>UB5`~@lU@w78{oo*S_?QzjScb zm2Xv=P6}O0ck;GpPMv#qVXn)j887#GDLwsI;WNA8M-AiY@9Rx|NUJcc<=<~+mV2uS z)C!Ie2i5QSb%EYL&Yqik`f0(-OG{OAcZ2l&mf3R4qV$!>rI%m!c%6wlz<<Exx4-7m z4<~$=J&)-8V6MP^!R`Zd!|k`R6HKqgcy2Cwc+@Fgucp9iZpF{1(>DqoyPdy(ZF}%i z>z^UQ?`Q9Q_O;6QvkEhF_ATLm-&V8a=>L#p2%KbKVX;L)(nHhqt#p9)i|2oxSs2y* z=450{o{-j~?B1s!*RRi6W8B|h*cPCXa*HwJ@iE_{Z|vLm-($K^_aQs^_&LMN-OCp* zUYy0Bu(9*d%a@roUoN_zj$(+b|6BT`Xs7DV-)ZZ)n;OoY^Sk_XzwO@U&y)9P%lzBb z&hVDap21@hbN010hP|tMxjnKH-|7k*Pioo!d-wk65?_ba{l@9%KpP%D>-<kj{(C9W z^y~AF6#^VA`6@EE1?sN(F<t2Ukez&YSKwuqORui3_KiIk<q&V>5_sgYzy01f5B!&3 z{&CX3j>$DJ>(Qs_|N3|B5)&&~xvI<h+r5?l>n;A2iZztA$@9E;^~!MVied|y4^?|h z8SmxDSQI4OY-QU2=czuZZ}j_%?y;xleI6=FR=PW@zGhX}F!I$ec5WzZiI@I<@Arb2 ziq6i?ysNi=udo3vy<Fn&xb63k<@LYRW$bDyPD<8Y=8hBqWjEXH3l}Ex)d#aI@O{YM zb=K)CtFr3i?c28}URbfJ3$$axqWW7-^x^-9xb+{r-F`nV;pg;CbEg+Zr2ezEvNABR zIP)-n2g4S_KevRf%ikHz?R)*=#g2_DU6|PvS?d2@ukXFQ+<&|5k10WaejK+y=B>Zi zg#X{tBfpj&x18&D`HVfoovPQe0h+OFA2yo_HKa0tmR_Zsy1AX1-5OyV?=Sdc_WqyJ zphbeY%>TD$UkBB`;_@{g8s9G6Qx;qI_|U?2>-0XG{y!)nqj&#P!+yVa4riMqT>~R+ z=H*rY*gEli>c=;m|DXND)bF;KH@4Ydv-SM*!}9+>$|skUY*{$3<F28#b@%nSYTo~U z&i~(|Ff&Sfbv^SZg|M|zsk@f^Xk_2daq?lD*@yL!m$|o}_MFxVYS!HP@#97Nj~9#Y z>o)WXG)-3be|0)a+BQZ{-0uI+^N;uaewV$1BNDV~$L8}HV~f&PQ=Urwk7``6>mem0 zV^aHnnQ!!lyu;H{Q-4nHW!NJ8=hlzw^_6u8^$s}{`u<)SynGhh+(jA!ER4?1&M)4+ zbqx;}SDMICV;3Lm;%J?&H{IE6_R%X>MAFjI3Vwb{J#*%aPR60c?R&T1y`LRpdE@^5 z{97O0C#l@|f8elyOJ?NGzo(rTZVA>IBxGeV>F?ijzl2L$s`n8nQEor9^sLpXtC|wY zFS3M+k3D8}T)|PTtGoZ>k9F(xSeO_uKbEifplC5?Un;9T^M-AFOc$2iZM@B0$ME9a zyQ4R5<n8(YubSIzUWbd)iRYg?+!p&X@1La-WxMVED)k>rr`>=4dFG+>5{GYXJ$zc| zUlniT;r#c^2FEt!-BoLGTF5$Ws$tu`dvO&u^QOJ$+B4&wRoI8?yQW+}^-B8w<D#89 zuMPgx*_Ykf)PDGW7Sn}yA6|>AC{{dZ)SoeJBhyh{w>f_4&WY>P-bB3VveuoW5xXl| z_s&(H_mQ36@$vIlp6FWCVg0?9FC<mwUzkwCQ-=K?HI+Jkyh;B)Q;b8<$-u@Y=KIQ> zM;rFv_;fpJ=by5N`sM!`8yPQO*I0UE@9(gVMcI{y&)@Q4m?r(_*4}$pnQv^#+*}#t zI?2GuDCwo@mBX#;MSm>a_$?^*UsiY3se||L-_MVFbz0(Z+r{1&aaZ@Bd6>W4>p&{c zezR@wC7kR3H@wW0;bS+;xe-t-xMKP4c&`1j<$LCB+$Z?q>gw>>>poqMyS?eD7+<|M z=YgX3_}vNipJ(pAa5wj!P_zI`PfyPk6`6?4jr%@Y%ScEV#LhY^(R^S2z&zXXrl6^Z z?^h`v_|zI-Ju9fq`u(2cTNqUXaz5<LySqzu>$$jd8|!Xz7xUHSPM&<};pd+P-);Gu zjCLtK$lpA-ahm;~Tdg`BLUMopOxNiSatS=LtMv7Q&p%(KeZ6gZZF^O}il>B(%$6_n zI23ohe)n7ZzqRF`v%XAv=|6hQSsjnu{cgiB|9pDZS`NjC&1t=Fk1g_Es}Ym4W`D%= z{kP7i>@++cGx7X|3lmcR9eMrr(dqbqwH!@bm>#C*sxWA?)V*yg+j*zr!{>Ot?3dx| z<9Zt#8@p6@d6)itD|3I%%e_U%*GFyTvbD9HcE4Yv3l!^4fj7IF_s8WvV&(gP>A`&Y z?Lsym9vn`!W@|m12&&5UrmtRQ^(X4sudg>c4)>kTH<jUI|MmO#SJ^&q_N5|?4h-M# z{x?WzUFY#bGPU$1yID>3ho@ZozjbMJ?f87IdSg}T{H3#BmsoAA`&(5~T6)#?aQv^` z*@<7XpMGL?P!PB<xi)P6&i(u4%kO`#{A|@A%2dDCreW6_ww|Lw+Cp`YV^2x+)mMqQ z_x)LyW81Ju1GIcr#C4_RpRFa|#hL4OK6VXsiI1PJdByFAdH%hw(%0J)mY(?>eb8U> z@QYuL6*hU7rZH`p^uS!M`t7UKJ%7Kwu9C0OPDo2nH?*|m3=Pda`gQG|s`W9G{BQ3( zo^{PdT5me{?QQR!^4~K^Eav{69Fh1Tcxuw)W2PUrEoN8~@*}_IB#-{SAKA6~Hm56m z1PY#>5>*l6y!$kp{m0uKzgq-;{H?p(Ywgv3{q@m(yWcfySnYf|zj?k<*IDWIhhMkN zWLQ)3qt_sAM*-J+Ijays3)et_UN=Kq+g$5u>$aYbUmY<&sH?A!ujTN=uB8{jtDI+V z%sVV5z#&t|{AZQd#?sRqHF5sGIT+gO_b~`CY`np9ul|3)(X>S^0xMR5mh8JIHP+bW za|Ub;+kbDPT`~_}J9FKB`Go--i!`Rq{S^`_I?;pWc6pDY$h(+-uYZa&+AkHkoga7K zPL84d{-62lWPiWhsQlx}<i*<@Pp563F=K|q!hi^aS_}V_Ss7nB6(^{0&Ye5=msnQW zv1x0g4T~;6+jRZ7SG#<|jh)HK2j3R$ii%@M(@kTv`19FaZ~D=X{{0WGtPI|koEtf9 z;_0UX-A5<*Eni)9JX`j4wkd1edhvq^0(#TA`PvVQ{pebhkdngk+I{<>*I$c&oIc>M z*{aehA%DB5Rlw-NJf;H=&H1@G9CON~<m)OVjEygE+n??z;GpShG}FasW{b}038^<t zDkHR~%WL@E_ngGiet6;cXUZ)%W-U+aaTIZFaY}r1V<Y#!1oxw_n-BXx=UTYuM&0K& z=9US+Z}~G={Mq*4#8ab+zgwF{)45O0Z4T-xvFa_c>di6Z4h>y>j#WZ;hl=Ni3L6mm zStG!bcyYe2P-nvRb<qz${$hM=Y4IoZVR}zu!2<T|>tAZS1wJ)@6Jp@AUu7Y~;G9#& zCayQ<=+Dp3C!Bx2`kiKi&$&Rag&JLThAUTDt(V#HrFM~q6KE&riu>1D7T#)GJ|(DQ zQODN9hb1H;y8HU{xHnk-3HuQ2^{{NWf!#J$t^`{}m0Na92Oh4M`uEd8Y;tS*xjAkN z16D*^X6Uwuy?I=+%fQ5>Wv%&rrqHFk9u)jo-5fv3B`0D(+rtU}XY;i`{=fR`|J8L& z2Oh=?{W-R9mDUp{IdT1YM?XHEzbHUs%aO2zrSk(PspQ<*!I^!1U9*OzxKDJ^kGBWc zuUf_T|HKD|LyG?=$E|;U|LWuaVe*VKd<%bMv(&#dRrh34Q&%&n{415VZ21H))vq@j zs!Wwki{CaT9i4alwR8FsZf54@7ab*zp_xzXMceBc|EyZ|>OuC0_0p0p=bu-qN_@E= zyZ(RbTo#3s57#sQ37c;>+4Iq*qn|D6|H*8+`DRh_w@;R=Oim2_$D1EMO#JeFedFoJ z=6hdQ{`vMWTqLx8|Bmy@7p~rH481Gp!T0j3hkM5TJ1Zk*b2Jz|XDs+3&G^5m*r?0L zj5|_q!G!b5@h6mKdMHdj$*|x=+U66_KXZr4b2Tw+zs<Y-Hg90jfrkZ3GF*ore|4Ny z{ibZUV@Cp$PW%3-AE8k!annRKx|p`#ZuD^HnAj&E66$I+vo5*h@Q1q$SGI{W@YOdz z*#7x!$e*oK|5xzUmwvds^GDoL5tar8p%q<R(@!%_Kh4>HoOgM!lg-3KM~;|;gjRk& zcfRL7yTd%;hFz)kulHGMfHv-EeGq@G(#1B_@WA`83IZPvy(~HKw(RA3>BGw(O?i3e zeL`{n^M7SwzqY^3J@<T<V&fU(jkClRP8${dWa46#{OH1UU2XY9pJfwIKNbA2`~J@F z^Y+FYMEu?LjdeO`!_8B^{a%_9mxXe@HopGgpkEW$A7`U_x=T^`eAn?HUB`0eKzGe% ztMW6wRa`H&3QsaA7iJY`d~rY2F>J#5rxQ;;n{YA(M4o;&(R|I@4?C8Wzu%kwe){t@ z`?}b2_%%R_>rac;9lIfOL`!68gVVx>bLR}&*nT~m7JFZ#Yf)6sJ(VA>jr;yTVE(<2 zX#s=7%;MjfdtDtKd?@}gUH^|(N~D|FY)7M+F5&4C+3jr4ibB^i+t2uOaQeQA`_~zs zFg)SiSbx`+p%Ju!X59yIE~8C1O(vaWvC&h%#>4v}!1+9<iFjS;$=V;)@pT_wGXybg z>TTJdC&vH@RdpfGBguw6ed{iF=R~pAxt{y??)&~vt?QXR7(Ar!{BJvOUxE?j66qNq z7WX&?hKPK!ToR;tB-yZ~sjg!0ylGAGdMS0^ue$I1QOfYq^7Z|KAFuTpWa{|pnIEPf zh;iHB;A7UDV$@h+(|7OQyq&3A*E0XVUibgudgcyO;eWrlz^+L6A(<Qy%DO4W!ECal z)=Vd@sV?E+>8DlxL~;K6#R*ly*bpQ-WtD~q7btzE8J|eq{NZ<P@q=QReZmi}cRdqv zZH<{Q@%;0N=btN{Hj2?pzua9DCHRj`h0*el%s;n-@qb=~Tot={B0yA>E42H3@egTT zuwx%TV9&d9D_eDc*Q$m<GjX|sAJQ6NiR6dZ#b2Gd8^V0%=o5wZ{R<(%=JBJqS?u}m zk_DiQqzP6S{IGqa^rUBu1wUp_U;!<5muA<!EO~%|fqf_FTmXCR&-IT#m`5{!lABuT zr^E!Af6E}gKVufhBlB+=#Ge`4@29jXwC|T>I`HuM2lJhc%gbci_g@6_wtIcQXUX4i z_`U`>Mxs}gZr>I(;qd(>Dhi;u%-CLkP*LXJGY)WoeetzE@G!rNq2Pz*pR@NjroVG~ z{=qy9Y<<{<KMV{`7Jqt07(nZ}Qy;h&&er3rKkN+VM&|whvssvluRfWz;qdu~`F4Lk z+c5CeKc3tGidAFX{dx=vaJQ@ckW4n;_Tt$Gb4RezY6ty!Wg9L2oE3nWHEq68!#x28 zzWT)w!)85QepK~9@zec1Mtt?oaIu6f9Ja|kd?y*bG~4$}DuO-W@gp>JZuY;0O^bH8 zH{5>Pwn#(fpBpz=Dq#EkAcMot<U@4Z7!L+;utq-nU>*dv-|et}D3@3M0srUU7+<`7 z>!>#S`3G}l7H|Si{i(V1=vn(IkM1>eEoutty1I97$MMNB|JpbiK%1#%Gw)Xt|IRW+ zlvOL#VP)68eOEh<3N8Kc-<pAwVW#DWmv^Q{zm7i5zw(;82&<wW2d89t8c5q~PWx1z zRg0XKa&c|q*%{EVXa#6V+WGg+HWOzx+yA|Iz3#vFe`n4=s}7cgHf@XIZ`AqtcTFsd z!+&Q^Md3vXEh3@d9Y9R>p_(D976nWQ)DQ`nz`Y>k=rLdE2|j6E=bs0=@B4RE|9?Nj zB!*4J4~!X-OsC%xa!BnKU}1{pJujusu;fzlluP_Bo)V!Ut)Li___IkzJfBsvYmtM1 zOV7Id4cU{8PdpED-}iB={(t}CF#G(5z*Qf7Z?N7wSC}(#{bbjMb6KhkOSl*n7y>jz z{#|`qRI@lhL*#!uD8gkQF7I&)6glyU_w{*=!`F0YKlrwz@Z+PO{rf?t=`$2Dq%<ED z&uz#NPuSM-eC@nsv-{gs90g7{_<OyRmgjDJ&T=@}aoW>he!GJI;h$MmokCB6HXca+ zOS)!1|GeVtlM1s>I|*>u{hoRI<I9ha=hu8(`1}9mIZyS^%nh2c`<-mD`N@L&KX%;t z{4_7=r2pG}8J~8Q3G>){zd!$Yb&K`tCCVEUc4$qXcSC^X^7D1EyT5t3yFZbC{E;Wb z3zXw`9az80^>w_)lZk%m6MWPHPCVa`ds|Jz>*0-e8)N@lcU`WT#jqkY>U!3E<KL0* z_I<R>`Z%j>^_Qxh_tRfaoxt@$T2Wkw<v_^Pq>VSqW&OU_`z+lQ0P%oKUFhD#zn5Q{ zJaLj^X0|QxIHaDOy!+qlm`(cW4fAgmyw`TjWs}Njh~tan{ZRBhbLQjKf~}SH+#4D9 z%RXqGRN(O9{rl<nYQNw8f93ApSNm0hBi$D+xDzq^ENC;3V4YX~k$*pH<XQ~Ad~*?) zJ-4#@{yyvf_jjhm*Xg|r&t=F}=i9E#RF;sQ%3%J_<lc;ve~XwBpPB0Xh<>~4=dzD= znqShk@Be#yf9&`6`XBB0)m?5)J1zb^J#=bQASn3trRtm*0u?66DBaOhuZeU2ax;DY z@BIHi?f>VmKkvA{as9N00B-a9r_L@Djj6Hz5PkQGYe!Qc%ZnsdhYde8r*Sl-3MmV9 zTFk3_HuLwkyt`HNufLZ4RADn)bL;#gDc)8aOjlV)9=><wNBn`Te{S0xzRX;w$-~0Z z<S?berYtcjDe2MG@Oaxld*9c-Ke5z%`X{y*y$5v6l=sh&*ng*ZOAkW`Q~cV37*$0B z(RE%-3R5=zbGy;$`YdQm#XX(FnNx4-OmBB+%~4+|@GQbM{rS(L&MRJ=*N*Z8El+-Z z&~o+R_eo;)i$6*wv6at{+Oe<uBZHVW!<8q?WnZrktGs_odW8nVq_g!bhI=|^teDXu z{q@Sy{T;XVB%D5L#L@P7A{SG@yX9A>cvvQ`*vg^9dLgaTVey0Y%PyUZP*^;zb9bU} ziS^9SM|b?cdgR~L13gCb_olTvq{Xw$W!@x`bMOiGij=Rc($n@&+Ny6}yir6yVI@<5 z_7BhI#PzB#)-Wn19B+;H`V-%O<bF~S!_=66Uf1r0$ei&~JbhJOdhMQoPwh861)?1F zBX;~Kb7e3UYsxyZK&t-$-<q{b0$&+wRz6P=yPj=&;kr=G4VHDYUhXfFb6A=(KlIW0 zZ>@oRi@e05R(z^6727ZL;c6Sh*$)g^5BHfXl|-!4KL1KZtnAhMx6?jdOJZ9P)z4t& zdLS>h;cj%~`4yL1vyZ<D^SWZn{AFoF{JWo3hyAlS3O5%qT#foz)$Oo!M*T*ofS2iO zxmw<c{kp*<C#uD$&FQ#xo!LgtV%6|kO|d;utNm8}&=Z@@o)Ko@ki}5F`0l@~2l~|; zZ;LcXRdP?YX)E${-ThsB^7n|MKOSP5B4xT?l5~t$e%gFhd0YG%jw`_eV!!WIrF_cP zR9M57v~kMPU0Y*VGsKg^o~Cd98aq)c)}w4qlCDnn+P=u1-FJD+_lX{ANGsX6(v{=j zw2FI2t0cBsExT2$^Qt~zwZ%rUY290HRA*~W=<T@ty7pexb=6H)b$v^@xUx5XzxJm7 z(u<#u-J&);Wk`xPnU=H3{lbdRo7n2=uB~z~tDYeGX}doAhuLL;+Yda?p8f67e|z5N Xp$oz*<Ej`K7#KWV{an^LB{Ts5Uu;^p literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-32.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-32.png new file mode 100644 index 0000000000000000000000000000000000000000..d6141fda59f14df75e0a1477bf6b4da64986b788 GIT binary patch literal 1939 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 N;OXk;vd$@?2>=fWvHbu5 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-48.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-48.png new file mode 100644 index 0000000000000000000000000000000000000000..e6fa7bb2ab65bbbb14518e25cf698e42a33b1e67 GIT binary patch literal 3250 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIY)RhkE)4%caKYZ?lNlHo zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_6O`DqGHS%+ckI?7<lYGT^vI)?(L28&XKu3 z)%Ja8XnE;o`$-y6O0Jt{`vz_DnCR4VAen{P$3U9D$-<$mO;(|hZRUXnM%5XdIVsaa z#Uv{p`!+Bb_H1mB_?Dn1wC99^^ljw>#vB4aoTHbxd{hq-^ELY&{Qvy!$dK#FUzgAF zNqeNk6&kyK*Xujw=l8z96P@znW(!B?eSWtVhVJ!-$0nT3`f^PA`puu8pYL0?>J<kA zBSWG@Q^%8uhHY!t`i8BSm#h0c+rQqz*jQXZkfDL0A+SV=!QezY2b;m77jipYzjG-S z9P_-sdD*H@3YF~Mbp>}7gjpW)Gk(-nT;TSA@t8%_+O!>e{sf-e_IAChBe$YJn_`m! z!*dx&fuy1$q37p)6KazF)SHBEKFHvB@o<M=2ZNf`F&RfzhlDFLC6g2cBsiKldOkWl z{Ntv!_^;9Q(?2xXD*Sil6-@3pckH^wa|0e`ZSDDw9wf+b%VlP$xj+3t@&_jl9X-3g zqery^LU~mV@y%d4a<1}G&~qEZ-y)Kd(pL)=b{zUvyz|WEqZ9wIe_WeoUYjA}pynxi z*<|CkZC!4Q`4}#g%I<rAy6~K32S=-ups?`dh^VMH62fW@Y8qE+SEetyEpW}$Nukl_ zLH9+Kw5`@m5i|buWzLF--{0)Em@o9|fe4qGGcNZSawtC7CAaFT)U)T}OB|UG2B*KP zJ{a6D{=`DIa!s+ZdW$oU#jTqA$M5c5ow0p(Kq#*hN5(9+)0-Box_I5@d~@#ZAFsmy zvl$y3*XMp<5M+6M>}E29XTs{$+8NttKbYejD5BBDbi8oF`R5NU_)L768A7x!3)Or- zA8`KVe0I6L4|aE|ncqw8T{R(7DkAP5$M4_#iY*qiLX}?Z;!|V^32jV`w5a|j7_)EB z1=e{%2QL2?t?`LoRKlN=lhbN#ecQs~275Bks#Qun&IRWVN-zsL28wKn(hbov-cj?@ zNW^ub{lEX5aXbwD`%0A(4@l?#nO*k$+^OPanNyPYu{$!j1~M!tPFZpN^^Sr>p?=#k zmVfJy{gkMw(NBKK7P9)X@V4Cldj**Gz4v}~<fa-2lSEtV$&>De_WKz^{}vq-X6$k0 zzI{L<p~sLxDQ=3+=_UatM}dZp3t3wY%*#2sxx@E;nd#rRyh>sE>4V#H1q6LAyDSW7 zc)jaFscd_D`-+gP6<4!X-c%D|+Ln6Ig!js8_KFV);%mc%dkmj^R+gAQ<H(Ddflu7s z7Ni$XZ13lcc-6Vuai)iq01IQ%FN0$Vx6Mjo+cTq9gk?uW?3gqwSL!TVSa{yzxclvk zG)_GKtTDCg#M4Egx3}f4Sy#h#{_}!N$q=p3kmczCTGQGamcLu#!0<X_X(QW_63!-u zdCwO{=o~1FiP>e+|MTb1#+z*&1=UZDb?QGE%GH(#ZQi^&AXIeS>qCVyp7u(UX0ExL z_wX?P|BkO;t0SUzb*((bqx9`tgLv~7;n$xVXKk6&u_PqH$8l?v?^Je|mfPFy4=<lz z*Q6aT7N9ltLf-a=7JNCkr@0;3{d!l?_FU=q_I3kbW=D;H)n`kWp1e16dghcs6~4<n z0!>yw<2-T~YcxMSc<NMc&9|FI&z|cm&pvy|#5cuA^3B_~jvR&pH8pv+&18-@X3n~p z(wJCPm8S9I$G#B$8{2plomd+C)#v|-K6B>GvaH#^5@qFUzqp?{e_na=$pa?71?LLC zz6yO(WO-*-DYuKt&-q{4Uhn!~!|%d!deerzdwY{UCil#fbzT_Y#X5h7g+*^)Z|^|^ z9v!_p*CzQHxrYSG-`_d-^YinGr=MQP+nyLHrgNG}aADZ$g)w@IuVx)I;E`xkJg$4X z>Gdulf#))B%cP~HUq{5pHwz^G@A4}#()}`d{vYpz$Z7WG<$>$=H9u)gjl6NYTEQ|u zavF=L%ZU`D6(L$5YUSrWKlr_N$2#u&-%}@fT$)zgm$tdHI=lRVMVVDa<dY2tMSk6V z|L^s`)B69vZ1qw5Y^-$Y(w5##sY}AkGQFx&j3j?L#9se+CD`Bdqq>Vo)!8)R_xpb9 znVFlXK1=L5SQ>lfHM;>1^UfGMy>+_|GAA(CTu^<s@88$`zwd{gHe$WFa*JqL{(Hrc z(5ai{qy?^@WVn#GePO25v}x0Jt>j3^%;a3Z=Mz_1S=p{*Jckd6$J-paxw)Tb#!m}2 z36FqXbGGUDD`&n)sNAr*SUyqWkO9w(Wt#nkYh~7JUSA)7e0Ki6&ZkdP*Q{Uv_}|~( z0!|AebRPYDKHq)y)k$#=<{bWPlasq{dDDBNlnhrllaSjoGH;ZppH}2?o|zbPed^QH zJ(hjV-|toP%h!Bh3<+fwd^j_G-a%o1o5qzZH7oOVE_ZUYUG!tUAMGt49keVoXil+3 zAM^Ro4|eJ4oNjtvS!6buw{gd=Vj1>*?++XF?5m!%x<vnZ^}*BAP8X^^-H@~-Lg(S< z^YSO=$UQIjz09`DLh~ehuBX(~&&n$wq$==#W+*kxC{Q)Ev}}F*RySfA>+Nk1E9F`Q z5_&uh5+x25%G_M3ti<BjV8FAx;kcuZ%ZaqjD?+l|A6SbhYp$){9noxaA^g_Y*FTG& zpF5crKIgJzGRNV7Z{5d^AOHB{aliM0jfz*ZLhZD84(qKK;bQIS>noiVb%ot7X!)A+ zr?c$8w~H(PoXF(0R0tIGUH>v%?;d!0dAXsvdHc$hE7!HV{=KTh@Zj(F`|cYfrrbZ9 zVx%$k^ojl+9!CGb=%R>lre>=&+o>Ub=N~^hdOSdH`fj7v=aoAuKQHs?-?dJ8`stIg z*GmnKZP>Q0t^8gk`_ZJps^tM13!+vk&b}IaP~P<u=cWrAj3yNKtvqF-HC4;!a!YFD zB(B%5Ust?bI{m|&&F2?|Xq8qr6!*=YD|=^0A@izLr*52h{ux}r`h{ADMp;!Pse3F| zUzzAX)jv5gQLydev1iZJa&AxCEXTqaSNk=T(@648kKvTlsgiqktxK+{ktr6}UmT&s z&n|!c#Ni}`NB>X$GP}IhvSqH%#;sdbRktq7l#rI5+$fzs`-jcS>F4HJ_a9xl{=^$& zo<D-sFH|C5{CLNh%(Hp(^aGOa3j+>(`(`H9%T{xLdUvqT+$X1IolOhW5HYZ`>+2SO zZWy}K_3ymo1MVwtw7)LxUTw$!$8s${!-}hEg=r-sx1(oAL`S>sjhnu@SVvsMwQ=2R zP&qsO^a{W8Nl8gUOE0b5H`D*y15LyGjsgb?W9FStz3pu4Q15tuS%_8d>CdV@*V7Vb zsqMS*TW0re>x}KQ85)vS_H=&awlKD8i_z=P3G?T@p}=81Uy^~*vE=}7q9vQ0r1fX{ zopb)RUkUeFJMAm~y66AfSf);%vwX`n_OILRQkc7bef_h+Rg1^@NeS=rQ>i~38P0z` zxRdD^2Rl2j#Ild~_y7O9Hvj*Zr{dq*y*ekCb+UiW4bomIk}J&5;>fW1?vX34-Uhki zy>8MhY_Zoj?EY;T_Wn-w>tpr%K0msh|2ONugr;N1|F+W3%fHMrr-sbh)@fzS)Ax2k zu0+Nb9*Jelt`AQ<FU`rjcl_A1Z?Bi#-1F^Q^!}_zJH9h_iYqQ~d|)l|f>l0vC4bGI zxgsAX+H`O!od0?uXXf0mD#FeG-yXhuSAOpY|NFViPS)L%7EsUKvTU1On}~9AP^wOU zvq8%;O$o`18~6TYke+>ez5SiLfA4M0w$Dg7vFuxB`L9n0oU3Ln$kgIew#><Smr$Gh zdTn#_-t6n^{x0ZD-e$)o*!N!O|58Tliit{<b@_ko-rc>u=W9Xr<D=s7KmKie@Z08q z<>SEbi=<CH2$`u<(f5R1byxb7h&d_|ktRn!H5~jJ6S1wiD*1Kp;=TV5_5b^Mw02HG z@Z>M$vELb=pW!>`ZBy59Mc-$is$x>Y8ui~LQTJI~{)Y=37Ipt0=iuYAc4CV=lYPtc zT?W11_TS(0^S1o{pR->dk+61F$#$N2<M27Xr02)DXCAXWAGf#4^z-ZMasQ78ZT!<~ zu%xHqfeMSVk{XNQga!ozrB{1&dit(Z^1134N?PsdtFvR$l{o6@#3mwmuuSIZySrC= z>;E1-A0HjLGgr;wcmgwn90Pj;>w!0Ob~F6B!O&_rO>vQr+3J<&ZNJYAo+q9bdp%6{ zdhnqG;fD>LBx=l1SlD6kWTM~|L%#w~wIm(8V{=WC=6)}p`22;&>q17pnolQ>`#s)p z@#lY0PP=)f`#;}&%TR8`R>|OWO_q6%YL)dG;U_vDXR0#Q2h47q+pw+V%npwylO3zC d=l|inGkL+X;#DgyF)%PNc)I$ztaD0e0swDp4gUZD literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-64.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-64.png new file mode 100644 index 0000000000000000000000000000000000000000..1338292e26222f1d5e3922b28f602d578af83b8b GIT binary patch literal 4741 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 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-72.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-72.png new file mode 100644 index 0000000000000000000000000000000000000000..4f15cadc6ff5c20e989840de725c1d12091e07eb GIT binary patch literal 5183 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84mJh`hS0a0-5D4d*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l<H3=9nHC7!;n><`#QL`96e<Gr>pFbJRVba4!+xOFzhvq$E7 z<-a)Hy&>Oy)*HW>*D5nvNjXrZ{d9-Wgl0u;E}o~4PO#-@c&J!z>rr5L-^j$+$nVGS zdXck$3fm$L#zY4Nk>)1egW_tIt`kzS8zmz|Tmvr!7~5u_|MKPD)_wNv@4|!c#$G)g z&=Kk28+zR+_WkSs|NngzJ>1aRF(dV6&x!wgbeK1-xnO+WM%nh;jb!(toiW=kW_$?H zU^yny!QsMk&XVCkp$r4VbD34EoUWI~?5~@vtFIr;Fl~PIyZo%5f7aZeyygF<b9v7z zRSJ6;dyZ%poi}DU^Y`$<i~I8a+Z(wYQcUUz6kzkZz<K&nSMF{8f^+E|2mY*2T5(nD z+F^eCKTA`LekN_c86tDMF*P#wLBYA*-z1Ya=m<Y8lH3@<v)q5~u{(GEoWJz)%X!P^ z-~REd>}z#cW4waTfrlY|pTRDMN;zSLBE~eA+zk@S%-43Mek>0S-SpS}g}$X=?Dl=n zEzaF%n(=M2!ZC)!1{*hR>hW6IbN6m+#57jdz=YdoGRGMO4;IQ4oV$D8rKP~4r)X!_ z%$brl9}c)a`E2~ef^T!K^u3FMhWrj!7&}TCZpm+`w!Q6M8o%h<dPPO^-wch7xvN)w za<Y;8DSm8*p^(F}OxrApYpLfnUw3OIUnrH`o?qX#cCGJ=D(<ymr&-+@lpHeoWiFJ; zcDgV<En+P$ZZG`zM`+Vc9u?15(^=l1u(@=_H00IZ_6x-lN2>b+w@(mMSbfP)N371N z+vVTt@->s>8RmQzb_r-cV93MVdWg$wsqBj?ZU%u)mje>bDxMQmI5+7yPfIPI-(g^F z-R-rs>F3YG5m8Z2X0v5^WqKI+PWX#jxje~>*;rt2%=A2FtIzWzCuCe$q*N?h4{>!J z)s<*tbPbHT`Y}#@+p<ij%O+c*bWLXaJ}KI~qacxUrq8v#>*VJwNM=5;P|jfGd!_KB z-}LWacYgG5nMT8szmc)mOV_Lyx0<wg_a&u}Q`)>vTaz|QSd=G7um!E;IX6Gv$!e|~ z17EG;g9{xG0@AneEZDdvVt?r74co#TR)(~!d(F}6q$l9Q5*nHq8Ft<FVFAaq)Wtz7 zAAfkrI>&BrM~If`uBgBE4Z;iNG+VAH-<T_{t-U=^C(r46>55giwlT=YtIO~m-j=&! z)vKaD6;A_S?x|k9#>Sg1YJa_n<?P$hzj-6WBE15x*3dWa#CBV_O-nU6wqTVO&*6q; znF>lLHu0RZ6zXJQP?&x8(V3a9OM*`B7L(yJdfu$sad4B4j(&a9*|VlEb}`0Y7vyV? zy(;uaxYH%!wwZ`)qt5A$qe&VfTme@F&duHZK-*HHal%Jd!J-9gKQor9@J&+blyqa> zp0A&ro-VyD_wcsdj9F~Ax9z^-?a{MH<G~!~Nh%`Uu1hXQ&ao<Gds)IZ=}3+42ZfU# z<-h-!#O(jWvDv58!RD#9<JPFNF_Ay#h<8V4n9bJHyuI!1tm5}!9_=cg8dJSy%<*}6 zWu@_qS+jO&A6&TZH;2y;@5NphcE8{ES@l@NmZ-Ik(^RiJi(OJWprB-M?9Pv@^mO)F zW{tn!+xO;}Rfo?kiuZi^hhHk;<S&M*zhAHKS37-a`{}1eEye6T2YJm{4dOQC)u-LK zbLYsD6ee~5d;Kd{?mogPbHegU>N9sXjk$jMX0y-EF_#j*^qAG{L*KgBE3Qjl*fQg^ zW%=qSKbo8yR0NkWbWHi@a5cp#s-<0~)8$UgxjH?Mwso&HrhlK{lUDfZ%FDj3yM7-~ zl;8U5kyBFMvjh!K?JlNc5;DgN=k9k~{7_C<ljoR3)>f%WCwctlS`~*|H!>f5`KEtO zlF|>6Bhl-x3-%w6<-fHp*F|X}-}5C_4|?9Q2&osmJ7B`Qr9{^HXl_n!?!vhBj@P3T ze4H#~+;qf_fBB-AvV55!r~87o5RXYb*9^_eSB3q(*(4b16QOy|(&uu^s#O_Vqc~cf z6qGc&nu1ogq!{%cJzAV>*|;n*p}3g2^^n%J>)d+VTb&e7Y<lz0V%I&VwP8nJy?UO| zqbS3tJb(VtGiPL$EiVqZvTaKL8<whb6aD9PZcHeT{CHBWOm#PlN8e+I(9kJf)gEe> zEo!R-UzXewpL4H$gAVu7AkO`N-<pba-|lcxnlW#lo6<xF6(L6rk*3s017G23sRuXd zNVGM&EoO|pzVNk`-QO=_cE8^UpE)moaAS3!^5k<)fmbHZIZ!+QtUjkZ^Vu2eSvs#4 z%ADB5bJ=8~*U}B!x3}kf6A%+W|Ld1!9e-fH$%ltu`3qiObN5gYvZ(uGVKkFR#I^CU zg`U8&%tI!=H@3ZHQsrq@@tkY3t@XTp{IPG})WpR49{uR7Ug(;9utkP}kGEUD&2z%_ zJ;mn?jg6a~7B<w>@pX4UpK!7#?bPb6T6KR{Z{M(SV_$14tBiFSj|<D`O&7AZ#;n|! zyW78M&+kWjBBIxdmBhMljXL`1CijdPGh}8?*ljeq>6i-t@?Ne9nX~fFrI&ASS-H~L zqWs+-fr4+-y|y-$*ZnmsE9=_ve&25cdwc%F8SHzMCh9LaW?f;Quty<kT5HtWHn+tO zy>84r`gz8j88W>SR!Ymn7s<0tNR7;EiCcf&(7c?*+nZlus#mXy<yW`8+;e=?n$tGF z+{NDOcG&m2RNF;%AAO6x^L;P7?2T(*_xc@w)xSfo8BsYo1s!Z5`hWf?Ok`l@xSA#E z-hZ!o>C$fH-<Hc#A0BJJzH#HmzQykSN^|{oHyim1fByV=N5Mm;MHe-^mI~>dZd&)+ zz*o2}amVU)uMKz(+v=T&x?Z~dUD)cY8+PyRK6|#b=Fdmoh-H#*o|iN_-_<hna|`_A zHPuV>`<>!N$*0^u7h4FHb=}@k_}J>%vu6gz#*g_z*7O)IxtwY9<3V%B@y8ldz4RFB z6e`0H8|<>FIwsL|RO#EdZyUlQ95eH`D+e}PF4y0^vHw9q!=sNDp<1Cbc6%C()6Xf+ zl)1~f#YfmYucqVVNzXIq&o922v#VJw+VDVy4JhI)`W|z)Ot4^_mN{!(|F!7Mo40Nq zdiPGx?)MilpUX0lYj??<3sik^Yio9o;e#_XH+w91FK6l$zIOfkhMhb6a?D;&+90uD zgH0SKD843ms06IPzFWm@O2@3TYEf&aMOS=T%$)e|^i!j2*TOdL+$i?q`;R8$8@#XB zU+n#UFSq5e;Qg-j@6&^(n^^WWU%h%>kgL_e(D0%vZ=C68uf|0hGiJ;vh~*00^ljRz zy@iTQe`E#4w?*mZ<g6)u_x5Xb(?LlUm)fv3A-8^~>P@enV))HNzoFTvv29^;a<c5n z6d{Hf`{UbkuLpHDR<p0nl#;7@p_no2S<~a(A_E>~X=&*n51RQmY~RlA8d!7n%qATZ zDPEE8+XlYE7gxye@Rg?b+_E>9woEL0d^NoO8~5o=@+yl?1}-kTo3Yy8M5<KvSoXHo z^fyx#9_G)rtrq)PQ>P$)XICkA__~;bZ*Om}wXHZ;scw?F@MeyJP$%CLnNNi}?&my> z)dUaxu$Xz^>(-{(XT&RW>_2^FQaNp;HI-{!+}^Ied-pan$lBM*_}hLJnRGHG-a_$^ z;KFmiRaZG?xi+$;YdFkhPq=G-`9RINZ{NgJJezWF$IekcAlPy@yHg^$;`iI_g)c9u z#_TRzo80<mdW~JbyL_!kTjCEU6`i}ssuFn$I9RxRX0V#-_so5>>guYz;%0->_dXmj z@ih;A;J(c~@6Lf&ue6GG%51u+leAGn=J>(A-|ux7KR>sV?eRo+xr5s4_dF8X5q`HN zJa*-jX3OR3?k$C$RvHJSrg5&h{yOj73GS|rbFL>>h%4kT4ZB{dqp!c)Ci%9R-LDtQ zDMpeyIyzrOxGo%bRSJK%wc@ZvdW4OA(C0%MnL(bD9$5IrGra0uy65V|16AvmZx1}u z>bT(Bs)+bi$qiAJ$4$dR7jG9Z%CfYO>RlAJnw4#yqn6l#`efa>`n7i_DgPGdsXh4e z1OLSzLMDDki<6Rsu4Y~3Rr?zjzy8i+L3Y*MO)6`rH@oZ)QkwW8bMs<{0+;=dZ@kri zJgs^8vSq&yaj$0;R$zHL|IdL0#*;~mJts`MLZ58={PU*bgMjeeA8!10tea!JCd|&* z`u(y%*E^}Ro&`*0x|QfP;mX%l7ZmPwxhQe8ANELJc9+Apqj;B>`<&kEcMB)%3bhb% zvydrUJ8^!<7T<!UuV)xMoR<34vnTiVjQR83^`^hp%~O)<{pLA&Stdv8p?5nSv#z>6 z66%fn_OketyUwfDLy1p{DnqnH%MZkv3fgv?=ifULzVDOh_xgDjg-qPs-21fLTsT^t zVn1G&&fjzK>O(cL(9ngMQk!mSuE@$LDR4L#yLxf)g%4fJbxzNiIn&)j#?40VeOCWR zzJJfo&aU|X_xtV+Q=v`;sormEXDusj4Az@o`1Mujk))05{Gu2-jwW%mAD&@n!t6PX zy_m1^z)O}@$;>8smNOa?)6&G;`*aj$`uuXc%Wxsbtnle6(K~x8i*<tIw5EQz<gLFj zYVEg1<<mxwK3Wv*lzdWToEIZ^!t%pw2kQlkE&SHT1cu5)T6DdAtGgvicg_0s-v!sa zaHxLRD*oZe<Nn8wj&?gA{P|?E|A$-I>kpdvidU?Em1ZQlBuG;x&f?WP`Bh=D4=&D7 zsXg#d=9t8?<wY~*#~1k4t_*pl93L9GaFv!`{Jx&k)Ag0*`pXNu%C#@4#jX4@&tGlw zj5%{$^z6GsHpNa_WcN{$hfh{T%sOLw>#9{2AFIDFiqhRJdB|(&CGilZw|o)#i(TG7 zaE;w(-t*y;pVrhvSFV`!$yzgph8~|L!{m_Tqy5Zmnd}C21G(7;9^F(mP}{lk>fE_= z_bCK3D9h}-cj4e|om;EJI$fAnhH$kPRnD=gWSb@SzVDhl%ki_Xdt|fjB-Z;G%{=tz z=4XXRdF<{RBTAItF6g+!J#D_a?rEb*Csl+xSvK9w1nHYR`DB8V921{lqj3A<U_;4Q z8uv2&6OxQNKa0y9SsN{%xOM}R@8tl!>4EF7-+eT{WB#8IpB)^NR1`(H950(3d0gn@ z=ci<E+@E~h&*5?sdysaaueI$V!64Bx2EIeG-&u}57Pct;ue5A=vBsLZzNu4h=ef79 zn9aFi4+Cd_)4~a_#ao>e&zzGz_U4U^(MwGY_9rrhyp}ANbJC?BZ9HKyIr2r{ftPPN zLqbG<{?Rd-nfc`N<{LL64qv<G7Or{XcJo<@;@#guLtRyb9Lrt_6zz=k@s)2<(w57< zdf3wC-}#%jjh_hkoOW7#(Zz>*`s1iCZw)P9bid_fk#JHJ-CW#0@$~saCO#5w+_R&l zZ`{>g6u5rp&SxGfO%XbMK51MxSI*J)^qJ&d#$@??!M8o$=Zgc>j}<3KioH6^wru&N zuN5}m{#+^fb?cx}SA4+gwbtin7@pYFv_ef;s8?yCPyf^e`DZLej{-zFJ43i8ok@u7 zn|(5;zWjH@-g^ye7VWFq{o(NOeZQXmm$1I;$n5iT?l+dcC_cF<oNtPmF0Na5swcTl zuRX(r<#b8F`t1R?Rxb?7J+R5L<nW<WudM1m?>C>7zwhU{`}W_i>i>US|L^<koINd) zi9hbJOEH|=Fz-O%se{f&jZeNlxH#|E&aGSfDq@<~MX$ITonaDfpv%2#RYH*w-!YEt z?Aa0bZ>$T`-hKc7>HR-8i|haW!u9*G{g1ze&(C&i{#xwIb5Nk*gt@Wh3K<&>;T!*L z65cB82wQ*HB-dk7Us`Z`V$|H1k%0y876jZl^6OYwX8HCXcgp|&@z<+;+rR(lyt+@) zCS@DEY;TpQ``-Gu%4Cm>O_17!<WoBzAK0d|rG(imJ-0xGb7RC3=5uV<%(rP>_4>K< zeckU)Z-x)u^Y>eAaazEm{9>beThGOBWy@RM_-^ZnY~GdEG?^tbOrXEq({E|iN}KxE z&%Vy@3pJE9oyGrT&V5shc|Uzviqh?lJr10pZ8>{s*3v(3*3OpS^XIYr|Bim<3F}!5 zUrl?xQLXJuLErz651&5xILF}~)AAKsYHB7H+j_5VdlFJ!-n}lWH@5h*^$EvD**ENu zWqPhQ8D*($kiPRR?DC-peZ5!Bo|}|+&9=ADuuxug<L=6^Cy(E5|E;3CW=&pFZtd#8 zo45a7G`|1ya{cdL*XJj&vuboKk$j<7#Ps*T%b#ku3lA{l<i1Z>ACn)Zy>x%|ua)`x zI2hLNEt|P7fHUdCp28R9Z&=utdw%aPJa;k2MDeti)Y-7DJGTCNbJ@S{^U?iP-+rzu zpCyx$X#1OmVQSXn=v{gLjuck>dZ+wkUW&N-jJD3LTSIU3Mc%u0@884k`#wL}uidrk z#<r{0x~4CT-Rmc_SBCa5PH1)6`}NuXJ*Dsd9Nn(k`8V+QR(Y1BHMZ+laSNEW)c=xJ zYhH4qwsO*P?s-*v^I!ebJ11*#r!h}L;n7ro`=3AL>pon+U(bBtBcDamQ#pYn`*;t$ z%+Wm`HmCfE@IvN3CI?mlwgdmZKArwPGHTb4UJnba*A_8lCnk0jAG0)6I@jUxXb!`p z9*zeL%!0~F&sIiks=fL!E;K1Nk0)?`x!2xLzK?8me>W7hA1;*H7Oofle((3y_y0VZ z|No_JZ2re{bN4n>u;-PB2{$l0e9!UaPvHApFp<e2v1g@=2vfsScD*BhGWLSTzV3#) z&f9dn%VgZka@?QYnP{Qjx#O^fpmLMT5mmn<3RN=Jz4a;$uS>Uk?3vo8xS{L%!o&Z* zHEr9j=E>9RHg{e8{=BMtTixs!CbG*c-y9|Dz!JbDQz-lXslqiz@3bDzxsIG)g`e-a zeeY`Z9c|_yhWETy&)&y;-Iep<i1gh%PPPsU*>xCagm0>NZ~KO!MQ+Pfz6_aTk;e?L i@42qcd4n(JAAgm&muHE6Xg31`1B0ilpUXO@geCwl+_!K5 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-80.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-80.png new file mode 100644 index 0000000000000000000000000000000000000000..8b22b3dfc8ca68f9b02e563a4af632a5e1bb1aaa GIT binary patch literal 5578 zcmeAS@N?(olHy`uVBq!ia0y~yU<d$V4mJh`hCabhYZ(|A*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l<H3=9nHC7!;n><`#QL`C?8&DoYRFo=eGx;TbZ+`2laHb*A2 z_W1tud-}iYmYwUHd$wEncwfBGeeWcmEv=KQw1o6^JytLVPT4I}AJlnif7DlgmR(Ar ztGY6@7l?Lv*;<5_3+aCFXLX&jYVRG!-W}YYIqKVzkAF+s8&_SPUKTfhdHox^$sgam zS>0T9??q?&apTu>&;Pquzdy3+_zwvwDW&+oufpBk+}-CJ@HBTW;&2UoP$|cuxL`_? zAsfSoT6u;863y>_pGYy<QT|>o@BTj5u&}VzOE2B~`@Z&leD0pZ@=NYAPWd%?-Rtky z*IR#6<Y;)j<6rr{&!2uAIdUYP*&^+>SxO||fva)d?`}W)5E9y&8oF`k&d#8fJb?m* zRV81#<$oMt{}(HgD)m1+JHKP%FSYI%flLb{3$-7nl_f^34iP^pm_jFZPg1$}j#DAo zr	ws^>rJ*)|0CG@4r9`)spj&7I__UXeF$-fT>boRBIxCFsXK{$mjg`g>xoyQ)og z+!^DzG%2B~O3K(+e8NeYmEN!Z^{@T5rtBgMPtO(oX;(cy)ppd}*jihvlOzBB0qc*- z7Q=uBL8lIf6Sb}r_pf<<%3jcEO&_DtF_z;W*ID%aK0n21;Z^m3=IuQ-cI`zwSx#?K z=z3)5(WG#o^e)4a*Si#294?#O*v5N#&7((0)eTKedrPc%9R-r^U9PKiQoc8%b`HPu zmz-k}%4c>>X<HgKh2_@u<u$APum5WmxU_(GV?oXR>6%OB-?7|#^@_{P{Cc3)TSpOB zjwS(y1yk52xCHX#%wLwdB5bvQt6*DVQrS08AK$Yk?bnO4=TF*p=d{wK!ydWKE+O*` z);((QuRZwrrMt}W$;oHKg&SH7)wVZp-?_7~rcRsT`7XU*zkVH%Xjb3|5MiB`syNk) z^|y&{bE3qI`SXwe`o(<aoLy-9^en#qqYVj$Ga`z2i*IEOY)qTTeE4~A|EYDak7ljD zazvM5f$qb#Yu6@aXEV=x-e}DeIql&ty_Cqd_rE{X%I{itxNy#c@?Gnk1z7fcJSIJ3 z_Uz+Vu9yU^Ec@^w;^9K>ipGRV&z9_}D%fh;W0Z57ZK+8E6T>2nCIKc#0j8}}RXBSL zmt4-=v97sjXA`%0TJ-Hbwf{FB*Q#9OT%mJJr$nQ1t;%~lt*N$e-oAC5mdeq@a7@A_ zkR_RClFGr)dwwks<4#P8Y+JU{phwH+^06mROw`nuTTR&W-+pWAS>D2Yp%V|^zIn27 z_ik?A%dZR<2vnGchCZy6Q}N_F@M;&|?QIV&^mq;*xw$#nqW+)2!N)<j_i}A!YY)`0 zVOc6NN#$T^Y{o1x3--uqiMPv|g$;R_A3uJqA;KjgE4y~}+lNipo4Gb0SYAD^`|8!y zH*azd7s^=lHSgPJ$I--aypZYYQ^oGryBejmyO<a%{(ik~XlmMg@?`mj!w%9FYuKe< zRO*F=y;~TebD&TrWOb;DP^SX#<E&GmGjBV+tMy(g#MR1lZvK3bmoB~JVQG}HJDGg; zuBVPeph!Zb*|h2JlhV_rmj-b%EXb6qxzBIJ>p96mlT}frKw^=`HRcrNqmT6b=2{7J zh2H!5A;MYfaMZIs8_gyNG~U}+d)usstE{}7+4O0*HP`KJ%%x9U8IKn}*rmtSDm%xb zaF_Ge+UFi}QMT_z9)$1vWL|vxHh=y9hlgcs&Nrt<28gh(d(9@4J}d1(Y2}Y33AT{X z!_S_Hxw&ncF>9999zJCe<4t0jt~v*9-n@C;#Me7|8mp6sLT=2ZLqZ-LoKu&Y=;-M^ zI?@?DW8OT=BYeu|eos1=yI@)7ajsUTlt@069_#iEr?{E~Znzva2v{j$QClVQ{Jg(h zR{KGx`J9SdFG*%Ta!FF1@O;~zX~&JZ;}$*oy~B4+h?d4wF9|8BV`t7Bt*R=ska5$l zIB3~pV%oluAvKck%B9aXCpPh1Hev3FY1KX5q~fVDU0Xu#oLp7=MF#y8=1B8Klko6k z5*&&vLbN>8CU3}<o}}V9Nd*)ulRb6x^pbLN79C#YrM%}B`_qG)R&B_U=D&75K<C{7 z6W<$UXW6v7xMJTQp7^gWWwZLiRaz#Y%n!F7KfY$$#;LBCTxCxD7Tcq8v{AE*>HhbH z8eA@cM^2sMQd3)2@b6EGKi}erI|rXVW6sH0lU!VEyyD4;O)?=x8cPj)nU`LYle399 ze0F<1YoLgsg+<H5i8^ily$?=I{CE73!J-dYW*gc=`c~_2ublVXQN-1vuerpE_jqA| z&bvovW)_#WPkiU~Y0awE=<WB8J$fV?F|D=4syBGKpW;+6$Ln7eQi@{5=RIe=-IjP@ zlTO66)|fo**|ViDn8-dq=bP~6jz!7Z_ZvOs>d)PJQ4rh^*sj>6^y}BJgjs6ezMYbg zfA5<+{r%me>+AoT746hb*&ONP@2@=3!y#~r0>^e8ZLx$M9tS0wU+*&TW!|2*w|DE- zP#@p355ByJI4<e7=4|rEllm{&5^lWy^r$gW;>GLBfjVM_mX?=GJx?vSyI1qrD)0V3 zQ-%jO)93Tf3H9|o%Mc+EC8Biy8DAgs@+psgef{sYa6|9%dAoRo>f&V&b!P`$4Vh88 z{LmteL!b93T+^QFHDmVd!~XTZ=I<!UT=y<_vh3mF81el-ewN<ZSIhnP@862^)%Tc> z7cy}?d}`sRbHrlDzW406pB70nGc!j-?BKor`NJ9G;t47{)wl8*r+=Mi_*QMgGocr| z^d_BDDTzIPzV2K6jM>%U?H`=hKmA?tx%dCUySu$D>i$?PTed9Wwpq>n_SDG6?fr+k zChyU3*mt&m-P*N@1qA}DwZAhQD|mTnw?aqI`42l}6?ChVd{0O2c^SJlEOAz-&EGFe z=RMjpfB#P_F>!GN8=Ib&FJCUuKXZF_TjGKM4TsRH)3l`7_5aNA{_^XW)ux*|)26?7 z5@3nh_2=EZhoz5NXNN5j?qpdNDtvWSYq$9PBVWF>u6uZQ_wJ6?R%?%?K{MveX?g#< zadCkRyR)-1D1gqK;VIfFqp@^D?(Ta`D@5~umfiXHf$RLOoV>h4-)nXB?;kz&DagD1 zB1gaNr+vR4{m~1uyk5$Da(6-EwtEa-Wvu5{gcN*!7F{62{`vFg54H93JbH$!RwdQc z$h@z8U%heLHn!`fx1FAx-lSvECwye>o{3L?+lh&ZDV#QXcV}m7*3~yB5|3=+(kN#P zxL*3?^X<}#y{Qu3-rOgif1WXazWdfF-h={99&cU-A0Housa~wd+Z<cZb2~r%S+V)} zqen_Umk(UJ+q&+G*_SoBrfOmjK7BIkleKP}IrHW^8Mgc38aa=zt(`3)Bf}&0>d&K{ zPYM_w9xpy`+g@Y0pFc!I&E8z6G(+h9%w?THD_g3|#RD~->~24P>-nLL4EL*E>-O~a zZuVLBdztxC5ozhy8*-(cot+zB9b2o>5~9WGdpSUBYQV~nrc?>GQ%{XfJuMQ@Kjs-% zBA?7-ZEal;E537WZ&lI-**#^sJJWW4*e>F_ORY<3@svq<tF-1lPpYlu?Q~&UwTewk zLi~Aa=#1Oz8yg!Jn4T&w+-I@CX<@_f@9z(9%U!WbYJO(Rk_+2%pM18QBR#)KXR=t( zj~As8E)#dJI#%<-S^nSToNq@m&wsAletXiC)W{n*H4_R71bQ~IH9S<X;F@6A=A+i! zU-xCPuIlN}HX_}wdp@1g-ck8kEoJ9VnJJGgG^U@{nEEXtQZ2>u?v=z186O)rF5~?= zP3zCT@3ybMd{r%zNRGY!S!Xxbu?T~iqR)MI3U_W!j*gDLar-v+skBKdtj*pvFL(7l zKHmTM^Zfrd_x}I;-n@nLS_R{xEUCFGbLT{-ZTWt&FRx0XeEPH|Nk=||oIi&jy3gUV zulXTx|KGRm8+Y!!dGW-_soLQNMn;#etmJ5F$lA(v{_}>Zx8HEx`}ff2lJtSR<x8}t z8nHNX9J}|Z>3*0H2ZMS3y(8E6eKY;@<G8(>-TeHY|3X5KvfKY~+_6sj^1QWQSO0u3 z_iWp)w&S--%x5nv>iHVhra#?Vq2<7)HPcpx&*6O{s^B?k!^Vw`;qkSi_P;LnU$}q2 zzr;$`<H~WnZ-zX4d~DBS_SIOvh%L@2d}?`k#g;OO-%%_RLzYax*cVqNu`H8K_k7sN zdgF$N`*;{cx?M%OkGiLBtP=llR6IT>AXKlkaMB6(d(oCWM?4aD-<GhN$Ny#vqY8&p zfI&uhXy&p*7rXmEA3EFD8~m{C#|Gxv;$Lz+_<fgV{JHD<-6u43#xlt(uV$?*jFGva zxB100!O+xqH+D6NtKIOm{h%PsyfQ?pEs<qoe9YaxbfLEozA*JWcQ;S>P}$WIp1o$f z)Vm3@oL+CtpY5U6%#<mSbfdB$`94pSdO@^j?3b-SWrLfY59`Xh-S}+wWntv>M4$D( z?>BDzsKDF(Y~R~e>T{jCs(Bp+q;llgSH8AV@jUe4LV<U?;Ozg~W|~Zonsq=<jV&a! zdEILRUt^7%=FH-yN-<}D)T*C-AZuH-<ze~5opU#c2stfSr4<tDYBcecNABTsZ7h?H zS8Z;*+ZUa&s;$JT*=wm##pAS9yEbmT7{I!}>Z_KSnc1vWJDyk0m_0jNgTcB_SVZg) zgQ};K(M%Vmi9Bw7j~^(q#hq<8_)u@lkP>@-RmY^3iH{>ixNaXjpvrzszw7o6>+*L8 z40>i2u8*%Uo9${g`>og7`0BT&8MCe>{QvQ{qH9q?!3PPKz*iBUe$-uBHcP=q@8siS z;Q}l@Jn4$3jeh<9{r%>W4aEvA3EA1)_y7O<&YvPaecH4S&t~U4g<h@AIvuVydF9Rh z$;w;f3Y!mi97yheP@2GW`^h0&KTlcPUo6Me)h3=yS@62*v%Mb2kCXm&P4a&pu-hJt zj)*v5zW?Xh+BBuC-$I=%e*Jxk2@g8A@jVtax%T82e-iU<8AEAV+wQAVt1Zgk$>iPH zana<cQp<y<)8mizN}GQ;#I5fTI3<EzXqv$F(!#&LO8<Olw{KbZdgDg<WS-^z^V^oa zl#rEu?Uj3Yx_3ojgRIJgcZqAvrurloePXKn*!_COj2Q<mT?#UYmt{3FFle}XHMHhk z@%+OF4mc#dS)5Semo34jvDC3qk;`AiHDz;T#k_vw^y3R(*Uq)<;n*{)K;{aY+=F9# zHx>(9l)f@aiF7lYeYZT`(`3%XR7q!NXGhJRcKeGT`Hx9VI;mnb)5rP2nf#(BXV<*x zoNW5C+e7ZP#64wwpIx61y$Mu*&@g%7biK39L2X~Z{$^<C?d@&*q5o#?<5gOP3udJ% zG&eVUEDe&7v+L^?pPxA^@r{1^(ZkHI%;U>#?_Fb9ZeEelprXCQ=KH%{=5dY7G9#j* zx>~Pr`EBm<{*iWESl!QIl8TJAS<_8*{p-v2?<$Z8toS2xN+Kg%wYKfJ)nuL1Tzaup z;(l|bT34-_F?X)3(afH$g@K)2>vnwcy=>Cc-|sw4bdF7>l)tU5c;ofnv$ypZI)+cU zbZ>8gb^FDW5o=YnXVibsPcC>NA@=Uhp&K_OzJ2?)VfSwC%O=gg5@M{xLW}L#*1Wa~ zS}8K=q)JHW;hP&f*=Aqgc-Y~wgW6Y3nd_~KJY2p$eaE<o*-I|T?&{|BbBP~171yj? zo0R#IPpFe6=XP77#EP)hj!|M~<0}3)H$Pr{VAGlhXKogsNb^^m=<!Wt)e%p*ubMf> z8*{BaRJX3Ww|tWx<0j_A*=Aw0eU=(pmI@wk>wlb>CmbCu{dM13(H^mO>6a}NYVUI_ z(%`x@QyJ7GzHouV$o*IJv?W%h5;gYIZ7#2Ix-JqQIwjor<&w+J{!h%S<=yK4GxW(= zHW{a@Cq%2|96$E=s;ER7tJ_kh<16pREz{5v>Qn)x<2hE$k&_m#lydQ8>9`Qu=BT+= z-dXAW?r-~_|0<9b7rlHy*xguXs@I>NCnq2Ne=XI&<tX>#{3muijGKHEPG_n}7#JAb z2)@5<?yVp<#ZD%+KAA=h&JQ;7Q;ZD4W=|JnVNw#<(HEZGqj{uu!^GdW?^nOu{%_Ut zWkJX9=QTFp_u3GjP~Cq0Okzk6m(fbQIX0F&=RYUTGFuhO9(ua%`Og3m$<7wO`-LA8 zl-s|uTfPx1|M$@N`no@v*7v2PkGbr>uzKl}jpb~6(j?nbqbp|IcB!wI>U(_9KzBwc z_q4YSL2R+%6TQAR{*sf+d9q>ZZMV$)=S$DW?EZAquSs}etnBNJ_ja_OJC^l*`qW23 z$Njv64&1vZ$FrE}e)<L-_N%LUR~=oREUF@8Wpn-LuV$HTMU`*fJlS9J_NR9IjJbP# ztQaL$AL`#X<@LvVJFZViEjLuXn#r&H#z2BO=X6`H^#)n-DZ2zKq+*UA{MD?R^K{$B z;1eLt{xyOP-&e=~m9h0!=q*)XFPGTQxiwez`N#cF+g19V0!6-jwQbtIbL#hRW##|= z-TFNLM|b+XdEYM{b$*fZJh4dP_ahsproY?XUdx{{Wuixh;?}!vnd^V_uAcwzhdlEE z{r}&stPKuq5VrK;TO4UPWnmc4iqI97dvtT2$|WuT@L?%PUHZj;*_*7`Jqjdb-F(^v zQ&d!#`mKB3>c>|96OONV_Oh}`PTuKrmV$K%pYk?|Q{_Sq*QZW6eB!#yv8&qgEDuUo zZ<}jAwP1#f+vYplzTMd<yT<;6?1{t@^O+YS?pLN;_o%6A-e1m;dgIj7iTCf>PB{D` zrZxMM{|&_=i4GnW+gV<wie5&iM44OP-ienxl32oHF!90+g|51{hxj`WcdULMX%=(c zF<z-Lsqpz}{e2%!PFL}q_IlRwdM}0}7ldAQx73xfOqj1x`$SnjX<O*u{x-d+_U$Lj zYlOYqmDBEC`Fv>V6R)EUI^G*}AHTQz*<SylpMlNhyeyLeOM<`wVTQhI*BKg=zfNVF zz;uD#fqlW%#&*T)Q7aTHlKv{BCME{j7AB=s7G=HKq2VRWu#n}0V~0M+5%vy$jv6PA zjH~mQoob~;1T+*|u5EMVto$;EL8Rr%H^wOfLDg})Eo<&?{B!44of9XADih~9;aA-Z zB0Yikzpr>)wIxP#=FdOBzW;j>JpYfFVhV#py=*`|N0{0ciybbq^Vl3X4+=kWN^3cB ziXp|R<CMf?h7IrceY$&JQp(2ck;T3z!Qmfl;y<j^5123HRXZ`zOCz<#&`Tn90Rv+) zQ|gv9_t)|E=QNl--dZM`$m9rx&3>k~V5^mS>L0u39l5Fr{a$|_9!q}FFzanHQ{#`* z()(+E^nyBi4pV;3HM;m-c=5vA{rmR(@?XC1&96<b&fop@iSdVuh;?`1gwo#!l=cfd z3rVNzFS?zvQMUK4HD^ZT=Tq<ZMK1X8yRV7+e-G0V#R+j7Go)`F?@ufgcbe6}>8z+& zsFo`hbs(Fqr$lJlw#E}luawuEbYjU6DXFNA-@Ej-NZXbPQlBK^Hcw}N{juVj)~mN) dj{f6+vhM$_qkl{{GcYhPc)I$ztaD0e0stZul1Tsn literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton-96.png b/mod/bigbluebuttonbn/pix/i/bigbluebutton-96.png new file mode 100644 index 0000000000000000000000000000000000000000..df2f650e2b4d090e7b694be308861e6e730f876a GIT binary patch literal 6229 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 literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/bigbluebutton.svg b/mod/bigbluebuttonbn/pix/i/bigbluebutton.svg new file mode 100644 index 0000000..9a1289c --- /dev/null +++ b/mod/bigbluebuttonbn/pix/i/bigbluebutton.svg @@ -0,0 +1,28 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata> +<json> +<![CDATA[ +{ + "fontFamily": "bigbluebutton", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "bigbluebutton", + "psName": "bigbluebutton", + "subFamily": "Regular", + "fullName": "bigbluebutton", + "description": "Font generated by IcoMoon." +} +]]> +</json> +</metadata> +<defs> +<font id="bigbluebutton" horiz-adv-x="1024"> +<font-face units-per-em="1024" ascent="960" descent="-64" /> +<missing-glyph horiz-adv-x="1024" /> +<glyph unicode=" " horiz-adv-x="512" d="" /> +<glyph unicode="🌑" d="M1025.284 449.788c0-282.77-229.23-512-512-512s-512 229.23-512 512c0 282.77 229.23 512 512 512s512-229.23 512-512z" /> +<glyph unicode="🌒" d="M285.368 761.582l0.804-466.185 1.608-10.449 3.215-13.664 4.019-8.038 4.019-12.86 6.43-11.253 12.86-14.468 9.645-10.449 7.234-5.626 6.43-7.234 11.253-8.841 15.272-8.841 9.645-6.43 12.86-2.411 14.468-5.626 249.168 0.804 12.86 5.626 11.253 6.43 12.86 10.449 13.664 8.841 5.626 5.626 10.449 8.038 9.645 9.645 7.234 8.038 7.234 8.841 8.038 14.468 6.43 16.879 0.804 207.372-1.607 16.075-6.43 20.898-10.449 16.879-10.449 14.468-12.86 13.664-12.057 12.057-10.449 9.645-12.86 8.038-16.879 8.841-17.683 0.804-12.86 0.804-214.606 3.215v-16.075l2.411-12.86 5.626-16.879 12.86-16.879 12.057-9.645 16.879-13.664 17.683-9.645 32.151-13.664 23.309-3.215 27.328-1.608h46.619l12.057-2.411 10.449-8.038 2.411-15.272v-155.931l-3.215-12.057-7.234-7.234-208.176-1.608-10.449 1.608-7.234 8.841-3.215 12.86v339.19l-3.215 16.075-3.215 10.449-8.841 15.272-6.43 13.664-8.038 12.057-8.841 10.449-11.253 11.253-10.449 11.253-14.468 10.449-10.449 4.019-11.253 1.608-12.057 3.215-9.645-1.608z" /> +</font></defs></svg> \ No newline at end of file diff --git a/mod/bigbluebuttonbn/pix/i/processing16.gif b/mod/bigbluebuttonbn/pix/i/processing16.gif new file mode 100644 index 0000000000000000000000000000000000000000..6389942df0d31d2522763d90ea2cb94fbd9a85c3 GIT binary patch literal 1877 zcmZ?wbhEHb6krfwXklPbQB#>SYtET-XWqPelaiEj=iZ$y+qQi9_Ql1?<<6};Po6zF za_GpPe}8oJbmq*PbLG;NH}Br`boD%W^yJU4KOtcuM~)r&|NlP&ZlL&|+s`#5*x50_ z)kx2PnUR5kLGdRGCntj#gAPa+$QcZ*^$S#^CV$c36Ld({)-4cZba=caWs^j=4>QL! zk047G7B03U4vro|LPBd!WEisTI=YZSIHZur@eO0}1GlbqO9E!haTVU?bg*pyV^-G6 z#5(?6G2C(KiH5=k+`{onJi?LeVyv$2%)-LVOp{rA`Z{ttTrKns8*THWJ%pH8CnzgI z-6W21Qwv(aY}vNu&b>QdzI_P^3t6&k$(>twTAEsp96Ivk*%P$jfrQPJNmKBL4QtH< z)wnAnrq4=x-W=J}_R&b#Tg&I*k<G5X!lxY-Gb}WCy9*c<_qOm|D`c$jNa1n3^2~vO zq4C7(L}}4|EuL?}_jzbdbxFRfCaJcpu=ayc!q!wiCIb;;k!U4%3m$DYCl_WB5oV@- zwoZ|5>j)kvGd+Vj(^J{mT!rhjl?TMbbp$;e5V&|x3*R-t;}$0*npT`uY;cX^y}jb2 zkWZ4mq2{%W1y{vAmrnX&bm$cmgTs!rqkJ6IjDGqeQA+OI4E)UEY~D;@FFSEFc8Y{p zhqsyO85miaCWW~QyJjlEyo{3WY9Q%uSxL{DEqh!)>Ir$Pb#FXUY}z~Nl%isQfm&CW zLE!8?Ej-r*w_BVLYg*B(*kBvSb=%{kkO#;~%K{c$eckkEH^*xBr`cg=S}o&+qm+1< zL$q1FnV5wI*hDylI9Z#8L#zcEW^$A{7_-h`;N{B-bLC@UWY)prAe2N0NpT@zAz!|I zS+j1<om+Px$*rZS1vAB^B&GcM^#^~7V_;xl1$lT1i!R8+OZK>a)DrSm<63y6*cI&I z2osI&E(1^;@Lm&KZgE1SX~kN_2FEz=+Zj*~Z;M!P_4T1gs}UX+iBbyI*JgF%VPg># zVP<j?U~LxRVVK4k!ewX1&o><u{>+TR970GjKuj)kadLs=GKiniJUnI66i6;3SOT%u zgAyH!At;w^+0*t>PuN?_dm}8DC75XNbb}KeFD!<^9_G2-0ZnwrJUmnczur2az`(%z zbAil`@GU{H)`ENt9Lx>k!TQ?#9Kw)n<|M$T#LFCF9nNisHPsEQpQ~L@MH_s|4qVKh zv`D}}ioNY<QNR@?K88mpvI2^_of#NbnB+`vV{m&Qc3{y#?+Ft;lrknAYB2I?c;Vc` zSZVkui<?QWDc&r}fZbT6O(`rwo6W}+oYf|?^@ST78vB^FaL+fWWOEZ{VnY-zNI4C6 z!d^wCgk8HpH7d|RQAkBL`L_jwfQXmwR<WZ;jAFy5Ok7fwAi*JcYLS!A9gY3Y1y>fR zWU<d*(74%zWh>K?Z7;gSOdFR3%qY`*bF}I0v4h;*8yDrsE!i4l5uX@fD$=MF8qTG| j>I*4ad|5lgOu2GqxmXCznv<9o$;2(h#HypL$Y2csyX4bs literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/i/processing64.gif b/mod/bigbluebuttonbn/pix/i/processing64.gif new file mode 100644 index 0000000000000000000000000000000000000000..506412d4e69900205071d9637cde5e66d98db320 GIT binary patch literal 7708 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 Y!K}&x6JBp%{Ft^-IN-=BHvtA~03yhU4FCWD literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/icon.gif b/mod/bigbluebuttonbn/pix/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a29f62e7be0ce4e15843f0db3950f14058ab90b GIT binary patch literal 1246 zcmZ?wbhEHblwgox_<oV$>$jilwp@Jp_}!TckM7>RTiv#G!pwuqSMJiVYPfdmMPcoF z4a<5BvnpM)if`Y(X;{>0ST|~z*TkpJ@Q;|ZZU6mQi;h(^Zp<xPSya7d@tTX}4I9>O zyU{*jk5}lV+P1A>@zcYT=Y_^i^9t@Ww5sl&azMkPR^6uA#iwoB{NtL&<yyvNI%XA` zrj?q;Woh|~qtX{?m{n_<R;b&y*f^KQq|Vl`X^KyqVdvf&o3$h{YyOheXPkXHckH|F z;N8{OwWGLxQ%2!ZSHCVzlk&KX*-3dz=Px^`XI8pm<CT`~EtRd?w(h&<7e3KBaH3Q2 zl-_9v*YCJx=G?k!^R=*)1=nvr-LdEFjT?6tt-dgE#=a>tcdgiTEjD98L+AD>bB|>e zE-0v3?;APQCua7htw&CuyV)~&i(5c%eEKXc$M%xiwX3(@sH$JEWbLJC^G~eVdVTV| z6V5@CoC7ClIkbBO_HIA$z%z12Quaa%$HvuLuIE;+jY^(nU|H4Fv#YsxcXQ9qg{v=i zPB~auwPxP(vt|yB2A0)fNpo~e%QH(?2gJ^D3+j(bUl^J&YuWnC8#Wzw3YpqAX@7pj zs;5t%?m2w#<k|biHZ?jH)#Xi_W7Fo&T71gJwYj`*m8ol+wtcH}(B#0V$*0cTt!&vg zan_N#_HA>Q94~L)V&h!4dDnFftHvi!-)-D=$Hui{=b?vY_H}0V^)|l!TlU`FedO`{ z73bnI=V(~gc}7f|z2vl+WAlWWhXdm0oV)PA!m+{GuUEsmsdd8MJ9i$ud-u*WYNnB0 zm4;QlhIwsv$#QkO*4>96)Hg2(h@BP?GsD8Me#QFp(-)jb$Xb|EuzcmFt8M+eHH}LD zg8{>!0g69a7#SEYGU$L@1j-W(9RC>>bIM5Y9$=6?CuDWmkb&V~J2NB8kBckLvsp`C zd$BWEqrtg@iM1o^ao3^A%IgCrd|2o>LrkB^fzP8!EmYeqE3Nj?1IA`<8}AbWk{<#N z@N*S)bZRa-!zs@#UewZ2tm)63X2`B}Vo?gSN=E;&1}o;4V=c0)Ic%%1IUhX0Q0M2m z(TPiHi$ULu8iB&(maeVY{j35WD`zS>#7w&RG_*5VAcmKLneT~N<8q#9S(e<X(;H%U z^Q~Gk@ehwn6T|M1O?xi=y!QP3Q5AWvgam&7`5sovhm&5F>MJ)?OB8HlT@~UpS9SF- zo8(;XD9-;=Y;UXXOnG)b=t#q(HHS9st<8<t&Hkryih!8Gy^seJH9Y2?5IVO0Z%>zF z3;U)D4hfZ)?>7rCkX3lV*xm5W+hC)@2PYp^4y8!-^{yVSoZJovycn5y84htZ%r$Ur zDRZ5~$+Rm&i8YYP@-kzlkVk^Er1OEKGfYdbC>)kKC&1#ssO`0EZY{q8LnG(mA3+D) znK=Uvh|6p%kmC_iN?0tJATr^qGlN8fhu&lc&NCAV3_dk6dNfUGFt(65)FM6GB$1ns UO@%?ogN-#Ioq_kPqyU380Gwp~B>(^b literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/icon.png b/mod/bigbluebuttonbn/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e9ec8689cc0fa0ac6bf563e749fec37c2bf39627 GIT binary patch literal 1393 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo*pj^6T^Rm@;DWu&Co?cG za29w(7BevLUI$@DCym(^3=9nHC7!;n><_s_1i9p9GB0yrU|{*@>EamTaeV5u$_$xM zndAH4@4b2Vrt)#Klh1DW`n=wj>D`mN>S#u`v>@y1Ev(8?K^m8sy+7&;_Sa8ya_RcF zI6zSFqS=MV{jcO(*;>R_*&f+;;aZUCLicxv&%QfzZlm$%XFreUsm(T9`SsxAvwO_n zf8M>%IG;oA(ym>7n*+|&_3b_vcJ3nEf_)p5j!7tAXZV#|XI1d;NT>OaB%Vzdp3Jk2 z|824|$NWHb@>S7)8IcZp`uu^XID8JbEqdXxJT%-q_wq9({{wb%>#~15%UN~#r$6)E z>fh6ZKm0r966zeC9lT(X63^u=3A4<6E;p^4ec@|Op^2g8%*MQ1%j^YYlzaA+Jh{-< z=*s(LPjQ8^>XR$GOwUf7HjQPY&T@|J2@=!TuWo5#481z>wB*rw;=O0TS_<rXdLrEV zoNdB_>iqjfo<h^Duc@Dw%U;!*d(*@JUen3N1+mW@OKsy>rXAv4v5L=0BkKB=6;m#L zyXo;F=NxB6{p(c~DuU}9m(ARuBW+zBclcCV;}?5Ljjpzwu)}A1x@Jl)wOHk`ocns; z;l$f!ZSQ;AS;YU&4o+7&-*E6yb&~1BUr$e3eB5)%hvC4o=jII-&L>ie<hF>c6P-Uh zH(2=c8G|zskB^+tV@QaNJub<8d-?UPy4?)|5^i#{(hh9PS(Bg7q{tFHIeABwEwAgv z1sawqo286q%2<?(Pg%Bb_CHnAi|OHdwXcgk*Oi$~ac`=#eBrK_?r@o_b&2ZJ^=otv z-@Y5VLWAFlp+R7SEITu^^N)8YSbO}>iZ7ma`Jn7O?rRpZlU`fhK6G%y8jW8zKLeHa z@H#PE&J@-!DLcblYV+A7?(U_|xx4uvy}$kW{?;#Fd<?Dy?CmXjXy+)oaI@<i?UZ## z8V)r2`!D|fAdrXeUTbRPy(kl@qwVi=>lJ)=<$39euN7j<EXZNMxyH`F&TgejPN2c1 z@;6++`eGRwTAb?sl$6*%`e?N#Vq#0b<%XRhBKD1M3mg;_8J#p%@jdMN&2yo>cG-g0 zV#}7L3G`ogoR%8!j7f%JA%iNzLV<<}9489W++yGEI@G*+Ro|wLp8r=l7&UYoI9u%> zuq!4-ESmCXVdhV9-^)u@MK##pQvdj`lFKntk;zGsJ+(cc%$Z41{jX!?`xh@lPMAFJ zntt-thFod8`TVQH*B<z$Yo2hs*lS{{y<Ge>28NW)kxybJ70!5{pOi7vlhe=Z=gR{> z1E!w1y-ir_YkRTybN#sE%X;5-9GAXOo$!CrP0R1ELpg%J%nNE!R`i&ZqMFPse)H^X z^&^x0_pX}~?q=rezwGZ)Y5ka8y?tMOSFG&Q<J$k_c4*CqljjR!*aFxbH71?iIOomh zz5kD<zyIgQkWl=pM?_xwS9VIqp3J10uBl8COEcnUJ9ry$9m`Ps$g)9J{Cb}6<6mpH zESGWA$Xcykv1r?)a~ZLZPi<c5cU(L8{Jz7UiUpM)&Rajd*|W*P@wvk0R-F}TdFwVQ z^`tEFnp(4O>;3wl&mJ_f_X#|@a$@ht3F4fJN8={kUn8=2=cC#+yUyfxg=Y0^3a_)c z^7r&2rZ+dsEi`w$Fz4C)?cUvs8<x$KJHj0Ns#3P|M8>-Ati}eDcb|X8&-?mkA=mvm zR?4P+J#QWzdi#{``_ju^7YuSW-xn~5{A*}g@TPoPh?`HRVRuuVNlf_vx5~1=zW?9G zl0Apd{lnS^8Nx0{7#lc@nA)=K-0EVYYzq{pdhP5s{KqPiw{pGG*|NV33=9mOu6{1- HoD!M<Af}cm literal 0 HcmV?d00001 diff --git a/mod/bigbluebuttonbn/pix/icon.svg b/mod/bigbluebuttonbn/pix/icon.svg new file mode 100644 index 0000000..7b92ab2 --- /dev/null +++ b/mod/bigbluebuttonbn/pix/icon.svg @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + width="24px" height="24px" viewBox="0 0 30 30" enable-background="new 0 0 30 30" xml:space="preserve" preserveAspectRatio="xMinYMid meet"> + <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-58.9473" y1="84.1865" x2="58.9479" y2="-84.1852"> + <stop offset="0" style="stop-color:#283274"/> + <stop offset="0.7088" style="stop-color:#293275"/> + <stop offset="0.967" style="stop-color:#273F87"/> + </linearGradient> +<symbol id="New_Symbol" viewBox="-107.423 -107.423 214.847 214.846"> + <g> + <path fill="url(#SVGID_1_)" stroke="#5D6AA4" stroke-width="9.3191" d="M102.765,0c0-56.755-46.009-102.764-102.765-102.764 + c-56.754,0-102.764,46.009-102.764,102.764S-56.754,102.764,0,102.764C56.756,102.764,102.765,56.755,102.765,0z"/> + + <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-75.0186" y1="112.8037" x2="-15.8528" y2="28.3063" gradientTransform="matrix(1 0 0 1 18.4946 -31.9512)"> + <stop offset="0" style="stop-color:#BEC7E5"/> + <stop offset="0.0956" style="stop-color:#B5BEDE"/> + <stop offset="0.2524" style="stop-color:#9CA5CC"/> + <stop offset="0.4508" style="stop-color:#747DAE"/> + <stop offset="0.6809" style="stop-color:#3D4684"/> + <stop offset="0.7582" style="stop-color:#293275"/> + </linearGradient> + <path fill="url(#SVGID_2_)" d="M0.282,98.502C26.323,98.31,54.293,87.893,74.49,63.84C73.631,43.207,36.703,22.057,5.886-1.229 + c-25.857-19.538-65.427-37.86-92.487-45.059c-25.73,49.263-7.016,96.619,25.25,123.238C-46.256,90.16-23.508,98.679,0.282,98.502z + "/> + </g> + <g> + <g> + <path d="M55.602-25.561c0-8.678-3.055-16.072-9.16-22.179c-6.11-6.109-13.501-9.16-22.179-9.16h-37.367 + c-8.679,0-16.073,3.051-22.179,9.16c-6.11,6.106-9.162,13.501-9.162,22.179v93.779c6.656,0,12.336-3.254,17.046-9.764 + c4.708-6.509,7.062-14.344,7.062-23.505v-60.511c0-4.821,2.411-7.231,7.232-7.231h37.367c4.821,0,7.231,2.41,7.231,7.231V3.128 + c0,4.66-2.41,7.07-7.231,7.232H17.03c-9.323,0.324-17.2,2.757-23.626,7.304c-6.43,4.543-9.643,10.145-9.643,16.805h40.501 + c8.678,0,16.068-3.056,22.179-9.161c6.105-6.11,9.16-13.5,9.16-22.18V-25.561z"/> + </g> + </g> + <g opacity="0.46"> + <g> + <path fill="#FFFFFF" d="M55.602-25.561c0-8.678-3.055-16.072-9.16-22.179c-6.11-6.109-13.501-9.16-22.179-9.16h-37.367 + c-8.679,0-16.073,3.051-22.179,9.16c-6.11,6.106-9.162,13.501-9.162,22.179v93.779c6.656,0,12.336-3.254,17.046-9.764 + c4.708-6.509,7.062-14.344,7.062-23.505v-60.511c0-4.821,2.411-7.231,7.232-7.231h37.367c4.821,0,7.231,2.41,7.231,7.231V3.128 + c0,4.66-2.41,7.07-7.231,7.232H17.03c-9.323,0.324-17.2,2.757-23.626,7.304c-6.43,4.543-9.643,10.145-9.643,16.805h40.501 + c8.678,0,16.068-3.056,22.179-9.161c6.105-6.11,9.16-13.5,9.16-22.18V-25.561z"/> + </g> + </g> + <g> + <path fill="#FFFFFF" d="M53.829-26.788c0-8.678-3.055-16.072-9.16-22.179c-6.11-6.109-13.501-9.16-22.18-9.16h-37.367 + c-8.679,0-16.073,3.051-22.179,9.16c-6.109,6.106-9.16,13.501-9.16,22.179v93.779c6.655,0,12.336-3.255,17.045-9.764 + c4.708-6.509,7.062-14.345,7.062-23.505v-60.511c0-4.821,2.41-7.231,7.232-7.231h37.367c4.821,0,7.232,2.41,7.232,7.231V1.901 + c0,4.66-2.411,7.07-7.232,7.232h-7.232C5.935,9.457-1.942,11.89-8.368,16.438c-6.43,4.542-9.644,10.144-9.644,16.803h40.501 + c8.679,0,16.069-3.055,22.18-9.16c6.105-6.109,9.16-13.5,9.16-22.179V-26.788z"/> + </g> +</symbol> +<use xlink:href="#New_Symbol" width="214.847" height="214.846" x="-107.423" y="-107.423" transform="matrix(0.1376 0 0 -0.1376 14.9995 15)" overflow="visible"/> +</svg> diff --git a/mod/bigbluebuttonbn/settings.php b/mod/bigbluebuttonbn/settings.php new file mode 100644 index 0000000..b561486 --- /dev/null +++ b/mod/bigbluebuttonbn/settings.php @@ -0,0 +1,64 @@ +<?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/>. + +/** + * Settings for BigBlueButtonBN. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +global $CFG; + +require_once(__DIR__.'/locallib.php'); + +if ($hassiteconfig) { + // Configuration for BigBlueButton. + $renderer = new \mod_bigbluebuttonbn\settings\renderer($settings); + // Renders general settings. + bigbluebuttonbn_settings_general($renderer); + // Evaluates if recordings are enabled for the Moodle site. + if (\mod_bigbluebuttonbn\locallib\config::recordings_enabled()) { + // Renders settings for record feature. + bigbluebuttonbn_settings_record($renderer); + // Renders settings for import recordings. + bigbluebuttonbn_settings_importrecordings($renderer); + // Renders settings for showing recordings. + bigbluebuttonbn_settings_showrecordings($renderer); + } + // Renders settings for meetings. + bigbluebuttonbn_settings_waitmoderator($renderer); + bigbluebuttonbn_settings_voicebridge($renderer); + bigbluebuttonbn_settings_preupload($renderer); + bigbluebuttonbn_settings_preupload_manage_default_file($renderer); + bigbluebuttonbn_settings_userlimit($renderer); + bigbluebuttonbn_settings_duration($renderer); + bigbluebuttonbn_settings_participants($renderer); + bigbluebuttonbn_settings_notifications($renderer); + bigbluebuttonbn_settings_clienttype($renderer); + bigbluebuttonbn_settings_muteonstart($renderer); + bigbluebuttonbn_settings_locksettings($renderer); + bigbluebuttonbn_settings_default_messages($renderer); + // Renders settings for extended capabilities. + bigbluebuttonbn_settings_extended($renderer); + // Renders settings for experimental features. + bigbluebuttonbn_settings_experimental($renderer); +} diff --git a/mod/bigbluebuttonbn/styles.css b/mod/bigbluebuttonbn/styles.css new file mode 100644 index 0000000..7b38495 --- /dev/null +++ b/mod/bigbluebuttonbn/styles.css @@ -0,0 +1,67 @@ +@charset "UTF-8"; + +/* bigbluebutton-font */ +@font-face { + font-family: "bigbluebutton-font"; + src: url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.eot"); + src: + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.eot?#iefix") format("embedded-opentype"), + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.woff") format("woff"), + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.ttf") format("truetype"), + url("../../../../mod/bigbluebuttonbn/fonts/bigbluebutton-font.svg#bigbluebutton-font") format("svg"); + font-weight: normal; + font-style: normal; +} + +[class^="icon-bigbluebutton"]:before, +[class*=" icon-bigbluebutton"]:before { + font-family: "bigbluebutton-font"; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-bigbluebutton:before { + content: "\e099"; +} + +/* bigbluebuttonbn resources */ +.recording-thumbnail { + border: 1px solid #ddd; + border-radius: 4px; + padding: 0 0 0 0; + transition: transform .2s; /* Animation */ + width: 113px; + height: 64px; +} + +.recording-thumbnail:hover { + box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5); + transform: scale(2.0); /* (200% zoom - Note: if the zoom is too large, it will go outside of the viewport) */ + -ms-transform: scale(2.0); /* Internet Explorer 9 */ + -moz-transform: scale(2.0); /* Firefox */ + -webkit-transform: scale(2.0); /* Safari and Chrome */ + -o-transform: scale(2.0); /* Opera */ + position: relative; + display: block; + z-index: 999; +} + +.fa-disabled { + cursor: not-allowed; + opacity: 0.2; +} + +.fa-invisible { + cursor: not-allowed; + visibility: hidden; +} + +.bbb_index_form { + display: inline-block; +} \ No newline at end of file diff --git a/mod/bigbluebuttonbn/templates/import_view.mustache b/mod/bigbluebuttonbn/templates/import_view.mustache new file mode 100644 index 0000000..02991b8 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/import_view.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_bigbluebuttonbn/import_view + + This template renders the import recordings page. + + Example context (json): + { + "backactionurl": "http://localhost/foo", + "cmid": 3, + "hascontent": true, + "selectoptions": [ + {"selected": true, "value": 1, "label": "First choice"}, + {"value": 2, "label": "Second choice"} + ], + "recordings": true, + "recordingtable": "<table></table>" + } +}} + +<div class="mod_bigbluebuttonbn_import_view"> + <h4>{{#str}} view_recording_button_import, mod_bigbluebuttonbn {{/str}}</h4> + {{^hascontent}} + <div>{{#str}} view_error_import_no_courses, mod_bigbluebuttonbn {{/str}}</div> + {{/hascontent}} + {{#hascontent}} + <div> + <select id="menuimport_recording_links_select" + class="select custom-select menuimport_recording_links_select" + name="import_recording_links_select"> + {{#selectoptions}} + <option {{#selected}}selected="selected"{{/selected}} value="{{value}}">{{label}}</option> + {{/selectoptions}} + </select> + {{#recordings}} + <span id="import_recording_links_table"></span> + {{{recordingtable}}} + {{/recordings}} + {{^recordings}} + <div>{{#str}} view_error_import_no_recordings, mod_bigbluebuttonbn {{/str}}</div> + {{/recordings}} + <br> + </div> + {{/hascontent}} + <form id="goback-button" action="{{backactionurl}}" method="get"> + <input type="hidden" name="id" value="{{cmid}}"> + <input type="submit" class="btn btn-secondary" value="{{#str}} view_recording_button_return, mod_bigbluebuttonbn {{/str}}"> + </form> +</div> \ No newline at end of file diff --git a/mod/bigbluebuttonbn/templates/mobile_view_error.mustache b/mod/bigbluebuttonbn/templates/mobile_view_error.mustache new file mode 100644 index 0000000..bac02f9 --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_error.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_error + + This template renders the error for mobile page. + + Example context (json): + { + "error": "This is wrong!" + } +}} +{{=<% %>=}} +<div> + <ion-list> + <ion-item text-wrap> + <% error %> + </ion-item> + </ion-list> +</div> diff --git a/mod/bigbluebuttonbn/templates/mobile_view_notification.mustache b/mod/bigbluebuttonbn/templates/mobile_view_notification.mustache new file mode 100644 index 0000000..19c455a --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_notification.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_notification + + This template renders the notifications for mobile page. + + Example context (json): + { + "bigbluebuttonbn.intro": "foo", + "cmid": 3, + "message": "A message", + "not_started": [ + {"not_started.starts_at": "aaa", "not_started.starts_at": "bbb"}, + {"not_started.starts_at": "aaa", "not_started.starts_at": "bbb"} + ] + } +}} +{{=<% %>=}} +<div> + <core-course-module-description description="<% bigbluebuttonbn.intro %>" component="mod_bigbluebuttonbn" componentId="<% cmid %>"></core-course-module-description> + <ion-list> + <ion-item text-wrap> + <% message %> + </ion-item> + <%#not_started%> + <ion-item text-wrap> + <p><% not_started.starts_at %></p> + <p><% not_started.ends_at %></p> + </ion-item> + <%/not_started%> + <span core-site-plugins-call-ws-on-load + name="mod_bigbluebuttonbn_can_join" [params]="{cmid: <% cmid %>}" (onSuccess)="onCanJoinReturns($event)"></span> + </ion-list> +</div> diff --git a/mod/bigbluebuttonbn/templates/mobile_view_page.mustache b/mod/bigbluebuttonbn/templates/mobile_view_page.mustache new file mode 100644 index 0000000..578c9ba --- /dev/null +++ b/mod/bigbluebuttonbn/templates/mobile_view_page.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_bigbluebuttonbn/mobile_view_page + + This template renders the mobile page. + + Example context (json): + { + "bigbluebuttonbn.intro": "foo", + "cmid": 3, + "message": "A message", + "errors": [ + {"error": "aaa"}, + {"error": "bbb"} + ], + "urltojoin": "http://foo" + } +}} +{{=<% %>=}} +<div> + <%#msjgroup%> + <div style="color:#856404; background-color: #fff3cd; padding-right: 35px; border-color: #ffeeba; padding: 15px; margin-bottom: 20px;"> + <% message %> + </div> + <%/msjgroup%> + <div id="bigbluebuttonbn-mobile-notifications" style="display:none; color:#31708f; background-color: #d9edf7; padding-right: 35px; border-color: #bce8f1; padding: 15px; margin-bottom: 20px;"> + {{ 'plugin.mod_bigbluebuttonbn.view_mobile_message_reload_page_creation_time_meeting' | translate }} + </div> + <core-course-module-description description="<% bigbluebuttonbn.intro %>" component="mod_bigbluebuttonbn" componentId="<% cmid %>"></core-course-module-description> + <ion-list> + <%#errors%> + <ion-item text-wrap> + <% error %> + </ion-item> + <%/errors%> + <%^errors%> + <ion-item text-wrap id="bigbluebuttonbn-mobile-meetingready"> + {{ 'plugin.mod_bigbluebuttonbn.view_message_conference_room_ready' | translate }} + </ion-item> + <ion-item> + <button id="bigbluebuttonbn-mobile-join" ion-button block onclick="window.open('<% urltojoin %>', '_system');"> + {{ 'plugin.mod_bigbluebuttonbn.view_conference_action_join' | translate }} + </button> + </ion-item> + <%/errors%> + </ion-list> +</div> diff --git a/mod/bigbluebuttonbn/tests/behat/add_instance.feature b/mod/bigbluebuttonbn/tests/behat/add_instance.feature new file mode 100644 index 0000000..0b7ac7e --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/add_instance.feature @@ -0,0 +1,63 @@ +@mod @mod_bigbluebuttonbn @core_form +Feature: bigbluebuttonbn instance + In order to create a room activity with recordings + As a user + I need to add three room activities to an existent course + + @javascript + Scenario: Add three room activities to an existent course + When I log in as "admin" + And I create a course with: + | Course full name | Test Course | + | Course short name | testcourse | + And I follow "Test Course" + And I turn editing mode on + And I add a "BigBlueButtonBN" to section "1" and I fill the form with: + | Instance type | Room/Activity with recordings | + | Virtual classroom name | RoomRecordings | + Then I should see "RoomRecordings" + When I follow "RoomRecordings" + Then I should see "RoomRecordings" + And "#bigbluebuttonbn_view_message_box" "css_element" should be visible + And "#bigbluebuttonbn_view_action_button_box" "css_element" should be visible + And "#bigbluebuttonbn_recordings_table" "css_element" should be visible + When I follow "testcourse" + And I add a "BigBlueButtonBN" to section "1" and I fill the form with: + | Instance type | Room/Activity only | + | Virtual classroom name | RoomOnly | + Then I should see "RoomOnly" + When I follow "RoomOnly" + Then I should see "RoomOnly" + And "#bigbluebuttonbn_view_message_box" "css_element" should be visible + And "#bigbluebuttonbn_view_action_button_box" "css_element" should be visible + And "#bigbluebuttonbn_recordings_table" "css_element" should not be visible + When I follow "testcourse" + And I add a "BigBlueButtonBN" to section "1" and I fill the form with: + | Instance type | Recordings only | + | Virtual classroom name | RecordingsOnly | + Then I should see "RecordingsOnly" + When I follow "RecordingsOnly" + Then I should see "RecordingsOnly" + And "#bigbluebuttonbn_view_message_box" "css_element" should not be visible + And "#bigbluebuttonbn_view_action_button_box" "css_element" should not be visible + And "#bigbluebuttonbn_recordings_table" "css_element" should be visible + + @javascript + Scenario: Add an activity and check that required settings are available for the three + types of instance types + When I log in as "admin" + And I create a course with: + | Course full name | Test Course | + | Course short name | testcourse | + And I follow "Test Course" + And I turn editing mode on + And I add a "BigBlueButtonBN" to section "1" + And I wait until the page is ready + When I select "Room/Activity with recordings" from the "Instance type" singleselect + Then I should see "Restrict access" + When I select "Room/Activity only" from the "Instance type" singleselect + Then I wait until the page is ready + Then I should see "Restrict access" + When I select "Recordings only" from the "Instance type" singleselect + Then I wait until the page is ready + Then I should see "Restrict access" diff --git a/mod/bigbluebuttonbn/tests/behat/group_mode.feature b/mod/bigbluebuttonbn/tests/behat/group_mode.feature new file mode 100644 index 0000000..24a9b2c --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/group_mode.feature @@ -0,0 +1,90 @@ +@mod @mod_bigbluebuttonbn @course +Feature: Test the module in group mode. + + Background: + Given the following "courses" exist: + | fullname | shortname | category | groupmode | groupmodeforce | + | Test Course 1 | C1 | 0 | 1 | 1 | + # 1 = separate groups, we force the group + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | TeacherG1 | 1 | teacher1@example.com | + | teacher2 | TeacherG2 | 2 | teacher2@example.com | + | user1 | UserG1 | 1 | user1@example.com | + | user2 | UserG1 | 2 | user2@example.com | + | user3 | UserG2 | 3 | user3@example.com | + | user4 | UserG2 | 4 | user4@example.com | + | user5 | UserG2 | 5 | user5@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | user1 | C1 | student | + | user2 | C1 | student | + | user3 | C1 | student | + | user4 | C1 | student | + | user5 | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + And the following "group members" exist: + | user | group | + | teacher1 | G1 | + | teacher2 | G2 | + | user1 | G1 | + | user2 | G1 | + | user3 | G2 | + | user4 | G2 | + | user5 | G2 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | type | recordings_imported | + | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | + + @javascript + Scenario: When I create a BBB activity as a teacher who cannot acces all groups, + I should only be able to select the group I belong on the main bigblue button page. + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 | + And I log in as "teacher1" + And I am on "Test Course 1" course homepage + Then I follow "RoomRecordings" + And I should see "Separate groups: Group 1" + + @javascript + Scenario: When I create a BBB activity as a teacher, I should only be able to specify individual "User" participants + with whom I share a group with (or can view on the course participants screen). + And I log in as "teacher1" + And I am on "Test Course 1" course homepage + Then I follow "RoomRecordings" + And I should see "Group 1" in the "select[name='group']" "css_element" + And I should see "Group 2" in the "select[name='group']" "css_element" + + @javascript + Scenario: When I create a BBB activity as a teacher, I should only be able to specify individual "User" participants + with whom I share a group with (or can view on the course participants screen). + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/site:accessallgroups | Prevent | editingteacher | Course | C1 | + And I log in as "teacher1" + And I am on "Test Course 1" course homepage with editing mode on + And I open "RoomRecordings" actions menu + And I click on "Edit settings" "link" in the "RoomRecordings" activity + Then I select "User" from the "bigbluebuttonbn_participant_selection_type" singleselect + And I should see "TeacherG1 1" in the "#bigbluebuttonbn_participant_selection" "css_element" + And I should see "UserG1 1" in the "#bigbluebuttonbn_participant_selection" "css_element" + And I should not see "UserG2 3" in the "#bigbluebuttonbn_participant_selection" "css_element" + And I should not see "TeacherG2 2" in the "#bigbluebuttonbn_participant_selection" "css_element" + + @javascript + Scenario: When I create a BBB activity as a teacher, I should only be able to specify individual "User" participants + with whom I share a group with (or can view on the course participants screen). + And I log in as "teacher1" + And I am on "Test Course 1" course homepage with editing mode on + And I open "RoomRecordings" actions menu + And I click on "Edit settings" "link" in the "RoomRecordings" activity + Then I select "User" from the "bigbluebuttonbn_participant_selection_type" singleselect + And I should see "TeacherG1 1" in the "#bigbluebuttonbn_participant_selection" "css_element" + And I should see "UserG1 1" in the "#bigbluebuttonbn_participant_selection" "css_element" + And I should see "UserG2 3" in the "#bigbluebuttonbn_participant_selection" "css_element" diff --git a/mod/bigbluebuttonbn/tests/behat/installed.feature b/mod/bigbluebuttonbn/tests/behat/installed.feature new file mode 100644 index 0000000..70f9f2a --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/installed.feature @@ -0,0 +1,12 @@ +@mod @mod_bigbluebuttonbn +Feature: Installation succeeds + In order to use this plugin + As a user + I need the installation to work + + Scenario: Check the Plugins overview for the name of this plugin + When I log in as "admin" + And I navigate to "Plugins > Plugins overview" in site administration + Then the following should exist in the "plugins-control-panel" table: + |BigBlueButtonBN| + |mod_bigbluebutton| diff --git a/mod/bigbluebuttonbn/tests/behat/module_test_operational.feature b/mod/bigbluebuttonbn/tests/behat/module_test_operational.feature new file mode 100644 index 0000000..a620958 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/module_test_operational.feature @@ -0,0 +1,34 @@ +@mod @mod_bigbluebuttonbn @rl +Feature: Test the complete sequence of operational steps for an activity module + In order to guarantee activity module functionality + As an administrator + I need to be able to add an instance of the module to a course + And backup/restore the course to verify the module instance persists + And delete an instance of the module within a course + And delete a course containing an instance of the module + + @javascript + Scenario: Testing the complete sequence of operational steps for an activity module + When I log in as "admin" + And I create a course with: + | Course full name | Test Course | + | Course short name | testcourse | + And I follow "Test Course" + And I turn editing mode on + And I add a "BigBlueButtonBN" to section "1" and I fill the form with: + | Virtual classroom name | TestActivity | + Then I should see "TestActivity" + When I backup "Test Course" course using this options: + | Confirmation | Filename | test_backup.mbz | + And I restore "test_backup.mbz" backup into a new course using this options: + | Schema | Course name | Test Restored Course | + Then I should see "TestActivity" + When I delete "TestActivity" activity + Then I should not see "TestActivity" + When I go to the courses management page + And I click on "delete" action for "Test Course" in management course listing + And I should see "Delete testcourse" + And I press "Delete" + And I should see "testcourse has been completely deleted" + And I press "Continue" + Then I should not see "Test Course" in the "#course-category-listings ul.ml" "css_element" diff --git a/mod/bigbluebuttonbn/tests/behat/recordings_import.feature b/mod/bigbluebuttonbn/tests/behat/recordings_import.feature new file mode 100644 index 0000000..2114dc7 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/behat/recordings_import.feature @@ -0,0 +1,95 @@ +@mod @mod_bigbluebuttonbn @core_form @course +Feature: Manage and list recordings + As a user I am able to import existing recording into another bigbluebutton activity + + Background: Make sure that import recording is enabled and course, activities and recording exists + Given the following config values are set as admin: + | bigbluebuttonbn_importrecordings_enabled | 1 | + And the following "courses" exist: + | fullname | shortname | category | + | Test Course 1 | C1 | 0 | + | Test Course 2 | C2 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | 1 | user1@example.com | + And the following "activities" exist: + | activity | name | intro | course | idnumber | type | recordings_imported | + | bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 | + | bigbluebuttonbn | RoomOnly | Test Recordings only description | C1 | bigbluebuttonbn2 | 1 | 0 | + | bigbluebuttonbn | RecordingsOnly1 | Test Recordings only description 1 | C2 | bigbluebuttonbn3 | 2 | 1 | + | bigbluebuttonbn | RecordingsOnly2 | Test Recordings only description 2 | C2 | bigbluebuttonbn4 | 2 | 1 | + And the following "mod_bigbluebuttonbn > recordings" exist: + | bigbluebuttonbn | meta_bbb-recording-name | + | RoomRecordings | Recording 1 | + | RoomRecordings | Recording 2 | + + @javascript + Scenario: I check that I can import recordings into the Recording Only activity from other activities + the imported recordings are only visible in one activity (CONTRIB-7961) + When I log in as "admin" + + Then I go to the courses management page + And I follow "Test Course 2" + Then I follow "View" + Then I follow "RecordingsOnly1" + Then I click on "Import recording links" "button" + Then I select "Test Course 1" from the "import_recording_links_select" singleselect + # add the first recording + And I click on "td.lastcol a" "css_element" + Then I wait until the page is ready + Then I click on "Yes" "button" + Then I wait until the page is ready + # add the second recording + And I click on "td.lastcol a" "css_element" + Then I wait until the page is ready + Then I click on "Yes" "button" + Then I wait until the page is ready + And I click on "Go back" "button" + Then I wait until the page is ready + Then I go to the courses management page + And I follow "Test Course 2" + Then I follow "View" + Then I follow "RecordingsOnly1" + And I should see "Recording 1" + And I should see "Recording 2" + + @javascript + Scenario: I check that I can import recordings into the Recording Only activity and that the list of + recording is displays the right information (Recording Name as name and Description) + When I log in as "admin" + Then I go to the courses management page + And I follow "Test Course 2" + Then I follow "View" + Then I follow "RecordingsOnly1" + Then I click on "Import recording links" "button" + Then I select "Test Course 1" from the "import_recording_links_select" singleselect + Then I wait until the page is ready + # We check column names regarding changes made in CONTRIB-7703. + And I should not see "Recording" in the "table.generaltable > thead > tr" "css_element" + And I should not see "Meeting" in the "table.generaltable > thead > tr" "css_element" + And I should see "Name" in the "table.generaltable > thead > tr" "css_element" + Then I select "Test Course 1" from the "import_recording_links_select" singleselect + # We check that columns are in the right order, see CONTRIB-7703. + Then I should see "Recording 1" in the "table.generaltable tr td.cell.c1" "css_element" + # add the first recording + And I click on "td.lastcol a" "css_element" + Then I wait until the page is ready + Then I click on "Yes" "button" + Then I wait until the page is ready + And I click on "Go back" "button" + Then I wait until the page is ready + And I should not see "Recording" in the "table > thead > tr" "css_element" + And I should not see "Meeting" in the "table > thead > tr" "css_element" + And I should see "Name" in the "table > thead > tr" "css_element" + # This should be refactored with the right classes for the table element + # We use javascript here to create the table so we don't get the same structure. + Then I should see "Recording 1" in the "#bigbluebuttonbn_recordings_table table.yui3-datatable-table tbody.yui3-datatable-data tr td:nth-child(2)" "css_element" + # Here we would need to test if there is no regression in the html by default view. This will have to be refactored + # alongside with the view + Then I wait until the page is ready + Then I go to the courses management page + And I follow "Test Course 2" + Then I follow "View" + Then I follow "RecordingsOnly2" + And I should not see "Recording 1" + And I should not see "Recording 2" diff --git a/mod/bigbluebuttonbn/tests/coverage.php b/mod/bigbluebuttonbn/tests/coverage.php new file mode 100644 index 0000000..2a4fa68 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/coverage.php @@ -0,0 +1,52 @@ +<?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/>. +/** + * Coverage information for the mod_bigbluebuttonbn component. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class bbb_coverage information for the core subsystem. + * + * Note that we had to change the definition of this class due to a bug in local_moodlecheck + * https://github.com/moodlehq/moodle-local_moodlecheck/issues/50 + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class bbb_coverage extends phpunit_coverage_info { + /** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */ + protected $whitelistfolders = [ + 'classes', + ]; + + /** @var array The list of files relative to the plugin root to whitelist in coverage generation. */ + protected $whitelistfiles = ['lib.php']; + + /** @var array The list of folders relative to the plugin root to excludelist in coverage generation. */ + protected $excludelistfolders = []; + + /** @var array The list of files relative to the plugin root to excludelist in coverage generation. */ + protected $excludelistfiles = []; +}; +return new bbb_coverage; \ No newline at end of file diff --git a/mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php b/mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php new file mode 100644 index 0000000..6561080 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/generator/behat_mod_bigbluebuttonbn_generator.php @@ -0,0 +1,71 @@ +<?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/>. + +/** + * Behat data generator for mod_bigbluebuttonbn. + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Behat data generator for mod_bigbluebuttonbn. + * + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_mod_bigbluebuttonbn_generator extends behat_generator_base { + + /** + * Get all entities that can be create through this behat_generator + * @return array + */ + protected function get_creatable_entities(): array { + return [ + 'recordings' => [ + 'datagenerator' => 'recording', + 'required' => ['bigbluebuttonbn', 'meta_bbb-recording-name'], + 'switchids' => ['bigbluebuttonbn' => 'bigbluebuttonbnid'], + ], + 'log' => [ + 'datagenerator' => 'override', + 'required' => ['bigbluebuttonbn', 'user'], + 'switchids' => ['bigbluebuttonbn' => 'bigbluebuttonbnid', 'user' => 'userid'], + ], + ]; + } + + /** + * Look up the id of a bigbluebutton activity from its name. + * + * @param string $bbactivityname the bigbluebutton activity name, for example 'Test meeting'. + * @return int corresponding id. + * @throws dml_exception + */ + protected function get_bigbluebuttonbn_id(string $bbactivityname): int { + global $DB; + + if (!$id = $DB->get_field('bigbluebuttonbn', 'id', ['name' => $bbactivityname])) { + throw new Exception('There is no bigbluebuttonbn with name "' . $bbactivityname . '" does not exist'); + } + return $id; + } +} diff --git a/mod/bigbluebuttonbn/tests/generator/lib.php b/mod/bigbluebuttonbn/tests/generator/lib.php new file mode 100644 index 0000000..aca68cf --- /dev/null +++ b/mod/bigbluebuttonbn/tests/generator/lib.php @@ -0,0 +1,232 @@ +<?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/>. + +/** + * mod_bigbluebuttonbn data generator + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/lib.php'); + +/** + * bigbluebuttonbn module data generator + * + * @package mod_bigbluebuttonbn + * @category test + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class mod_bigbluebuttonbn_generator extends \testing_module_generator { + + /** + * Creates an instance of bigbluebuttonbn for testing purposes. + * + * @param array|stdClass $record data for module being generated. + * @param null|array $options general options for course module. + * @return stdClass record from module-defined table with additional field cmid + */ + public function create_instance($record = null, array $options = null) { + $now = time(); + $defaults = array( + "type" => 0, + "meetingid" => sha1(rand()), + "record" => true, + "moderatorpass" => "mp", + "viewerpass" => "ap", + "participants" => "{}", + "timecreated" => $now, + "timemodified" => $now, + "presentation" => null, + ); + $record = (array) $record; + foreach ($defaults as $key => $value) { + if (!isset($record[$key])) { + $record[$key] = $value; + } + } + return parent::create_instance((object) $record, (array) $options); + } + + /** + * Create a recording for the given bbb activity + * + * @param object $record for the record + * @param array|null $options other specific options + * @return array the recording array + * @throws dml_exception + */ + public function create_recording($record, array $options = null) { + global $CFG, $DB; + $record = (array) $record; + + $bbactivityid = $record['bigbluebuttonbnid']; // Must be there. + unset($record['bigbluebuttonbnid']); + $bbactivity = $DB->get_record('bigbluebuttonbn', array('id' => $bbactivityid)); + + $options = (array) $options; + $defaultoptions = [ + 'playbacknotes' => true, + 'playbackpresentation' => true, + 'recordingtime' => 1000, + 'remotehost' => 'localhost.com' + ]; + $options = array_merge($defaultoptions, $options); + + // Get options. + $recordingtime = $options['recordingtime']; + $playbacknotes = $options['playbacknotes']; + $playbackpresentation = $options['playbackpresentation']; + $remotehost = $options['remotehost']; + + $timenow = time(); + $recordid = sha1(rand()) . '-' . $timenow; + $playbacks = []; + if ($playbacknotes) { + $playbacks['notes'] = array( + 'type' => 'notes', + 'url' => "https://{$remotehost}/test-install/{$recordid}/notes", + 'length' => '', + ); + } + if ($playbackpresentation) { + $playbacks['presentation'] = array( + 'type' => 'presentation', + 'url' => + "https://{$remotehost}/test-install/{$recordid}/presentation", + 'length' => '', + ); + } + + // Build the recording data. + $recording = [ + 'recordID' => $recordid, + 'meetingID' => $bbactivity->meetingid, + 'meetingName' => $bbactivity->name, + 'startTime' => $timenow, + 'endTime' => $timenow + $recordingtime, + 'playbacks' => $playbacks, + 'published' => 'true', + 'protected' => 'false', + 'meta_bbb-context-label' => 'testcourse_12', + 'meta_bbb-origin-server-name' => 'bigbluebuttonm.local', + 'meta_bbb-context' => 'Test course: BBB', + 'meta_analytics-callback-url' => $CFG->wwwroot . + '/mod/bigbluebuttonbn/bbb_broker.php?action=meeting_events&bigbluebuttonbn=' . $bbactivity->id, + 'meta_bbb-origin-tag' => 'moodle-mod_bigbluebuttonbn (2019101001)', + 'meta_bbb-origin-version' => '3.7.4+ (Build: 20200117)', + 'meta_bbb-recording-description' => '', + 'meta_bbb-recording-name' => $bbactivity->name, + 'meta_bbb-origin-server-common-name' => '', + 'meta_bbb-context-name' => get_course($bbactivity->course)->fullname, + 'meta_bbb-context-id' => \context_course::instance($bbactivity->course)->instanceid, + 'meta_bbb-recording-tags' => '', + 'meta_bbb-origin' => 'Moodle', + 'meta_isBreakout' => 'false', + 'meta_bn-presenter-name' => fullname(core_user::get_support_user()), + ]; + + $recording = array_merge($recording, $record); // Get all other values. + + // Add the logs if not we won't find anything. + $this->create_log(['bigbluebuttonbnid' => $bbactivity->id, 'userid' => core_user::get_support_user()->id, + 'meta' => "{'record':true}"]); + + $this->bigbluebuttonbn_add_to_recordings_array_fetch($recording); + return $recording; + } + + /** + * Create a log record + * @param null $record + * @param array|null $options + * @throws dml_exception + */ + public function create_log($record = null, array $options = null) { + global $DB; + $record = (array) $record; + $bigbluebuttonbnid = $record['bigbluebuttonbnid']; + $bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', array('id' => $bigbluebuttonbnid)); + $default = [ + 'meetingid' => $bigbluebuttonbn->meetingid . '-' . $bigbluebuttonbn->course . '-' . $bigbluebuttonbn->id, + ]; + $record = array_merge($default, $record); + bigbluebuttonbn_log($bigbluebuttonbn, BIGBLUEBUTTONBN_LOG_EVENT_CREATE, $record); + } + + /** + * Manages fake recording so we can cut off the API call while testing + */ + + /** + * This the name of the $CFG entry to store the recording info in + */ + const FAKE_RECORDING_VAR_NAME = 'bbb_fake_recordings'; + + /** + * This add a new mocked up recording + * @param array $recording + * @throws dml_exception + */ + public function bigbluebuttonbn_add_to_recordings_array_fetch($recording) { + global $CFG; + $currentrecordings = get_config('mod_bigbluebuttonbn', static::FAKE_RECORDING_VAR_NAME); + if (!$currentrecordings) { + $currentrecordings = []; + } else { + $currentrecordings = unserialize($currentrecordings); + } + $currentrecordings[$recording['recordID']] = $recording; + set_config(static::FAKE_RECORDING_VAR_NAME, serialize($currentrecordings), 'mod_bigbluebuttonbn'); + } + + /** + * Method to fetch all mocked up recordings + * @param int $meetingsid + * @return array + * @throws dml_exception + */ + public static function bigbluebuttonbn_get_recordings_array_fetch($meetingsid) { + global $CFG; + $allrecordings = get_config('mod_bigbluebuttonbn', static::FAKE_RECORDING_VAR_NAME); + if (!$allrecordings) { + $allrecordings = []; + } + $allrecordings = unserialize($allrecordings); + return array_filter($allrecordings, + function($bbitem) use ($meetingsid) { + $meetingidrexp = "/{$bbitem['meetingID']}.*/"; + return !empty(preg_grep($meetingidrexp, $meetingsid)); + } + ); + } + + /** + * Clean local recording array (between tests) + */ + public function bigbluebuttonbn_clean_recordings_array_fetch() { + global $CFG; + set_config(static::FAKE_RECORDING_VAR_NAME, null, 'mod_bigbluebuttonbn'); + } +} diff --git a/mod/bigbluebuttonbn/tests/lib_test.php b/mod/bigbluebuttonbn/tests/lib_test.php new file mode 100644 index 0000000..6c52c36 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/lib_test.php @@ -0,0 +1,783 @@ +<?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/>. + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/lib.php'); + +/** + * BBB Library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class mod_bigbluebuttonbn_lib_testcase extends advanced_testcase { + /** + * @var testing_data_generator|null $generator + */ + public $generator = null; + /** + * @var object|null $bbactivity + */ + public $bbactivity = null; + /** + * @var object|null $course + */ + public $course = null; + + /** + * Convenience function to create a instance of an bigbluebuttonactivty. + * + * @param object|null $course course to add the module to + * @param array $params Array of parameters to pass to the generator + * @param array $options Array of options to pass to the generator + * @return array($context, $cm, $instance) Testable wrapper around the assign class. + * @throws coding_exception + * @throws moodle_exception + */ + protected function create_instance($course = null, $params = [], $options = []) { + if (!$course) { + $course = $this->course; + } + $params['course'] = $course->id; + $options['visible'] = 1; + $instance = $this->generator->create_module('bigbluebuttonbn', $params, $options); + list($course, $cm) = get_course_and_cm_from_instance($instance, 'bigbluebuttonbn'); + $context = context_module::instance($cm->id); + + return array($context, $cm, $instance); + } + + /** + * Get the corresponding form data + * + * @param object $bbactivity the current bigbluebutton activity + * @param object|null $course the course or null (taken from $this->course if null) + * @return mixed + * @throws coding_exception + */ + protected function get_form_data_from_instance($bbactivity, $course = null) { + global $USER; + if (!$course) { + $course = $this->course; + } + $currentuser = $USER; + $this->setAdminUser(); + $bbactivitycm = get_coursemodule_from_instance('bigbluebuttonbn', $bbactivity->id); + list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($bbactivitycm, $course); + $this->setUser($USER); + return $data; + } + + public function setUp(): void { + global $CFG; + parent::setUp(); + set_config('enablecompletion', true); // Enable completion for all tests. + $this->generator = $this->getDataGenerator(); + $this->course = $this->generator->create_course(['enablecompletion' => 1]); + } + + public function test_bigbluebuttonbn_supports() { + $this->resetAfterTest(); + $this->assertTrue(bigbluebuttonbn_supports(FEATURE_IDNUMBER)); + $this->assertTrue(bigbluebuttonbn_supports(FEATURE_MOD_INTRO)); + $this->assertFalse(bigbluebuttonbn_supports(FEATURE_GRADE_HAS_GRADE)); + } + + public function test_bigbluebuttonbn_get_completion_state() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $user = $this->generator->create_user(); + $this->setUser($user); + $result = bigbluebuttonbn_get_completion_state($this->course, $bbactivitycm, $user->id, COMPLETION_AND); + $this->assertEquals(COMPLETION_AND, $result); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['completionattendance' => 1, 'completionengagementchats' => 1, + 'completionengagementtalks' => 1]); + + // Add a couple of fake logs. + $overrides = array('meetingid' => $bbactivity->meetingid); + $meta = '{"origin":0, "data": {"duration": 120, "engagement": {"chats": 2, "talks":2} }}'; + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTON_LOG_EVENT_SUMMARY, $overrides, $meta); + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTON_LOG_EVENT_SUMMARY, $overrides, $meta); + $result = bigbluebuttonbn_get_completion_state($this->course, $bbactivitycm, $user->id, COMPLETION_AND); + $this->assertEquals(COMPLETION_AND, $result); + } + + public function test_bigbluebuttonbn_add_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $id = bigbluebuttonbn_add_instance($bbformdata); + $this->assertNotNull($id); + } + + public function test_bigbluebuttonbn_update_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $result = bigbluebuttonbn_update_instance($bbformdata); + $this->assertTrue($result); + } + + public function test_bigbluebuttonbn_delete_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $result = bigbluebuttonbn_delete_instance($bbactivity->id); + $this->assertTrue($result); + } + + public function test_bigbluebuttonbn_delete_instance_log() { + global $DB; + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + bigbluebuttonbn_delete_instance_log($bbactivity); + $this->assertTrue($DB->record_exists('bigbluebuttonbn_logs', array('bigbluebuttonbnid' => $bbactivity->id, + 'log' => BIGBLUEBUTTONBN_LOG_EVENT_DELETE))); + } + + public function test_bigbluebuttonbn_user_outline() { + $this->resetAfterTest(); + $user = $this->generator->create_user(); + $this->setUser($user); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $result = bigbluebuttonbn_user_outline($this->course, $user, null, $bbactivity); + $this->assertEquals('', $result); + + // Now create a couple of logs. + $overrides = array('meetingid' => $bbactivity->meetingid); + $meta = '{"origin":0}'; + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_JOIN, $overrides, $meta); + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_PLAYED, $overrides); + $result = bigbluebuttonbn_user_outline($this->course, $user, null, $bbactivity); + $this->assertRegExp('/.* has joined the session for 2 times/', $result); + } + + public function test_bigbluebuttonbn_user_complete() { + $this->resetAfterTest(); + $user = $this->generator->create_user(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $this->setUser($user); + $overrides = array('meetingid' => $bbactivity->meetingid); + $meta = '{"origin":0}'; + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_JOIN, $overrides, $meta); + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_PLAYED, $overrides); + $result = bigbluebuttonbn_user_complete($this->course, $user, $bbactivity); + $this->assertEquals(2, $result); + } + + public function test_bigbluebuttonbn_get_extra_capabilities() { + $this->resetAfterTest(); + $this->assertEquals(array('moodle/site:accessallgroups'), bigbluebuttonbn_get_extra_capabilities()); + } + + public function test_bigbluebuttonbn_reset_course_items() { + global $CFG; + $this->resetAfterTest(); + $CFG->bigbluebuttonbn_recordings_enabled = false; + $results = bigbluebuttonbn_reset_course_items(); + $this->assertEquals(array("events" => 0, "tags" => 0, "logs" => 0), $results); + $CFG->bigbluebuttonbn_recordings_enabled = true; + $results = bigbluebuttonbn_reset_course_items(); + $this->assertEquals(array("events" => 0, "tags" => 0, "logs" => 0, "recordings" => 0), $results); + } + + public function test_bigbluebuttonbn_reset_course_form_definition() { + global $CFG, $PAGE; + $PAGE->set_course($this->course); + $this->setAdminUser(); + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + include_once($CFG->dirroot . '/mod/bigbluebuttonbn/mod_form.php'); + $data = new stdClass(); + $data->instance = $bbactivity; + $data->id = $bbactivity->id; + $data->course = $bbactivity->course; + + $form = new mod_bigbluebuttonbn_mod_form($data, 1, $bbactivitycm, $this->course); + $refclass = new ReflectionClass("mod_bigbluebuttonbn_mod_form"); + $formprop = $refclass->getProperty('_form'); + $formprop->setAccessible(true); + + /* @var $mform MoodleQuickForm quickform object definition */ + $mform = $formprop->getValue($form); + bigbluebuttonbn_reset_course_form_definition($mform); + $this->assertNotNull($mform->getElement('bigbluebuttonbnheader')); + } + + public function test_bigbluebuttonbn_reset_course_form_defaults() { + global $CFG; + $this->resetAfterTest(); + $results = bigbluebuttonbn_reset_course_form_defaults($this->course); + $this->assertEquals(array( + 'reset_bigbluebuttonbn_events' => 0, + 'reset_bigbluebuttonbn_tags' => 0, + 'reset_bigbluebuttonbn_logs' => 0, + 'reset_bigbluebuttonbn_recordings' => 0, + ), $results); + } + + public function test_bigbluebuttonbn_reset_userdata() { + global $CFG; + $this->resetAfterTest(); + $data = new stdClass(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $data->courseid = $this->course->id; + $data->reset_bigbluebuttonbn_tags = true; + $data->reset_bigbluebuttonbn_tags = true; + $data->course = $bbactivity->course; + $results = bigbluebuttonbn_reset_userdata($data); + $this->assertEquals(array( + 'component' => 'BigBlueButtonBN', + 'item' => 'Deleted tags', + 'error' => false, + ), $results[0]); + } + + public function test_bigbluebuttonbn_reset_getstatus() { + $this->resetAfterTest(); + $result = bigbluebuttonbn_reset_getstatus('events'); + $this->assertEquals(array( + 'component' => 'BigBlueButtonBN', + 'item' => 'Deleted events', + 'error' => false, + ), $result); + } + + public function test_bigbluebuttonbn_reset_events() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance( + null, + ['openingtime' => time()] + ); + $formdata = $this->get_form_data_from_instance($bbactivity); + bigbluebuttonbn_process_post_save_event($formdata); + $this->assertEquals(1, $DB->count_records( + 'event', + array('modulename' => 'bigbluebuttonbn', 'courseid' => $this->course->id))); + bigbluebuttonbn_reset_events($this->course->id); + $this->assertEquals(0, $DB->count_records( + 'event', + array('modulename' => 'bigbluebuttonbn', 'courseid' => $this->course->id))); + } + + public function test_bigbluebuttonbn_reset_tags() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, + array('course' => $this->course->id), + ['visible' => true] + ); + core_tag_tag::add_item_tag('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id, $bbactivitycontext, 'newtag'); + $alltags = core_tag_tag::get_item_tags('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id); + $this->assertCount(1, $alltags); + bigbluebuttonbn_reset_tags($this->course->id); + $alltags = core_tag_tag::get_item_tags('mod_bigbluebuttonbn', 'bbitem', $bbactivity->id); + $this->assertCount(0, $alltags); + } + + public function test_bigbluebuttonbn_reset_logs() { + global $DB; + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(null, + array('course' => $this->course->id), + ['visible' => true] + ); + + // User has already joined the meeting (there is log event BIGBLUEBUTTONBN_LOG_EVENT_JOIN already for this user). + $overrides = array('meetingid' => $bbactivity->meetingid); + $meta = '{"origin":0}'; + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_JOIN, $overrides, $meta); + + bigbluebuttonbn_reset_logs($this->course->id); + $this->assertEquals(0, $DB->count_records( + 'bigbluebuttonbn_logs', + array('bigbluebuttonbnid' => $bbactivity->id, 'courseid' => $this->course->id))); + } + + public function test_bigbluebuttonbn_reset_recordings() { + $this->resetAfterTest(); + // TODO complete this test. + $this->markTestSkipped( + 'For now this test relies on an API call so we need to mock the API CALL.' + ); + } + + public function test_bigbluebuttonbn_get_view_actions() { + $this->resetAfterTest(); + $this->assertEquals(array('view', 'view all'), bigbluebuttonbn_get_view_actions()); + } + + public function test_bigbluebuttonbn_get_post_actions() { + $this->resetAfterTest(); + $this->assertEquals(array('update', 'add', 'delete'), bigbluebuttonbn_get_post_actions()); + } + + public function test_bigbluebuttonbn_print_overview() { + $this->resetAfterTest(); + + $this->setAdminUser(); // If not modules won't be visible. + list($bbactivitycontext, $bbactivitycm, $bbactivity1) = $this->create_instance(null, + array('course' => $this->course->id, 'openingtime' => time()), + ['visible' => true] + ); + + list($bbactivitycontext, $bbactivitycm, $bbactivity2) = $this->create_instance(null, + array('course' => $this->course->id, 'openingtime' => time()), + ['visible' => true] + ); + + $htmlarray = []; + bigbluebuttonbn_print_overview([$this->course->id => $this->course], $htmlarray); + $this->assertRegExp("/BigBlueButtonBN (1|2)/", $htmlarray[$this->course->id]['bigbluebuttonbn']); + } + + public function test_bigbluebuttonbn_print_overview_element() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + // We tweak the record as it should also contain all fields from the activity instance. + /* @var cm_info $bbactivitycm */ + $cmrecord = (object) array_merge((array) $bbactivity, (array) $bbactivitycm->get_course_module_record()); + $cmrecord->coursemodule = $bbactivity->id; + $str = bigbluebuttonbn_print_overview_element($cmrecord, time()); + $this->assertRegExp("/bigbluebuttonbn overview/", $str); + } + + public function test_bigbluebuttonbn_get_coursemodule_info() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $info = bigbluebuttonbn_get_coursemodule_info($bbactivitycm); + $this->assertEquals($info->name, $bbactivity->name); + } + + public function test_mod_bigbluebuttonbn_get_completion_active_rule_descriptions() { + $this->resetAfterTest(); + // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. + // Inspired from the same test in forum. + list($bbactivitycontext, $cm1, $bbactivity) = $this->create_instance($this->course, + ['completion' => '2', 'completionsubmit' => '1']); + list($bbactivitycontext, $cm2, $bbactivity) = $this->create_instance($this->course, + ['completion' => '2', 'completionsubmit' => '0']); + + // Data for the stdClass input type. + // This type of input would occur when checking the default completion rules for an activity type, where we don't have + // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. + $moddefaults = (object) [ + 'customdata' => [ + 'customcompletionrules' => [ + 'completionsubmit' => '1', + ], + ], + 'completion' => 2, + ]; + + $activeruledescriptions = [get_string('completionsubmit', 'assign')]; + // TODO: check the return value here as there might be an issue with the function compared to the forum for example. + /* + $this->assertEquals(mod_bigbluebuttonbn_get_completion_active_rule_descriptions($cm1), $activeruledescriptions); + $this->assertEquals(mod_bigbluebuttonbn_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); + */ + + $this->assertEquals(mod_bigbluebuttonbn_get_completion_active_rule_descriptions($cm2), []); + $this->assertEquals(mod_bigbluebuttonbn_get_completion_active_rule_descriptions(new stdClass()), []); + + } + + public function test_bigbluebuttonbn_process_pre_save() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->participants = '<p>this -> "</p>\n'; + $bbformdata->timemodified = time(); + bigbluebuttonbn_process_pre_save($bbformdata); + $this->assertTrue($bbformdata->timemodified != 0); + $this->assertEquals('<p>this -> "</p>\n', $bbformdata->participants); + } + + public function test_bigbluebuttonbn_process_pre_save_instance() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->instance = 0; + $bbformdata->timemodified = time(); + bigbluebuttonbn_process_pre_save_instance($bbformdata); + $this->assertTrue($bbformdata->timemodified == 0); + } + + public function test_bigbluebuttonbn_process_pre_save_checkboxes() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + unset($bbformdata->wait); + unset($bbformdata->recordallfromstart); + bigbluebuttonbn_process_pre_save_checkboxes($bbformdata); + $this->assertTrue(isset($bbformdata->wait)); + $this->assertTrue(isset($bbformdata->recordallfromstart)); + } + + public function test_bigbluebuttonbn_process_pre_save_common() { + global $CFG; + require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + $this->resetAfterTest(); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + + $bbformdata->groupmode = '1'; + bigbluebuttonbn_process_pre_save_common($bbformdata); + $this->assertEquals(0, $bbformdata->groupmode); + } + + public function test_bigbluebuttonbn_process_post_save() { + global $CFG; + require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + $this->resetAfterTest(); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + + // Enrol users in a course so he will receive the message. + $teacher = $this->generator->create_user(['role' => 'editingteacher']); + $this->generator->enrol_user($teacher->id, $this->course->id); + + // Mark the form to trigger notification. + $bbformdata->notification = true; + $messagesink = $this->redirectMessages(); + bigbluebuttonbn_process_post_save($bbformdata); + // Now run cron. + ob_start(); + $this->runAdhocTasks(); + ob_get_clean(); // Suppress output as it can fail the test. + $this->assertEquals(1, $messagesink->count()); + } + + public function test_bigbluebuttonbn_process_post_save_notification() { + global $CFG; + require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + $this->resetAfterTest(); + + list($bbactivitycontext, $bbactivitycm, $bbactivity) = + $this->create_instance(null, ['type' => BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY]); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->add = "1"; + $messagesink = $this->redirectMessages(); + // Enrol users in a course so he will receive the message. + $teacher = $this->generator->create_user(['role' => 'editingteacher']); + $this->generator->enrol_user($teacher->id, $this->course->id); + + bigbluebuttonbn_process_post_save_notification($bbformdata); + // Now run cron. + ob_start(); + $this->runAdhocTasks(); + ob_get_clean(); // Suppress output as it can fail the test. + $this->assertEquals(1, $messagesink->count()); + } + + public function test_bigbluebuttonbn_process_post_save_event() { + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $eventsink = $this->redirectEvents(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $bbformdata->openingtime = time(); + bigbluebuttonbn_process_post_save_event($bbformdata); + $this->assertNotEmpty($eventsink->get_events()); + } + + public function test_bigbluebuttonbn_process_post_save_completion() { + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $eventsink = $this->redirectEvents(); + $bbformdata->completionexpected = 1; + bigbluebuttonbn_process_post_save_completion($bbformdata); + $this->assertNotEmpty($eventsink->get_events()); + } + + public function test_bigbluebuttonbn_get_media_file() { + $this->resetAfterTest(); + $user = $this->generator->create_user(); + $this->setUser($user); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $mediafilepath = bigbluebuttonbn_get_media_file($bbformdata); + $this->assertEmpty($mediafilepath); + + // From test_delete_original_file_from_draft (lib/test/filelib_test.php) + // Create a bbb private file. + $bbbfilerecord = new stdClass; + $bbbfilerecord->contextid = context_module::instance($bbformdata->coursemodule)->id; + $bbbfilerecord->component = 'mod_bigbluebuttonbn'; + $bbbfilerecord->filearea = 'presentation'; + $bbbfilerecord->itemid = 0; + $bbbfilerecord->filepath = '/'; + $bbbfilerecord->filename = 'bbfile.pptx'; + $bbbfilerecord->source = 'test'; + $fs = get_file_storage(); + $bbbfile = $fs->create_file_from_string($bbbfilerecord, 'Presentation file content'); + file_prepare_draft_area($bbformdata->presentation, + context_module::instance($bbformdata->coursemodule)->id, + 'mod_bigbluebuttonbn', + 'presentation', 0); + + $mediafilepath = bigbluebuttonbn_get_media_file($bbformdata); + $this->assertEquals('/bbfile.pptx', $mediafilepath); + } + + public function test_bigbluebuttonbn_pluginfile() { + $this->resetAfterTest(); + $this->markTestSkipped( + 'For now this test on send file and it should be mocked to avoid the real API CALL.' + ); + + /* + $mediafilepath = bigbluebuttonbn_pluginfile($this->course, $bbactivitycm, context_module::instance($bbactivitycm->id), + 'presentation', ['bbfile.pptx'], false, ['preview'=>true, 'dontdie'=>true]); + $this->assertEquals('/bbfile.pptx', $mediafilepath); + */ + } + + public function test_bigbluebuttonbn_pluginfile_valid() { + $this->resetAfterTest(); + $this->assertFalse(bigbluebuttonbn_pluginfile_valid(context_course::instance($this->course->id), 'presentation')); + $this->assertTrue(bigbluebuttonbn_pluginfile_valid(context_system::instance(), 'presentation')); + $this->assertFalse(bigbluebuttonbn_pluginfile_valid(context_system::instance(), 'otherfilearea')); + } + + public function test_bigbluebuttonbn_pluginfile_file() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $user = $this->generator->create_user(); + $this->setUser($user); + $this->generator->enrol_user($user->id, $this->course->id, 'editingteacher'); + // From test_delete_original_file_from_draft (lib/test/filelib_test.php) + // Create a bbb private file. + $bbformdata = $this->get_form_data_from_instance($bbactivity); + $context = context_module::instance($bbformdata->coursemodule); + + $bbbfilerecord = new stdClass; + $bbbfilerecord->contextid = $context->id; + $bbbfilerecord->component = 'mod_bigbluebuttonbn'; + $bbbfilerecord->filearea = 'presentation'; + $bbbfilerecord->itemid = 0; + $bbbfilerecord->filepath = '/'; + $bbbfilerecord->filename = 'bbfile.pptx'; + $bbbfilerecord->source = 'test'; + $fs = get_file_storage(); + $bbbfile = $fs->create_file_from_string($bbbfilerecord, 'Presentation file content'); + file_prepare_draft_area($bbformdata->presentation, + context_module::instance($bbformdata->coursemodule)->id, + 'mod_bigbluebuttonbn', + 'presentation', 0); + list($course, $bbactivitycmuser) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); + /** @var stored_file $mediafile */ + $mediafile = bigbluebuttonbn_pluginfile_file($this->course, $bbactivitycmuser, $context, 'presentation', ['bbfile.pptx']); + $this->assertEquals('bbfile.pptx', $mediafile->get_filename()); + } + + public function test_bigbluebuttonbn_default_presentation_get_file() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $user = $this->generator->create_user(); + $this->setUser($user); + $this->generator->enrol_user($user->id, $this->course->id, 'editingteacher'); + $bbformdata = $this->get_form_data_from_instance($bbactivity); + // From test_delete_original_file_from_draft (lib/test/filelib_test.php) + // Create a bbb private file. + $context = context_module::instance($bbformdata->coursemodule); + list($course, $bbactivitycmuser) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); + $mediafile = bigbluebuttonbn_default_presentation_get_file($this->course, $bbactivitycmuser, $context, ['presentation'], + '/bbfile.pptx'); + $this->assertEquals('presentation', $mediafile); + } + + public function test_bigbluebuttonbn_pluginfile_filename() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $user = $this->generator->create_user(); + $this->setUser($user); + $this->generator->enrol_user($user->id, $this->course->id, 'editingteacher'); + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'presentation_cache'); + $noncekey = sha1($bbactivity->id); + $presentationnonce = $cache->get($noncekey); + $filename = bigbluebuttonbn_pluginfile_filename($this->course, $bbactivitycm, $bbactivitycontext, + [$presentationnonce, 'bbfile.pptx']); + $this->assertEquals('bbfile.pptx', $filename); + } + + public function test_bigbluebuttonbn_get_file_areas() { + $this->resetAfterTest(); + $this->assertEquals(array( + 'presentation' => 'Presentation content', + 'presentationdefault' => 'Presentation default content', + ), bigbluebuttonbn_get_file_areas()); + } + + public function test_bigbluebuttonbn_view() { + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance([], + array('completion' => 2, 'completionview' => 1)); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + bigbluebuttonbn_view($bbactivity, $this->course, $bbactivitycm, context_module::instance($bbactivitycm->id)); + + $events = $sink->get_events(); + $this->assertCount(3, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_bigbluebuttonbn\event\activity_viewed', $event); + $this->assertEquals($bbactivitycontext, $event->get_context()); + $url = new \moodle_url('/mod/bigbluebuttonbn/view.php', array('id' => $bbactivitycontext->instanceid)); + $this->assertEquals($url, $event->get_url()); + $this->assertEventContextNotUsed($event); + $this->assertNotEmpty($event->get_name()); + + // Check completion status. + $completion = new completion_info($this->course); + $completiondata = $completion->get_data($bbactivitycm); + $this->assertEquals(1, $completiondata->completionstate); + } + + public function test_bigbluebuttonbn_check_updates_since() { + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $result = bigbluebuttonbn_check_updates_since($bbactivitycm, 0); + $this->assertEquals( + '{"configuration":{"updated":false},"contentfiles":{"updated":false},"introfiles":{"updated":false},"completion":{"updated":false}}', + json_encode($result) + ); + } + + public function test_mod_bigbluebuttonbn_get_fontawesome_icon_map() { + $this->resetAfterTest(); + $this->assertEquals(array('mod_bigbluebuttonbn:icon' => 'icon-bigbluebutton'), + mod_bigbluebuttonbn_get_fontawesome_icon_map()); + } + + public function test_mod_bigbluebuttonbn_core_calendar_provide_event_action() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + + // Standard use case, the meeting start and we want add an action event to join the meeting. + $event = $this->create_action_event($this->course, $bbactivity, BIGBLUEBUTTON_EVENT_MEETING_START); + $factory = new \core_calendar\action_factory(); + $actionevent = mod_bigbluebuttonbn_core_calendar_provide_event_action($event, $factory); + $this->assertEquals("Join session", $actionevent->get_name()); + + // User has already joined the meeting (there is log event BIGBLUEBUTTONBN_LOG_EVENT_JOIN already for this user). + $overrides = array('meetingid' => $bbactivity->meetingid); + $meta = '{"origin":0}'; + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_JOIN, $overrides, $meta); + $bbactivity->closingtime = time() - 1000; + $bbactivity->openingtime = time() - 2000; + $DB->update_record('bigbluebuttonbn', $bbactivity); + $event = $this->create_action_event($this->course, $bbactivity, BIGBLUEBUTTON_EVENT_MEETING_START); + $actionevent = mod_bigbluebuttonbn_core_calendar_provide_event_action($event, $factory); + $this->assertNull($actionevent); + } + + /** + * Creates an action event. + * + * @param \stdClass $course The course the bigbluebutton activity is in + * @param object $bbbactivity The bigbluebutton activity to create an event for + * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE. + * @return bool|calendar_event + * @throws coding_exception + */ + private function create_action_event($course, $bbbactivity, $eventtype) { + $event = new stdClass(); + $event->name = 'Calendar event'; + $event->modulename = 'bigbluebuttonbn'; + $event->courseid = $course->id; + $event->instance = $bbbactivity->id; + $event->type = CALENDAR_EVENT_TYPE_ACTION; + $event->eventtype = $eventtype; + $event->timestart = time(); + + return calendar_event::create($event); + } + + public function test_bigbluebuttonbn_log() { + global $DB; + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + bigbluebuttonbn_log($bbactivity, BIGBLUEBUTTONBN_LOG_EVENT_PLAYED); + $this->assertTrue($DB->record_exists('bigbluebuttonbn_logs', array('bigbluebuttonbnid' => $bbactivity->id))); + } + + public function test_bigbluebuttonbn_extend_settings_navigation_admin() { + global $PAGE, $CFG; + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $CFG->bigbluebuttonbn_meetingevents_enabled = true; + + $PAGE->set_cm($bbactivitycm); + $PAGE->set_context(context_module::instance($bbactivitycm->id)); + $PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycm->id]); + $settingnav = $PAGE->settingsnav; + + $this->setAdminUser(); + $node = navigation_node::create('testnavigationnode'); + bigbluebuttonbn_extend_settings_navigation($settingnav, $node); + $this->assertCount(1, $node->get_children_key_list()); + } + + public function test_bigbluebuttonbn_extend_settings_navigation_user() { + global $PAGE, $CFG; + $this->resetAfterTest(); + list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(); + $user = $this->generator->create_user(); + $this->setUser($user); + list($course, $bbactivitycmuser) = get_course_and_cm_from_instance($bbactivity->id, 'bigbluebuttonbn'); + + $CFG->bigbluebuttonbn_meetingevents_enabled = true; + + $PAGE->set_cm($bbactivitycmuser); + $PAGE->set_context(context_module::instance($bbactivitycm->id)); + $PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $bbactivitycm->id]); + + $settingnav = $PAGE->settingsnav; + $node = navigation_node::create('testnavigationnode'); + bigbluebuttonbn_extend_settings_navigation($settingnav, $node); + $this->assertCount(0, $node->get_children_key_list()); + } +} + + diff --git a/mod/bigbluebuttonbn/tests/locallib_test.php b/mod/bigbluebuttonbn/tests/locallib_test.php new file mode 100644 index 0000000..cc77008 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/locallib_test.php @@ -0,0 +1,167 @@ +<?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/>. + +/** + * Local library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Local library tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Laurent David (laurent@call-learning.fr) + */ +class mod_bigbluebuttonbn_locallib_testcase extends advanced_testcase { + /** + * Clean the temporary mocked up recordings + * + * @throws coding_exception + */ + public function tearDown(): void { + parent::tearDown(); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->bigbluebuttonbn_clean_recordings_array_fetch(); + } + + /** + * Test for provider::get_metadata(). + */ + public function test_bigbluebuttonbn_get_recording_type_text() { + $this->resetAfterTest(true); + $this->assertEquals('Presentation', bigbluebuttonbn_get_recording_type_text('presentation')); + $this->assertEquals('Video', bigbluebuttonbn_get_recording_type_text('video')); + $this->assertEquals('Videos', bigbluebuttonbn_get_recording_type_text('videos')); + $this->assertEquals('Whatever', bigbluebuttonbn_get_recording_type_text('whatever')); + $this->assertEquals('Whatever It Can Be', bigbluebuttonbn_get_recording_type_text('whatever it can be')); + } + + public function test_bigbluebuttonbn_get_users_select_separate_groups_prevent_all() { + $this->resetAfterTest(); + $numstudents = 12; + $numteachers = 3; + $groupsnum = 3; + + list($course, $groups, $students, $teachers, $bbactivity, $roleids) = + $this->setup_course_students_teachers( + ['enablecompletion' => true, 'groupmode' => strval(SEPARATEGROUPS), 'groupmodeforce' => 1], + $numstudents, $numteachers, $groupsnum); + $context = context_course::instance($course->id); + // Prevent access all groups. + role_change_permission($roleids['teacher'], $context, 'moodle/site:accessallgroups', CAP_PREVENT); + $this->setUser($teachers[0]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); + $this->setUser($teachers[1]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); + $this->setUser($teachers[2]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount(($numstudents + $numteachers) / $groupsnum, $users); + $course->groupmode = strval(SEPARATEGROUPS); + $course->groupmodeforce = "0"; + update_course($course); + $this->setUser($teachers[2]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + + } + + public function test_bigbluebuttonbn_get_users_select_separate_groups() { + $this->resetAfterTest(); + $numstudents = 12; + $numteachers = 3; + $groupsnum = 3; + list($course, $groups, $students, $teachers, $bbactivity, $roleids) = + $this->setup_course_students_teachers( + ['enablecompletion' => true, 'groupmode' => strval(VISIBLEGROUPS), 'groupmodeforce' => 1], + $numstudents, $numteachers, $groupsnum); + + $context = context_course::instance($course->id); + $this->setUser($teachers[0]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + $this->setUser($teachers[1]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + $this->setUser($teachers[1]); + $users = bigbluebuttonbn_get_users_select($context, $bbactivity); + $this->assertCount($numstudents + $numteachers, $users); + } + + /** + * Generate a course, several students and several groups + * + * @param object $courserecord + * @param int $numstudents + * @param int $numteachers + * @param int $groupsnum + * @return array + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + protected function setup_course_students_teachers($courserecord, $numstudents, $numteachers, $groupsnum) { + global $DB; + $generator = $this->getDataGenerator(); + $course = $generator->create_course($courserecord); + $groups = []; + for ($i = 0; $i < $groupsnum; $i++) { + $groups[] = $generator->create_group(array('courseid' => $course->id)); + } + $group1 = $generator->create_group(array('courseid' => $course->id)); + $group2 = $generator->create_group(array('courseid' => $course->id)); + + $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); + + $students = []; + for ($i = 0; $i < $numstudents; $i++) { + $student = $generator->create_user(); + $generator->enrol_user($student->id, $course->id, $roleids['student']); + $groupid = $groups[$i % $groupsnum]->id; + groups_add_member($groupid, $student->id); + $students[] = $student; + } + + $teachers = []; + for ($i = 0; $i < $numteachers; $i++) { + $teacher = $generator->create_user(); + $generator->enrol_user($teacher->id, $course->id, $roleids['teacher']); + $groupid = $groups[$i % $groupsnum]->id; + groups_add_member($groupid, $teacher->id); + $teachers[] = $teacher; + } + $bbactivity = $generator->create_module( + 'bigbluebuttonbn', + array('course' => $course->id), + ['visible' => true]); + + get_fast_modinfo(0, 0, true); + return array($course, $groups, $students, $teachers, $bbactivity, $roleids); + } +} + diff --git a/mod/bigbluebuttonbn/tests/privacy_provider_test.php b/mod/bigbluebuttonbn/tests/privacy_provider_test.php new file mode 100644 index 0000000..0e226b7 --- /dev/null +++ b/mod/bigbluebuttonbn/tests/privacy_provider_test.php @@ -0,0 +1,311 @@ +<?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 provider tests. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_privacy\tests\provider_testcase; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use mod_bigbluebuttonbn\privacy\provider; + +if (!class_exists("\\core_privacy\\tests\\provider_testcase", true)) { + die(); +} + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/lib.php'); + +/** + * Privacy provider tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class mod_bigbluebuttonbn_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + /** + * Setup Course + */ + + + /** + * Clean the temporary mocked up recordings + * + * @throws coding_exception + */ + public function tearDown(): void { + parent::tearDown(); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn')->bigbluebuttonbn_clean_recordings_array_fetch(); + } + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $this->resetAfterTest(true); + + $collection = new collection('mod_bigbluebuttonbn'); + $newcollection = \mod_bigbluebuttonbn\privacy\provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(3, $itemcollection); + + $instancetable = array_shift($itemcollection); + $this->assertEquals('bigbluebuttonbn', $instancetable->get_name()); + + $instancelogstable = array_shift($itemcollection); + $this->assertEquals('bigbluebuttonbn_logs', $instancelogstable->get_name()); + + $bigbluebuttonserver = array_shift($itemcollection); + $this->assertEquals('bigbluebutton', $bigbluebuttonserver->get_name()); + + $privacyfields = $instancetable->get_privacy_fields(); + $this->assertArrayHasKey('participants', $privacyfields); + $this->assertEquals('privacy:metadata:bigbluebuttonbn', $instancetable->get_summary()); + + $privacyfields = $instancelogstable->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + $this->assertArrayHasKey('meetingid', $privacyfields); + $this->assertArrayHasKey('log', $privacyfields); + $this->assertArrayHasKey('meta', $privacyfields); + $this->assertEquals('privacy:metadata:bigbluebuttonbn_logs', $instancelogstable->get_summary()); + + $privacyfields = $bigbluebuttonserver->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('fullname', $privacyfields); + $this->assertEquals('privacy:metadata:bigbluebutton', $bigbluebuttonserver->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + $bigbluebuttonbn = $e['instance']; + $course = $e['course']; + + // Another bigbluebuttonbn activity that has no user activity. + $this->getDataGenerator()->create_module('bigbluebuttonbn', array('course' => $course)); + + // Create a user which will make a submission. + $user = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user->id]); + + // Check the contexts supplied are correct. + $contextlist = \mod_bigbluebuttonbn\privacy\provider::get_contexts_for_userid($user->id); + $this->assertCount(1, $contextlist); + + $contextformodule = $contextlist->current(); + $cmcontext = context_module::instance($bigbluebuttonbn->cmid); + $this->assertEquals($cmcontext->id, $contextformodule->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_logs() { + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + $bigbluebuttonbn = $e['instance']; + + // Create users which will make submissions. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user2->id]); + + // Export all of the data for the context for user 1. + $cmcontext = context_module::instance($bigbluebuttonbn->cmid); + $this->export_context_data_for_user($user1->id, $cmcontext, 'mod_bigbluebuttonbn'); + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + + $this->assertTrue($writer->has_any_data()); + + $data = $writer->get_data(); + $this->assertCount(2, $data->logs); + } + + /** + * Test that only users with relevant contexts are fetched. + */ + public function test_get_users_in_context() { + // For backward compatibility with old versions of Moodle. + if (!class_exists('\core_privacy\local\request\userlist')) { + return; + } + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + $bigbluebuttonbn = $e['instance']; + + // Users which will make submissions. + $user1 = $e['users'][0]; + $user2 = $e['users'][1]; + + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user1->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $bigbluebuttonbn->id, 'userid' => $user2->id]); + + // Export all of the data for the context for user 1. + $cmcontext = context_module::instance($bigbluebuttonbn->cmid); + + $userlist = new \core_privacy\local\request\userlist($cmcontext, 'mod_bigbluebuttonbn'); + \mod_bigbluebuttonbn\privacy\provider::get_users_in_context($userlist); + + // Ensure correct users are found in relevant contexts. + $this->assertCount(2, $userlist); + $expected = [intval($user1->id), intval($user2->id)]; + $actual = $userlist->get_userids(); + $this->assertEquals(sort($expected), sort($actual)); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + + // Before deletion, we should have 3 responses, 1 Add event and 2 Create events (1 per user). + $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id]); + $this->assertEquals(3, $count); + + // Delete data based on context. + $cmcontext = context_module::instance($e['instance']->cmid); + \mod_bigbluebuttonbn\privacy\provider::delete_data_for_all_users_in_context($cmcontext); + + // After deletion, the bigbluebuttonbn logs for that activity should have been deleted. + $count = $DB->count_records('bigbluebuttonbn_logs', ['bigbluebuttonbnid' => $e['instance']->id]); + $this->assertEquals(0, $count); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + + // Delete data for the first user. + $context = \context_module::instance($e['instance']->cmid); + $contextlist = new \core_privacy\local\request\approved_contextlist($e['users'][0], 'bigbluebuttonbn', + [$context->id]); + \mod_bigbluebuttonbn\privacy\provider::delete_data_for_user($contextlist); + + // After deletion the bigbluebuttonbn logs for the first user should have been deleted. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); + $this->assertEquals(0, $count); + + // Check the logs for the other user is still there. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); + $this->assertEquals(1, $count); + } + + /** + * Test that data for users in approved userlist is deleted. + */ + public function test_delete_data_for_users() { + global $DB; + + // For backward compatibility with old versions of Moodle. + if (!class_exists('\core_privacy\local\request\approved_userlist')) { + return; + } + + $this->resetAfterTest(); + + $e = $this->get_bigbluebuttonbn_environemnt(); + + // Delete user 1 and 2 data from chat 1 context only. + $context = \context_module::instance($e['instance']->cmid); + $approveduserids = [$e['users'][0]->id]; + $approvedlist = new \core_privacy\local\request\approved_userlist($context, 'mod_bigbluebuttonbn', $approveduserids); + \mod_bigbluebuttonbn\privacy\provider::delete_data_for_users($approvedlist); + + // After deletion the bigbluebuttonbn logs for the first user should have been deleted. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); + $this->assertEquals(0, $count); + + // Check the logs for the other user is still there. + $count = $DB->count_records('bigbluebuttonbn_logs', + ['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); + $this->assertEquals(1, $count); + } + + /** + * Prepares the environment for testing. + * + * @return array $e + */ + protected function get_bigbluebuttonbn_environemnt() { + $e = array(); + + // Create a course. + $e['course'] = $this->getDataGenerator()->create_course(); + + // Create a bigbluebuttonbn instance. + $e['instance'] = $this->getDataGenerator()->create_module('bigbluebuttonbn', + array('course' => $e['course']->id)); + + // Create users that will use the bigbluebuttonbn instance. + $e['users'][] = $this->getDataGenerator()->create_user(); + $e['users'][] = $this->getDataGenerator()->create_user(); + + // Create the bigbluebuttonbn logs. + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][0]->id]); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->create_log(['bigbluebuttonbnid' => $e['instance']->id, 'userid' => $e['users'][1]->id]); + + return $e; + } +} diff --git a/mod/bigbluebuttonbn/tests/recordings_test.php b/mod/bigbluebuttonbn/tests/recordings_test.php new file mode 100644 index 0000000..90839db --- /dev/null +++ b/mod/bigbluebuttonbn/tests/recordings_test.php @@ -0,0 +1,118 @@ +<?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 provider tests. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/lib.php'); +require_once($CFG->dirroot . '/mod/bigbluebuttonbn/locallib.php'); + +/** + * Privacy provider tests class. + * + * @package mod_bigbluebuttonbn + * @copyright 2018 - present, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + */ +class mod_bigbluebuttonbn_recordings_testcase extends advanced_testcase { + + /** + * @var array of courses + */ + public $courses = []; + /** + * @var array of activities (bbb) + */ + public $bbactivities = []; + /** + * Model to build + */ + const BB_ACTIVITIES = [ + 'BBACTIVITY1' => ['courseindex' => 0, 'type' => BIGBLUEBUTTONBN_TYPE_ALL, 'nbrecordings' => 2], + 'BBACTIVITY2' => ['courseindex' => 0, 'type' => BIGBLUEBUTTONBN_TYPE_ALL, 'nbrecordings' => 3], + 'BBACTIVITY3' => ['courseindex' => 1, 'type' => BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY, 'nbrecordings' => 3], + ]; + + public function setUp(): void { + parent::setUp(); + + $maxcourseindexindex = array_reduce( + static::BB_ACTIVITIES, + function($acc, $item) { + return $acc > $item['courseindex'] ? $acc : $item['courseindex']; + }, + 0 + ); + for ($i = 0; $i <= $maxcourseindexindex; $i++) { + $this->courses[] = $this->getDataGenerator()->create_course(); + } + $bbngenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn'); + /* @var $bbngenerator mod_bigbluebuttonbn_generator */ + foreach (static::BB_ACTIVITIES as $aname => $activity) { + $bbactivity = $bbngenerator->create_instance( + [ + 'course' => $this->courses[$activity['courseindex']]->id, + 'type' => $activity['type'], + 'name' => $aname + ] + ); + for ($nbrecordings = 0; $nbrecordings < $activity['nbrecordings']; $nbrecordings++) { + $this->getDataGenerator() + ->get_plugin_generator('mod_bigbluebuttonbn') + ->create_recording(['bigbluebuttonbnid' => $bbactivity->id]); + } + $this->bbactivities[] = $bbactivity; + } + } + + /** + * Clean the temporary mocked up recordings + * + * @throws coding_exception + */ + public function tearDown(): void { + parent::tearDown(); + $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn') + ->bigbluebuttonbn_clean_recordings_array_fetch(); + } + + /** + * Test for bigbluebuttonbn_get_allrecordings(). + */ + public function test_bigbluebuttonbn_get_allrecordings() { + $this->resetAfterTest(); + + $recordings = bigbluebuttonbn_get_allrecordings($this->bbactivities[0]->course, $this->bbactivities[0]->id); + $this->assertCount(2, $recordings); + + $recordings = bigbluebuttonbn_get_allrecordings($this->bbactivities[1]->course, $this->bbactivities[1]->id); + $this->assertCount(3, $recordings); + + $recordings = bigbluebuttonbn_get_allrecordings($this->bbactivities[2]->course, $this->bbactivities[2]->id); + $this->assertCount(3, $recordings); + + } +} diff --git a/mod/bigbluebuttonbn/thirdpartylibs.xml b/mod/bigbluebuttonbn/thirdpartylibs.xml new file mode 100644 index 0000000..fa363a1 --- /dev/null +++ b/mod/bigbluebuttonbn/thirdpartylibs.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<libraries> + <library> + <location>vendor/firebase/php-jwt</location> + <name>PHP-JWT is used by BigBlueButtonBN for encoding and decoding callbacks that include JSON Web Tokens (JWT). The library is bundled with the plugin for backward compatibility but is used only with Moodle 3.2 to 3.6</name> + <version>4.0.0</version> + <license>BSD</license> + <licenseversion>3-Clause</licenseversion> + </library> +</libraries> diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/LICENSE b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/LICENSE new file mode 100644 index 0000000..cb0c49b --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2011, Neuman Vong + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Neuman Vong nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/README.md b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/README.md new file mode 100644 index 0000000..d4589b1 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/README.md @@ -0,0 +1,119 @@ +[](https://travis-ci.org/firebase/php-jwt) +[](https://packagist.org/packages/firebase/php-jwt) +[](https://packagist.org/packages/firebase/php-jwt) +[](https://packagist.org/packages/firebase/php-jwt) + +PHP-JWT +======= +A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). + +Installation +------------ + +Use composer to manage your dependencies and download PHP-JWT: + +```bash +composer require firebase/php-jwt +``` + +Example +------- +```php +<?php +use \Firebase\JWT\JWT; + +$key = "example_key"; +$token = array( + "iss" => "http://example.org", + "aud" => "http://example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +/** + * IMPORTANT: + * You must specify supported algorithms for your application. See + * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 + * for a list of spec-compliant algorithms. + */ +$jwt = JWT::encode($token, $key); +$decoded = JWT::decode($jwt, $key, array('HS256')); + +print_r($decoded); + +/* + NOTE: This will now be an object instead of an associative array. To get + an associative array, you will need to cast it as such: +*/ + +$decoded_array = (array) $decoded; + +/** + * You can add a leeway to account for when there is a clock skew times between + * the signing and verifying servers. It is recommended that this leeway should + * not be bigger than a few minutes. + * + * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef + */ +JWT::$leeway = 60; // $leeway in seconds +$decoded = JWT::decode($jwt, $key, array('HS256')); + +?> +``` + +Changelog +--------- + +#### 4.0.0 / 2016-07-17 +- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! +- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! +- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! +- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! + +#### 3.0.0 / 2015-07-22 +- Minimum PHP version updated from `5.2.0` to `5.3.0`. +- Add `\Firebase\JWT` namespace. See +[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to +[@Dashron](https://github.com/Dashron)! +- Require a non-empty key to decode and verify a JWT. See +[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to +[@sjones608](https://github.com/sjones608)! +- Cleaner documentation blocks in the code. See +[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to +[@johanderuijter](https://github.com/johanderuijter)! + +#### 2.2.0 / 2015-06-22 +- Add support for adding custom, optional JWT headers to `JWT::encode()`. See +[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to +[@mcocaro](https://github.com/mcocaro)! + +#### 2.1.0 / 2015-05-20 +- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew +between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! +- Add support for passing an object implementing the `ArrayAccess` interface for +`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! + +#### 2.0.0 / 2015-04-01 +- **Note**: It is strongly recommended that you update to > v2.0.0 to address + known security vulnerabilities in prior versions when both symmetric and + asymmetric keys are used together. +- Update signature for `JWT::decode(...)` to require an array of supported + algorithms to use when verifying token signatures. + + +Tests +----- +Run the tests using phpunit: + +```bash +$ pear install PHPUnit +$ phpunit --configuration phpunit.xml.dist +PHPUnit 3.7.10 by Sebastian Bergmann. +..... +Time: 0 seconds, Memory: 2.50Mb +OK (5 tests, 5 assertions) +``` + +License +------- +[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.json b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.json new file mode 100644 index 0000000..1a5e93b --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.json @@ -0,0 +1,27 @@ +{ + "name": "firebase/php-jwt", + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "license": "BSD-3-Clause", + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "minimum-stability": "dev" +} diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.lock b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.lock new file mode 100644 index 0000000..5518ae4 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/composer.lock @@ -0,0 +1,19 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "60a5df5d283a7ae9000173248eba8909", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.2.0" + }, + "platform-dev": [] +} diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/package.xml b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/package.xml new file mode 100644 index 0000000..a95b056 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/package.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<package packagerversion="1.9.2" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 + http://pear.php.net/dtd/tasks-1.0.xsd + http://pear.php.net/dtd/package-2.0 + http://pear.php.net/dtd/package-2.0.xsd"> + <name>JWT</name> + <channel>pear.php.net</channel> + <summary>A JWT encoder/decoder.</summary> + <description>A JWT encoder/decoder library for PHP.</description> + <lead> + <name>Neuman Vong</name> + <user>lcfrs</user> + <email>neuman+pear@twilio.com</email> + <active>yes</active> + </lead> + <lead> + <name>Firebase Operations</name> + <user>firebase</user> + <email>operations@firebase.com</email> + <active>yes</active> + </lead> + <date>2015-07-22</date> + <version> + <release>3.0.0</release> + <api>3.0.0</api> + </version> + <stability> + <release>beta</release> + <api>beta</api> + </stability> + <license uri="http://opensource.org/licenses/BSD-3-Clause">BSD 3-Clause License</license> + <notes> +Initial release with basic support for JWT encoding, decoding and signature verification. + </notes> + <contents> + <dir baseinstalldir="/" name="/"> + <dir name="tests"> + <file name="JWTTest.php" role="test" /> + </dir> + <file name="Authentication/JWT.php" role="php" /> + </dir> + </contents> + <dependencies> + <required> + <php> + <min>5.1</min> + </php> + <pearinstaller> + <min>1.7.0</min> + </pearinstaller> + <extension> + <name>json</name> + </extension> + <extension> + <name>hash</name> + </extension> + </required> + </dependencies> + <phprelease /> + <changelog> + <release> + <version> + <release>0.1.0</release> + <api>0.1.0</api> + </version> + <stability> + <release>beta</release> + <api>beta</api> + </stability> + <date>2015-04-01</date> + <license uri="http://opensource.org/licenses/BSD-3-Clause">BSD 3-Clause License</license> + <notes> +Initial release with basic support for JWT encoding, decoding and signature verification. + </notes> + </release> + </changelog> +</package> diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/BeforeValidException.php b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/BeforeValidException.php new file mode 100644 index 0000000..a6ee2f7 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/BeforeValidException.php @@ -0,0 +1,7 @@ +<?php +namespace Firebase\JWT; + +class BeforeValidException extends \UnexpectedValueException +{ + +} diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/ExpiredException.php b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/ExpiredException.php new file mode 100644 index 0000000..3597370 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/ExpiredException.php @@ -0,0 +1,7 @@ +<?php +namespace Firebase\JWT; + +class ExpiredException extends \UnexpectedValueException +{ + +} diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/JWT.php b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/JWT.php new file mode 100644 index 0000000..6d30e94 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/JWT.php @@ -0,0 +1,370 @@ +<?php + +namespace Firebase\JWT; +use \DomainException; +use \InvalidArgumentException; +use \UnexpectedValueException; +use \DateTime; + +/** + * JSON Web Token implementation, based on this spec: + * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06 + * + * PHP version 5 + * + * @category Authentication + * @package Authentication_JWT + * @author Neuman Vong <neuman@twilio.com> + * @author Anant Narayanan <anant@php.net> + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWT +{ + + /** + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. + */ + public static $leeway = 0; + + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * + * Will default to PHP time() value if null. + */ + public static $timestamp = null; + + public static $supported_algs = array( + 'HS256' => array('hash_hmac', 'SHA256'), + 'HS512' => array('hash_hmac', 'SHA512'), + 'HS384' => array('hash_hmac', 'SHA384'), + 'RS256' => array('openssl', 'SHA256'), + ); + + /** + * Decodes a JWT string into a PHP object. + * + * @param string $jwt The JWT + * @param string|array $key The key, or map of keys. + * If the algorithm used is asymmetric, this is the public key + * @param array $allowed_algs List of supported verification algorithms + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * + * @return object The JWT's payload as a PHP object + * + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim + * + * @uses jsonDecode + * @uses urlsafeB64Decode + */ + public static function decode($jwt, $key, $allowed_algs = array()) + { + $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; + + if (empty($key)) { + throw new InvalidArgumentException('Key may not be empty'); + } + if (!is_array($allowed_algs)) { + throw new InvalidArgumentException('Algorithm not allowed'); + } + $tks = explode('.', $jwt); + if (count($tks) != 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $bodyb64, $cryptob64) = $tks; + if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + $sig = static::urlsafeB64Decode($cryptob64); + + if (empty($header->alg)) { + throw new UnexpectedValueException('Empty algorithm'); + } + if (empty(static::$supported_algs[$header->alg])) { + throw new UnexpectedValueException('Algorithm not supported'); + } + if (!in_array($header->alg, $allowed_algs)) { + throw new UnexpectedValueException('Algorithm not allowed'); + } + if (is_array($key) || $key instanceof \ArrayAccess) { + if (isset($header->kid)) { + $key = $key[$header->kid]; + } else { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + } + + // Check the signature + if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + throw new SignatureInvalidException('Signature verification failed'); + } + + // Check if the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) + ); + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { + throw new BeforeValidException( + 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) + ); + } + + // Check if this token has expired. + if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { + throw new ExpiredException('Expired token'); + } + + return $payload; + } + + /** + * Converts and signs a PHP object or array into a JWT string. + * + * @param object|array $payload PHP object or array + * @param string $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param mixed $keyId + * @param array $head An array with header elements to attach + * + * @return string A signed JWT + * + * @uses jsonEncode + * @uses urlsafeB64Encode + */ + public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) + { + $header = array('typ' => 'JWT', 'alg' => $alg); + if ($keyId !== null) { + $header['kid'] = $keyId; + } + if ( isset($head) && is_array($head) ) { + $header = array_merge($head, $header); + } + $segments = array(); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); + $signing_input = implode('.', $segments); + + $signature = static::sign($signing_input, $key, $alg); + $segments[] = static::urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * + * @return string An encrypted message + * + * @throws DomainException Unsupported algorithm was specified + */ + public static function sign($msg, $key, $alg = 'HS256') + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algorithm) = static::$supported_algs[$alg]; + switch($function) { + case 'hash_hmac': + return hash_hmac($algorithm, $msg, $key, true); + case 'openssl': + $signature = ''; + $success = openssl_sign($msg, $signature, $key, $algorithm); + if (!$success) { + throw new DomainException("OpenSSL unable to sign data"); + } else { + return $signature; + } + } + } + + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $alg The algorithm + * + * @return bool + * + * @throws DomainException Invalid Algorithm or OpenSSL failure + */ + private static function verify($msg, $signature, $key, $alg) + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + + list($function, $algorithm) = static::$supported_algs[$alg]; + switch($function) { + case 'openssl': + $success = openssl_verify($msg, $signature, $key, $algorithm); + if (!$success) { + throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string()); + } else { + return $signature; + } + case 'hash_hmac': + default: + $hash = hash_hmac($algorithm, $msg, $key, true); + if (function_exists('hash_equals')) { + return hash_equals($signature, $hash); + } + $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (ord($signature[$i]) ^ ord($hash[$i])); + } + $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); + + return ($status === 0); + } + } + + /** + * Decode a JSON string into a PHP object. + * + * @param string $input JSON string + * + * @return object Object representation of JSON string + * + * @throws DomainException Provided string was invalid JSON + */ + public static function jsonDecode($input) + { + if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { + /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you + * to specify that large ints (like Steam Transaction IDs) should be treated as + * strings, rather than the PHP default behaviour of converting them to floats. + */ + $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + } else { + /** Not all servers will support that, however, so for older versions we must + * manually detect large ints in the JSON string and quote them (thus converting + *them to strings) before decoding, hence the preg_replace() call. + */ + $max_int_length = strlen((string) PHP_INT_MAX) - 1; + $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); + $obj = json_decode($json_without_bigints); + } + + if (function_exists('json_last_error') && $errno = json_last_error()) { + static::handleJsonError($errno); + } elseif ($obj === null && $input !== 'null') { + throw new DomainException('Null result with non-null input'); + } + return $obj; + } + + /** + * Encode a PHP object into a JSON string. + * + * @param object|array $input A PHP object or array + * + * @return string JSON representation of the PHP object or array + * + * @throws DomainException Provided object could not be encoded to valid JSON + */ + public static function jsonEncode($input) + { + $json = json_encode($input); + if (function_exists('json_last_error') && $errno = json_last_error()) { + static::handleJsonError($errno); + } elseif ($json === 'null' && $input !== null) { + throw new DomainException('Null result with non-null input'); + } + return $json; + } + + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string A decoded string + */ + public static function urlsafeB64Decode($input) + { + $remainder = strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= str_repeat('=', $padlen); + } + return base64_decode(strtr($input, '-_', '+/')); + } + + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafeB64Encode($input) + { + return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + } + + /** + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error() + * + * @return void + */ + private static function handleJsonError($errno) + { + $messages = array( + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON' + ); + throw new DomainException( + isset($messages[$errno]) + ? $messages[$errno] + : 'Unknown JSON error: ' . $errno + ); + } + + /** + * Get the number of bytes in cryptographic strings. + * + * @param string + * + * @return int + */ + private static function safeStrlen($str) + { + if (function_exists('mb_strlen')) { + return mb_strlen($str, '8bit'); + } + return strlen($str); + } +} diff --git a/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/SignatureInvalidException.php b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/SignatureInvalidException.php new file mode 100644 index 0000000..27332b2 --- /dev/null +++ b/mod/bigbluebuttonbn/vendor/firebase/php-jwt/src/SignatureInvalidException.php @@ -0,0 +1,7 @@ +<?php +namespace Firebase\JWT; + +class SignatureInvalidException extends \UnexpectedValueException +{ + +} diff --git a/mod/bigbluebuttonbn/version.php b/mod/bigbluebuttonbn/version.php new file mode 100644 index 0000000..bdb1263 --- /dev/null +++ b/mod/bigbluebuttonbn/version.php @@ -0,0 +1,34 @@ +<?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/>. + +/** + * Version for BigBlueButtonBN Moodle Activity Module. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2019101004; +$plugin->requires = 2016120500; +$plugin->cron = 0; +$plugin->component = 'mod_bigbluebuttonbn'; +$plugin->maturity = MATURITY_BETA; +$plugin->release = '2.4-beta'; diff --git a/mod/bigbluebuttonbn/view.php b/mod/bigbluebuttonbn/view.php new file mode 100644 index 0000000..54a4390 --- /dev/null +++ b/mod/bigbluebuttonbn/view.php @@ -0,0 +1,122 @@ +<?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 a BigBlueButton room. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +use mod_bigbluebuttonbn\plugin; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/locallib.php'); +require_once(__DIR__.'/viewlib.php'); + +$id = required_param('id', PARAM_INT); +$bn = optional_param('bn', 0, PARAM_INT); +$group = optional_param('group', 0, PARAM_INT); + +$viewinstance = bigbluebuttonbn_view_validator($id, $bn); // In locallib. +if (!$viewinstance) { + print_error('view_error_url_missing_parameters', plugin::COMPONENT); +} + +$cm = $viewinstance['cm']; +$course = $viewinstance['course']; +$bigbluebuttonbn = $viewinstance['bigbluebuttonbn']; + +require_login($course, true, $cm); + +// In locallib. +bigbluebuttonbn_event_log(\mod_bigbluebuttonbn\event\events::$events['view'], $bigbluebuttonbn); + +// Additional info related to the course. +$bbbsession['course'] = $course; +$bbbsession['coursename'] = $course->fullname; +$bbbsession['cm'] = $cm; +$bbbsession['bigbluebuttonbn'] = $bigbluebuttonbn; +// In locallib. +mod_bigbluebuttonbn\locallib\bigbluebutton::view_bbbsession_set($PAGE->context, $bbbsession); + +// Validates if the BigBlueButton server is working. +$serverversion = bigbluebuttonbn_get_server_version(); // In locallib. +if ($serverversion === null) { + $errmsg = 'view_error_unable_join_student'; + $errurl = '/course/view.php'; + $errurlparams = ['id' => $bigbluebuttonbn->course]; + if ($bbbsession['administrator']) { + $errmsg = 'view_error_unable_join'; + $errurl = '/admin/settings.php'; + $errurlparams = ['section' => 'modsettingbigbluebuttonbn']; + } else if ($bbbsession['moderator']) { + $errmsg = 'view_error_unable_join_teacher'; + } + print_error($errmsg, plugin::COMPONENT, new moodle_url($errurl, $errurlparams)); +} +$bbbsession['serverversion'] = (string) $serverversion; + +// Mark viewed by user (if required). +$completion = new completion_info($course); +$completion->set_module_viewed($cm); + +// Print the page header. +$PAGE->set_url('/mod/bigbluebuttonbn/view.php', ['id' => $cm->id]); +$PAGE->set_title($bigbluebuttonbn->name); +$PAGE->set_cacheable(false); +$PAGE->set_heading($course->fullname); + +/** @var core_renderer $OUTPUT */ +$OUTPUT; + +// Validate if the user is in a role allowed to join. +if (!has_any_capability(['moodle/category:manage', 'mod/bigbluebuttonbn:join'], $PAGE->context)) { + echo $OUTPUT->header(); + echo $OUTPUT->confirm( + sprintf( + '<p>%s</p>%s', + get_string(isguestuser() ? 'view_noguests' : 'view_nojoin', plugin::COMPONENT), + get_string('liketologin') + ), + get_login_url(), + new moodle_url('/course/view.php', ['id' => $course->id]) + ); + echo $OUTPUT->footer(); + exit; +} + +$activitystatus = bigbluebuttonbn_view_session_config($bbbsession, $id); + +// Output starts. +echo $OUTPUT->header(); + +bigbluebuttonbn_view_groups($bbbsession); + +bigbluebuttonbn_view_render($bbbsession, $activitystatus); + +// Output finishes. +echo $OUTPUT->footer(); + +// Shows version as a comment. +echo '<!-- '.$bbbsession['originTag'].' -->'."\n"; + +// Initialize session variable used across views. +$SESSION->bigbluebuttonbn_bbbsession = $bbbsession; \ No newline at end of file diff --git a/mod/bigbluebuttonbn/viewlib.php b/mod/bigbluebuttonbn/viewlib.php new file mode 100644 index 0000000..b5aacc2 --- /dev/null +++ b/mod/bigbluebuttonbn/viewlib.php @@ -0,0 +1,364 @@ +<?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 a BigBlueButton room. + * + * @package mod_bigbluebuttonbn + * @copyright 2010 onwards, Blindside Networks Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Jesus Federico (jesus [at] blindsidenetworks [dt] com) + * @author Fred Dixon (ffdixon [at] blindsidenetworks [dt] com) + * @author Darko Miletic (darko.miletic [at] gmail [dt] com) + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Displays the view for groups. + * + * @param array $bbbsession + * @return void + */ +function bigbluebuttonbn_view_groups(&$bbbsession) { + global $CFG; + // Find out current group mode. + $groupmode = groups_get_activity_groupmode($bbbsession['cm']); + if ($groupmode == NOGROUPS) { + // No groups mode. + return; + } + // Separate or visible group mode. + $groups = groups_get_activity_allowed_groups($bbbsession['cm']); + if (empty($groups)) { + // No groups in this course. + bigbluebuttonbn_view_message_box($bbbsession, get_string('view_groups_nogroups_warning', 'bigbluebuttonbn'), 'info', true); + return; + } + $bbbsession['group'] = groups_get_activity_group($bbbsession['cm'], true); + $groupname = get_string('allparticipants'); + if ($bbbsession['group'] != 0) { + $groupname = groups_get_group_name($bbbsession['group']); + } + // Assign group default values. + $bbbsession['meetingid'] .= '['.$bbbsession['group'].']'; + $bbbsession['meetingname'] .= ' ('.$groupname.')'; + if (count($groups) == 0) { + // Only the All participants group exists. + bigbluebuttonbn_view_message_box($bbbsession, get_string('view_groups_notenrolled_warning', 'bigbluebuttonbn'), 'info'); + return; + } + $context = context_module::instance($bbbsession['cm']->id); + if (has_capability('moodle/site:accessallgroups', $context)) { + bigbluebuttonbn_view_message_box($bbbsession, get_string('view_groups_selection_warning', 'bigbluebuttonbn')); + } + $urltoroot = $CFG->wwwroot.'/mod/bigbluebuttonbn/view.php?id='.$bbbsession['cm']->id; + groups_print_activity_menu($bbbsession['cm'], $urltoroot); + echo '<br><br>'; +} + +/** + * Displays the view for messages. + * + * @param array $bbbsession + * @param string $message + * @param string $type + * @param boolean $onlymoderator + * @return void + */ +function bigbluebuttonbn_view_message_box(&$bbbsession, $message, $type = 'warning', $onlymoderator = false) { + global $OUTPUT; + if ($onlymoderator && !$bbbsession['moderator'] && !$bbbsession['administrator']) { + return; + } + echo $OUTPUT->box_start('generalbox boxaligncenter'); + echo '<br><div class="alert alert-' . $type . '">' . $message . '</div>'; + echo $OUTPUT->box_end(); +} + +/** + * Displays the general view. + * + * @param array $bbbsession + * @param string $activity + * @return void + */ +function bigbluebuttonbn_view_render(&$bbbsession, $activity) { + global $OUTPUT, $PAGE; + $type = null; + if (isset($bbbsession['bigbluebuttonbn']->type)) { + $type = $bbbsession['bigbluebuttonbn']->type; + } + $typeprofiles = bigbluebuttonbn_get_instance_type_profiles(); + $enabledfeatures = bigbluebuttonbn_get_enabled_features($typeprofiles, $type); + $pinginterval = (int)\mod_bigbluebuttonbn\locallib\config::get('waitformoderator_ping_interval') * 1000; + // JavaScript for locales. + $PAGE->requires->strings_for_js(array_keys(bigbluebuttonbn_get_strings_for_js()), 'bigbluebuttonbn'); + // JavaScript variables. + $jsvars = array('activity' => $activity, 'ping_interval' => $pinginterval, + 'locale' => bigbluebuttonbn_get_localcode(), 'profile_features' => $typeprofiles[0]['features']); + $output = ''; + // Renders warning messages when configured. + $output .= bigbluebuttonbn_view_warning_default_server($bbbsession); + $output .= bigbluebuttonbn_view_warning_general($bbbsession); + + // Renders the rest of the page. + $output .= $OUTPUT->heading($bbbsession['meetingname'], 3); + // Renders the completed description. + $desc = file_rewrite_pluginfile_urls($bbbsession['meetingdescription'], 'pluginfile.php', + $bbbsession['context']->id, 'mod_bigbluebuttonbn', 'intro', null); + $output .= $OUTPUT->heading($desc, 5); + + if ($enabledfeatures['showroom']) { + $output .= bigbluebuttonbn_view_render_room($bbbsession, $activity, $jsvars); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-rooms', + 'M.mod_bigbluebuttonbn.rooms.init', array($jsvars)); + } + // Show recordings should only be enabled if recordings are also enabled in session. + if ($enabledfeatures['showrecordings'] && $bbbsession['record']) { + $output .= html_writer::start_tag('div', array('id' => 'bigbluebuttonbn_view_recordings')); + $output .= bigbluebuttonbn_view_render_recording_section($bbbsession, $type, $enabledfeatures, $jsvars); + $output .= html_writer::end_tag('div'); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-recordings', + 'M.mod_bigbluebuttonbn.recordings.init', array($jsvars)); + } else if ($type == BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY) { + $recordingsdisabled = get_string('view_message_recordings_disabled', 'bigbluebuttonbn'); + $output .= bigbluebuttonbn_render_warning($recordingsdisabled, 'danger'); + } + echo $output.html_writer::empty_tag('br').html_writer::empty_tag('br').html_writer::empty_tag('br'); + $PAGE->requires->yui_module('moodle-mod_bigbluebuttonbn-broker', 'M.mod_bigbluebuttonbn.broker.init', array($jsvars)); +} + +/** + * Renders the view for recordings. + * + * @param array $bbbsession + * @param integer $type + * @param array $enabledfeatures + * @param array $jsvars + * @return string + */ +function bigbluebuttonbn_view_render_recording_section(&$bbbsession, $type, $enabledfeatures, &$jsvars) { + if ($type == BIGBLUEBUTTONBN_TYPE_ROOM_ONLY) { + return ''; + } + $output = ''; + if ($type == BIGBLUEBUTTONBN_TYPE_ALL && $bbbsession['record']) { + $output .= html_writer::start_tag('div', array('id' => 'bigbluebuttonbn_view_recordings_header')); + $output .= html_writer::tag('h4', get_string('view_section_title_recordings', 'bigbluebuttonbn')); + $output .= html_writer::end_tag('div'); + } + if ($type == BIGBLUEBUTTONBN_TYPE_RECORDING_ONLY || $bbbsession['record']) { + $output .= html_writer::start_tag('div', array('id' => 'bigbluebuttonbn_view_recordings_content')); + $output .= bigbluebuttonbn_view_render_recordings($bbbsession, $enabledfeatures, $jsvars); + $output .= html_writer::end_tag('div'); + $output .= html_writer::start_tag('div', array('id' => 'bigbluebuttonbn_view_recordings_footer')); + $output .= bigbluebuttonbn_view_render_imported($bbbsession, $enabledfeatures); + $output .= html_writer::end_tag('div'); + } + return $output; +} + +/** + * Evaluates if the warning box should be shown. + * + * @param array $bbbsession + * + * @return boolean + */ +function bigbluebuttonbn_view_warning_shown($bbbsession) { + if (is_siteadmin($bbbsession['userID'])) { + return true; + } + $generalwarningroles = explode(',', \mod_bigbluebuttonbn\locallib\config::get('general_warning_roles')); + $userroles = bigbluebuttonbn_get_user_roles($bbbsession['context'], $bbbsession['userID']); + foreach ($userroles as $userrole) { + if (in_array($userrole->shortname, $generalwarningroles)) { + return true; + } + } + return false; +} + +/** + * Renders the view for room. + * + * @param array $bbbsession + * @param string $activity + * @param array $jsvars + * + * @return string + */ +function bigbluebuttonbn_view_render_room(&$bbbsession, $activity, &$jsvars) { + global $OUTPUT; + // JavaScript variables for room. + $openingtime = ''; + if ($bbbsession['openingtime']) { + $openingtime = get_string('mod_form_field_openingtime', 'bigbluebuttonbn').': '. + userdate($bbbsession['openingtime']); + } + $closingtime = ''; + if ($bbbsession['closingtime']) { + $closingtime = get_string('mod_form_field_closingtime', 'bigbluebuttonbn').': '. + userdate($bbbsession['closingtime']); + } + $jsvars += array( + 'meetingid' => $bbbsession['meetingid'], + 'bigbluebuttonbnid' => $bbbsession['bigbluebuttonbn']->id, + 'userlimit' => $bbbsession['userlimit'], + 'opening' => $openingtime, + 'closing' => $closingtime, + ); + // Main box. + $output = $OUTPUT->box_start('generalbox boxaligncenter', 'bigbluebuttonbn_view_message_box'); + $output .= '<br><span id="status_bar"></span>'; + $output .= '<br><span id="control_panel"></span>'; + $output .= $OUTPUT->box_end(); + // Action button box. + $output .= $OUTPUT->box_start('generalbox boxaligncenter', 'bigbluebuttonbn_view_action_button_box'); + $output .= '<br><br><span id="join_button"></span> <span id="end_button"></span>'."\n"; + $output .= $OUTPUT->box_end(); + if ($activity == 'ended') { + $output .= bigbluebuttonbn_view_ended($bbbsession); + } + return $output; +} + +/** + * Renders the view for recordings. + * + * @param array $bbbsession + * @param array $enabledfeatures + * @param array $jsvars + * + * @return string + */ +function bigbluebuttonbn_view_render_recordings(&$bbbsession, $enabledfeatures, &$jsvars) { + $recordings = bigbluebutton_get_recordings_for_table_view($bbbsession, $enabledfeatures); + + if (empty($recordings) || array_key_exists('messageKey', $recordings)) { + // There are no recordings to be shown. + return html_writer::div(get_string('view_message_norecordings', 'bigbluebuttonbn'), '', + array('id' => 'bigbluebuttonbn_recordings_table')); + } + // There are recordings for this meeting. + // JavaScript variables for recordings. + $jsvars += array( + 'recordings_html' => $bbbsession['bigbluebuttonbn']->recordings_html == '1', + ); + // If there are meetings with recordings load the data to the table. + if ($bbbsession['bigbluebuttonbn']->recordings_html) { + // Render a plain html table. + return bigbluebuttonbn_output_recording_table($bbbsession, $recordings)."\n"; + } + // JavaScript variables for recordings with YUI. + $jsvars += array( + 'bbbid' => $bbbsession['bigbluebuttonbn']->id, + ); + // Render a YUI table. + $reset = get_string('reset'); + $search = get_string('search'); + $output = "<form id='bigbluebuttonbn_recordings_searchform'> + <input id='searchtext' type='text'> + <input id='searchsubmit' type='submit' value='{$search}'> + <input id='searchreset' type='submit' value='{$reset}'> + </form>"; + $output .= html_writer::div('', '', array('id' => 'bigbluebuttonbn_recordings_table')); + + return $output; +} + +/** + * Renders the view for importing recordings. + * + * @param array $bbbsession + * @param array $enabledfeatures + * + * @return string + */ +function bigbluebuttonbn_view_render_imported($bbbsession, $enabledfeatures) { + global $CFG; + if (!$enabledfeatures['importrecordings'] || !$bbbsession['importrecordings']) { + return ''; + } + $button = html_writer::tag('input', '', + array('type' => 'button', + 'value' => get_string('view_recording_button_import', 'bigbluebuttonbn'), + 'class' => 'btn btn-secondary', + 'onclick' => 'window.location=\''.$CFG->wwwroot.'/mod/bigbluebuttonbn/import_view.php?bn='. + $bbbsession['bigbluebuttonbn']->id.'\'')); + $output = html_writer::empty_tag('br'); + $output .= html_writer::tag('span', $button, array('id' => 'import_recording_links_button')); + $output .= html_writer::tag('span', '', array('id' => 'import_recording_links_table')); + return $output; +} + +/** + * Renders the content for ended meeting. + * + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_view_ended(&$bbbsession) { + global $OUTPUT; + if (!is_null($bbbsession['presentation']['url'])) { + $attributes = array('title' => $bbbsession['presentation']['name']); + $icon = new pix_icon($bbbsession['presentation']['icon'], $bbbsession['presentation']['mimetype_description']); + return '<h4>'.get_string('view_section_title_presentation', 'bigbluebuttonbn').'</h4>'. + $OUTPUT->action_icon($bbbsession['presentation']['url'], $icon, null, array(), false). + $OUTPUT->action_link($bbbsession['presentation']['url'], + $bbbsession['presentation']['name'], null, $attributes).'<br><br>'; + } + return ''; +} + +/** + * Renders a default server warning message when using test-install. + * + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_view_warning_default_server(&$bbbsession) { + if (!is_siteadmin($bbbsession['userID'])) { + return ''; + } + if (BIGBLUEBUTTONBN_DEFAULT_SERVER_URL != \mod_bigbluebuttonbn\locallib\config::get('server_url')) { + return ''; + } + return bigbluebuttonbn_render_warning(get_string('view_warning_default_server', 'bigbluebuttonbn'), 'warning'); +} + +/** + * Renders a general warning message when it is configured. + * + * @param array $bbbsession + * + * @return string + */ +function bigbluebuttonbn_view_warning_general(&$bbbsession) { + if (!bigbluebuttonbn_view_warning_shown($bbbsession)) { + return ''; + } + return bigbluebuttonbn_render_warning( + (string)\mod_bigbluebuttonbn\locallib\config::get('general_warning_message'), + (string)\mod_bigbluebuttonbn\locallib\config::get('general_warning_box_type'), + (string)\mod_bigbluebuttonbn\locallib\config::get('general_warning_button_href'), + (string)\mod_bigbluebuttonbn\locallib\config::get('general_warning_button_text'), + (string)\mod_bigbluebuttonbn\locallib\config::get('general_warning_button_class') + ); +} diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-debug.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-debug.js new file mode 100644 index 0000000..74e977a --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-debug.js @@ -0,0 +1,199 @@ +YUI.add('moodle-mod_bigbluebuttonbn-broker', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.broker = { + + datasource: null, + bigbluebuttonbn: {}, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + "&" + }); + this.bigbluebuttonbn = bigbluebuttonbn; + }, + + joinRedirect: function(joinUrl) { + window.open(joinUrl); + }, + + recordingActionPerform: function(data) { + var qs = "action=recording_" + data.action + "&id=" + data.recordingid + "&idx=" + data.meetingid; + qs += this.recordingActionMetaQS(data); + data.attempt = 1; + if (typeof data.attempts === 'undefined') { + data.attempts = 5; + } + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + // Something went wrong. + if (!e.data.status) { + data.message = e.data.message; + return M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + // There is no need for verification. + if (typeof data.goalstate === 'undefined') { + return M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(data); + } + // Use the current response for verification. + if (data.attempts <= 1) { + return M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(e, data); + } + // Iterate the verification. + return M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(data); + }, + failure: function(e) { + data.message = e.error.message; + return M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + } + }); + }, + + recordingActionMetaQS: function(data) { + var qs = ''; + if (typeof data.source !== 'undefined') { + var meta = {}; + meta[data.source] = encodeURIComponent(data.goalstate); + qs += "&meta=" + JSON.stringify(meta); + } + return qs; + }, + + recordingActionPerformedValidate: function(data) { + var qs = "action=recording_info&id=" + data.recordingid + "&idx=" + data.meetingid; + qs += this.recordingActionMetaQS(data); + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + // Evaluates if the current attempt has been completed. + if (M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(e, data)) { + // It has been completed, so stop the action. + return; + } + // Evaluates if more attempts have to be performed. + if (data.attempt < data.attempts) { + data.attempt += 1; + setTimeout(((function() { + return function() { + M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(data); + }; + })(this)), (data.attempt - 1) * 1000); + return; + } + // No more attempts to perform, it stops with failing over. + data.message = M.util.get_string('view_error_action_not_completed', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + }, + failure: function(e) { + data.message = e.error.message; + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + } + }); + }, + + recordingActionPerformedComplete: function(e, data) { + // Something went wrong. + if (typeof e.data[data.source] === 'undefined') { + data.message = M.util.get_string('view_error_current_state_not_found', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + return true; + } + // Evaluates if the state is as expected. + if (e.data[data.source] === data.goalstate) { + M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(data); + return true; + } + return false; + }, + + recordingCurrentState: function(action, data) { + if (action === 'publish' || action === 'unpublish') { + return data.published; + } + if (action === 'delete') { + return data.status; + } + if (action === 'protect' || action === 'unprotect') { + return data.secured; // The broker responds with secured as protected is a reserverd word. + } + if (action === 'update') { + return data.updated; + } + return null; + }, + + endMeeting: function() { + var qs = 'action=meeting_end&id=' + this.bigbluebuttonbn.meetingid; + qs += '&bigbluebuttonbn=' + this.bigbluebuttonbn.bigbluebuttonbnid; + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + if (e.data.status) { + M.mod_bigbluebuttonbn.rooms.endMeeting(); + location.reload(); + } + } + } + }); + }, + + completionValidate: function(qs) { + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + if (e.data.status) { + var message = M.util.get_string('completionvalidatestatetriggered', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.helpers.alertError(message, 'info'); + return; + } + } + } + }); + } + +}; + + + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] +}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-min.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-min.js new file mode 100644 index 0000000..83f5ac2 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_bigbluebuttonbn-broker",function(e,t){M.mod_bigbluebuttonbn=M.mod_bigbluebuttonbn||{},M.mod_bigbluebuttonbn.broker={datasource:null,bigbluebuttonbn:{},init:function(t){this.datasource=new e.DataSource.Get({source:M.cfg.wwwroot+"/mod/bigbluebuttonbn/bbb_ajax.php?sesskey="+M.cfg.sesskey+"&"}),this.bigbluebuttonbn=t},joinRedirect:function(e){window.open(e)},recordingActionPerform:function(e){var t="action=recording_"+e.action+"&id="+e.recordingid+"&idx="+e.meetingid;t+=this.recordingActionMetaQS(e),e.attempt=1,typeof e.attempts=="undefined"&&(e.attempts=5),this.datasource.sendRequest({request:t,callback:{success:function(t){return t.data.status?typeof e.goalstate=="undefined"?M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(e):e.attempts<=1?M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(t,e):M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(e):(e.message=t.data.message,M.mod_bigbluebuttonbn.recordings.recordingActionFailover(e))},failure:function(t){return e.message=t.error.message,M.mod_bigbluebuttonbn.recordings.recordingActionFailover(e)}}})},recordingActionMetaQS:function(e){var t="";if(typeof e.source!="undefined"){var n={};n[e.source]=encodeURIComponent(e.goalstate),t+="&meta="+JSON.stringify(n)}return t},recordingActionPerformedValidate:function(e){var t="action=recording_info&id="+e.recordingid+"&idx="+e.meetingid;t+=this.recordingActionMetaQS(e),this.datasource.sendRequest({request:t,callback:{success:function(t){if(M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(t,e))return;if(e.attempt<e.attempts){e.attempt+=1,setTimeout(function(){return function(){M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(e)}}(this),(e.attempt-1)*1e3);return}e.message=M.util.get_string("view_error_action_not_completed","bigbluebuttonbn"),M.mod_bigbluebuttonbn.recordings.recordingActionFailover(e)},failure:function(t){e.message=t.error.message,M.mod_bigbluebuttonbn.recordings.recordingActionFailover(e)}}})},recordingActionPerformedComplete:function(e,t){return typeof e.data[t.source]=="undefined"?(t.message=M.util.get_string("view_error_current_state_not_found","bigbluebuttonbn"),M.mod_bigbluebuttonbn.recordings.recordingActionFailover(t),!0):e.data[t.source]===t.goalstate?(M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(t),!0):!1},recordingCurrentState:function(e,t){return e==="publish"||e==="unpublish"?t.published:e==="delete"?t.status:e==="protect"||e==="unprotect"?t.secured:e==="update"?t.updated:null},endMeeting:function(){var e="action=meeting_end&id="+this.bigbluebuttonbn.meetingid;e+="&bigbluebuttonbn="+this.bigbluebuttonbn.bigbluebuttonbnid,this.datasource.sendRequest({request:e,callback:{success:function(e){e.data.status&&(M.mod_bigbluebuttonbn.rooms.endMeeting(),location.reload())}}})},completionValidate:function(e){this.datasource.sendRequest({request:e,callback:{success:function(e){if(e.data.status){var t=M.util.get_string("completionvalidatestatetriggered","bigbluebuttonbn");M.mod_bigbluebuttonbn.helpers.alertError(t,"info");return}}}})}}},"@VERSION@",{requires:["base","node","datasource-get","datasource-jsonschema","datasource-polling","moodle-core-notification"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker.js new file mode 100644 index 0000000..74e977a --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-broker/moodle-mod_bigbluebuttonbn-broker.js @@ -0,0 +1,199 @@ +YUI.add('moodle-mod_bigbluebuttonbn-broker', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.broker = { + + datasource: null, + bigbluebuttonbn: {}, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + "&" + }); + this.bigbluebuttonbn = bigbluebuttonbn; + }, + + joinRedirect: function(joinUrl) { + window.open(joinUrl); + }, + + recordingActionPerform: function(data) { + var qs = "action=recording_" + data.action + "&id=" + data.recordingid + "&idx=" + data.meetingid; + qs += this.recordingActionMetaQS(data); + data.attempt = 1; + if (typeof data.attempts === 'undefined') { + data.attempts = 5; + } + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + // Something went wrong. + if (!e.data.status) { + data.message = e.data.message; + return M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + // There is no need for verification. + if (typeof data.goalstate === 'undefined') { + return M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(data); + } + // Use the current response for verification. + if (data.attempts <= 1) { + return M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(e, data); + } + // Iterate the verification. + return M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(data); + }, + failure: function(e) { + data.message = e.error.message; + return M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + } + }); + }, + + recordingActionMetaQS: function(data) { + var qs = ''; + if (typeof data.source !== 'undefined') { + var meta = {}; + meta[data.source] = encodeURIComponent(data.goalstate); + qs += "&meta=" + JSON.stringify(meta); + } + return qs; + }, + + recordingActionPerformedValidate: function(data) { + var qs = "action=recording_info&id=" + data.recordingid + "&idx=" + data.meetingid; + qs += this.recordingActionMetaQS(data); + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + // Evaluates if the current attempt has been completed. + if (M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(e, data)) { + // It has been completed, so stop the action. + return; + } + // Evaluates if more attempts have to be performed. + if (data.attempt < data.attempts) { + data.attempt += 1; + setTimeout(((function() { + return function() { + M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(data); + }; + })(this)), (data.attempt - 1) * 1000); + return; + } + // No more attempts to perform, it stops with failing over. + data.message = M.util.get_string('view_error_action_not_completed', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + }, + failure: function(e) { + data.message = e.error.message; + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + } + }); + }, + + recordingActionPerformedComplete: function(e, data) { + // Something went wrong. + if (typeof e.data[data.source] === 'undefined') { + data.message = M.util.get_string('view_error_current_state_not_found', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + return true; + } + // Evaluates if the state is as expected. + if (e.data[data.source] === data.goalstate) { + M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(data); + return true; + } + return false; + }, + + recordingCurrentState: function(action, data) { + if (action === 'publish' || action === 'unpublish') { + return data.published; + } + if (action === 'delete') { + return data.status; + } + if (action === 'protect' || action === 'unprotect') { + return data.secured; // The broker responds with secured as protected is a reserverd word. + } + if (action === 'update') { + return data.updated; + } + return null; + }, + + endMeeting: function() { + var qs = 'action=meeting_end&id=' + this.bigbluebuttonbn.meetingid; + qs += '&bigbluebuttonbn=' + this.bigbluebuttonbn.bigbluebuttonbnid; + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + if (e.data.status) { + M.mod_bigbluebuttonbn.rooms.endMeeting(); + location.reload(); + } + } + } + }); + }, + + completionValidate: function(qs) { + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + if (e.data.status) { + var message = M.util.get_string('completionvalidatestatetriggered', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.helpers.alertError(message, 'info'); + return; + } + } + } + }); + } + +}; + + + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] +}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-debug.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-debug.js new file mode 100644 index 0000000..fb5e5a7 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-debug.js @@ -0,0 +1,44 @@ +YUI.add('moodle-mod_bigbluebuttonbn-imports', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.imports = { + + /** + * Initialise the broker code. + * + * @method init + * @param {object} data + */ + init: function(data) { + // Init event listener for course selector. + Y.one('#menuimport_recording_links_select').on('change', function() { + var endpoint = '/mod/bigbluebuttonbn/import_view.php'; + var qs = '?bn=' + data.bn + '&tc=' + this.get('value'); + Y.config.win.location = M.cfg.wwwroot + endpoint + qs; + }); + } + +}; + + + +}, '@VERSION@', {"requires": ["base", "node"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-min.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-min.js new file mode 100644 index 0000000..4b7479d --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_bigbluebuttonbn-imports",function(e,t){M.mod_bigbluebuttonbn=M.mod_bigbluebuttonbn||{},M.mod_bigbluebuttonbn.imports={init:function(t){e.one("#menuimport_recording_links_select").on("change",function(){var n="/mod/bigbluebuttonbn/import_view.php",r="?bn="+t.bn+"&tc="+this.get("value");e.config.win.location=M.cfg.wwwroot+n+r})}}},"@VERSION@",{requires:["base","node"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports.js new file mode 100644 index 0000000..fb5e5a7 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-imports/moodle-mod_bigbluebuttonbn-imports.js @@ -0,0 +1,44 @@ +YUI.add('moodle-mod_bigbluebuttonbn-imports', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.imports = { + + /** + * Initialise the broker code. + * + * @method init + * @param {object} data + */ + init: function(data) { + // Init event listener for course selector. + Y.one('#menuimport_recording_links_select').on('change', function() { + var endpoint = '/mod/bigbluebuttonbn/import_view.php'; + var qs = '?bn=' + data.bn + '&tc=' + this.get('value'); + Y.config.win.location = M.cfg.wwwroot + endpoint + qs; + }); + } + +}; + + + +}, '@VERSION@', {"requires": ["base", "node"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-debug.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-debug.js new file mode 100644 index 0000000..334c923 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-debug.js @@ -0,0 +1,345 @@ +YUI.add('moodle-mod_bigbluebuttonbn-modform', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.modform = { + + bigbluebuttonbn: {}, + strings: {}, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.bigbluebuttonbn = bigbluebuttonbn; + this.strings = { + as: M.util.get_string('mod_form_field_participant_list_text_as', 'bigbluebuttonbn'), + viewer: M.util.get_string('mod_form_field_participant_bbb_role_viewer', 'bigbluebuttonbn'), + moderator: M.util.get_string('mod_form_field_participant_bbb_role_moderator', 'bigbluebuttonbn'), + remove: M.util.get_string('mod_form_field_participant_list_action_remove', 'bigbluebuttonbn') + }; + this.updateInstanceTypeProfile(); + this.participantListInit(); + }, + + updateInstanceTypeProfile: function() { + var selectedType, profileType; + selectedType = Y.one('#id_type'); + profileType = this.bigbluebuttonbn.instanceTypeDefault; + if (selectedType !== null) { + profileType = selectedType.get('value'); + } + this.applyInstanceTypeProfile(profileType); + }, + + applyInstanceTypeProfile: function(profileType) { + var showAll = this.isFeatureEnabled(profileType, 'all'); + // Show room settings validation. + this.showFieldset('id_room', showAll || + this.isFeatureEnabled(profileType, 'showroom')); + this.showInput('id_record', showAll || + this.isFeatureEnabled(profileType, 'showroom')); + // Show recordings settings validation. + this.showFieldset('id_recordings', showAll || + this.isFeatureEnabled(profileType, 'showrecordings')); + // Show recordings imported settings validation. + this.showInput('id_recordings_imported', showAll || + this.isFeatureEnabled(profileType, 'showrecordings')); + // Preuploadpresentation feature validation. + this.showFieldset('id_preuploadpresentation', showAll || + this.isFeatureEnabled(profileType, 'preuploadpresentation')); + // Participants feature validation. + this.showFieldset('id_permissions', showAll || + this.isFeatureEnabled(profileType, 'permissions')); + // Schedule feature validation. + this.showFieldset('id_schedule', showAll || + this.isFeatureEnabled(profileType, 'schedule')); + // Common module settings validation. + this.showFieldset('id_modstandardelshdr', showAll || + this.isFeatureEnabled(profileType, 'modstandardelshdr')); + // Restrict access validation. + this.showFieldset('id_availabilityconditionsheader', showAll || + this.isFeatureEnabled(profileType, 'availabilityconditionsheader')); + // Tags validation. + this.showFieldset('id_tagshdr', showAll || this.isFeatureEnabled(profileType, 'tagshdr')); + // Competencies validation. + this.showFieldset('id_competenciessection', showAll || + this.isFeatureEnabled(profileType, 'competenciessection')); + // Completion validation. + this.showFormGroup('completionattendancegroup', showAll || + this.isFeatureEnabled(profileType, 'completionattendance')); + // Completion validation. + this.showFormGroup('completionengagementgroup', showAll || + this.isFeatureEnabled(profileType, 'completionengagement')); + }, + + isFeatureEnabled: function(profileType, feature) { + var features = this.bigbluebuttonbn.instanceTypeProfiles[profileType].features; + return (features.indexOf(feature) != -1); + }, + + showFieldset: function(id, show) { + // Show room settings validation. + var node = Y.one('#' + id); + if (!node) { + return; + } + if (show) { + node.setStyle('display', 'block'); + return; + } + node.setStyle('display', 'none'); + }, + + showInput: function(id, show) { + // Show room settings validation. + var node = Y.one('#' + id); + if (!node) { + return; + } + var ancestor = node.ancestor('div').ancestor('div'); + if (show) { + ancestor.setStyle('display', 'block'); + return; + } + ancestor.setStyle('display', 'none'); + }, + + showFormGroup: function(id, show) { + // Show room settings validation. + var node = Y.one('#fgroup_id_' + id); + if (!node) { + return; + } + if (show) { + node.removeClass('hidden'); + return; + } + node.addClass('hidden'); + }, + + participantSelectionSet: function() { + this.selectClear('bigbluebuttonbn_participant_selection'); + var type = document.getElementById('bigbluebuttonbn_participant_selection_type'); + for (var i = 0; i < type.options.length; i++) { + if (type.options[i].selected) { + var options = this.bigbluebuttonbn.participantData[type.options[i].value].children; + for (var option in options) { + if (options.hasOwnProperty(option)) { + this.selectAddOption( + 'bigbluebuttonbn_participant_selection', options[option].name, options[option].id + ); + } + } + if (type.options[i].value === 'all') { + this.selectAddOption('bigbluebuttonbn_participant_selection', + '---------------', 'all'); + this.selectDisable('bigbluebuttonbn_participant_selection'); + } else { + this.selectEnable('bigbluebuttonbn_participant_selection'); + } + } + } + }, + + participantListInit: function() { + var selectionTypeValue, selectionValue, selectionRole, participantSelectionTypes; + this.participantListClear(); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + selectionTypeValue = this.bigbluebuttonbn.participantList[i].selectiontype; + selectionValue = this.bigbluebuttonbn.participantList[i].selectionid; + selectionRole = this.bigbluebuttonbn.participantList[i].role; + participantSelectionTypes = this.bigbluebuttonbn.participantData[selectionTypeValue]; + if (selectionTypeValue != 'all' && typeof participantSelectionTypes.children[selectionValue] == 'undefined') { + // Remove from memory. + this.participantRemoveFromMemory(selectionTypeValue, selectionValue); + continue; + } + // Add it to the form, but don't add the delete button if it is the first item. + this.participantAddToForm(selectionTypeValue, selectionValue, selectionRole, (i > 0)); + } + // Update in the form. + this.participantListUpdate(); + }, + + participantListClear: function() { + var table, rows; + table = document.getElementById('participant_list_table'); + rows = table.getElementsByTagName('tr'); + for (var i = rows.length; i > 0; i--) { + table.deleteRow(0); + } + }, + + participantListUpdate: function() { + var participantList = document.getElementsByName('participants')[0]; + participantList.value = JSON.stringify(this.bigbluebuttonbn.participantList).replace(/"/g, '"'); + }, + + participantRemove: function(selectionTypeValue, selectionValue) { + // Remove from memory. + this.participantRemoveFromMemory(selectionTypeValue, selectionValue); + + // Remove from the form. + this.participantRemoveFromForm(selectionTypeValue, selectionValue); + + // Update in the form. + this.participantListUpdate(); + }, + + participantRemoveFromMemory: function(selectionTypeValue, selectionValue) { + var selectionid = (selectionValue === '' ? null : selectionValue); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == selectionTypeValue && + this.bigbluebuttonbn.participantList[i].selectionid == selectionid) { + this.bigbluebuttonbn.participantList.splice(i, 1); + } + } + }, + + participantRemoveFromForm: function(selectionTypeValue, selectionValue) { + var id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue; + var participantListTable = document.getElementById('participant_list_table'); + for (var i = 0; i < participantListTable.rows.length; i++) { + if (participantListTable.rows[i].id == id) { + participantListTable.deleteRow(i); + } + } + }, + + participantAdd: function() { + var selectionType = document.getElementById('bigbluebuttonbn_participant_selection_type'); + var selection = document.getElementById('bigbluebuttonbn_participant_selection'); + // Lookup to see if it has been added already. + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == selectionType.value && + this.bigbluebuttonbn.participantList[i].selectionid == selection.value) { + return; + } + } + // Add it to memory. + this.participantAddToMemory(selectionType.value, selection.value); + // Add it to the form. + this.participantAddToForm(selectionType.value, selection.value, 'viewer', true); + // Update in the form. + this.participantListUpdate(); + }, + + participantAddToMemory: function(selectionTypeValue, selectionValue) { + this.bigbluebuttonbn.participantList.push({ + "selectiontype": selectionTypeValue, + "selectionid": selectionValue, + "role": "viewer" + }); + }, + + participantAddToForm: function(selectionTypeValue, selectionValue, selectionRole, canDelete) { + var listTable, innerHTML, selectedHtml, removeHtml, removeClass, bbbRoles, i, row, cell0, cell1, cell2, cell3; + listTable = document.getElementById('participant_list_table'); + row = listTable.insertRow(listTable.rows.length); + row.id = "participant_list_tr_" + selectionTypeValue + "-" + selectionValue; + cell0 = row.insertCell(0); + cell0.width = "125px"; + cell0.innerHTML = '<b><i>' + this.bigbluebuttonbn.participantData[selectionTypeValue].name; + cell0.innerHTML += (selectionTypeValue !== 'all' ? ': ' : '') + '</i></b>'; + cell1 = row.insertCell(1); + cell1.innerHTML = ''; + if (selectionTypeValue !== 'all') { + cell1.innerHTML = this.bigbluebuttonbn.participantData[selectionTypeValue].children[selectionValue].name; + } + innerHTML = ' <i>' + this.strings.as + '</i> '; + innerHTML += '<select id="participant_list_role_' + selectionTypeValue + '-' + selectionValue + '"'; + innerHTML += ' onchange="M.mod_bigbluebuttonbn.modform.participantListRoleUpdate(\''; + innerHTML += selectionTypeValue + '\', \'' + selectionValue; + innerHTML += '\'); return 0;" class="select custom-select">'; + bbbRoles = ['viewer', 'moderator']; + for (i = 0; i < bbbRoles.length; i++) { + selectedHtml = ''; + if (bbbRoles[i] === selectionRole) { + selectedHtml = ' selected="selected"'; + } + innerHTML += '<option value="' + bbbRoles[i] + '"' + selectedHtml + '>' + this.strings[bbbRoles[i]] + '</option>'; + } + innerHTML += '</select>'; + cell2 = row.insertCell(2); + cell2.innerHTML = innerHTML; + cell3 = row.insertCell(3); + cell3.width = "20px"; + removeHtml = this.strings.remove; + removeClass = "btn btn-secondary btn-sm"; + if (this.bigbluebuttonbn.iconsEnabled) { + removeHtml = this.bigbluebuttonbn.pixIconDelete; + removeClass = "btn btn-link"; + } + innerHTML = ""; + if (canDelete) { + innerHTML = '<a class="' + removeClass + '" onclick="M.mod_bigbluebuttonbn.modform.participantRemove(\''; + innerHTML += selectionTypeValue + '\', \'' + selectionValue; + innerHTML += '\'); return 0;" title="' + this.strings.remove + '">' + removeHtml + '</a>'; + } + cell3.innerHTML = innerHTML; + }, + + participantListRoleUpdate: function(type, id) { + // Update in memory. + var participantListRoleSelection = document.getElementById('participant_list_role_' + type + '-' + id); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == type && + this.bigbluebuttonbn.participantList[i].selectionid == (id === '' ? null : id)) { + this.bigbluebuttonbn.participantList[i].role = participantListRoleSelection.value; + } + } + // Update in the form. + this.participantListUpdate(); + }, + + selectClear: function(id) { + var select = document.getElementById(id); + while (select.length > 0) { + select.remove(select.length - 1); + } + }, + + selectEnable: function(id) { + var select = document.getElementById(id); + select.disabled = false; + }, + + selectDisable: function(id) { + var select = document.getElementById(id); + select.disabled = true; + }, + + selectAddOption: function(id, text, value) { + var select = document.getElementById(id); + var option = document.createElement('option'); + option.text = text; + option.value = value; + select.add(option, option.length); + } + +}; + + +}, '@VERSION@', {"requires": ["base", "node"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-min.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-min.js new file mode 100644 index 0000000..ea6177b --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform-min.js @@ -0,0 +1,2 @@ +YUI.add("moodle-mod_bigbluebuttonbn-modform",function(e,t){M.mod_bigbluebuttonbn=M.mod_bigbluebuttonbn||{},M.mod_bigbluebuttonbn.modform={bigbluebuttonbn:{},strings:{},init:function(e){this.bigbluebuttonbn=e,this.strings={as:M.util.get_string("mod_form_field_participant_list_text_as","bigbluebuttonbn"),viewer:M.util.get_string("mod_form_field_participant_bbb_role_viewer","bigbluebuttonbn"),moderator:M.util.get_string("mod_form_field_participant_bbb_role_moderator","bigbluebuttonbn"),remove:M.util.get_string("mod_form_field_participant_list_action_remove","bigbluebuttonbn")},this.updateInstanceTypeProfile(),this.participantListInit()},updateInstanceTypeProfile:function(){var t,n;t=e.one("#id_type"),n=this.bigbluebuttonbn.instanceTypeDefault,t!==null&&(n=t.get("value")),this.applyInstanceTypeProfile(n)},applyInstanceTypeProfile:function(e){var t=this.isFeatureEnabled(e,"all");this.showFieldset("id_room",t||this.isFeatureEnabled(e,"showroom")),this.showInput("id_record",t||this.isFeatureEnabled(e,"showroom")),this.showFieldset("id_recordings",t||this.isFeatureEnabled(e,"showrecordings")),this.showInput("id_recordings_imported",t||this.isFeatureEnabled(e,"showrecordings")),this.showFieldset("id_preuploadpresentation",t||this.isFeatureEnabled(e,"preuploadpresentation")),this.showFieldset("id_permissions",t||this.isFeatureEnabled(e,"permissions")),this.showFieldset("id_schedule",t||this.isFeatureEnabled(e,"schedule")),this.showFieldset("id_modstandardelshdr",t||this.isFeatureEnabled(e,"modstandardelshdr")),this.showFieldset("id_availabilityconditionsheader",t||this.isFeatureEnabled(e,"availabilityconditionsheader")),this.showFieldset("id_tagshdr",t||this.isFeatureEnabled(e,"tagshdr")),this.showFieldset("id_competenciessection",t||this.isFeatureEnabled(e,"competenciessection")),this.showFormGroup("completionattendancegroup",t||this.isFeatureEnabled(e,"completionattendance")),this.showFormGroup("completionengagementgroup",t||this.isFeatureEnabled(e,"completionengagement"))},isFeatureEnabled:function(e,t){var n=this.bigbluebuttonbn.instanceTypeProfiles[e].features;return n.indexOf(t)!=-1},showFieldset:function(t,n){var r=e.one("#"+t);if(!r)return;if(n){r.setStyle("display","block");return}r.setStyle("display","none")},showInput:function(t,n){var r=e.one("#"+t);if(!r)return;var i=r.ancestor("div").ancestor("div");if(n){i.setStyle("display","block");return}i.setStyle("display","none")},showFormGroup:function(t,n){var r=e.one("#fgroup_id_"+t);if(!r)return;if(n){r.removeClass("hidden");return}r.addClass("hidden")},participantSelectionSet:function(){this.selectClear("bigbluebuttonbn_participant_selection");var e=document.getElementById("bigbluebuttonbn_participant_selection_type");for(var t=0;t<e.options.length;t++)if(e.options[t].selected){var n=this.bigbluebuttonbn.participantData[e.options[t].value].children;for(var r in n)n.hasOwnProperty(r)&&this.selectAddOption("bigbluebuttonbn_participant_selection",n[r].name,n[r].id);e.options[t].value==="all"?(this.selectAddOption("bigbluebuttonbn_participant_selection","---------------","all"),this.selectDisable("bigbluebuttonbn_participant_selection")):this.selectEnable("bigbluebuttonbn_participant_selection")}},participantListInit:function(){var e,t,n,r;this.participantListClear();for(var i=0;i<this.bigbluebuttonbn.participantList.length;i++){e=this.bigbluebuttonbn.participantList[i].selectiontype,t=this.bigbluebuttonbn.participantList[i].selectionid,n=this.bigbluebuttonbn.participantList[i].role,r=this.bigbluebuttonbn.participantData[e];if(e!="all"&&typeof r.children[t]=="undefined"){this.participantRemoveFromMemory(e,t);continue}this.participantAddToForm(e,t,n,i>0)}this.participantListUpdate()},participantListClear:function(){var e,t;e=document.getElementById("participant_list_table"),t=e.getElementsByTagName("tr");for(var n=t.length;n>0;n--)e.deleteRow(0)},participantListUpdate:function(){var e=document.getElementsByName("participants")[0];e.value=JSON.stringify(this.bigbluebuttonbn.participantList).replace(/"/g,""")},participantRemove:function(e,t){this.participantRemoveFromMemory(e,t),this.participantRemoveFromForm(e,t),this.participantListUpdate()},participantRemoveFromMemory:function(e,t){var n=t===""?null:t;for(var r=0;r<this.bigbluebuttonbn.participantList.length;r++)this.bigbluebuttonbn.participantList[r].selectiontype==e&&this.bigbluebuttonbn.participantList[r].selectionid==n&&this.bigbluebuttonbn.participantList.splice(r,1)},participantRemoveFromForm:function(e,t){var n="participant_list_tr_"+e+"-"+t,r=document.getElementById("participant_list_table");for(var i=0;i<r.rows.length;i++)r.rows[i].id==n&&r.deleteRow(i)},participantAdd:function(){var e=document.getElementById("bigbluebuttonbn_participant_selection_type"),t=document.getElementById("bigbluebuttonbn_participant_selection");for(var n=0;n<this.bigbluebuttonbn.participantList.length;n++)if(this.bigbluebuttonbn.participantList[n].selectiontype==e.value&&this.bigbluebuttonbn.participantList[n].selectionid==t.value)return;this.participantAddToMemory(e.value,t.value),this.participantAddToForm(e.value,t.value,"viewer",!0),this.participantListUpdate()},participantAddToMemory:function(e,t){this.bigbluebuttonbn.participantList.push({selectiontype:e,selectionid:t,role:"viewer"})},participantAddToForm:function(e,t,n,r){var i,s,o,u,a,f,l,c,h,p,d,v;i=document.getElementById("participant_list_table"),c=i.insertRow(i.rows.length),c.id="participant_list_tr_"+e+"-"+t,h=c.insertCell(0),h.width="125px",h.innerHTML="<b><i>"+this.bigbluebuttonbn.participantData[e].name,h.innerHTML+=(e!=="all"?": ":"")+"</i></b>",p=c.insertCell(1),p.innerHTML="",e!=="all"&&(p.innerHTML=this.bigbluebuttonbn.participantData[e].children[t].name),s=" <i>"+this.strings.as+"</i> ",s+='<select id="participant_list_role_'+e+"-"+t+'"',s+=" onchange=\"M.mod_bigbluebuttonbn.modform.participantListRoleUpdate('",s+=e+"', '"+t,s+='\'); return 0;" class="select custom-select">',f=["viewer","moderator"];for(l=0;l<f.length;l++ +)o="",f[l]===n&&(o=' selected="selected"'),s+='<option value="'+f[l]+'"'+o+">"+this.strings[f[l]]+"</option>";s+="</select>",d=c.insertCell(2),d.innerHTML=s,v=c.insertCell(3),v.width="20px",u=this.strings.remove,a="btn btn-secondary btn-sm",this.bigbluebuttonbn.iconsEnabled&&(u=this.bigbluebuttonbn.pixIconDelete,a="btn btn-link"),s="",r&&(s='<a class="'+a+'" onclick="M.mod_bigbluebuttonbn.modform.participantRemove(\'',s+=e+"', '"+t,s+='\'); return 0;" title="'+this.strings.remove+'">'+u+"</a>"),v.innerHTML=s},participantListRoleUpdate:function(e,t){var n=document.getElementById("participant_list_role_"+e+"-"+t);for(var r=0;r<this.bigbluebuttonbn.participantList.length;r++)this.bigbluebuttonbn.participantList[r].selectiontype==e&&this.bigbluebuttonbn.participantList[r].selectionid==(t===""?null:t)&&(this.bigbluebuttonbn.participantList[r].role=n.value);this.participantListUpdate()},selectClear:function(e){var t=document.getElementById(e);while(t.length>0)t.remove(t.length-1)},selectEnable:function(e){var t=document.getElementById(e);t.disabled=!1},selectDisable:function(e){var t=document.getElementById(e);t.disabled=!0},selectAddOption:function(e,t,n){var r=document.getElementById(e),i=document.createElement("option");i.text=t,i.value=n,r.add(i,i.length)}}},"@VERSION@",{requires:["base","node"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform.js new file mode 100644 index 0000000..334c923 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-modform/moodle-mod_bigbluebuttonbn-modform.js @@ -0,0 +1,345 @@ +YUI.add('moodle-mod_bigbluebuttonbn-modform', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.modform = { + + bigbluebuttonbn: {}, + strings: {}, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.bigbluebuttonbn = bigbluebuttonbn; + this.strings = { + as: M.util.get_string('mod_form_field_participant_list_text_as', 'bigbluebuttonbn'), + viewer: M.util.get_string('mod_form_field_participant_bbb_role_viewer', 'bigbluebuttonbn'), + moderator: M.util.get_string('mod_form_field_participant_bbb_role_moderator', 'bigbluebuttonbn'), + remove: M.util.get_string('mod_form_field_participant_list_action_remove', 'bigbluebuttonbn') + }; + this.updateInstanceTypeProfile(); + this.participantListInit(); + }, + + updateInstanceTypeProfile: function() { + var selectedType, profileType; + selectedType = Y.one('#id_type'); + profileType = this.bigbluebuttonbn.instanceTypeDefault; + if (selectedType !== null) { + profileType = selectedType.get('value'); + } + this.applyInstanceTypeProfile(profileType); + }, + + applyInstanceTypeProfile: function(profileType) { + var showAll = this.isFeatureEnabled(profileType, 'all'); + // Show room settings validation. + this.showFieldset('id_room', showAll || + this.isFeatureEnabled(profileType, 'showroom')); + this.showInput('id_record', showAll || + this.isFeatureEnabled(profileType, 'showroom')); + // Show recordings settings validation. + this.showFieldset('id_recordings', showAll || + this.isFeatureEnabled(profileType, 'showrecordings')); + // Show recordings imported settings validation. + this.showInput('id_recordings_imported', showAll || + this.isFeatureEnabled(profileType, 'showrecordings')); + // Preuploadpresentation feature validation. + this.showFieldset('id_preuploadpresentation', showAll || + this.isFeatureEnabled(profileType, 'preuploadpresentation')); + // Participants feature validation. + this.showFieldset('id_permissions', showAll || + this.isFeatureEnabled(profileType, 'permissions')); + // Schedule feature validation. + this.showFieldset('id_schedule', showAll || + this.isFeatureEnabled(profileType, 'schedule')); + // Common module settings validation. + this.showFieldset('id_modstandardelshdr', showAll || + this.isFeatureEnabled(profileType, 'modstandardelshdr')); + // Restrict access validation. + this.showFieldset('id_availabilityconditionsheader', showAll || + this.isFeatureEnabled(profileType, 'availabilityconditionsheader')); + // Tags validation. + this.showFieldset('id_tagshdr', showAll || this.isFeatureEnabled(profileType, 'tagshdr')); + // Competencies validation. + this.showFieldset('id_competenciessection', showAll || + this.isFeatureEnabled(profileType, 'competenciessection')); + // Completion validation. + this.showFormGroup('completionattendancegroup', showAll || + this.isFeatureEnabled(profileType, 'completionattendance')); + // Completion validation. + this.showFormGroup('completionengagementgroup', showAll || + this.isFeatureEnabled(profileType, 'completionengagement')); + }, + + isFeatureEnabled: function(profileType, feature) { + var features = this.bigbluebuttonbn.instanceTypeProfiles[profileType].features; + return (features.indexOf(feature) != -1); + }, + + showFieldset: function(id, show) { + // Show room settings validation. + var node = Y.one('#' + id); + if (!node) { + return; + } + if (show) { + node.setStyle('display', 'block'); + return; + } + node.setStyle('display', 'none'); + }, + + showInput: function(id, show) { + // Show room settings validation. + var node = Y.one('#' + id); + if (!node) { + return; + } + var ancestor = node.ancestor('div').ancestor('div'); + if (show) { + ancestor.setStyle('display', 'block'); + return; + } + ancestor.setStyle('display', 'none'); + }, + + showFormGroup: function(id, show) { + // Show room settings validation. + var node = Y.one('#fgroup_id_' + id); + if (!node) { + return; + } + if (show) { + node.removeClass('hidden'); + return; + } + node.addClass('hidden'); + }, + + participantSelectionSet: function() { + this.selectClear('bigbluebuttonbn_participant_selection'); + var type = document.getElementById('bigbluebuttonbn_participant_selection_type'); + for (var i = 0; i < type.options.length; i++) { + if (type.options[i].selected) { + var options = this.bigbluebuttonbn.participantData[type.options[i].value].children; + for (var option in options) { + if (options.hasOwnProperty(option)) { + this.selectAddOption( + 'bigbluebuttonbn_participant_selection', options[option].name, options[option].id + ); + } + } + if (type.options[i].value === 'all') { + this.selectAddOption('bigbluebuttonbn_participant_selection', + '---------------', 'all'); + this.selectDisable('bigbluebuttonbn_participant_selection'); + } else { + this.selectEnable('bigbluebuttonbn_participant_selection'); + } + } + } + }, + + participantListInit: function() { + var selectionTypeValue, selectionValue, selectionRole, participantSelectionTypes; + this.participantListClear(); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + selectionTypeValue = this.bigbluebuttonbn.participantList[i].selectiontype; + selectionValue = this.bigbluebuttonbn.participantList[i].selectionid; + selectionRole = this.bigbluebuttonbn.participantList[i].role; + participantSelectionTypes = this.bigbluebuttonbn.participantData[selectionTypeValue]; + if (selectionTypeValue != 'all' && typeof participantSelectionTypes.children[selectionValue] == 'undefined') { + // Remove from memory. + this.participantRemoveFromMemory(selectionTypeValue, selectionValue); + continue; + } + // Add it to the form, but don't add the delete button if it is the first item. + this.participantAddToForm(selectionTypeValue, selectionValue, selectionRole, (i > 0)); + } + // Update in the form. + this.participantListUpdate(); + }, + + participantListClear: function() { + var table, rows; + table = document.getElementById('participant_list_table'); + rows = table.getElementsByTagName('tr'); + for (var i = rows.length; i > 0; i--) { + table.deleteRow(0); + } + }, + + participantListUpdate: function() { + var participantList = document.getElementsByName('participants')[0]; + participantList.value = JSON.stringify(this.bigbluebuttonbn.participantList).replace(/"/g, '"'); + }, + + participantRemove: function(selectionTypeValue, selectionValue) { + // Remove from memory. + this.participantRemoveFromMemory(selectionTypeValue, selectionValue); + + // Remove from the form. + this.participantRemoveFromForm(selectionTypeValue, selectionValue); + + // Update in the form. + this.participantListUpdate(); + }, + + participantRemoveFromMemory: function(selectionTypeValue, selectionValue) { + var selectionid = (selectionValue === '' ? null : selectionValue); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == selectionTypeValue && + this.bigbluebuttonbn.participantList[i].selectionid == selectionid) { + this.bigbluebuttonbn.participantList.splice(i, 1); + } + } + }, + + participantRemoveFromForm: function(selectionTypeValue, selectionValue) { + var id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue; + var participantListTable = document.getElementById('participant_list_table'); + for (var i = 0; i < participantListTable.rows.length; i++) { + if (participantListTable.rows[i].id == id) { + participantListTable.deleteRow(i); + } + } + }, + + participantAdd: function() { + var selectionType = document.getElementById('bigbluebuttonbn_participant_selection_type'); + var selection = document.getElementById('bigbluebuttonbn_participant_selection'); + // Lookup to see if it has been added already. + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == selectionType.value && + this.bigbluebuttonbn.participantList[i].selectionid == selection.value) { + return; + } + } + // Add it to memory. + this.participantAddToMemory(selectionType.value, selection.value); + // Add it to the form. + this.participantAddToForm(selectionType.value, selection.value, 'viewer', true); + // Update in the form. + this.participantListUpdate(); + }, + + participantAddToMemory: function(selectionTypeValue, selectionValue) { + this.bigbluebuttonbn.participantList.push({ + "selectiontype": selectionTypeValue, + "selectionid": selectionValue, + "role": "viewer" + }); + }, + + participantAddToForm: function(selectionTypeValue, selectionValue, selectionRole, canDelete) { + var listTable, innerHTML, selectedHtml, removeHtml, removeClass, bbbRoles, i, row, cell0, cell1, cell2, cell3; + listTable = document.getElementById('participant_list_table'); + row = listTable.insertRow(listTable.rows.length); + row.id = "participant_list_tr_" + selectionTypeValue + "-" + selectionValue; + cell0 = row.insertCell(0); + cell0.width = "125px"; + cell0.innerHTML = '<b><i>' + this.bigbluebuttonbn.participantData[selectionTypeValue].name; + cell0.innerHTML += (selectionTypeValue !== 'all' ? ': ' : '') + '</i></b>'; + cell1 = row.insertCell(1); + cell1.innerHTML = ''; + if (selectionTypeValue !== 'all') { + cell1.innerHTML = this.bigbluebuttonbn.participantData[selectionTypeValue].children[selectionValue].name; + } + innerHTML = ' <i>' + this.strings.as + '</i> '; + innerHTML += '<select id="participant_list_role_' + selectionTypeValue + '-' + selectionValue + '"'; + innerHTML += ' onchange="M.mod_bigbluebuttonbn.modform.participantListRoleUpdate(\''; + innerHTML += selectionTypeValue + '\', \'' + selectionValue; + innerHTML += '\'); return 0;" class="select custom-select">'; + bbbRoles = ['viewer', 'moderator']; + for (i = 0; i < bbbRoles.length; i++) { + selectedHtml = ''; + if (bbbRoles[i] === selectionRole) { + selectedHtml = ' selected="selected"'; + } + innerHTML += '<option value="' + bbbRoles[i] + '"' + selectedHtml + '>' + this.strings[bbbRoles[i]] + '</option>'; + } + innerHTML += '</select>'; + cell2 = row.insertCell(2); + cell2.innerHTML = innerHTML; + cell3 = row.insertCell(3); + cell3.width = "20px"; + removeHtml = this.strings.remove; + removeClass = "btn btn-secondary btn-sm"; + if (this.bigbluebuttonbn.iconsEnabled) { + removeHtml = this.bigbluebuttonbn.pixIconDelete; + removeClass = "btn btn-link"; + } + innerHTML = ""; + if (canDelete) { + innerHTML = '<a class="' + removeClass + '" onclick="M.mod_bigbluebuttonbn.modform.participantRemove(\''; + innerHTML += selectionTypeValue + '\', \'' + selectionValue; + innerHTML += '\'); return 0;" title="' + this.strings.remove + '">' + removeHtml + '</a>'; + } + cell3.innerHTML = innerHTML; + }, + + participantListRoleUpdate: function(type, id) { + // Update in memory. + var participantListRoleSelection = document.getElementById('participant_list_role_' + type + '-' + id); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == type && + this.bigbluebuttonbn.participantList[i].selectionid == (id === '' ? null : id)) { + this.bigbluebuttonbn.participantList[i].role = participantListRoleSelection.value; + } + } + // Update in the form. + this.participantListUpdate(); + }, + + selectClear: function(id) { + var select = document.getElementById(id); + while (select.length > 0) { + select.remove(select.length - 1); + } + }, + + selectEnable: function(id) { + var select = document.getElementById(id); + select.disabled = false; + }, + + selectDisable: function(id) { + var select = document.getElementById(id); + select.disabled = true; + }, + + selectAddOption: function(id, text, value) { + var select = document.getElementById(id); + var option = document.createElement('option'); + option.text = text; + option.value = value; + select.add(option, option.length); + } + +}; + + +}, '@VERSION@', {"requires": ["base", "node"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-debug.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-debug.js new file mode 100644 index 0000000..fc93ed8 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-debug.js @@ -0,0 +1,699 @@ +YUI.add('moodle-mod_bigbluebuttonbn-recordings', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ +/** global: YUI */ +/** global: event */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.recordings = { + + datasource: null, + datatable: {}, + locale: 'en', + windowVideoPlay: null, + table: null, + bbbid: 0, + + /** + * Initialise recordings code. + * + * @method init + * @param {object} dataobj + */ + init: function(dataobj) { + this.bbbid = dataobj.bbbid; + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + '&' + }); + var thisbbb = this; + this.datasource.sendRequest({ + request: "id=" + this.bbbid + "&action=recording_list_table", + callback: { + success: function (data) { + var bbinfo = data.data; + if (bbinfo.recordings_html === false && + (bbinfo.profile_features.indexOf('all') != -1 || bbinfo.profile_features.indexOf('showrecordings') != -1)) { + thisbbb.locale = bbinfo.locale; + thisbbb.datatable.columns = bbinfo.data.columns; + thisbbb.datatable.data = thisbbb.datatableInitFormatDates(bbinfo.data.data); + thisbbb.datatableInit(); + } + } + } + }); + var searchform = Y.one('#bigbluebuttonbn_recordings_searchform'); + if (searchform) { + searchform.delegate('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + var value = null; + if (e.target.get('id') == 'searchsubmit') { + value = Y.one('#searchtext').get('value'); + } else { + Y.one('#searchtext').set('value', ''); + } + + this.filterByText(value); + }, 'input[type=submit]', this); + } + M.mod_bigbluebuttonbn.helpers.init(); + }, + + datatableInitFormatDates: function(data) { + for (var i = 0; i < data.length; i++) { + var date = new Date(data[i].date); + data[i].date = date.toLocaleDateString(this.locale, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + return data; + }, + + initExtraLanguage: function(Y1) { + Y1.Intl.add( + 'datatable-paginator', + Y1.config.lang, + { + first: M.util.get_string('view_recording_yui_first', 'bigbluebuttonbn'), + prev: M.util.get_string('view_recording_yui_prev', 'bigbluebuttonbn'), + next: M.util.get_string('view_recording_yui_next', 'bigbluebuttonbn'), + last: M.util.get_string('view_recording_yui_last', 'bigbluebuttonbn'), + goToLabel: M.util.get_string('view_recording_yui_page', 'bigbluebuttonbn'), + goToAction: M.util.get_string('view_recording_yui_go', 'bigbluebuttonbn'), + perPage: M.util.get_string('view_recording_yui_rows', 'bigbluebuttonbn'), + showAll: M.util.get_string('view_recording_yui_show_all', 'bigbluebuttonbn') + } + ); + }, + + escapeRegex: function(value) { + return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + }, + + filterByText: function(searchvalue) { + if (this.table) { + this.table.set('data', this.datatable.data); + if (searchvalue) { + var tlist = this.table.data; + var rsearch = new RegExp('<span>.*?' + this.escapeRegex(searchvalue) + '.*?</span>', 'i'); + var filterdata = tlist.filter({asList: true}, function(item) { + var name = item.get('recording'); + var description = item.get('description'); + return ( + (name && rsearch.test(name)) || (description && rsearch.test(description)) + ); + }); + this.table.set('data', filterdata); + } + } + }, + + datatableInit: function() { + var columns = this.datatable.columns; + var data = this.datatable.data; + var func = this.initExtraLanguage; + YUI({ + lang: this.locale + }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', function(Y) { + func(Y); + var table = new Y.DataTable({ + width: "1195px", + columns: columns, + data: data, + rowsPerPage: 10, + paginatorLocation: ['header', 'footer'] + }).render('#bigbluebuttonbn_recordings_table'); + M.mod_bigbluebuttonbn.recordings.table = table; + return table; + }); + }, + + recordingElementPayload: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('div'); + return { + action: nodeelement.getAttribute('data-action'), + recordingid: node.getAttribute('data-recordingid'), + meetingid: node.getAttribute('data-meetingid') + }; + }, + + recordingAction: function(element, confirmation, extras) { + var payload = this.recordingElementPayload(element); + for (var attrname in extras) { + payload[attrname] = extras[attrname]; + } + // The action doesn't require confirmation. + if (!confirmation) { + this.recordingActionPerform(payload); + return; + } + // Create the confirmation dialogue. + var confirm = new M.core.confirm({ + modal: true, + centered: true, + question: this.recordingConfirmationMessage(payload) + }); + // If it is confirmed. + confirm.on('complete-yes', function() { + this.recordingActionPerform(payload); + }, this); + }, + + recordingActionPerform: function(data) { + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOn(data); + M.mod_bigbluebuttonbn.broker.recordingActionPerform(data); + + var thisbbb = this; + this.datasource.sendRequest({ + request: "&id=" + this.bbbid + "&action=recording_list_table", + callback: { + success: function (data) { + var bbinfo = data.data; + if (bbinfo.recordings_html === false && + (bbinfo.profile_features.indexOf('all') != -1 || bbinfo.profile_features.indexOf('showrecordings') != -1)) { + thisbbb.locale = bbinfo.locale; + thisbbb.datatable.columns = bbinfo.data.columns; + thisbbb.datatable.data = thisbbb.datatableInitFormatDates(bbinfo.data.data); + } + } + } + }); + }, + + recordingPublish: function(element) { + var extras = { + source: 'published', + goalstate: 'true' + }; + this.recordingAction(element, false, extras); + }, + + recordingUnpublish: function(element) { + var extras = { + source: 'published', + goalstate: 'false' + }; + this.recordingAction(element, false, extras); + }, + + recordingProtect: function(element) { + var extras = { + source: 'protected', + goalstate: 'true' + }; + this.recordingAction(element, false, extras); + }, + + recordingUnprotect: function(element) { + var extras = { + source: 'protected', + goalstate: 'false' + }; + this.recordingAction(element, false, extras); + }, + + recordingDelete: function(element) { + var extras = { + source: 'found', + goalstate: false + }; + var requireConfirmation = true; + if (this.recordingIsImported(element)) { + // When recordingDelete is performed on imported recordings use default response for validation. + requireConfirmation = false; + extras.source = 'status'; + extras.goalstate = true; + extras.attempts = 1; + } + this.recordingAction(element, requireConfirmation, extras); + }, + + recordingImport: function(element) { + var extras = {}; + this.recordingAction(element, true, extras); + }, + + recordingUpdate: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('div'); + var extras = { + target: node.getAttribute('data-target'), + source: node.getAttribute('data-source'), + goalstate: nodeelement.getAttribute('data-goalstate') + }; + this.recordingAction(element, false, extras); + }, + + recordingEdit: function(element) { + var link = Y.one(element); + var node = link.ancestor('div'); + var text = node.one('> span'); + text.hide(); + link.hide(); + var inputtext = Y.Node.create('<input type="text" class="form-control"></input>'); + inputtext.setAttribute('id', link.getAttribute('id')); + inputtext.setAttribute('value', text.getHTML()); + inputtext.setAttribute('data-value', text.getHTML()); + inputtext.on('keydown', M.mod_bigbluebuttonbn.recordings.recordingEditKeydown); + inputtext.on('focusout', M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout); + node.append(inputtext); + inputtext.focus().select(); + }, + + recordingEditKeydown: function(event) { + var keyCode = event.which || event.keyCode; + if (keyCode == 13) { + M.mod_bigbluebuttonbn.recordings.recordingEditPerform(event.currentTarget); + return; + } + if (keyCode == 27) { + M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout(event.currentTarget); + } + }, + + recordingEditOnfocusout: function(nodeelement) { + var node = nodeelement.ancestor('div'); + nodeelement.hide(); + node.one('> span').show(); + node.one('> a').show(); + }, + + recordingEditPerform: function(nodeelement) { + var node = nodeelement.ancestor('div'); + var text = nodeelement.get('value').trim(); + // Perform the update. + nodeelement.setAttribute('data-action', 'edit'); + nodeelement.setAttribute('data-goalstate', text); + nodeelement.hide(); + this.recordingUpdate(nodeelement.getDOMNode()); + node.one('> span').setHTML(text).show(); + node.one('> a').show(); + }, + + recordingEditCompletion: function(data, failed) { + var elementid = M.mod_bigbluebuttonbn.helpers.elementId(data.action, data.target); + var link = Y.one('a#' + elementid + '-' + data.recordingid); + var node = link.ancestor('div'); + var text = node.one('> span'); + if (typeof text === 'undefined') { + return; + } + var inputtext = node.one('> input'); + if (failed) { + text.setHTML(inputtext.getAttribute('data-value')); + } + inputtext.remove(); + }, + + recordingPlay: function(element) { + var nodeelement = Y.one(element); + if (nodeelement.getAttribute('data-href') === '') { + M.mod_bigbluebuttonbn.helpers.alertError( + M.util.get_string('view_recording_format_errror_unreachable', 'bigbluebuttonbn') + ); + return; + } + var extras = { + target: nodeelement.getAttribute('data-target'), + source: 'published', + goalstate: 'true', + attempts: 1, + dataset: nodeelement.getData() + }; + // New window for video play must be created previous to ajax requests. + this.windowVideoPlay = window.open('', '_blank'); + // Prevent malicious modification over window opener to use window.open(). + this.windowVideoPlay.opener = null; + this.recordingAction(element, false, extras); + }, + + recordingConfirmationMessage: function(data) { + var confirmation, recordingType, elementid, associatedLinks, confirmationWarning; + confirmation = M.util.get_string('view_recording_' + data.action + '_confirmation', 'bigbluebuttonbn'); + if (typeof confirmation === 'undefined') { + return ''; + } + recordingType = M.util.get_string('view_recording', 'bigbluebuttonbn'); + if (Y.one('#playbacks-' + data.recordingid).get('dataset').imported === 'true') { + recordingType = M.util.get_string('view_recording_link', 'bigbluebuttonbn'); + } + confirmation = confirmation.replace("{$a}", recordingType); + if (data.action === 'import') { + return confirmation; + } + // If it has associated links imported in a different course/activity, show that in confirmation dialog. + elementid = M.mod_bigbluebuttonbn.helpers.elementId(data.action, data.target); + associatedLinks = Y.one('a#' + elementid + '-' + data.recordingid).get('dataset').links; + if (associatedLinks === 0) { + return confirmation; + } + confirmationWarning = M.util.get_string('view_recording_' + data.action + '_confirmation_warning_p', + 'bigbluebuttonbn'); + if (associatedLinks == 1) { + confirmationWarning = M.util.get_string('view_recording_' + data.action + '_confirmation_warning_s', + 'bigbluebuttonbn'); + } + confirmationWarning = confirmationWarning.replace("{$a}", associatedLinks) + '. '; + return confirmationWarning + '\n\n' + confirmation; + }, + + recordingActionCompletion: function(data) { + var container, table, row; + if (data.action == 'delete') { + row = Y.one('div#recording-actionbar-' + data.recordingid).ancestor('td').ancestor('tr'); + table = row.ancestor('tbody'); + if (table.all('tr').size() == 1) { + container = Y.one('#bigbluebuttonbn_view_recordings_content'); + container.prepend('<span>' + M.util.get_string('view_message_norecordings', 'bigbluebuttonbn') + '</span>'); + container.one('#bigbluebuttonbn_recordings_table').remove(); + return; + } + row.remove(); + return; + } + if (data.action == 'import') { + row = Y.one('div#recording-actionbar-' + data.recordingid).ancestor('td').ancestor('tr'); + row.remove(); + return; + } + if (data.action == 'play') { + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + // Update url in window video to show the video. + this.windowVideoPlay.location.href = data.dataset.href; + return; + } + M.mod_bigbluebuttonbn.helpers.updateData(data); + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + M.mod_bigbluebuttonbn.helpers.updateId(data); + if (data.action === 'publish') { + this.recordingPublishCompletion(data.recordingid); + return; + } + if (data.action === 'unpublish') { + this.recordingUnpublishCompletion(data.recordingid); + return; + } + }, + + recordingActionFailover: function(data) { + M.mod_bigbluebuttonbn.helpers.alertError(data.message); + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + if (data.action === 'edit') { + this.recordingEditCompletion(data, true); + } + }, + + recordingPublishCompletion: function(recordingid) { + var playbacks = Y.one('#playbacks-' + recordingid); + playbacks.show(); + var preview = Y.one('#preview-' + recordingid); + if (preview === null) { + return; + } + preview.show(); + M.mod_bigbluebuttonbn.helpers.reloadPreview(recordingid); + }, + + recordingUnpublishCompletion: function(recordingid) { + var playbacks = Y.one('#playbacks-' + recordingid); + playbacks.hide(); + var preview = Y.one('#preview-' + recordingid); + if (preview === null) { + return; + } + preview.hide(); + }, + + recordingIsImported: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('tr'); + return (node.getAttribute('data-imported') === 'true'); + } + +}; +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.helpers = { + + elementTag: {}, + elementFaClass: {}, + elementActionReversed: {}, + + /** + * Initialise helpers code. + * + * @method init + */ + init: function() { + this.elementTag = this.initElementTag(); + this.elementFaClass = this.initElementFAClass(); + this.elementActionReversed = this.initElementActionReversed(); + }, + + toggleSpinningWheelOn: function(data) { + var elementid, link, button, text; + elementid = this.elementId(data.action, data.target); + text = M.util.get_string('view_recording_list_action_' + data.action, 'bigbluebuttonbn'); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('data-onclick', link.getAttribute('onclick')); + link.setAttribute('onclick', ''); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.toggleSpinningWheelOnCompatible(link, text); + return; + } + button.setAttribute('data-aria-label', button.getAttribute('aria-label')); + button.setAttribute('aria-label', text); + button.setAttribute('data-title', button.getAttribute('title')); + button.setAttribute('title', text); + button.setAttribute('data-class', button.getAttribute('class')); + button.setAttribute('class', this.elementFaClass.process); + }, + + toggleSpinningWheelOnCompatible: function(link, text) { + var button = link.one('> img'); + if (button === null) { + // Button doesn't even have an icon. + return; + } + button.setAttribute('data-alt', button.getAttribute('alt')); + button.setAttribute('alt', text); + button.setAttribute('data-title', button.getAttribute('title')); + button.setAttribute('title', text); + button.setAttribute('data-src', button.getAttribute('src')); + button.setAttribute('src', 'pix/i/processing16.gif'); + }, + + toggleSpinningWheelOff: function(data) { + var elementid, link, button; + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('onclick', link.getAttribute('data-onclick')); + link.removeAttribute('data-onclick'); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.toggleSpinningWheelOffCompatible(link.one('> img')); + return; + } + button.setAttribute('aria-label', button.getAttribute('data-aria-label')); + button.removeAttribute('data-aria-label'); + button.setAttribute('title', button.getAttribute('data-title')); + button.removeAttribute('data-title'); + button.setAttribute('class', button.getAttribute('data-class')); + button.removeAttribute('data-class'); + }, + + toggleSpinningWheelOffCompatible: function(button) { + if (button === null) { + // Button doesn't have an icon. + return; + } + button.setAttribute('alt', button.getAttribute('data-alt')); + button.removeAttribute('data-alt'); + button.setAttribute('title', button.getAttribute('data-title')); + button.removeAttribute('data-title'); + button.setAttribute('src', button.getAttribute('data-src')); + button.removeAttribute('data-src'); + }, + + updateData: function(data) { + var action, elementid, link, linkdataonclick, button, buttondatatext, buttondatatag; + action = this.elementActionReversed[data.action]; + if (action === data.action) { + return; + } + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('data-action', action); + linkdataonclick = link.getAttribute('data-onclick').replace(this.capitalize(data.action), this.capitalize(action)); + link.setAttribute('data-onclick', linkdataonclick); + buttondatatext = M.util.get_string('view_recording_list_actionbar_' + action, 'bigbluebuttonbn'); + buttondatatag = this.elementTag[action]; + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.updateDataCompatible(link.one('> img'), this.elementTag[data.action], buttondatatag, buttondatatext); + return; + } + button.setAttribute('data-aria-label', buttondatatext); + button.setAttribute('data-title', buttondatatext); + button.setAttribute('data-class', this.elementFaClass[action]); + }, + + updateDataCompatible: function(button, action, buttondatatag, buttondatatext) { + if (button === null) { + // Button doesn't have an icon. + return; + } + var buttondatasrc = button.getAttribute('data-src'); + button.setAttribute('data-alt', buttondatatext); + button.setAttribute('data-title', buttondatatext); + button.setAttribute('data-src', buttondatasrc.replace(buttondatatag, action)); + }, + + updateId: function(data) { + var action, elementid, link, button, id; + action = this.elementActionReversed[data.action]; + if (action === data.action) { + return; + } + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + id = '' + elementid.replace(data.action, action) + '-' + data.recordingid; + link.setAttribute('id', id); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + button = link.one('> img'); + } + button.removeAttribute('id'); + }, + + elementId: function(action, target) { + var elementid = 'recording-' + action; + if (typeof target !== 'undefined') { + elementid += '-' + target; + } + return elementid; + }, + + initElementTag: function() { + var tags = {}; + tags.play = 'play'; + tags.publish = 'hide'; + tags.unpublish = 'show'; + tags.protect = 'lock'; + tags.unprotect = 'unlock'; + tags.edit = 'edit'; + tags.process = 'process'; + tags['import'] = 'import'; + tags['delete'] = 'delete'; + return tags; + }, + + initElementFAClass: function() { + var tags = {}; + tags.publish = 'icon fa fa-eye-slash fa-fw iconsmall'; + tags.unpublish = 'icon fa fa-eye fa-fw iconsmall'; + tags.protect = 'icon fa fa-unlock fa-fw iconsmall'; + tags.unprotect = 'icon fa fa-lock fa-fw iconsmall'; + tags.edit = 'icon fa fa-pencil fa-fw iconsmall'; + tags.process = 'icon fa fa-spinner fa-spin iconsmall'; + tags['import'] = 'icon fa fa-download fa-fw iconsmall'; + tags['delete'] = 'icon fa fa-trash fa-fw iconsmall'; + return tags; + }, + + initElementActionReversed: function() { + var actions = {}; + actions.play = 'play'; + actions.publish = 'unpublish'; + actions.unpublish = 'publish'; + actions.protect = 'unprotect'; + actions.unprotect = 'protect'; + actions.edit = 'edit'; + actions['import'] = 'import'; + actions['delete'] = 'delete'; + return actions; + }, + + reloadPreview: function(recordingid) { + var thumbnails = Y.one('#preview-' + recordingid).all('> img'); + thumbnails.each(function(thumbnail) { + var thumbnailsrc = thumbnail.getAttribute('src'); + thumbnailsrc = thumbnailsrc.substring(0, thumbnailsrc.indexOf('?')); + thumbnailsrc += '?' + new Date().getTime(); + thumbnail.setAttribute('src', thumbnailsrc); + }); + }, + + capitalize: function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }, + + alertError: function(message, title) { + if (typeof title == 'undefined') { + title = 'error'; + } + var alert = new M.core.alert({ + title: M.util.get_string(title, 'moodle'), + message: message + }); + alert.show(); + } + +}; + + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] +}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-min.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-min.js new file mode 100644 index 0000000..e3cb0ed --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings-min.js @@ -0,0 +1,3 @@ +YUI.add("moodle-mod_bigbluebuttonbn-recordings",function(e,t){M.mod_bigbluebuttonbn=M.mod_bigbluebuttonbn||{},M.mod_bigbluebuttonbn.recordings={datasource:null,datatable:{},locale:"en",windowVideoPlay:null,table:null,bbbid:0,init:function(t){this.bbbid=t.bbbid,this.datasource=new e.DataSource.Get({source:M.cfg.wwwroot+"/mod/bigbluebuttonbn/bbb_ajax.php?sesskey="+M.cfg.sesskey+"&"});var n=this;this.datasource.sendRequest({request:"id="+this.bbbid+"&action=recording_list_table",callback:{success:function(e){var t=e.data;t.recordings_html===!1&&(t.profile_features.indexOf("all")!=-1||t.profile_features.indexOf("showrecordings")!=-1)&&(n.locale=t.locale,n.datatable.columns=t.data.columns,n.datatable.data=n.datatableInitFormatDates(t.data.data),n.datatableInit())}}});var r=e.one("#bigbluebuttonbn_recordings_searchform");r&&r.delegate("click",function(t){t.preventDefault(),t.stopPropagation();var n=null;t.target.get("id")=="searchsubmit"?n=e.one("#searchtext").get("value"):e.one("#searchtext").set("value",""),this.filterByText(n)},"input[type=submit]",this),M.mod_bigbluebuttonbn.helpers.init()},datatableInitFormatDates:function(e){for(var t=0;t<e.length;t++){var n=new Date(e[t].date);e[t].date=n.toLocaleDateString(this.locale,{weekday:"long",year:"numeric",month:"long",day:"numeric"})}return e},initExtraLanguage:function(e){e.Intl.add("datatable-paginator",e.config.lang,{first:M.util.get_string("view_recording_yui_first","bigbluebuttonbn"),prev:M.util.get_string("view_recording_yui_prev","bigbluebuttonbn"),next:M.util.get_string("view_recording_yui_next","bigbluebuttonbn"),last:M.util.get_string("view_recording_yui_last","bigbluebuttonbn"),goToLabel:M.util.get_string("view_recording_yui_page","bigbluebuttonbn"),goToAction:M.util.get_string("view_recording_yui_go","bigbluebuttonbn"),perPage:M.util.get_string("view_recording_yui_rows","bigbluebuttonbn"),showAll:M.util.get_string("view_recording_yui_show_all","bigbluebuttonbn")})},escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filterByText:function(e){if(this.table){this.table.set("data",this.datatable.data);if(e){var t=this.table.data,n=new RegExp("<span>.*?"+this.escapeRegex(e)+".*?</span>","i"),r=t.filter({asList:!0},function(e){var t=e.get("recording"),r=e.get("description");return t&&n.test(t)||r&&n.test(r)});this.table.set("data",r)}}},datatableInit:function(){var e=this.datatable.columns,t=this.datatable.data,n=this.initExtraLanguage;YUI({lang:this.locale}).use("intl","datatable","datatable-sort","datatable-paginator","datatype-number",function(r){n(r);var i=(new r.DataTable({width:"1195px",columns:e,data:t,rowsPerPage:10,paginatorLocation:["header","footer"]})).render("#bigbluebuttonbn_recordings_table");return M.mod_bigbluebuttonbn.recordings.table=i,i})},recordingElementPayload:function(t){var n=e.one(t),r=n.ancestor("div");return{action:n.getAttribute("data-action"),recordingid:r.getAttribute("data-recordingid"),meetingid:r.getAttribute("data-meetingid")}},recordingAction:function(e,t,n){var r=this.recordingElementPayload(e);for(var i in n)r[i]=n[i];if(!t){this.recordingActionPerform(r);return}var s=new M.core.confirm({modal:!0,centered:!0,question:this.recordingConfirmationMessage(r)});s.on("complete-yes",function(){this.recordingActionPerform(r)},this)},recordingActionPerform:function(e){M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOn(e),M.mod_bigbluebuttonbn.broker.recordingActionPerform(e);var t=this;this.datasource.sendRequest({request:"&id="+this.bbbid+"&action=recording_list_table",callback:{success:function(e){var n=e.data;n.recordings_html===!1&&(n.profile_features.indexOf("all")!=-1||n.profile_features.indexOf("showrecordings")!=-1)&&(t.locale=n.locale,t.datatable.columns=n.data.columns,t.datatable.data=t.datatableInitFormatDates(n.data.data))}}})},recordingPublish:function(e){var t={source:"published",goalstate:"true"};this.recordingAction(e,!1,t)},recordingUnpublish:function(e){var t={source:"published",goalstate:"false"};this.recordingAction(e,!1,t)},recordingProtect:function(e){var t={source:"protected",goalstate:"true"};this.recordingAction(e,!1,t)},recordingUnprotect:function(e){var t={source:"protected",goalstate:"false"};this.recordingAction(e,!1,t)},recordingDelete:function(e){var t={source:"found",goalstate:!1},n=!0;this.recordingIsImported(e)&&(n=!1,t.source="status",t.goalstate=!0,t.attempts=1),this.recordingAction(e,n,t)},recordingImport:function(e){var t={};this.recordingAction(e,!0,t)},recordingUpdate:function(t){var n=e.one(t),r=n.ancestor("div"),i={target:r.getAttribute("data-target"),source:r.getAttribute("data-source"),goalstate:n.getAttribute("data-goalstate")};this.recordingAction(t,!1,i)},recordingEdit:function(t){var n=e.one(t),r=n.ancestor("div"),i=r.one("> span");i.hide(),n.hide();var s=e.Node.create('<input type="text" class="form-control"></input>');s.setAttribute("id",n.getAttribute("id")),s.setAttribute("value",i.getHTML()),s.setAttribute("data-value",i.getHTML()),s.on("keydown",M.mod_bigbluebuttonbn.recordings.recordingEditKeydown),s.on("focusout",M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout),r.append(s),s.focus().select()},recordingEditKeydown:function(e){var t=e.which||e.keyCode;if(t==13){M.mod_bigbluebuttonbn.recordings.recordingEditPerform(e.currentTarget);return}t==27&&M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout(e.currentTarget)},recordingEditOnfocusout:function(e){var t=e.ancestor("div");e.hide(),t.one("> span").show(),t.one("> a").show()},recordingEditPerform:function(e){var t=e.ancestor("div"),n=e.get("value").trim();e.setAttribute("data-action","edit"),e.setAttribute("data-goalstate",n),e.hide(),this.recordingUpdate(e.getDOMNode()),t.one("> span").setHTML(n).show(),t.one("> a").show()},recordingEditCompletion:function(t,n){var r=M.mod_bigbluebuttonbn.helpers.elementId(t.action,t.target),i=e.one("a#"+r+"-"+t.recordingid),s=i.ancestor("div"),o=s.one("> span");if(typeof o=="undefined")return;var u=s.one("> input");n&&o.setHTML(u.getAttribute +("data-value")),u.remove()},recordingPlay:function(t){var n=e.one(t);if(n.getAttribute("data-href")===""){M.mod_bigbluebuttonbn.helpers.alertError(M.util.get_string("view_recording_format_errror_unreachable","bigbluebuttonbn"));return}var r={target:n.getAttribute("data-target"),source:"published",goalstate:"true",attempts:1,dataset:n.getData()};this.windowVideoPlay=window.open("","_blank"),this.windowVideoPlay.opener=null,this.recordingAction(t,!1,r)},recordingConfirmationMessage:function(t){var n,r,i,s,o;return n=M.util.get_string("view_recording_"+t.action+"_confirmation","bigbluebuttonbn"),typeof n=="undefined"?"":(r=M.util.get_string("view_recording","bigbluebuttonbn"),e.one("#playbacks-"+t.recordingid).get("dataset").imported==="true"&&(r=M.util.get_string("view_recording_link","bigbluebuttonbn")),n=n.replace("{$a}",r),t.action==="import"?n:(i=M.mod_bigbluebuttonbn.helpers.elementId(t.action,t.target),s=e.one("a#"+i+"-"+t.recordingid).get("dataset").links,s===0?n:(o=M.util.get_string("view_recording_"+t.action+"_confirmation_warning_p","bigbluebuttonbn"),s==1&&(o=M.util.get_string("view_recording_"+t.action+"_confirmation_warning_s","bigbluebuttonbn")),o=o.replace("{$a}",s)+". ",o+"\n\n"+n)))},recordingActionCompletion:function(t){var n,r,i;if(t.action=="delete"){i=e.one("div#recording-actionbar-"+t.recordingid).ancestor("td").ancestor("tr"),r=i.ancestor("tbody");if(r.all("tr").size()==1){n=e.one("#bigbluebuttonbn_view_recordings_content"),n.prepend("<span>"+M.util.get_string("view_message_norecordings","bigbluebuttonbn")+"</span>"),n.one("#bigbluebuttonbn_recordings_table").remove();return}i.remove();return}if(t.action=="import"){i=e.one("div#recording-actionbar-"+t.recordingid).ancestor("td").ancestor("tr"),i.remove();return}if(t.action=="play"){M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(t),this.windowVideoPlay.location.href=t.dataset.href;return}M.mod_bigbluebuttonbn.helpers.updateData(t),M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(t),M.mod_bigbluebuttonbn.helpers.updateId(t);if(t.action==="publish"){this.recordingPublishCompletion(t.recordingid);return}if(t.action==="unpublish"){this.recordingUnpublishCompletion(t.recordingid);return}},recordingActionFailover:function(e){M.mod_bigbluebuttonbn.helpers.alertError(e.message),M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(e),e.action==="edit"&&this.recordingEditCompletion(e,!0)},recordingPublishCompletion:function(t){var n=e.one("#playbacks-"+t);n.show();var r=e.one("#preview-"+t);if(r===null)return;r.show(),M.mod_bigbluebuttonbn.helpers.reloadPreview(t)},recordingUnpublishCompletion:function(t){var n=e.one("#playbacks-"+t);n.hide();var r=e.one("#preview-"+t);if(r===null)return;r.hide()},recordingIsImported:function(t){var n=e.one(t),r=n.ancestor("tr");return r.getAttribute("data-imported")==="true"}},M.mod_bigbluebuttonbn=M.mod_bigbluebuttonbn||{},M.mod_bigbluebuttonbn.helpers={elementTag:{},elementFaClass:{},elementActionReversed:{},init:function(){this.elementTag=this.initElementTag(),this.elementFaClass=this.initElementFAClass(),this.elementActionReversed=this.initElementActionReversed()},toggleSpinningWheelOn:function(t){var n,r,i,s;n=this.elementId(t.action,t.target),s=M.util.get_string("view_recording_list_action_"+t.action,"bigbluebuttonbn"),r=e.one("a#"+n+"-"+t.recordingid),r.setAttribute("data-onclick",r.getAttribute("onclick")),r.setAttribute("onclick",""),i=r.one("> i");if(i===null){this.toggleSpinningWheelOnCompatible(r,s);return}i.setAttribute("data-aria-label",i.getAttribute("aria-label")),i.setAttribute("aria-label",s),i.setAttribute("data-title",i.getAttribute("title")),i.setAttribute("title",s),i.setAttribute("data-class",i.getAttribute("class")),i.setAttribute("class",this.elementFaClass.process)},toggleSpinningWheelOnCompatible:function(e,t){var n=e.one("> img");if(n===null)return;n.setAttribute("data-alt",n.getAttribute("alt")),n.setAttribute("alt",t),n.setAttribute("data-title",n.getAttribute("title")),n.setAttribute("title",t),n.setAttribute("data-src",n.getAttribute("src")),n.setAttribute("src","pix/i/processing16.gif")},toggleSpinningWheelOff:function(t){var n,r,i;n=this.elementId(t.action,t.target),r=e.one("a#"+n+"-"+t.recordingid),r.setAttribute("onclick",r.getAttribute("data-onclick")),r.removeAttribute("data-onclick"),i=r.one("> i");if(i===null){this.toggleSpinningWheelOffCompatible(r.one("> img"));return}i.setAttribute("aria-label",i.getAttribute("data-aria-label")),i.removeAttribute("data-aria-label"),i.setAttribute("title",i.getAttribute("data-title")),i.removeAttribute("data-title"),i.setAttribute("class",i.getAttribute("data-class")),i.removeAttribute("data-class")},toggleSpinningWheelOffCompatible:function(e){if(e===null)return;e.setAttribute("alt",e.getAttribute("data-alt")),e.removeAttribute("data-alt"),e.setAttribute("title",e.getAttribute("data-title")),e.removeAttribute("data-title"),e.setAttribute("src",e.getAttribute("data-src")),e.removeAttribute("data-src")},updateData:function(t){var n,r,i,s,o,u,a;n=this.elementActionReversed[t.action];if(n===t.action)return;r=this.elementId(t.action,t.target),i=e.one("a#"+r+"-"+t.recordingid),i.setAttribute("data-action",n),s=i.getAttribute("data-onclick").replace(this.capitalize(t.action),this.capitalize(n)),i.setAttribute("data-onclick",s),u=M.util.get_string("view_recording_list_actionbar_"+n,"bigbluebuttonbn"),a=this.elementTag[n],o=i.one("> i");if(o===null){this.updateDataCompatible(i.one("> img"),this.elementTag[t.action],a,u);return}o.setAttribute("data-aria-label",u),o.setAttribute("data-title",u),o.setAttribute("data-class",this.elementFaClass[n])},updateDataCompatible:function(e,t,n,r){if(e===null)return;var i=e.getAttribute("data-src");e.setAttribute("data-alt",r),e.setAttribute("data-title",r),e.setAttribute("data-src",i.replace(n,t))},updateId:function(t){var n,r,i,s,o;n=this.elementActionReversed[t.action];if(n===t.action)return;r=this.elementId(t.action,t.target),i=e.one("a#"+r+"-"+t.recordingid),o=""+r.replace(t.action +,n)+"-"+t.recordingid,i.setAttribute("id",o),s=i.one("> i"),s===null&&(s=i.one("> img")),s.removeAttribute("id")},elementId:function(e,t){var n="recording-"+e;return typeof t!="undefined"&&(n+="-"+t),n},initElementTag:function(){var e={};return e.play="play",e.publish="hide",e.unpublish="show",e.protect="lock",e.unprotect="unlock",e.edit="edit",e.process="process",e["import"]="import",e["delete"]="delete",e},initElementFAClass:function(){var e={};return e.publish="icon fa fa-eye-slash fa-fw iconsmall",e.unpublish="icon fa fa-eye fa-fw iconsmall",e.protect="icon fa fa-unlock fa-fw iconsmall",e.unprotect="icon fa fa-lock fa-fw iconsmall",e.edit="icon fa fa-pencil fa-fw iconsmall",e.process="icon fa fa-spinner fa-spin iconsmall",e["import"]="icon fa fa-download fa-fw iconsmall",e["delete"]="icon fa fa-trash fa-fw iconsmall",e},initElementActionReversed:function(){var e={};return e.play="play",e.publish="unpublish",e.unpublish="publish",e.protect="unprotect",e.unprotect="protect",e.edit="edit",e["import"]="import",e["delete"]="delete",e},reloadPreview:function(t){var n=e.one("#preview-"+t).all("> img");n.each(function(e){var t=e.getAttribute("src");t=t.substring(0,t.indexOf("?")),t+="?"+(new Date).getTime(),e.setAttribute("src",t)})},capitalize:function(e){return e.charAt(0).toUpperCase()+e.slice(1)},alertError:function(e,t){typeof t=="undefined"&&(t="error");var n=new M.core.alert({title:M.util.get_string(t,"moodle"),message:e});n.show()}}},"@VERSION@",{requires:["base","node","datasource-get","datasource-jsonschema","datasource-polling","moodle-core-notification"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings.js new file mode 100644 index 0000000..fc93ed8 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-recordings/moodle-mod_bigbluebuttonbn-recordings.js @@ -0,0 +1,699 @@ +YUI.add('moodle-mod_bigbluebuttonbn-recordings', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ +/** global: YUI */ +/** global: event */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.recordings = { + + datasource: null, + datatable: {}, + locale: 'en', + windowVideoPlay: null, + table: null, + bbbid: 0, + + /** + * Initialise recordings code. + * + * @method init + * @param {object} dataobj + */ + init: function(dataobj) { + this.bbbid = dataobj.bbbid; + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + '&' + }); + var thisbbb = this; + this.datasource.sendRequest({ + request: "id=" + this.bbbid + "&action=recording_list_table", + callback: { + success: function (data) { + var bbinfo = data.data; + if (bbinfo.recordings_html === false && + (bbinfo.profile_features.indexOf('all') != -1 || bbinfo.profile_features.indexOf('showrecordings') != -1)) { + thisbbb.locale = bbinfo.locale; + thisbbb.datatable.columns = bbinfo.data.columns; + thisbbb.datatable.data = thisbbb.datatableInitFormatDates(bbinfo.data.data); + thisbbb.datatableInit(); + } + } + } + }); + var searchform = Y.one('#bigbluebuttonbn_recordings_searchform'); + if (searchform) { + searchform.delegate('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + var value = null; + if (e.target.get('id') == 'searchsubmit') { + value = Y.one('#searchtext').get('value'); + } else { + Y.one('#searchtext').set('value', ''); + } + + this.filterByText(value); + }, 'input[type=submit]', this); + } + M.mod_bigbluebuttonbn.helpers.init(); + }, + + datatableInitFormatDates: function(data) { + for (var i = 0; i < data.length; i++) { + var date = new Date(data[i].date); + data[i].date = date.toLocaleDateString(this.locale, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + return data; + }, + + initExtraLanguage: function(Y1) { + Y1.Intl.add( + 'datatable-paginator', + Y1.config.lang, + { + first: M.util.get_string('view_recording_yui_first', 'bigbluebuttonbn'), + prev: M.util.get_string('view_recording_yui_prev', 'bigbluebuttonbn'), + next: M.util.get_string('view_recording_yui_next', 'bigbluebuttonbn'), + last: M.util.get_string('view_recording_yui_last', 'bigbluebuttonbn'), + goToLabel: M.util.get_string('view_recording_yui_page', 'bigbluebuttonbn'), + goToAction: M.util.get_string('view_recording_yui_go', 'bigbluebuttonbn'), + perPage: M.util.get_string('view_recording_yui_rows', 'bigbluebuttonbn'), + showAll: M.util.get_string('view_recording_yui_show_all', 'bigbluebuttonbn') + } + ); + }, + + escapeRegex: function(value) { + return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + }, + + filterByText: function(searchvalue) { + if (this.table) { + this.table.set('data', this.datatable.data); + if (searchvalue) { + var tlist = this.table.data; + var rsearch = new RegExp('<span>.*?' + this.escapeRegex(searchvalue) + '.*?</span>', 'i'); + var filterdata = tlist.filter({asList: true}, function(item) { + var name = item.get('recording'); + var description = item.get('description'); + return ( + (name && rsearch.test(name)) || (description && rsearch.test(description)) + ); + }); + this.table.set('data', filterdata); + } + } + }, + + datatableInit: function() { + var columns = this.datatable.columns; + var data = this.datatable.data; + var func = this.initExtraLanguage; + YUI({ + lang: this.locale + }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', function(Y) { + func(Y); + var table = new Y.DataTable({ + width: "1195px", + columns: columns, + data: data, + rowsPerPage: 10, + paginatorLocation: ['header', 'footer'] + }).render('#bigbluebuttonbn_recordings_table'); + M.mod_bigbluebuttonbn.recordings.table = table; + return table; + }); + }, + + recordingElementPayload: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('div'); + return { + action: nodeelement.getAttribute('data-action'), + recordingid: node.getAttribute('data-recordingid'), + meetingid: node.getAttribute('data-meetingid') + }; + }, + + recordingAction: function(element, confirmation, extras) { + var payload = this.recordingElementPayload(element); + for (var attrname in extras) { + payload[attrname] = extras[attrname]; + } + // The action doesn't require confirmation. + if (!confirmation) { + this.recordingActionPerform(payload); + return; + } + // Create the confirmation dialogue. + var confirm = new M.core.confirm({ + modal: true, + centered: true, + question: this.recordingConfirmationMessage(payload) + }); + // If it is confirmed. + confirm.on('complete-yes', function() { + this.recordingActionPerform(payload); + }, this); + }, + + recordingActionPerform: function(data) { + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOn(data); + M.mod_bigbluebuttonbn.broker.recordingActionPerform(data); + + var thisbbb = this; + this.datasource.sendRequest({ + request: "&id=" + this.bbbid + "&action=recording_list_table", + callback: { + success: function (data) { + var bbinfo = data.data; + if (bbinfo.recordings_html === false && + (bbinfo.profile_features.indexOf('all') != -1 || bbinfo.profile_features.indexOf('showrecordings') != -1)) { + thisbbb.locale = bbinfo.locale; + thisbbb.datatable.columns = bbinfo.data.columns; + thisbbb.datatable.data = thisbbb.datatableInitFormatDates(bbinfo.data.data); + } + } + } + }); + }, + + recordingPublish: function(element) { + var extras = { + source: 'published', + goalstate: 'true' + }; + this.recordingAction(element, false, extras); + }, + + recordingUnpublish: function(element) { + var extras = { + source: 'published', + goalstate: 'false' + }; + this.recordingAction(element, false, extras); + }, + + recordingProtect: function(element) { + var extras = { + source: 'protected', + goalstate: 'true' + }; + this.recordingAction(element, false, extras); + }, + + recordingUnprotect: function(element) { + var extras = { + source: 'protected', + goalstate: 'false' + }; + this.recordingAction(element, false, extras); + }, + + recordingDelete: function(element) { + var extras = { + source: 'found', + goalstate: false + }; + var requireConfirmation = true; + if (this.recordingIsImported(element)) { + // When recordingDelete is performed on imported recordings use default response for validation. + requireConfirmation = false; + extras.source = 'status'; + extras.goalstate = true; + extras.attempts = 1; + } + this.recordingAction(element, requireConfirmation, extras); + }, + + recordingImport: function(element) { + var extras = {}; + this.recordingAction(element, true, extras); + }, + + recordingUpdate: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('div'); + var extras = { + target: node.getAttribute('data-target'), + source: node.getAttribute('data-source'), + goalstate: nodeelement.getAttribute('data-goalstate') + }; + this.recordingAction(element, false, extras); + }, + + recordingEdit: function(element) { + var link = Y.one(element); + var node = link.ancestor('div'); + var text = node.one('> span'); + text.hide(); + link.hide(); + var inputtext = Y.Node.create('<input type="text" class="form-control"></input>'); + inputtext.setAttribute('id', link.getAttribute('id')); + inputtext.setAttribute('value', text.getHTML()); + inputtext.setAttribute('data-value', text.getHTML()); + inputtext.on('keydown', M.mod_bigbluebuttonbn.recordings.recordingEditKeydown); + inputtext.on('focusout', M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout); + node.append(inputtext); + inputtext.focus().select(); + }, + + recordingEditKeydown: function(event) { + var keyCode = event.which || event.keyCode; + if (keyCode == 13) { + M.mod_bigbluebuttonbn.recordings.recordingEditPerform(event.currentTarget); + return; + } + if (keyCode == 27) { + M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout(event.currentTarget); + } + }, + + recordingEditOnfocusout: function(nodeelement) { + var node = nodeelement.ancestor('div'); + nodeelement.hide(); + node.one('> span').show(); + node.one('> a').show(); + }, + + recordingEditPerform: function(nodeelement) { + var node = nodeelement.ancestor('div'); + var text = nodeelement.get('value').trim(); + // Perform the update. + nodeelement.setAttribute('data-action', 'edit'); + nodeelement.setAttribute('data-goalstate', text); + nodeelement.hide(); + this.recordingUpdate(nodeelement.getDOMNode()); + node.one('> span').setHTML(text).show(); + node.one('> a').show(); + }, + + recordingEditCompletion: function(data, failed) { + var elementid = M.mod_bigbluebuttonbn.helpers.elementId(data.action, data.target); + var link = Y.one('a#' + elementid + '-' + data.recordingid); + var node = link.ancestor('div'); + var text = node.one('> span'); + if (typeof text === 'undefined') { + return; + } + var inputtext = node.one('> input'); + if (failed) { + text.setHTML(inputtext.getAttribute('data-value')); + } + inputtext.remove(); + }, + + recordingPlay: function(element) { + var nodeelement = Y.one(element); + if (nodeelement.getAttribute('data-href') === '') { + M.mod_bigbluebuttonbn.helpers.alertError( + M.util.get_string('view_recording_format_errror_unreachable', 'bigbluebuttonbn') + ); + return; + } + var extras = { + target: nodeelement.getAttribute('data-target'), + source: 'published', + goalstate: 'true', + attempts: 1, + dataset: nodeelement.getData() + }; + // New window for video play must be created previous to ajax requests. + this.windowVideoPlay = window.open('', '_blank'); + // Prevent malicious modification over window opener to use window.open(). + this.windowVideoPlay.opener = null; + this.recordingAction(element, false, extras); + }, + + recordingConfirmationMessage: function(data) { + var confirmation, recordingType, elementid, associatedLinks, confirmationWarning; + confirmation = M.util.get_string('view_recording_' + data.action + '_confirmation', 'bigbluebuttonbn'); + if (typeof confirmation === 'undefined') { + return ''; + } + recordingType = M.util.get_string('view_recording', 'bigbluebuttonbn'); + if (Y.one('#playbacks-' + data.recordingid).get('dataset').imported === 'true') { + recordingType = M.util.get_string('view_recording_link', 'bigbluebuttonbn'); + } + confirmation = confirmation.replace("{$a}", recordingType); + if (data.action === 'import') { + return confirmation; + } + // If it has associated links imported in a different course/activity, show that in confirmation dialog. + elementid = M.mod_bigbluebuttonbn.helpers.elementId(data.action, data.target); + associatedLinks = Y.one('a#' + elementid + '-' + data.recordingid).get('dataset').links; + if (associatedLinks === 0) { + return confirmation; + } + confirmationWarning = M.util.get_string('view_recording_' + data.action + '_confirmation_warning_p', + 'bigbluebuttonbn'); + if (associatedLinks == 1) { + confirmationWarning = M.util.get_string('view_recording_' + data.action + '_confirmation_warning_s', + 'bigbluebuttonbn'); + } + confirmationWarning = confirmationWarning.replace("{$a}", associatedLinks) + '. '; + return confirmationWarning + '\n\n' + confirmation; + }, + + recordingActionCompletion: function(data) { + var container, table, row; + if (data.action == 'delete') { + row = Y.one('div#recording-actionbar-' + data.recordingid).ancestor('td').ancestor('tr'); + table = row.ancestor('tbody'); + if (table.all('tr').size() == 1) { + container = Y.one('#bigbluebuttonbn_view_recordings_content'); + container.prepend('<span>' + M.util.get_string('view_message_norecordings', 'bigbluebuttonbn') + '</span>'); + container.one('#bigbluebuttonbn_recordings_table').remove(); + return; + } + row.remove(); + return; + } + if (data.action == 'import') { + row = Y.one('div#recording-actionbar-' + data.recordingid).ancestor('td').ancestor('tr'); + row.remove(); + return; + } + if (data.action == 'play') { + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + // Update url in window video to show the video. + this.windowVideoPlay.location.href = data.dataset.href; + return; + } + M.mod_bigbluebuttonbn.helpers.updateData(data); + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + M.mod_bigbluebuttonbn.helpers.updateId(data); + if (data.action === 'publish') { + this.recordingPublishCompletion(data.recordingid); + return; + } + if (data.action === 'unpublish') { + this.recordingUnpublishCompletion(data.recordingid); + return; + } + }, + + recordingActionFailover: function(data) { + M.mod_bigbluebuttonbn.helpers.alertError(data.message); + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + if (data.action === 'edit') { + this.recordingEditCompletion(data, true); + } + }, + + recordingPublishCompletion: function(recordingid) { + var playbacks = Y.one('#playbacks-' + recordingid); + playbacks.show(); + var preview = Y.one('#preview-' + recordingid); + if (preview === null) { + return; + } + preview.show(); + M.mod_bigbluebuttonbn.helpers.reloadPreview(recordingid); + }, + + recordingUnpublishCompletion: function(recordingid) { + var playbacks = Y.one('#playbacks-' + recordingid); + playbacks.hide(); + var preview = Y.one('#preview-' + recordingid); + if (preview === null) { + return; + } + preview.hide(); + }, + + recordingIsImported: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('tr'); + return (node.getAttribute('data-imported') === 'true'); + } + +}; +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.helpers = { + + elementTag: {}, + elementFaClass: {}, + elementActionReversed: {}, + + /** + * Initialise helpers code. + * + * @method init + */ + init: function() { + this.elementTag = this.initElementTag(); + this.elementFaClass = this.initElementFAClass(); + this.elementActionReversed = this.initElementActionReversed(); + }, + + toggleSpinningWheelOn: function(data) { + var elementid, link, button, text; + elementid = this.elementId(data.action, data.target); + text = M.util.get_string('view_recording_list_action_' + data.action, 'bigbluebuttonbn'); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('data-onclick', link.getAttribute('onclick')); + link.setAttribute('onclick', ''); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.toggleSpinningWheelOnCompatible(link, text); + return; + } + button.setAttribute('data-aria-label', button.getAttribute('aria-label')); + button.setAttribute('aria-label', text); + button.setAttribute('data-title', button.getAttribute('title')); + button.setAttribute('title', text); + button.setAttribute('data-class', button.getAttribute('class')); + button.setAttribute('class', this.elementFaClass.process); + }, + + toggleSpinningWheelOnCompatible: function(link, text) { + var button = link.one('> img'); + if (button === null) { + // Button doesn't even have an icon. + return; + } + button.setAttribute('data-alt', button.getAttribute('alt')); + button.setAttribute('alt', text); + button.setAttribute('data-title', button.getAttribute('title')); + button.setAttribute('title', text); + button.setAttribute('data-src', button.getAttribute('src')); + button.setAttribute('src', 'pix/i/processing16.gif'); + }, + + toggleSpinningWheelOff: function(data) { + var elementid, link, button; + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('onclick', link.getAttribute('data-onclick')); + link.removeAttribute('data-onclick'); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.toggleSpinningWheelOffCompatible(link.one('> img')); + return; + } + button.setAttribute('aria-label', button.getAttribute('data-aria-label')); + button.removeAttribute('data-aria-label'); + button.setAttribute('title', button.getAttribute('data-title')); + button.removeAttribute('data-title'); + button.setAttribute('class', button.getAttribute('data-class')); + button.removeAttribute('data-class'); + }, + + toggleSpinningWheelOffCompatible: function(button) { + if (button === null) { + // Button doesn't have an icon. + return; + } + button.setAttribute('alt', button.getAttribute('data-alt')); + button.removeAttribute('data-alt'); + button.setAttribute('title', button.getAttribute('data-title')); + button.removeAttribute('data-title'); + button.setAttribute('src', button.getAttribute('data-src')); + button.removeAttribute('data-src'); + }, + + updateData: function(data) { + var action, elementid, link, linkdataonclick, button, buttondatatext, buttondatatag; + action = this.elementActionReversed[data.action]; + if (action === data.action) { + return; + } + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('data-action', action); + linkdataonclick = link.getAttribute('data-onclick').replace(this.capitalize(data.action), this.capitalize(action)); + link.setAttribute('data-onclick', linkdataonclick); + buttondatatext = M.util.get_string('view_recording_list_actionbar_' + action, 'bigbluebuttonbn'); + buttondatatag = this.elementTag[action]; + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.updateDataCompatible(link.one('> img'), this.elementTag[data.action], buttondatatag, buttondatatext); + return; + } + button.setAttribute('data-aria-label', buttondatatext); + button.setAttribute('data-title', buttondatatext); + button.setAttribute('data-class', this.elementFaClass[action]); + }, + + updateDataCompatible: function(button, action, buttondatatag, buttondatatext) { + if (button === null) { + // Button doesn't have an icon. + return; + } + var buttondatasrc = button.getAttribute('data-src'); + button.setAttribute('data-alt', buttondatatext); + button.setAttribute('data-title', buttondatatext); + button.setAttribute('data-src', buttondatasrc.replace(buttondatatag, action)); + }, + + updateId: function(data) { + var action, elementid, link, button, id; + action = this.elementActionReversed[data.action]; + if (action === data.action) { + return; + } + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + id = '' + elementid.replace(data.action, action) + '-' + data.recordingid; + link.setAttribute('id', id); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + button = link.one('> img'); + } + button.removeAttribute('id'); + }, + + elementId: function(action, target) { + var elementid = 'recording-' + action; + if (typeof target !== 'undefined') { + elementid += '-' + target; + } + return elementid; + }, + + initElementTag: function() { + var tags = {}; + tags.play = 'play'; + tags.publish = 'hide'; + tags.unpublish = 'show'; + tags.protect = 'lock'; + tags.unprotect = 'unlock'; + tags.edit = 'edit'; + tags.process = 'process'; + tags['import'] = 'import'; + tags['delete'] = 'delete'; + return tags; + }, + + initElementFAClass: function() { + var tags = {}; + tags.publish = 'icon fa fa-eye-slash fa-fw iconsmall'; + tags.unpublish = 'icon fa fa-eye fa-fw iconsmall'; + tags.protect = 'icon fa fa-unlock fa-fw iconsmall'; + tags.unprotect = 'icon fa fa-lock fa-fw iconsmall'; + tags.edit = 'icon fa fa-pencil fa-fw iconsmall'; + tags.process = 'icon fa fa-spinner fa-spin iconsmall'; + tags['import'] = 'icon fa fa-download fa-fw iconsmall'; + tags['delete'] = 'icon fa fa-trash fa-fw iconsmall'; + return tags; + }, + + initElementActionReversed: function() { + var actions = {}; + actions.play = 'play'; + actions.publish = 'unpublish'; + actions.unpublish = 'publish'; + actions.protect = 'unprotect'; + actions.unprotect = 'protect'; + actions.edit = 'edit'; + actions['import'] = 'import'; + actions['delete'] = 'delete'; + return actions; + }, + + reloadPreview: function(recordingid) { + var thumbnails = Y.one('#preview-' + recordingid).all('> img'); + thumbnails.each(function(thumbnail) { + var thumbnailsrc = thumbnail.getAttribute('src'); + thumbnailsrc = thumbnailsrc.substring(0, thumbnailsrc.indexOf('?')); + thumbnailsrc += '?' + new Date().getTime(); + thumbnail.setAttribute('src', thumbnailsrc); + }); + }, + + capitalize: function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }, + + alertError: function(message, title) { + if (typeof title == 'undefined') { + title = 'error'; + } + var alert = new M.core.alert({ + title: M.util.get_string(title, 'moodle'), + message: message + }); + alert.show(); + } + +}; + + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] +}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-debug.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-debug.js new file mode 100644 index 0000000..2d50202 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-debug.js @@ -0,0 +1,297 @@ +YUI.add('moodle-mod_bigbluebuttonbn-rooms', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ +/** global: opener */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.rooms = { + + datasource: null, + bigbluebuttonbn: {}, + panel: null, + pinginterval: null, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + "&" + }); + this.bigbluebuttonbn = bigbluebuttonbn; + this.pinginterval = bigbluebuttonbn.ping_interval; + if (this.pinginterval === 0) { + this.pinginterval = 10000; + } + if (this.bigbluebuttonbn.profile_features.indexOf('all') != -1 || + this.bigbluebuttonbn.profile_features.indexOf('showroom') != -1) { + this.initRoom(); + } + this.initCompletionValidate(); + }, + + initRoom: function() { + if (this.bigbluebuttonbn.activity !== 'open') { + var statusBar = [M.util.get_string('view_message_conference_has_ended', 'bigbluebuttonbn')]; + if (this.bigbluebuttonbn.activity !== 'ended') { + statusBar = [ + M.util.get_string('view_message_conference_not_started', 'bigbluebuttonbn'), + this.bigbluebuttonbn.opening, + this.bigbluebuttonbn.closing + ]; + } + Y.DOM.addHTML(Y.one('#status_bar'), this.initStatusBar(statusBar)); + return; + } + this.updateRoom(); + }, + + updateRoom: function(f) { + var updatecache = 'false'; + if (typeof f !== 'undefined' && f) { + updatecache = 'true'; + } + var id = this.bigbluebuttonbn.meetingid; + var bnid = this.bigbluebuttonbn.bigbluebuttonbnid; + this.datasource.sendRequest({ + request: 'action=meeting_info&id=' + id + '&bigbluebuttonbn=' + bnid + '&updatecache=' + updatecache, + callback: { + success: function(e) { + Y.DOM.addHTML(Y.one('#status_bar'), + M.mod_bigbluebuttonbn.rooms.initStatusBar(e.data.status.message)); + Y.DOM.addHTML(Y.one('#control_panel'), + M.mod_bigbluebuttonbn.rooms.initControlPanel(e.data)); + if (typeof e.data.status.can_join != 'undefined') { + Y.DOM.addHTML(Y.one('#join_button'), + M.mod_bigbluebuttonbn.rooms.initJoinButton(e.data.status)); + } + if (typeof e.data.status.can_end != 'undefined' && e.data.status.can_end) { + Y.DOM.addHTML(Y.one('#end_button'), + M.mod_bigbluebuttonbn.rooms.initEndButton(e.data.status)); + } + if (!e.data.status.can_join) { + M.mod_bigbluebuttonbn.rooms.waitModerator({ + id: id, + bnid: bnid + }); + } + } + } + }); + }, + + initStatusBar: function(statusMessage) { + var statusBarSpan = Y.DOM.create('<span id="status_bar_span">'); + if (statusMessage.constructor !== Array) { + Y.DOM.setText(statusBarSpan, statusMessage); + return statusBarSpan; + } + for (var message in statusMessage) { + if (!statusMessage.hasOwnProperty(message)) { + continue; // Skip keys from the prototype. + } + var statusBarSpanSpan = Y.DOM.create('<span id="status_bar_span_span">'); + Y.DOM.setText(statusBarSpanSpan, statusMessage[message]); + Y.DOM.addHTML(statusBarSpan, statusBarSpanSpan); + Y.DOM.addHTML(statusBarSpan, Y.DOM.create('<br>')); + } + return statusBarSpan; + }, + + initControlPanel: function(data) { + var controlPanelDiv = Y.DOM.create('<div>'); + Y.DOM.setAttribute(controlPanelDiv, 'id', 'control_panel_div'); + var controlPanelDivHtml = ''; + if (data.running) { + controlPanelDivHtml += this.msgStartedAt(data.info.startTime) + ' '; + controlPanelDivHtml += this.msgAttendeesIn(data.info.moderatorCount, data.info.participantCount); + } + Y.DOM.addHTML(controlPanelDiv, controlPanelDivHtml); + return (controlPanelDiv); + }, + + msgStartedAt: function(startTime) { + var startTimestamp = (parseInt(startTime, 10) - parseInt(startTime, 10) % 1000); + var date = new Date(startTimestamp); + var hours = date.getHours(); + var minutes = date.getMinutes(); + var startedAt = M.util.get_string('view_message_session_started_at', 'bigbluebuttonbn'); + return startedAt + ' <b>' + hours + ':' + (minutes < 10 ? '0' : '') + minutes + '</b>.'; + }, + + msgModeratorsIn: function(moderators) { + var msgModerators = M.util.get_string('view_message_moderators', 'bigbluebuttonbn'); + if (moderators == 1) { + msgModerators = M.util.get_string('view_message_moderator', 'bigbluebuttonbn'); + } + return msgModerators; + }, + + msgViewersIn: function(viewers) { + var msgViewers = M.util.get_string('view_message_viewers', 'bigbluebuttonbn'); + if (viewers == 1) { + msgViewers = M.util.get_string('view_message_viewer', 'bigbluebuttonbn'); + } + return msgViewers; + }, + + msgAttendeesIn: function(moderators, participants) { + var msgModerators, viewers, msgViewers, msg; + if (!this.hasParticipants(participants)) { + return M.util.get_string('view_message_session_no_users', 'bigbluebuttonbn') + '.'; + } + msgModerators = this.msgModeratorsIn(moderators); + viewers = participants - moderators; + msgViewers = this.msgViewersIn(viewers); + msg = M.util.get_string('view_message_session_has_users', 'bigbluebuttonbn'); + if (participants > 1) { + return msg + ' <b>' + moderators + '</b> ' + msgModerators + ' ' + + M.util.get_string('view_message_and', 'bigbluebuttonbn') + ' <b>' + viewers + '</b> ' + msgViewers + '.'; + } + msg = M.util.get_string('view_message_session_has_user', 'bigbluebuttonbn'); + if (moderators > 0) { + return msg + ' <b>1</b> ' + msgModerators + '.'; + } + return msg + ' <b>1</b> ' + msgViewers + '.'; + }, + + hasParticipants: function(participants) { + return (typeof participants != 'undefined' && participants > 0); + }, + + initJoinButton: function(status) { + var joinButtonInput = Y.DOM.create('<input>'); + Y.DOM.setAttribute(joinButtonInput, 'id', 'join_button_input'); + Y.DOM.setAttribute(joinButtonInput, 'type', 'button'); + Y.DOM.setAttribute(joinButtonInput, 'value', status.join_button_text); + Y.DOM.setAttribute(joinButtonInput, 'class', 'btn btn-primary'); + var inputHtml = 'M.mod_bigbluebuttonbn.rooms.join(\'' + status.join_url + '\');'; + Y.DOM.setAttribute(joinButtonInput, 'onclick', inputHtml); + if (!status.can_join) { + // Disable join button. + Y.DOM.setAttribute(joinButtonInput, 'disabled', true); + var statusBarSpan = Y.one('#status_bar_span'); + // Create a img element. + var spinningWheel = Y.DOM.create('<img>'); + Y.DOM.setAttribute(spinningWheel, 'id', 'spinning_wheel'); + Y.DOM.setAttribute(spinningWheel, 'src', 'pix/i/processing16.gif'); + // Add the spinning wheel. + Y.DOM.addHTML(statusBarSpan, ' '); + Y.DOM.addHTML(statusBarSpan, spinningWheel); + } + return joinButtonInput; + }, + + initEndButton: function(status) { + var endButtonInput = Y.DOM.create('<input>'); + Y.DOM.setAttribute(endButtonInput, 'id', 'end_button_input'); + Y.DOM.setAttribute(endButtonInput, 'type', 'button'); + Y.DOM.setAttribute(endButtonInput, 'value', status.end_button_text); + Y.DOM.setAttribute(endButtonInput, 'class', 'btn btn-secondary'); + if (status.can_end) { + Y.DOM.setAttribute(endButtonInput, 'onclick', 'M.mod_bigbluebuttonbn.broker.endMeeting();'); + } + return endButtonInput; + }, + + endMeeting: function() { + Y.one('#control_panel_div').remove(); + Y.one('#join_button').hide(); + Y.one('#end_button').hide(); + }, + + remoteUpdate: function(delay) { + setTimeout(function() { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(true); + }, delay); + }, + + cleanRoom: function() { + Y.one('#status_bar_span').remove(); + Y.one('#control_panel_div').remove(); + Y.one('#join_button').setContent(''); + Y.one('#end_button').setContent(''); + }, + + windowClose: function() { + window.onunload = function() { + opener.M.mod_bigbluebuttonbn.rooms.remoteUpdate(5000); + }; + window.close(); + }, + + waitModerator: function(payload) { + var pooling = setInterval(function() { + M.mod_bigbluebuttonbn.rooms.datasource.sendRequest({ + request: "action=meeting_info&id=" + payload.id + "&bigbluebuttonbn=" + payload.bnid, + callback: { + success: function(e) { + if (e.data.running) { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(); + clearInterval(pooling); + return; + } + }, + failure: function(e) { + payload.message = e.error.message; + } + } + }); + }, this.pinginterval); + }, + + join: function(joinUrl) { + M.mod_bigbluebuttonbn.broker.joinRedirect(joinUrl); + // Update view. + setTimeout(function() { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(true); + }, 15000); + }, + + initCompletionValidate: function() { + var node = Y.one('a[href*=completion_validate]'); + if (!node) { + return; + } + var qs = node.get('hash').substr(1); + node.on("click", function() { + M.mod_bigbluebuttonbn.broker.completionValidate(qs); + }); + } + +}; + + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] +}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-min.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-min.js new file mode 100644 index 0000000..0a91bb2 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_bigbluebuttonbn-rooms",function(e,t){M.mod_bigbluebuttonbn=M.mod_bigbluebuttonbn||{},M.mod_bigbluebuttonbn.rooms={datasource:null,bigbluebuttonbn:{},panel:null,pinginterval:null,init:function(t){this.datasource=new e.DataSource.Get({source:M.cfg.wwwroot+"/mod/bigbluebuttonbn/bbb_ajax.php?sesskey="+M.cfg.sesskey+"&"}),this.bigbluebuttonbn=t,this.pinginterval=t.ping_interval,this.pinginterval===0&&(this.pinginterval=1e4),(this.bigbluebuttonbn.profile_features.indexOf("all")!=-1||this.bigbluebuttonbn.profile_features.indexOf("showroom")!=-1)&&this.initRoom(),this.initCompletionValidate()},initRoom:function(){if(this.bigbluebuttonbn.activity!=="open"){var t=[M.util.get_string("view_message_conference_has_ended","bigbluebuttonbn")];this.bigbluebuttonbn.activity!=="ended"&&(t=[M.util.get_string("view_message_conference_not_started","bigbluebuttonbn"),this.bigbluebuttonbn.opening,this.bigbluebuttonbn.closing]),e.DOM.addHTML(e.one("#status_bar"),this.initStatusBar(t));return}this.updateRoom()},updateRoom:function(t){var n="false";typeof t!="undefined"&&t&&(n="true");var r=this.bigbluebuttonbn.meetingid,i=this.bigbluebuttonbn.bigbluebuttonbnid;this.datasource.sendRequest({request:"action=meeting_info&id="+r+"&bigbluebuttonbn="+i+"&updatecache="+n,callback:{success:function(t){e.DOM.addHTML(e.one("#status_bar"),M.mod_bigbluebuttonbn.rooms.initStatusBar(t.data.status.message)),e.DOM.addHTML(e.one("#control_panel"),M.mod_bigbluebuttonbn.rooms.initControlPanel(t.data)),typeof t.data.status.can_join!="undefined"&&e.DOM.addHTML(e.one("#join_button"),M.mod_bigbluebuttonbn.rooms.initJoinButton(t.data.status)),typeof t.data.status.can_end!="undefined"&&t.data.status.can_end&&e.DOM.addHTML(e.one("#end_button"),M.mod_bigbluebuttonbn.rooms.initEndButton(t.data.status)),t.data.status.can_join||M.mod_bigbluebuttonbn.rooms.waitModerator({id:r,bnid:i})}}})},initStatusBar:function(t){var n=e.DOM.create('<span id="status_bar_span">');if(t.constructor!==Array)return e.DOM.setText(n,t),n;for(var r in t){if(!t.hasOwnProperty(r))continue;var i=e.DOM.create('<span id="status_bar_span_span">');e.DOM.setText(i,t[r]),e.DOM.addHTML(n,i),e.DOM.addHTML(n,e.DOM.create("<br>"))}return n},initControlPanel:function(t){var n=e.DOM.create("<div>");e.DOM.setAttribute(n,"id","control_panel_div");var r="";return t.running&&(r+=this.msgStartedAt(t.info.startTime)+" ",r+=this.msgAttendeesIn(t.info.moderatorCount,t.info.participantCount)),e.DOM.addHTML(n,r),n},msgStartedAt:function(e){var t=parseInt(e,10)-parseInt(e,10)%1e3,n=new Date(t),r=n.getHours(),i=n.getMinutes(),s=M.util.get_string("view_message_session_started_at","bigbluebuttonbn");return s+" <b>"+r+":"+(i<10?"0":"")+i+"</b>."},msgModeratorsIn:function(e){var t=M.util.get_string("view_message_moderators","bigbluebuttonbn");return e==1&&(t=M.util.get_string("view_message_moderator","bigbluebuttonbn")),t},msgViewersIn:function(e){var t=M.util.get_string("view_message_viewers","bigbluebuttonbn");return e==1&&(t=M.util.get_string("view_message_viewer","bigbluebuttonbn")),t},msgAttendeesIn:function(e,t){var n,r,i,s;return this.hasParticipants(t)?(n=this.msgModeratorsIn(e),r=t-e,i=this.msgViewersIn(r),s=M.util.get_string("view_message_session_has_users","bigbluebuttonbn"),t>1?s+" <b>"+e+"</b> "+n+" "+M.util.get_string("view_message_and","bigbluebuttonbn")+" <b>"+r+"</b> "+i+".":(s=M.util.get_string("view_message_session_has_user","bigbluebuttonbn"),e>0?s+" <b>1</b> "+n+".":s+" <b>1</b> "+i+".")):M.util.get_string("view_message_session_no_users","bigbluebuttonbn")+"."},hasParticipants:function(e){return typeof e!="undefined"&&e>0},initJoinButton:function(t){var n=e.DOM.create("<input>");e.DOM.setAttribute(n,"id","join_button_input"),e.DOM.setAttribute(n,"type","button"),e.DOM.setAttribute(n,"value",t.join_button_text),e.DOM.setAttribute(n,"class","btn btn-primary");var r="M.mod_bigbluebuttonbn.rooms.join('"+t.join_url+"');";e.DOM.setAttribute(n,"onclick",r);if(!t.can_join){e.DOM.setAttribute(n,"disabled",!0);var i=e.one("#status_bar_span"),s=e.DOM.create("<img>");e.DOM.setAttribute(s,"id","spinning_wheel"),e.DOM.setAttribute(s,"src","pix/i/processing16.gif"),e.DOM.addHTML(i," "),e.DOM.addHTML(i,s)}return n},initEndButton:function(t){var n=e.DOM.create("<input>");return e.DOM.setAttribute(n,"id","end_button_input"),e.DOM.setAttribute(n,"type","button"),e.DOM.setAttribute(n,"value",t.end_button_text),e.DOM.setAttribute(n,"class","btn btn-secondary"),t.can_end&&e.DOM.setAttribute(n,"onclick","M.mod_bigbluebuttonbn.broker.endMeeting();"),n},endMeeting:function(){e.one("#control_panel_div").remove(),e.one("#join_button").hide(),e.one("#end_button").hide()},remoteUpdate:function(e){setTimeout(function(){M.mod_bigbluebuttonbn.rooms.cleanRoom(),M.mod_bigbluebuttonbn.rooms.updateRoom(!0)},e)},cleanRoom:function(){e.one("#status_bar_span").remove(),e.one("#control_panel_div").remove(),e.one("#join_button").setContent(""),e.one("#end_button").setContent("")},windowClose:function(){window.onunload=function(){opener.M.mod_bigbluebuttonbn.rooms.remoteUpdate(5e3)},window.close()},waitModerator:function(e){var t=setInterval(function(){M.mod_bigbluebuttonbn.rooms.datasource.sendRequest({request:"action=meeting_info&id="+e.id+"&bigbluebuttonbn="+e.bnid,callback:{success:function(e){if(e.data.running){M.mod_bigbluebuttonbn.rooms.cleanRoom(),M.mod_bigbluebuttonbn.rooms.updateRoom(),clearInterval(t);return}},failure:function(t){e.message=t.error.message}}})},this.pinginterval)},join:function(e){M.mod_bigbluebuttonbn.broker.joinRedirect(e),setTimeout(function(){M.mod_bigbluebuttonbn.rooms.cleanRoom(),M.mod_bigbluebuttonbn.rooms.updateRoom(!0)},15e3)},initCompletionValidate:function(){var t=e.one("a[href*=completion_validate]");if(!t)return;var n=t.get("hash").substr(1);t.on("click",function(){M.mod_bigbluebuttonbn.broker.completionValidate(n)})}}},"@VERSION@",{requires:["base","node","datasource-get","datasource-jsonschema","datasource-polling","moodle-core-notification"]}); diff --git a/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms.js b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms.js new file mode 100644 index 0000000..2d50202 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/build/moodle-mod_bigbluebuttonbn-rooms/moodle-mod_bigbluebuttonbn-rooms.js @@ -0,0 +1,297 @@ +YUI.add('moodle-mod_bigbluebuttonbn-rooms', function (Y, NAME) { + +// 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/>. + +/** global: M */ +/** global: Y */ +/** global: opener */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.rooms = { + + datasource: null, + bigbluebuttonbn: {}, + panel: null, + pinginterval: null, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + "&" + }); + this.bigbluebuttonbn = bigbluebuttonbn; + this.pinginterval = bigbluebuttonbn.ping_interval; + if (this.pinginterval === 0) { + this.pinginterval = 10000; + } + if (this.bigbluebuttonbn.profile_features.indexOf('all') != -1 || + this.bigbluebuttonbn.profile_features.indexOf('showroom') != -1) { + this.initRoom(); + } + this.initCompletionValidate(); + }, + + initRoom: function() { + if (this.bigbluebuttonbn.activity !== 'open') { + var statusBar = [M.util.get_string('view_message_conference_has_ended', 'bigbluebuttonbn')]; + if (this.bigbluebuttonbn.activity !== 'ended') { + statusBar = [ + M.util.get_string('view_message_conference_not_started', 'bigbluebuttonbn'), + this.bigbluebuttonbn.opening, + this.bigbluebuttonbn.closing + ]; + } + Y.DOM.addHTML(Y.one('#status_bar'), this.initStatusBar(statusBar)); + return; + } + this.updateRoom(); + }, + + updateRoom: function(f) { + var updatecache = 'false'; + if (typeof f !== 'undefined' && f) { + updatecache = 'true'; + } + var id = this.bigbluebuttonbn.meetingid; + var bnid = this.bigbluebuttonbn.bigbluebuttonbnid; + this.datasource.sendRequest({ + request: 'action=meeting_info&id=' + id + '&bigbluebuttonbn=' + bnid + '&updatecache=' + updatecache, + callback: { + success: function(e) { + Y.DOM.addHTML(Y.one('#status_bar'), + M.mod_bigbluebuttonbn.rooms.initStatusBar(e.data.status.message)); + Y.DOM.addHTML(Y.one('#control_panel'), + M.mod_bigbluebuttonbn.rooms.initControlPanel(e.data)); + if (typeof e.data.status.can_join != 'undefined') { + Y.DOM.addHTML(Y.one('#join_button'), + M.mod_bigbluebuttonbn.rooms.initJoinButton(e.data.status)); + } + if (typeof e.data.status.can_end != 'undefined' && e.data.status.can_end) { + Y.DOM.addHTML(Y.one('#end_button'), + M.mod_bigbluebuttonbn.rooms.initEndButton(e.data.status)); + } + if (!e.data.status.can_join) { + M.mod_bigbluebuttonbn.rooms.waitModerator({ + id: id, + bnid: bnid + }); + } + } + } + }); + }, + + initStatusBar: function(statusMessage) { + var statusBarSpan = Y.DOM.create('<span id="status_bar_span">'); + if (statusMessage.constructor !== Array) { + Y.DOM.setText(statusBarSpan, statusMessage); + return statusBarSpan; + } + for (var message in statusMessage) { + if (!statusMessage.hasOwnProperty(message)) { + continue; // Skip keys from the prototype. + } + var statusBarSpanSpan = Y.DOM.create('<span id="status_bar_span_span">'); + Y.DOM.setText(statusBarSpanSpan, statusMessage[message]); + Y.DOM.addHTML(statusBarSpan, statusBarSpanSpan); + Y.DOM.addHTML(statusBarSpan, Y.DOM.create('<br>')); + } + return statusBarSpan; + }, + + initControlPanel: function(data) { + var controlPanelDiv = Y.DOM.create('<div>'); + Y.DOM.setAttribute(controlPanelDiv, 'id', 'control_panel_div'); + var controlPanelDivHtml = ''; + if (data.running) { + controlPanelDivHtml += this.msgStartedAt(data.info.startTime) + ' '; + controlPanelDivHtml += this.msgAttendeesIn(data.info.moderatorCount, data.info.participantCount); + } + Y.DOM.addHTML(controlPanelDiv, controlPanelDivHtml); + return (controlPanelDiv); + }, + + msgStartedAt: function(startTime) { + var startTimestamp = (parseInt(startTime, 10) - parseInt(startTime, 10) % 1000); + var date = new Date(startTimestamp); + var hours = date.getHours(); + var minutes = date.getMinutes(); + var startedAt = M.util.get_string('view_message_session_started_at', 'bigbluebuttonbn'); + return startedAt + ' <b>' + hours + ':' + (minutes < 10 ? '0' : '') + minutes + '</b>.'; + }, + + msgModeratorsIn: function(moderators) { + var msgModerators = M.util.get_string('view_message_moderators', 'bigbluebuttonbn'); + if (moderators == 1) { + msgModerators = M.util.get_string('view_message_moderator', 'bigbluebuttonbn'); + } + return msgModerators; + }, + + msgViewersIn: function(viewers) { + var msgViewers = M.util.get_string('view_message_viewers', 'bigbluebuttonbn'); + if (viewers == 1) { + msgViewers = M.util.get_string('view_message_viewer', 'bigbluebuttonbn'); + } + return msgViewers; + }, + + msgAttendeesIn: function(moderators, participants) { + var msgModerators, viewers, msgViewers, msg; + if (!this.hasParticipants(participants)) { + return M.util.get_string('view_message_session_no_users', 'bigbluebuttonbn') + '.'; + } + msgModerators = this.msgModeratorsIn(moderators); + viewers = participants - moderators; + msgViewers = this.msgViewersIn(viewers); + msg = M.util.get_string('view_message_session_has_users', 'bigbluebuttonbn'); + if (participants > 1) { + return msg + ' <b>' + moderators + '</b> ' + msgModerators + ' ' + + M.util.get_string('view_message_and', 'bigbluebuttonbn') + ' <b>' + viewers + '</b> ' + msgViewers + '.'; + } + msg = M.util.get_string('view_message_session_has_user', 'bigbluebuttonbn'); + if (moderators > 0) { + return msg + ' <b>1</b> ' + msgModerators + '.'; + } + return msg + ' <b>1</b> ' + msgViewers + '.'; + }, + + hasParticipants: function(participants) { + return (typeof participants != 'undefined' && participants > 0); + }, + + initJoinButton: function(status) { + var joinButtonInput = Y.DOM.create('<input>'); + Y.DOM.setAttribute(joinButtonInput, 'id', 'join_button_input'); + Y.DOM.setAttribute(joinButtonInput, 'type', 'button'); + Y.DOM.setAttribute(joinButtonInput, 'value', status.join_button_text); + Y.DOM.setAttribute(joinButtonInput, 'class', 'btn btn-primary'); + var inputHtml = 'M.mod_bigbluebuttonbn.rooms.join(\'' + status.join_url + '\');'; + Y.DOM.setAttribute(joinButtonInput, 'onclick', inputHtml); + if (!status.can_join) { + // Disable join button. + Y.DOM.setAttribute(joinButtonInput, 'disabled', true); + var statusBarSpan = Y.one('#status_bar_span'); + // Create a img element. + var spinningWheel = Y.DOM.create('<img>'); + Y.DOM.setAttribute(spinningWheel, 'id', 'spinning_wheel'); + Y.DOM.setAttribute(spinningWheel, 'src', 'pix/i/processing16.gif'); + // Add the spinning wheel. + Y.DOM.addHTML(statusBarSpan, ' '); + Y.DOM.addHTML(statusBarSpan, spinningWheel); + } + return joinButtonInput; + }, + + initEndButton: function(status) { + var endButtonInput = Y.DOM.create('<input>'); + Y.DOM.setAttribute(endButtonInput, 'id', 'end_button_input'); + Y.DOM.setAttribute(endButtonInput, 'type', 'button'); + Y.DOM.setAttribute(endButtonInput, 'value', status.end_button_text); + Y.DOM.setAttribute(endButtonInput, 'class', 'btn btn-secondary'); + if (status.can_end) { + Y.DOM.setAttribute(endButtonInput, 'onclick', 'M.mod_bigbluebuttonbn.broker.endMeeting();'); + } + return endButtonInput; + }, + + endMeeting: function() { + Y.one('#control_panel_div').remove(); + Y.one('#join_button').hide(); + Y.one('#end_button').hide(); + }, + + remoteUpdate: function(delay) { + setTimeout(function() { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(true); + }, delay); + }, + + cleanRoom: function() { + Y.one('#status_bar_span').remove(); + Y.one('#control_panel_div').remove(); + Y.one('#join_button').setContent(''); + Y.one('#end_button').setContent(''); + }, + + windowClose: function() { + window.onunload = function() { + opener.M.mod_bigbluebuttonbn.rooms.remoteUpdate(5000); + }; + window.close(); + }, + + waitModerator: function(payload) { + var pooling = setInterval(function() { + M.mod_bigbluebuttonbn.rooms.datasource.sendRequest({ + request: "action=meeting_info&id=" + payload.id + "&bigbluebuttonbn=" + payload.bnid, + callback: { + success: function(e) { + if (e.data.running) { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(); + clearInterval(pooling); + return; + } + }, + failure: function(e) { + payload.message = e.error.message; + } + } + }); + }, this.pinginterval); + }, + + join: function(joinUrl) { + M.mod_bigbluebuttonbn.broker.joinRedirect(joinUrl); + // Update view. + setTimeout(function() { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(true); + }, 15000); + }, + + initCompletionValidate: function() { + var node = Y.one('a[href*=completion_validate]'); + if (!node) { + return; + } + var qs = node.get('hash').substr(1); + node.on("click", function() { + M.mod_bigbluebuttonbn.broker.completionValidate(qs); + }); + } + +}; + + +}, '@VERSION@', { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] +}); diff --git a/mod/bigbluebuttonbn/yui/src/broker/build.json b/mod/bigbluebuttonbn/yui/src/broker/build.json new file mode 100644 index 0000000..367fbd3 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/broker/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_bigbluebuttonbn-broker", + "builds": { + "moodle-mod_bigbluebuttonbn-broker": { + "jsfiles": [ + "broker.js" + ] + } + } +} diff --git a/mod/bigbluebuttonbn/yui/src/broker/js/broker.js b/mod/bigbluebuttonbn/yui/src/broker/js/broker.js new file mode 100644 index 0000000..a548916 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/broker/js/broker.js @@ -0,0 +1,185 @@ +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.broker = { + + datasource: null, + bigbluebuttonbn: {}, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + "&" + }); + this.bigbluebuttonbn = bigbluebuttonbn; + }, + + joinRedirect: function(joinUrl) { + window.open(joinUrl); + }, + + recordingActionPerform: function(data) { + var qs = "action=recording_" + data.action + "&id=" + data.recordingid + "&idx=" + data.meetingid; + qs += this.recordingActionMetaQS(data); + data.attempt = 1; + if (typeof data.attempts === 'undefined') { + data.attempts = 5; + } + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + // Something went wrong. + if (!e.data.status) { + data.message = e.data.message; + return M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + // There is no need for verification. + if (typeof data.goalstate === 'undefined') { + return M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(data); + } + // Use the current response for verification. + if (data.attempts <= 1) { + return M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(e, data); + } + // Iterate the verification. + return M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(data); + }, + failure: function(e) { + data.message = e.error.message; + return M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + } + }); + }, + + recordingActionMetaQS: function(data) { + var qs = ''; + if (typeof data.source !== 'undefined') { + var meta = {}; + meta[data.source] = encodeURIComponent(data.goalstate); + qs += "&meta=" + JSON.stringify(meta); + } + return qs; + }, + + recordingActionPerformedValidate: function(data) { + var qs = "action=recording_info&id=" + data.recordingid + "&idx=" + data.meetingid; + qs += this.recordingActionMetaQS(data); + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + // Evaluates if the current attempt has been completed. + if (M.mod_bigbluebuttonbn.broker.recordingActionPerformedComplete(e, data)) { + // It has been completed, so stop the action. + return; + } + // Evaluates if more attempts have to be performed. + if (data.attempt < data.attempts) { + data.attempt += 1; + setTimeout(((function() { + return function() { + M.mod_bigbluebuttonbn.broker.recordingActionPerformedValidate(data); + }; + })(this)), (data.attempt - 1) * 1000); + return; + } + // No more attempts to perform, it stops with failing over. + data.message = M.util.get_string('view_error_action_not_completed', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + }, + failure: function(e) { + data.message = e.error.message; + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + } + } + }); + }, + + recordingActionPerformedComplete: function(e, data) { + // Something went wrong. + if (typeof e.data[data.source] === 'undefined') { + data.message = M.util.get_string('view_error_current_state_not_found', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.recordings.recordingActionFailover(data); + return true; + } + // Evaluates if the state is as expected. + if (e.data[data.source] === data.goalstate) { + M.mod_bigbluebuttonbn.recordings.recordingActionCompletion(data); + return true; + } + return false; + }, + + recordingCurrentState: function(action, data) { + if (action === 'publish' || action === 'unpublish') { + return data.published; + } + if (action === 'delete') { + return data.status; + } + if (action === 'protect' || action === 'unprotect') { + return data.secured; // The broker responds with secured as protected is a reserverd word. + } + if (action === 'update') { + return data.updated; + } + return null; + }, + + endMeeting: function() { + var qs = 'action=meeting_end&id=' + this.bigbluebuttonbn.meetingid; + qs += '&bigbluebuttonbn=' + this.bigbluebuttonbn.bigbluebuttonbnid; + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + if (e.data.status) { + M.mod_bigbluebuttonbn.rooms.endMeeting(); + location.reload(); + } + } + } + }); + }, + + completionValidate: function(qs) { + this.datasource.sendRequest({ + request: qs, + callback: { + success: function(e) { + if (e.data.status) { + var message = M.util.get_string('completionvalidatestatetriggered', 'bigbluebuttonbn'); + M.mod_bigbluebuttonbn.helpers.alertError(message, 'info'); + return; + } + } + } + }); + } + +}; + diff --git a/mod/bigbluebuttonbn/yui/src/broker/meta/broker.json b/mod/bigbluebuttonbn/yui/src/broker/meta/broker.json new file mode 100644 index 0000000..bc845e4 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/broker/meta/broker.json @@ -0,0 +1,12 @@ +{ + "moodle-mod_bigbluebuttonbn-broker": { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] + } +} diff --git a/mod/bigbluebuttonbn/yui/src/imports/build.json b/mod/bigbluebuttonbn/yui/src/imports/build.json new file mode 100644 index 0000000..31b8240 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/imports/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_bigbluebuttonbn-imports", + "builds": { + "moodle-mod_bigbluebuttonbn-imports": { + "jsfiles": [ + "imports.js" + ] + } + } +} diff --git a/mod/bigbluebuttonbn/yui/src/imports/js/imports.js b/mod/bigbluebuttonbn/yui/src/imports/js/imports.js new file mode 100644 index 0000000..3966dec --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/imports/js/imports.js @@ -0,0 +1,39 @@ +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.imports = { + + /** + * Initialise the broker code. + * + * @method init + * @param {object} data + */ + init: function(data) { + // Init event listener for course selector. + Y.one('#menuimport_recording_links_select').on('change', function() { + var endpoint = '/mod/bigbluebuttonbn/import_view.php'; + var qs = '?bn=' + data.bn + '&tc=' + this.get('value'); + Y.config.win.location = M.cfg.wwwroot + endpoint + qs; + }); + } + +}; + diff --git a/mod/bigbluebuttonbn/yui/src/imports/meta/imports.json b/mod/bigbluebuttonbn/yui/src/imports/meta/imports.json new file mode 100644 index 0000000..6f88793 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/imports/meta/imports.json @@ -0,0 +1,8 @@ +{ + "moodle-mod_bigbluebuttonbn-imports": { + "requires": [ + "base", + "node" + ] + } +} diff --git a/mod/bigbluebuttonbn/yui/src/modform/build.json b/mod/bigbluebuttonbn/yui/src/modform/build.json new file mode 100644 index 0000000..559a55e --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/modform/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_bigbluebuttonbn-modform", + "builds": { + "moodle-mod_bigbluebuttonbn-modform": { + "jsfiles": [ + "modform.js" + ] + } + } +} diff --git a/mod/bigbluebuttonbn/yui/src/modform/js/modform.js b/mod/bigbluebuttonbn/yui/src/modform/js/modform.js new file mode 100644 index 0000000..b1a084f --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/modform/js/modform.js @@ -0,0 +1,340 @@ +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.modform = { + + bigbluebuttonbn: {}, + strings: {}, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.bigbluebuttonbn = bigbluebuttonbn; + this.strings = { + as: M.util.get_string('mod_form_field_participant_list_text_as', 'bigbluebuttonbn'), + viewer: M.util.get_string('mod_form_field_participant_bbb_role_viewer', 'bigbluebuttonbn'), + moderator: M.util.get_string('mod_form_field_participant_bbb_role_moderator', 'bigbluebuttonbn'), + remove: M.util.get_string('mod_form_field_participant_list_action_remove', 'bigbluebuttonbn') + }; + this.updateInstanceTypeProfile(); + this.participantListInit(); + }, + + updateInstanceTypeProfile: function() { + var selectedType, profileType; + selectedType = Y.one('#id_type'); + profileType = this.bigbluebuttonbn.instanceTypeDefault; + if (selectedType !== null) { + profileType = selectedType.get('value'); + } + this.applyInstanceTypeProfile(profileType); + }, + + applyInstanceTypeProfile: function(profileType) { + var showAll = this.isFeatureEnabled(profileType, 'all'); + // Show room settings validation. + this.showFieldset('id_room', showAll || + this.isFeatureEnabled(profileType, 'showroom')); + this.showInput('id_record', showAll || + this.isFeatureEnabled(profileType, 'showroom')); + // Show recordings settings validation. + this.showFieldset('id_recordings', showAll || + this.isFeatureEnabled(profileType, 'showrecordings')); + // Show recordings imported settings validation. + this.showInput('id_recordings_imported', showAll || + this.isFeatureEnabled(profileType, 'showrecordings')); + // Preuploadpresentation feature validation. + this.showFieldset('id_preuploadpresentation', showAll || + this.isFeatureEnabled(profileType, 'preuploadpresentation')); + // Participants feature validation. + this.showFieldset('id_permissions', showAll || + this.isFeatureEnabled(profileType, 'permissions')); + // Schedule feature validation. + this.showFieldset('id_schedule', showAll || + this.isFeatureEnabled(profileType, 'schedule')); + // Common module settings validation. + this.showFieldset('id_modstandardelshdr', showAll || + this.isFeatureEnabled(profileType, 'modstandardelshdr')); + // Restrict access validation. + this.showFieldset('id_availabilityconditionsheader', showAll || + this.isFeatureEnabled(profileType, 'availabilityconditionsheader')); + // Tags validation. + this.showFieldset('id_tagshdr', showAll || this.isFeatureEnabled(profileType, 'tagshdr')); + // Competencies validation. + this.showFieldset('id_competenciessection', showAll || + this.isFeatureEnabled(profileType, 'competenciessection')); + // Completion validation. + this.showFormGroup('completionattendancegroup', showAll || + this.isFeatureEnabled(profileType, 'completionattendance')); + // Completion validation. + this.showFormGroup('completionengagementgroup', showAll || + this.isFeatureEnabled(profileType, 'completionengagement')); + }, + + isFeatureEnabled: function(profileType, feature) { + var features = this.bigbluebuttonbn.instanceTypeProfiles[profileType].features; + return (features.indexOf(feature) != -1); + }, + + showFieldset: function(id, show) { + // Show room settings validation. + var node = Y.one('#' + id); + if (!node) { + return; + } + if (show) { + node.setStyle('display', 'block'); + return; + } + node.setStyle('display', 'none'); + }, + + showInput: function(id, show) { + // Show room settings validation. + var node = Y.one('#' + id); + if (!node) { + return; + } + var ancestor = node.ancestor('div').ancestor('div'); + if (show) { + ancestor.setStyle('display', 'block'); + return; + } + ancestor.setStyle('display', 'none'); + }, + + showFormGroup: function(id, show) { + // Show room settings validation. + var node = Y.one('#fgroup_id_' + id); + if (!node) { + return; + } + if (show) { + node.removeClass('hidden'); + return; + } + node.addClass('hidden'); + }, + + participantSelectionSet: function() { + this.selectClear('bigbluebuttonbn_participant_selection'); + var type = document.getElementById('bigbluebuttonbn_participant_selection_type'); + for (var i = 0; i < type.options.length; i++) { + if (type.options[i].selected) { + var options = this.bigbluebuttonbn.participantData[type.options[i].value].children; + for (var option in options) { + if (options.hasOwnProperty(option)) { + this.selectAddOption( + 'bigbluebuttonbn_participant_selection', options[option].name, options[option].id + ); + } + } + if (type.options[i].value === 'all') { + this.selectAddOption('bigbluebuttonbn_participant_selection', + '---------------', 'all'); + this.selectDisable('bigbluebuttonbn_participant_selection'); + } else { + this.selectEnable('bigbluebuttonbn_participant_selection'); + } + } + } + }, + + participantListInit: function() { + var selectionTypeValue, selectionValue, selectionRole, participantSelectionTypes; + this.participantListClear(); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + selectionTypeValue = this.bigbluebuttonbn.participantList[i].selectiontype; + selectionValue = this.bigbluebuttonbn.participantList[i].selectionid; + selectionRole = this.bigbluebuttonbn.participantList[i].role; + participantSelectionTypes = this.bigbluebuttonbn.participantData[selectionTypeValue]; + if (selectionTypeValue != 'all' && typeof participantSelectionTypes.children[selectionValue] == 'undefined') { + // Remove from memory. + this.participantRemoveFromMemory(selectionTypeValue, selectionValue); + continue; + } + // Add it to the form, but don't add the delete button if it is the first item. + this.participantAddToForm(selectionTypeValue, selectionValue, selectionRole, (i > 0)); + } + // Update in the form. + this.participantListUpdate(); + }, + + participantListClear: function() { + var table, rows; + table = document.getElementById('participant_list_table'); + rows = table.getElementsByTagName('tr'); + for (var i = rows.length; i > 0; i--) { + table.deleteRow(0); + } + }, + + participantListUpdate: function() { + var participantList = document.getElementsByName('participants')[0]; + participantList.value = JSON.stringify(this.bigbluebuttonbn.participantList).replace(/"/g, '"'); + }, + + participantRemove: function(selectionTypeValue, selectionValue) { + // Remove from memory. + this.participantRemoveFromMemory(selectionTypeValue, selectionValue); + + // Remove from the form. + this.participantRemoveFromForm(selectionTypeValue, selectionValue); + + // Update in the form. + this.participantListUpdate(); + }, + + participantRemoveFromMemory: function(selectionTypeValue, selectionValue) { + var selectionid = (selectionValue === '' ? null : selectionValue); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == selectionTypeValue && + this.bigbluebuttonbn.participantList[i].selectionid == selectionid) { + this.bigbluebuttonbn.participantList.splice(i, 1); + } + } + }, + + participantRemoveFromForm: function(selectionTypeValue, selectionValue) { + var id = 'participant_list_tr_' + selectionTypeValue + '-' + selectionValue; + var participantListTable = document.getElementById('participant_list_table'); + for (var i = 0; i < participantListTable.rows.length; i++) { + if (participantListTable.rows[i].id == id) { + participantListTable.deleteRow(i); + } + } + }, + + participantAdd: function() { + var selectionType = document.getElementById('bigbluebuttonbn_participant_selection_type'); + var selection = document.getElementById('bigbluebuttonbn_participant_selection'); + // Lookup to see if it has been added already. + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == selectionType.value && + this.bigbluebuttonbn.participantList[i].selectionid == selection.value) { + return; + } + } + // Add it to memory. + this.participantAddToMemory(selectionType.value, selection.value); + // Add it to the form. + this.participantAddToForm(selectionType.value, selection.value, 'viewer', true); + // Update in the form. + this.participantListUpdate(); + }, + + participantAddToMemory: function(selectionTypeValue, selectionValue) { + this.bigbluebuttonbn.participantList.push({ + "selectiontype": selectionTypeValue, + "selectionid": selectionValue, + "role": "viewer" + }); + }, + + participantAddToForm: function(selectionTypeValue, selectionValue, selectionRole, canDelete) { + var listTable, innerHTML, selectedHtml, removeHtml, removeClass, bbbRoles, i, row, cell0, cell1, cell2, cell3; + listTable = document.getElementById('participant_list_table'); + row = listTable.insertRow(listTable.rows.length); + row.id = "participant_list_tr_" + selectionTypeValue + "-" + selectionValue; + cell0 = row.insertCell(0); + cell0.width = "125px"; + cell0.innerHTML = '<b><i>' + this.bigbluebuttonbn.participantData[selectionTypeValue].name; + cell0.innerHTML += (selectionTypeValue !== 'all' ? ': ' : '') + '</i></b>'; + cell1 = row.insertCell(1); + cell1.innerHTML = ''; + if (selectionTypeValue !== 'all') { + cell1.innerHTML = this.bigbluebuttonbn.participantData[selectionTypeValue].children[selectionValue].name; + } + innerHTML = ' <i>' + this.strings.as + '</i> '; + innerHTML += '<select id="participant_list_role_' + selectionTypeValue + '-' + selectionValue + '"'; + innerHTML += ' onchange="M.mod_bigbluebuttonbn.modform.participantListRoleUpdate(\''; + innerHTML += selectionTypeValue + '\', \'' + selectionValue; + innerHTML += '\'); return 0;" class="select custom-select">'; + bbbRoles = ['viewer', 'moderator']; + for (i = 0; i < bbbRoles.length; i++) { + selectedHtml = ''; + if (bbbRoles[i] === selectionRole) { + selectedHtml = ' selected="selected"'; + } + innerHTML += '<option value="' + bbbRoles[i] + '"' + selectedHtml + '>' + this.strings[bbbRoles[i]] + '</option>'; + } + innerHTML += '</select>'; + cell2 = row.insertCell(2); + cell2.innerHTML = innerHTML; + cell3 = row.insertCell(3); + cell3.width = "20px"; + removeHtml = this.strings.remove; + removeClass = "btn btn-secondary btn-sm"; + if (this.bigbluebuttonbn.iconsEnabled) { + removeHtml = this.bigbluebuttonbn.pixIconDelete; + removeClass = "btn btn-link"; + } + innerHTML = ""; + if (canDelete) { + innerHTML = '<a class="' + removeClass + '" onclick="M.mod_bigbluebuttonbn.modform.participantRemove(\''; + innerHTML += selectionTypeValue + '\', \'' + selectionValue; + innerHTML += '\'); return 0;" title="' + this.strings.remove + '">' + removeHtml + '</a>'; + } + cell3.innerHTML = innerHTML; + }, + + participantListRoleUpdate: function(type, id) { + // Update in memory. + var participantListRoleSelection = document.getElementById('participant_list_role_' + type + '-' + id); + for (var i = 0; i < this.bigbluebuttonbn.participantList.length; i++) { + if (this.bigbluebuttonbn.participantList[i].selectiontype == type && + this.bigbluebuttonbn.participantList[i].selectionid == (id === '' ? null : id)) { + this.bigbluebuttonbn.participantList[i].role = participantListRoleSelection.value; + } + } + // Update in the form. + this.participantListUpdate(); + }, + + selectClear: function(id) { + var select = document.getElementById(id); + while (select.length > 0) { + select.remove(select.length - 1); + } + }, + + selectEnable: function(id) { + var select = document.getElementById(id); + select.disabled = false; + }, + + selectDisable: function(id) { + var select = document.getElementById(id); + select.disabled = true; + }, + + selectAddOption: function(id, text, value) { + var select = document.getElementById(id); + var option = document.createElement('option'); + option.text = text; + option.value = value; + select.add(option, option.length); + } + +}; diff --git a/mod/bigbluebuttonbn/yui/src/modform/meta/modform.json b/mod/bigbluebuttonbn/yui/src/modform/meta/modform.json new file mode 100644 index 0000000..37baebb --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/modform/meta/modform.json @@ -0,0 +1,8 @@ +{ + "moodle-mod_bigbluebuttonbn-modform": { + "requires": [ + "base", + "node" + ] + } +} diff --git a/mod/bigbluebuttonbn/yui/src/recordings/build.json b/mod/bigbluebuttonbn/yui/src/recordings/build.json new file mode 100644 index 0000000..7d40413 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/recordings/build.json @@ -0,0 +1,11 @@ +{ + "name": "moodle-mod_bigbluebuttonbn-recordings", + "builds": { + "moodle-mod_bigbluebuttonbn-recordings": { + "jsfiles": [ + "recordings.js", + "helpers.js" + ] + } + } +} diff --git a/mod/bigbluebuttonbn/yui/src/recordings/js/helpers.js b/mod/bigbluebuttonbn/yui/src/recordings/js/helpers.js new file mode 100644 index 0000000..052e718 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/recordings/js/helpers.js @@ -0,0 +1,232 @@ +// 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/>. + +/** global: M */ +/** global: Y */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.helpers = { + + elementTag: {}, + elementFaClass: {}, + elementActionReversed: {}, + + /** + * Initialise helpers code. + * + * @method init + */ + init: function() { + this.elementTag = this.initElementTag(); + this.elementFaClass = this.initElementFAClass(); + this.elementActionReversed = this.initElementActionReversed(); + }, + + toggleSpinningWheelOn: function(data) { + var elementid, link, button, text; + elementid = this.elementId(data.action, data.target); + text = M.util.get_string('view_recording_list_action_' + data.action, 'bigbluebuttonbn'); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('data-onclick', link.getAttribute('onclick')); + link.setAttribute('onclick', ''); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.toggleSpinningWheelOnCompatible(link, text); + return; + } + button.setAttribute('data-aria-label', button.getAttribute('aria-label')); + button.setAttribute('aria-label', text); + button.setAttribute('data-title', button.getAttribute('title')); + button.setAttribute('title', text); + button.setAttribute('data-class', button.getAttribute('class')); + button.setAttribute('class', this.elementFaClass.process); + }, + + toggleSpinningWheelOnCompatible: function(link, text) { + var button = link.one('> img'); + if (button === null) { + // Button doesn't even have an icon. + return; + } + button.setAttribute('data-alt', button.getAttribute('alt')); + button.setAttribute('alt', text); + button.setAttribute('data-title', button.getAttribute('title')); + button.setAttribute('title', text); + button.setAttribute('data-src', button.getAttribute('src')); + button.setAttribute('src', 'pix/i/processing16.gif'); + }, + + toggleSpinningWheelOff: function(data) { + var elementid, link, button; + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('onclick', link.getAttribute('data-onclick')); + link.removeAttribute('data-onclick'); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.toggleSpinningWheelOffCompatible(link.one('> img')); + return; + } + button.setAttribute('aria-label', button.getAttribute('data-aria-label')); + button.removeAttribute('data-aria-label'); + button.setAttribute('title', button.getAttribute('data-title')); + button.removeAttribute('data-title'); + button.setAttribute('class', button.getAttribute('data-class')); + button.removeAttribute('data-class'); + }, + + toggleSpinningWheelOffCompatible: function(button) { + if (button === null) { + // Button doesn't have an icon. + return; + } + button.setAttribute('alt', button.getAttribute('data-alt')); + button.removeAttribute('data-alt'); + button.setAttribute('title', button.getAttribute('data-title')); + button.removeAttribute('data-title'); + button.setAttribute('src', button.getAttribute('data-src')); + button.removeAttribute('data-src'); + }, + + updateData: function(data) { + var action, elementid, link, linkdataonclick, button, buttondatatext, buttondatatag; + action = this.elementActionReversed[data.action]; + if (action === data.action) { + return; + } + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + link.setAttribute('data-action', action); + linkdataonclick = link.getAttribute('data-onclick').replace(this.capitalize(data.action), this.capitalize(action)); + link.setAttribute('data-onclick', linkdataonclick); + buttondatatext = M.util.get_string('view_recording_list_actionbar_' + action, 'bigbluebuttonbn'); + buttondatatag = this.elementTag[action]; + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + this.updateDataCompatible(link.one('> img'), this.elementTag[data.action], buttondatatag, buttondatatext); + return; + } + button.setAttribute('data-aria-label', buttondatatext); + button.setAttribute('data-title', buttondatatext); + button.setAttribute('data-class', this.elementFaClass[action]); + }, + + updateDataCompatible: function(button, action, buttondatatag, buttondatatext) { + if (button === null) { + // Button doesn't have an icon. + return; + } + var buttondatasrc = button.getAttribute('data-src'); + button.setAttribute('data-alt', buttondatatext); + button.setAttribute('data-title', buttondatatext); + button.setAttribute('data-src', buttondatasrc.replace(buttondatatag, action)); + }, + + updateId: function(data) { + var action, elementid, link, button, id; + action = this.elementActionReversed[data.action]; + if (action === data.action) { + return; + } + elementid = this.elementId(data.action, data.target); + link = Y.one('a#' + elementid + '-' + data.recordingid); + id = '' + elementid.replace(data.action, action) + '-' + data.recordingid; + link.setAttribute('id', id); + button = link.one('> i'); + if (button === null) { + // For backward compatibility. + button = link.one('> img'); + } + button.removeAttribute('id'); + }, + + elementId: function(action, target) { + var elementid = 'recording-' + action; + if (typeof target !== 'undefined') { + elementid += '-' + target; + } + return elementid; + }, + + initElementTag: function() { + var tags = {}; + tags.play = 'play'; + tags.publish = 'hide'; + tags.unpublish = 'show'; + tags.protect = 'lock'; + tags.unprotect = 'unlock'; + tags.edit = 'edit'; + tags.process = 'process'; + tags['import'] = 'import'; + tags['delete'] = 'delete'; + return tags; + }, + + initElementFAClass: function() { + var tags = {}; + tags.publish = 'icon fa fa-eye-slash fa-fw iconsmall'; + tags.unpublish = 'icon fa fa-eye fa-fw iconsmall'; + tags.protect = 'icon fa fa-unlock fa-fw iconsmall'; + tags.unprotect = 'icon fa fa-lock fa-fw iconsmall'; + tags.edit = 'icon fa fa-pencil fa-fw iconsmall'; + tags.process = 'icon fa fa-spinner fa-spin iconsmall'; + tags['import'] = 'icon fa fa-download fa-fw iconsmall'; + tags['delete'] = 'icon fa fa-trash fa-fw iconsmall'; + return tags; + }, + + initElementActionReversed: function() { + var actions = {}; + actions.play = 'play'; + actions.publish = 'unpublish'; + actions.unpublish = 'publish'; + actions.protect = 'unprotect'; + actions.unprotect = 'protect'; + actions.edit = 'edit'; + actions['import'] = 'import'; + actions['delete'] = 'delete'; + return actions; + }, + + reloadPreview: function(recordingid) { + var thumbnails = Y.one('#preview-' + recordingid).all('> img'); + thumbnails.each(function(thumbnail) { + var thumbnailsrc = thumbnail.getAttribute('src'); + thumbnailsrc = thumbnailsrc.substring(0, thumbnailsrc.indexOf('?')); + thumbnailsrc += '?' + new Date().getTime(); + thumbnail.setAttribute('src', thumbnailsrc); + }); + }, + + capitalize: function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }, + + alertError: function(message, title) { + if (typeof title == 'undefined') { + title = 'error'; + } + var alert = new M.core.alert({ + title: M.util.get_string(title, 'moodle'), + message: message + }); + alert.show(); + } + +}; diff --git a/mod/bigbluebuttonbn/yui/src/recordings/js/recordings.js b/mod/bigbluebuttonbn/yui/src/recordings/js/recordings.js new file mode 100644 index 0000000..1478382 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/recordings/js/recordings.js @@ -0,0 +1,453 @@ +// 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/>. + +/** global: M */ +/** global: Y */ +/** global: YUI */ +/** global: event */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.recordings = { + + datasource: null, + datatable: {}, + locale: 'en', + windowVideoPlay: null, + table: null, + bbbid: 0, + + /** + * Initialise recordings code. + * + * @method init + * @param {object} dataobj + */ + init: function(dataobj) { + this.bbbid = dataobj.bbbid; + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + '&' + }); + var thisbbb = this; + this.datasource.sendRequest({ + request: "id=" + this.bbbid + "&action=recording_list_table", + callback: { + success: function (data) { + var bbinfo = data.data; + if (bbinfo.recordings_html === false && + (bbinfo.profile_features.indexOf('all') != -1 || bbinfo.profile_features.indexOf('showrecordings') != -1)) { + thisbbb.locale = bbinfo.locale; + thisbbb.datatable.columns = bbinfo.data.columns; + thisbbb.datatable.data = thisbbb.datatableInitFormatDates(bbinfo.data.data); + thisbbb.datatableInit(); + } + } + } + }); + var searchform = Y.one('#bigbluebuttonbn_recordings_searchform'); + if (searchform) { + searchform.delegate('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + var value = null; + if (e.target.get('id') == 'searchsubmit') { + value = Y.one('#searchtext').get('value'); + } else { + Y.one('#searchtext').set('value', ''); + } + + this.filterByText(value); + }, 'input[type=submit]', this); + } + M.mod_bigbluebuttonbn.helpers.init(); + }, + + datatableInitFormatDates: function(data) { + for (var i = 0; i < data.length; i++) { + var date = new Date(data[i].date); + data[i].date = date.toLocaleDateString(this.locale, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + return data; + }, + + initExtraLanguage: function(Y1) { + Y1.Intl.add( + 'datatable-paginator', + Y1.config.lang, + { + first: M.util.get_string('view_recording_yui_first', 'bigbluebuttonbn'), + prev: M.util.get_string('view_recording_yui_prev', 'bigbluebuttonbn'), + next: M.util.get_string('view_recording_yui_next', 'bigbluebuttonbn'), + last: M.util.get_string('view_recording_yui_last', 'bigbluebuttonbn'), + goToLabel: M.util.get_string('view_recording_yui_page', 'bigbluebuttonbn'), + goToAction: M.util.get_string('view_recording_yui_go', 'bigbluebuttonbn'), + perPage: M.util.get_string('view_recording_yui_rows', 'bigbluebuttonbn'), + showAll: M.util.get_string('view_recording_yui_show_all', 'bigbluebuttonbn') + } + ); + }, + + escapeRegex: function(value) { + return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + }, + + filterByText: function(searchvalue) { + if (this.table) { + this.table.set('data', this.datatable.data); + if (searchvalue) { + var tlist = this.table.data; + var rsearch = new RegExp('<span>.*?' + this.escapeRegex(searchvalue) + '.*?</span>', 'i'); + var filterdata = tlist.filter({asList: true}, function(item) { + var name = item.get('recording'); + var description = item.get('description'); + return ( + (name && rsearch.test(name)) || (description && rsearch.test(description)) + ); + }); + this.table.set('data', filterdata); + } + } + }, + + datatableInit: function() { + var columns = this.datatable.columns; + var data = this.datatable.data; + var func = this.initExtraLanguage; + YUI({ + lang: this.locale + }).use('intl', 'datatable', 'datatable-sort', 'datatable-paginator', 'datatype-number', function(Y) { + func(Y); + var table = new Y.DataTable({ + width: "1195px", + columns: columns, + data: data, + rowsPerPage: 10, + paginatorLocation: ['header', 'footer'] + }).render('#bigbluebuttonbn_recordings_table'); + M.mod_bigbluebuttonbn.recordings.table = table; + return table; + }); + }, + + recordingElementPayload: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('div'); + return { + action: nodeelement.getAttribute('data-action'), + recordingid: node.getAttribute('data-recordingid'), + meetingid: node.getAttribute('data-meetingid') + }; + }, + + recordingAction: function(element, confirmation, extras) { + var payload = this.recordingElementPayload(element); + for (var attrname in extras) { + payload[attrname] = extras[attrname]; + } + // The action doesn't require confirmation. + if (!confirmation) { + this.recordingActionPerform(payload); + return; + } + // Create the confirmation dialogue. + var confirm = new M.core.confirm({ + modal: true, + centered: true, + question: this.recordingConfirmationMessage(payload) + }); + // If it is confirmed. + confirm.on('complete-yes', function() { + this.recordingActionPerform(payload); + }, this); + }, + + recordingActionPerform: function(data) { + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOn(data); + M.mod_bigbluebuttonbn.broker.recordingActionPerform(data); + + var thisbbb = this; + this.datasource.sendRequest({ + request: "&id=" + this.bbbid + "&action=recording_list_table", + callback: { + success: function (data) { + var bbinfo = data.data; + if (bbinfo.recordings_html === false && + (bbinfo.profile_features.indexOf('all') != -1 || bbinfo.profile_features.indexOf('showrecordings') != -1)) { + thisbbb.locale = bbinfo.locale; + thisbbb.datatable.columns = bbinfo.data.columns; + thisbbb.datatable.data = thisbbb.datatableInitFormatDates(bbinfo.data.data); + } + } + } + }); + }, + + recordingPublish: function(element) { + var extras = { + source: 'published', + goalstate: 'true' + }; + this.recordingAction(element, false, extras); + }, + + recordingUnpublish: function(element) { + var extras = { + source: 'published', + goalstate: 'false' + }; + this.recordingAction(element, false, extras); + }, + + recordingProtect: function(element) { + var extras = { + source: 'protected', + goalstate: 'true' + }; + this.recordingAction(element, false, extras); + }, + + recordingUnprotect: function(element) { + var extras = { + source: 'protected', + goalstate: 'false' + }; + this.recordingAction(element, false, extras); + }, + + recordingDelete: function(element) { + var extras = { + source: 'found', + goalstate: false + }; + var requireConfirmation = true; + if (this.recordingIsImported(element)) { + // When recordingDelete is performed on imported recordings use default response for validation. + requireConfirmation = false; + extras.source = 'status'; + extras.goalstate = true; + extras.attempts = 1; + } + this.recordingAction(element, requireConfirmation, extras); + }, + + recordingImport: function(element) { + var extras = {}; + this.recordingAction(element, true, extras); + }, + + recordingUpdate: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('div'); + var extras = { + target: node.getAttribute('data-target'), + source: node.getAttribute('data-source'), + goalstate: nodeelement.getAttribute('data-goalstate') + }; + this.recordingAction(element, false, extras); + }, + + recordingEdit: function(element) { + var link = Y.one(element); + var node = link.ancestor('div'); + var text = node.one('> span'); + text.hide(); + link.hide(); + var inputtext = Y.Node.create('<input type="text" class="form-control"></input>'); + inputtext.setAttribute('id', link.getAttribute('id')); + inputtext.setAttribute('value', text.getHTML()); + inputtext.setAttribute('data-value', text.getHTML()); + inputtext.on('keydown', M.mod_bigbluebuttonbn.recordings.recordingEditKeydown); + inputtext.on('focusout', M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout); + node.append(inputtext); + inputtext.focus().select(); + }, + + recordingEditKeydown: function(event) { + var keyCode = event.which || event.keyCode; + if (keyCode == 13) { + M.mod_bigbluebuttonbn.recordings.recordingEditPerform(event.currentTarget); + return; + } + if (keyCode == 27) { + M.mod_bigbluebuttonbn.recordings.recordingEditOnfocusout(event.currentTarget); + } + }, + + recordingEditOnfocusout: function(nodeelement) { + var node = nodeelement.ancestor('div'); + nodeelement.hide(); + node.one('> span').show(); + node.one('> a').show(); + }, + + recordingEditPerform: function(nodeelement) { + var node = nodeelement.ancestor('div'); + var text = nodeelement.get('value').trim(); + // Perform the update. + nodeelement.setAttribute('data-action', 'edit'); + nodeelement.setAttribute('data-goalstate', text); + nodeelement.hide(); + this.recordingUpdate(nodeelement.getDOMNode()); + node.one('> span').setHTML(text).show(); + node.one('> a').show(); + }, + + recordingEditCompletion: function(data, failed) { + var elementid = M.mod_bigbluebuttonbn.helpers.elementId(data.action, data.target); + var link = Y.one('a#' + elementid + '-' + data.recordingid); + var node = link.ancestor('div'); + var text = node.one('> span'); + if (typeof text === 'undefined') { + return; + } + var inputtext = node.one('> input'); + if (failed) { + text.setHTML(inputtext.getAttribute('data-value')); + } + inputtext.remove(); + }, + + recordingPlay: function(element) { + var nodeelement = Y.one(element); + if (nodeelement.getAttribute('data-href') === '') { + M.mod_bigbluebuttonbn.helpers.alertError( + M.util.get_string('view_recording_format_errror_unreachable', 'bigbluebuttonbn') + ); + return; + } + var extras = { + target: nodeelement.getAttribute('data-target'), + source: 'published', + goalstate: 'true', + attempts: 1, + dataset: nodeelement.getData() + }; + // New window for video play must be created previous to ajax requests. + this.windowVideoPlay = window.open('', '_blank'); + // Prevent malicious modification over window opener to use window.open(). + this.windowVideoPlay.opener = null; + this.recordingAction(element, false, extras); + }, + + recordingConfirmationMessage: function(data) { + var confirmation, recordingType, elementid, associatedLinks, confirmationWarning; + confirmation = M.util.get_string('view_recording_' + data.action + '_confirmation', 'bigbluebuttonbn'); + if (typeof confirmation === 'undefined') { + return ''; + } + recordingType = M.util.get_string('view_recording', 'bigbluebuttonbn'); + if (Y.one('#playbacks-' + data.recordingid).get('dataset').imported === 'true') { + recordingType = M.util.get_string('view_recording_link', 'bigbluebuttonbn'); + } + confirmation = confirmation.replace("{$a}", recordingType); + if (data.action === 'import') { + return confirmation; + } + // If it has associated links imported in a different course/activity, show that in confirmation dialog. + elementid = M.mod_bigbluebuttonbn.helpers.elementId(data.action, data.target); + associatedLinks = Y.one('a#' + elementid + '-' + data.recordingid).get('dataset').links; + if (associatedLinks === 0) { + return confirmation; + } + confirmationWarning = M.util.get_string('view_recording_' + data.action + '_confirmation_warning_p', + 'bigbluebuttonbn'); + if (associatedLinks == 1) { + confirmationWarning = M.util.get_string('view_recording_' + data.action + '_confirmation_warning_s', + 'bigbluebuttonbn'); + } + confirmationWarning = confirmationWarning.replace("{$a}", associatedLinks) + '. '; + return confirmationWarning + '\n\n' + confirmation; + }, + + recordingActionCompletion: function(data) { + var container, table, row; + if (data.action == 'delete') { + row = Y.one('div#recording-actionbar-' + data.recordingid).ancestor('td').ancestor('tr'); + table = row.ancestor('tbody'); + if (table.all('tr').size() == 1) { + container = Y.one('#bigbluebuttonbn_view_recordings_content'); + container.prepend('<span>' + M.util.get_string('view_message_norecordings', 'bigbluebuttonbn') + '</span>'); + container.one('#bigbluebuttonbn_recordings_table').remove(); + return; + } + row.remove(); + return; + } + if (data.action == 'import') { + row = Y.one('div#recording-actionbar-' + data.recordingid).ancestor('td').ancestor('tr'); + row.remove(); + return; + } + if (data.action == 'play') { + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + // Update url in window video to show the video. + this.windowVideoPlay.location.href = data.dataset.href; + return; + } + M.mod_bigbluebuttonbn.helpers.updateData(data); + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + M.mod_bigbluebuttonbn.helpers.updateId(data); + if (data.action === 'publish') { + this.recordingPublishCompletion(data.recordingid); + return; + } + if (data.action === 'unpublish') { + this.recordingUnpublishCompletion(data.recordingid); + return; + } + }, + + recordingActionFailover: function(data) { + M.mod_bigbluebuttonbn.helpers.alertError(data.message); + M.mod_bigbluebuttonbn.helpers.toggleSpinningWheelOff(data); + if (data.action === 'edit') { + this.recordingEditCompletion(data, true); + } + }, + + recordingPublishCompletion: function(recordingid) { + var playbacks = Y.one('#playbacks-' + recordingid); + playbacks.show(); + var preview = Y.one('#preview-' + recordingid); + if (preview === null) { + return; + } + preview.show(); + M.mod_bigbluebuttonbn.helpers.reloadPreview(recordingid); + }, + + recordingUnpublishCompletion: function(recordingid) { + var playbacks = Y.one('#playbacks-' + recordingid); + playbacks.hide(); + var preview = Y.one('#preview-' + recordingid); + if (preview === null) { + return; + } + preview.hide(); + }, + + recordingIsImported: function(element) { + var nodeelement = Y.one(element); + var node = nodeelement.ancestor('tr'); + return (node.getAttribute('data-imported') === 'true'); + } + +}; diff --git a/mod/bigbluebuttonbn/yui/src/recordings/meta/recordings.json b/mod/bigbluebuttonbn/yui/src/recordings/meta/recordings.json new file mode 100644 index 0000000..abbf4cf --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/recordings/meta/recordings.json @@ -0,0 +1,12 @@ +{ + "moodle-mod_bigbluebuttonbn-recordings": { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] + } +} diff --git a/mod/bigbluebuttonbn/yui/src/rooms/build.json b/mod/bigbluebuttonbn/yui/src/rooms/build.json new file mode 100644 index 0000000..f2a600e --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/rooms/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_bigbluebuttonbn-rooms", + "builds": { + "moodle-mod_bigbluebuttonbn-rooms": { + "jsfiles": [ + "rooms.js" + ] + } + } +} diff --git a/mod/bigbluebuttonbn/yui/src/rooms/js/rooms.js b/mod/bigbluebuttonbn/yui/src/rooms/js/rooms.js new file mode 100644 index 0000000..5a071cd --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/rooms/js/rooms.js @@ -0,0 +1,283 @@ +// 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/>. + +/** global: M */ +/** global: Y */ +/** global: opener */ + +M.mod_bigbluebuttonbn = M.mod_bigbluebuttonbn || {}; + +M.mod_bigbluebuttonbn.rooms = { + + datasource: null, + bigbluebuttonbn: {}, + panel: null, + pinginterval: null, + + /** + * Initialise the broker code. + * + * @method init + * @param {object} bigbluebuttonbn + */ + init: function(bigbluebuttonbn) { + this.datasource = new Y.DataSource.Get({ + source: M.cfg.wwwroot + "/mod/bigbluebuttonbn/bbb_ajax.php?sesskey=" + M.cfg.sesskey + "&" + }); + this.bigbluebuttonbn = bigbluebuttonbn; + this.pinginterval = bigbluebuttonbn.ping_interval; + if (this.pinginterval === 0) { + this.pinginterval = 10000; + } + if (this.bigbluebuttonbn.profile_features.indexOf('all') != -1 || + this.bigbluebuttonbn.profile_features.indexOf('showroom') != -1) { + this.initRoom(); + } + this.initCompletionValidate(); + }, + + initRoom: function() { + if (this.bigbluebuttonbn.activity !== 'open') { + var statusBar = [M.util.get_string('view_message_conference_has_ended', 'bigbluebuttonbn')]; + if (this.bigbluebuttonbn.activity !== 'ended') { + statusBar = [ + M.util.get_string('view_message_conference_not_started', 'bigbluebuttonbn'), + this.bigbluebuttonbn.opening, + this.bigbluebuttonbn.closing + ]; + } + Y.DOM.addHTML(Y.one('#status_bar'), this.initStatusBar(statusBar)); + return; + } + this.updateRoom(); + }, + + updateRoom: function(f) { + var updatecache = 'false'; + if (typeof f !== 'undefined' && f) { + updatecache = 'true'; + } + var id = this.bigbluebuttonbn.meetingid; + var bnid = this.bigbluebuttonbn.bigbluebuttonbnid; + this.datasource.sendRequest({ + request: 'action=meeting_info&id=' + id + '&bigbluebuttonbn=' + bnid + '&updatecache=' + updatecache, + callback: { + success: function(e) { + Y.DOM.addHTML(Y.one('#status_bar'), + M.mod_bigbluebuttonbn.rooms.initStatusBar(e.data.status.message)); + Y.DOM.addHTML(Y.one('#control_panel'), + M.mod_bigbluebuttonbn.rooms.initControlPanel(e.data)); + if (typeof e.data.status.can_join != 'undefined') { + Y.DOM.addHTML(Y.one('#join_button'), + M.mod_bigbluebuttonbn.rooms.initJoinButton(e.data.status)); + } + if (typeof e.data.status.can_end != 'undefined' && e.data.status.can_end) { + Y.DOM.addHTML(Y.one('#end_button'), + M.mod_bigbluebuttonbn.rooms.initEndButton(e.data.status)); + } + if (!e.data.status.can_join) { + M.mod_bigbluebuttonbn.rooms.waitModerator({ + id: id, + bnid: bnid + }); + } + } + } + }); + }, + + initStatusBar: function(statusMessage) { + var statusBarSpan = Y.DOM.create('<span id="status_bar_span">'); + if (statusMessage.constructor !== Array) { + Y.DOM.setText(statusBarSpan, statusMessage); + return statusBarSpan; + } + for (var message in statusMessage) { + if (!statusMessage.hasOwnProperty(message)) { + continue; // Skip keys from the prototype. + } + var statusBarSpanSpan = Y.DOM.create('<span id="status_bar_span_span">'); + Y.DOM.setText(statusBarSpanSpan, statusMessage[message]); + Y.DOM.addHTML(statusBarSpan, statusBarSpanSpan); + Y.DOM.addHTML(statusBarSpan, Y.DOM.create('<br>')); + } + return statusBarSpan; + }, + + initControlPanel: function(data) { + var controlPanelDiv = Y.DOM.create('<div>'); + Y.DOM.setAttribute(controlPanelDiv, 'id', 'control_panel_div'); + var controlPanelDivHtml = ''; + if (data.running) { + controlPanelDivHtml += this.msgStartedAt(data.info.startTime) + ' '; + controlPanelDivHtml += this.msgAttendeesIn(data.info.moderatorCount, data.info.participantCount); + } + Y.DOM.addHTML(controlPanelDiv, controlPanelDivHtml); + return (controlPanelDiv); + }, + + msgStartedAt: function(startTime) { + var startTimestamp = (parseInt(startTime, 10) - parseInt(startTime, 10) % 1000); + var date = new Date(startTimestamp); + var hours = date.getHours(); + var minutes = date.getMinutes(); + var startedAt = M.util.get_string('view_message_session_started_at', 'bigbluebuttonbn'); + return startedAt + ' <b>' + hours + ':' + (minutes < 10 ? '0' : '') + minutes + '</b>.'; + }, + + msgModeratorsIn: function(moderators) { + var msgModerators = M.util.get_string('view_message_moderators', 'bigbluebuttonbn'); + if (moderators == 1) { + msgModerators = M.util.get_string('view_message_moderator', 'bigbluebuttonbn'); + } + return msgModerators; + }, + + msgViewersIn: function(viewers) { + var msgViewers = M.util.get_string('view_message_viewers', 'bigbluebuttonbn'); + if (viewers == 1) { + msgViewers = M.util.get_string('view_message_viewer', 'bigbluebuttonbn'); + } + return msgViewers; + }, + + msgAttendeesIn: function(moderators, participants) { + var msgModerators, viewers, msgViewers, msg; + if (!this.hasParticipants(participants)) { + return M.util.get_string('view_message_session_no_users', 'bigbluebuttonbn') + '.'; + } + msgModerators = this.msgModeratorsIn(moderators); + viewers = participants - moderators; + msgViewers = this.msgViewersIn(viewers); + msg = M.util.get_string('view_message_session_has_users', 'bigbluebuttonbn'); + if (participants > 1) { + return msg + ' <b>' + moderators + '</b> ' + msgModerators + ' ' + + M.util.get_string('view_message_and', 'bigbluebuttonbn') + ' <b>' + viewers + '</b> ' + msgViewers + '.'; + } + msg = M.util.get_string('view_message_session_has_user', 'bigbluebuttonbn'); + if (moderators > 0) { + return msg + ' <b>1</b> ' + msgModerators + '.'; + } + return msg + ' <b>1</b> ' + msgViewers + '.'; + }, + + hasParticipants: function(participants) { + return (typeof participants != 'undefined' && participants > 0); + }, + + initJoinButton: function(status) { + var joinButtonInput = Y.DOM.create('<input>'); + Y.DOM.setAttribute(joinButtonInput, 'id', 'join_button_input'); + Y.DOM.setAttribute(joinButtonInput, 'type', 'button'); + Y.DOM.setAttribute(joinButtonInput, 'value', status.join_button_text); + Y.DOM.setAttribute(joinButtonInput, 'class', 'btn btn-primary'); + var inputHtml = 'M.mod_bigbluebuttonbn.rooms.join(\'' + status.join_url + '\');'; + Y.DOM.setAttribute(joinButtonInput, 'onclick', inputHtml); + if (!status.can_join) { + // Disable join button. + Y.DOM.setAttribute(joinButtonInput, 'disabled', true); + var statusBarSpan = Y.one('#status_bar_span'); + // Create a img element. + var spinningWheel = Y.DOM.create('<img>'); + Y.DOM.setAttribute(spinningWheel, 'id', 'spinning_wheel'); + Y.DOM.setAttribute(spinningWheel, 'src', 'pix/i/processing16.gif'); + // Add the spinning wheel. + Y.DOM.addHTML(statusBarSpan, ' '); + Y.DOM.addHTML(statusBarSpan, spinningWheel); + } + return joinButtonInput; + }, + + initEndButton: function(status) { + var endButtonInput = Y.DOM.create('<input>'); + Y.DOM.setAttribute(endButtonInput, 'id', 'end_button_input'); + Y.DOM.setAttribute(endButtonInput, 'type', 'button'); + Y.DOM.setAttribute(endButtonInput, 'value', status.end_button_text); + Y.DOM.setAttribute(endButtonInput, 'class', 'btn btn-secondary'); + if (status.can_end) { + Y.DOM.setAttribute(endButtonInput, 'onclick', 'M.mod_bigbluebuttonbn.broker.endMeeting();'); + } + return endButtonInput; + }, + + endMeeting: function() { + Y.one('#control_panel_div').remove(); + Y.one('#join_button').hide(); + Y.one('#end_button').hide(); + }, + + remoteUpdate: function(delay) { + setTimeout(function() { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(true); + }, delay); + }, + + cleanRoom: function() { + Y.one('#status_bar_span').remove(); + Y.one('#control_panel_div').remove(); + Y.one('#join_button').setContent(''); + Y.one('#end_button').setContent(''); + }, + + windowClose: function() { + window.onunload = function() { + opener.M.mod_bigbluebuttonbn.rooms.remoteUpdate(5000); + }; + window.close(); + }, + + waitModerator: function(payload) { + var pooling = setInterval(function() { + M.mod_bigbluebuttonbn.rooms.datasource.sendRequest({ + request: "action=meeting_info&id=" + payload.id + "&bigbluebuttonbn=" + payload.bnid, + callback: { + success: function(e) { + if (e.data.running) { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(); + clearInterval(pooling); + return; + } + }, + failure: function(e) { + payload.message = e.error.message; + } + } + }); + }, this.pinginterval); + }, + + join: function(joinUrl) { + M.mod_bigbluebuttonbn.broker.joinRedirect(joinUrl); + // Update view. + setTimeout(function() { + M.mod_bigbluebuttonbn.rooms.cleanRoom(); + M.mod_bigbluebuttonbn.rooms.updateRoom(true); + }, 15000); + }, + + initCompletionValidate: function() { + var node = Y.one('a[href*=completion_validate]'); + if (!node) { + return; + } + var qs = node.get('hash').substr(1); + node.on("click", function() { + M.mod_bigbluebuttonbn.broker.completionValidate(qs); + }); + } + +}; diff --git a/mod/bigbluebuttonbn/yui/src/rooms/meta/rooms.json b/mod/bigbluebuttonbn/yui/src/rooms/meta/rooms.json new file mode 100644 index 0000000..35dc6c0 --- /dev/null +++ b/mod/bigbluebuttonbn/yui/src/rooms/meta/rooms.json @@ -0,0 +1,12 @@ +{ + "moodle-mod_bigbluebuttonbn-rooms": { + "requires": [ + "base", + "node", + "datasource-get", + "datasource-jsonschema", + "datasource-polling", + "moodle-core-notification" + ] + } +} diff --git a/mod/choicegroup/README.md b/mod/choicegroup/README.md new file mode 100644 index 0000000..2ee058b --- /dev/null +++ b/mod/choicegroup/README.md @@ -0,0 +1,39 @@ +General information +==================== + +This module allows students to enrol themselves in a group within a course. The teacher can choose from which groups the students can chose, and the maximum nummber of students allowed in each group. + +The students can view the members of each group before making a choise, and (if the teacher allows it) change their selected group until the deadline. + +This module is heavily based on the "choice" activity module, and behaves roughly like it. Making a choice enrols you in a group, changing your choice unenrols you from the precedent group and enrols you in the new one, and so on. + + +Installation +============= + +1. unzip, and copy into Moodle's /mod folder +2. visit administration page to install module +3. use in any course as wished + + +Operation +========== + +1. create groups within your course +2. create a choicegroup activity and select groups which users can chose from + + +See also +========= + + - [Moodle plugins entry page](http://moodle.org/plugins/view.php?plugin=mod_choicegroup) + - [Moodle.org forum discussion thread](http://moodle.org/mod/forum/discuss.php?d=174424) + - [Moodlefairy's review](http://www.youtube.com/watch?v=JQFaDLtHZdY) + - [Another review by Gavin Henrick](http://www.somerandomthoughts.com/blog/2011/10/13/review-activity-module-choice-group-for-moodle-2/) + + +Thanks to +========== + + - André Lausch : German translation + - Luiggi Sansonetti : French translation diff --git a/mod/choicegroup/amd/build/choicegroupdatadisplay.min.js b/mod/choicegroup/amd/build/choicegroupdatadisplay.min.js new file mode 100644 index 0000000..2d657f6 --- /dev/null +++ b/mod/choicegroup/amd/build/choicegroupdatadisplay.min.js @@ -0,0 +1 @@ +define(["jquery","core/str"],function(a,b){return{init:function(){a(".choicegroup-memberdisplay").click(function(c){c.preventDefault(),a(".choicegroups-membersnames").toggleClass("hidden");var d=b.get_string("showgroupmembers","mod_choicegroup"),e=b.get_string("hidegroupmembers","mod_choicegroup");a(".choicegroups-membersnames").is(":visible")?a.when(e).done(function(b){a(".choicegroup-memberdisplay").html(b)}):a.when(d).done(function(b){a(".choicegroup-memberdisplay").html(b)})}),a(".choicegroup-descriptiondisplay").click(function(c){c.preventDefault(),a(".choicegroups-descriptions").toggleClass("hidden");var d=b.get_string("hidedescription","mod_choicegroup"),e=b.get_string("showdescription","mod_choicegroup");a(".choicegroups-descriptions").is(":visible")?a.when(d).done(function(b){a(".choicegroup-descriptiondisplay").html(b)}):a.when(e).done(function(b){a(".choicegroup-descriptiondisplay").html(b)})})}}}); \ No newline at end of file diff --git a/mod/choicegroup/amd/build/select_all_choices.min.js b/mod/choicegroup/amd/build/select_all_choices.min.js new file mode 100644 index 0000000..bdbfb17 --- /dev/null +++ b/mod/choicegroup/amd/build/select_all_choices.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){return{init:function(){a(".selectallnone a").on("click",function(b){b.preventDefault(),a("#attemptsform").find("input:checkbox").prop("checked",a(this).data("selectInfo"))})}}}); \ No newline at end of file diff --git a/mod/choicegroup/amd/src/choicegroupdatadisplay.js b/mod/choicegroup/amd/src/choicegroupdatadisplay.js new file mode 100644 index 0000000..e6ece78 --- /dev/null +++ b/mod/choicegroup/amd/src/choicegroupdatadisplay.js @@ -0,0 +1,42 @@ +define(['jquery', 'core/str'], function ($, str) { + return { + init: function () { + $('.choicegroup-memberdisplay').click( function (e) { + e.preventDefault(); + $('.choicegroups-membersnames').toggleClass('hidden'); + var showusersstring = str.get_string('showgroupmembers', 'mod_choicegroup'); + var hideusersstring = str.get_string('hidegroupmembers', 'mod_choicegroup'); + if ($('.choicegroups-membersnames').is(":visible")) { + $.when(hideusersstring).done(function (hidestring) { + $(".choicegroup-memberdisplay").html(hidestring); + }); + } + else { + $.when(showusersstring).done(function (showstring) { + $(".choicegroup-memberdisplay").html(showstring); + }); + } + + }); + + $('.choicegroup-descriptiondisplay').click( function (e) { + e.preventDefault(); + $('.choicegroups-descriptions').toggleClass('hidden'); + var hidedescriptionstring = str.get_string('hidedescription', 'mod_choicegroup'); + var showdescriptionstring = str.get_string('showdescription', 'mod_choicegroup'); + if ($('.choicegroups-descriptions').is(":visible")) { + $.when(hidedescriptionstring).done(function (hidestring) { + $(".choicegroup-descriptiondisplay").html(hidestring); + }); + } + else { + $.when(showdescriptionstring).done(function (showstring) { + $(".choicegroup-descriptiondisplay").html(showstring); + + }); + } + }); + } + }; + +}); \ No newline at end of file diff --git a/mod/choicegroup/amd/src/select_all_choices.js b/mod/choicegroup/amd/src/select_all_choices.js new file mode 100644 index 0000000..348a35f --- /dev/null +++ b/mod/choicegroup/amd/src/select_all_choices.js @@ -0,0 +1,33 @@ +// 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/>. + +/** + * Ticks or unticks all checkboxes when clicking the Select all or Deselect all elements when viewing the response overview. + * + * @module mod_choicegroup/select_all_choices + * @copyright 2017 Marcus Fabriczy <marcus.fabriczy@blackboard.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + return { + init: function () { + $('.selectallnone a').on('click', function(e) { + e.preventDefault(); + $('#attemptsform').find('input:checkbox').prop('checked', $(this).data('selectInfo')); + }); + } + }; +}); diff --git a/mod/choicegroup/backup/moodle2/backup_choicegroup_activity_task.class.php b/mod/choicegroup/backup/moodle2/backup_choicegroup_activity_task.class.php new file mode 100644 index 0000000..754e2d3 --- /dev/null +++ b/mod/choicegroup/backup/moodle2/backup_choicegroup_activity_task.class.php @@ -0,0 +1,70 @@ +<?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/>. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/choicegroup/backup/moodle2/backup_choicegroup_stepslib.php'); // Because it exists (must) +require_once($CFG->dirroot . '/mod/choicegroup/backup/moodle2/backup_choicegroup_settingslib.php'); // Because it exists (optional) + +/** + * choicegroup backup task that provides all the settings and steps to perform one + * complete backup of the activity + */ +class backup_choicegroup_activity_task extends backup_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + // Choice only has one structure step + $this->add_step(new backup_choicegroup_activity_structure_step('choicegroup_structure', 'choicegroup.xml')); + } + + /** + * Code the transformations to perform in the activity in + * order to get transportable (encoded) links + */ + static public function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot,"/"); + + // Link to the list of choicegroups + $search="/(".$base."\/mod\/choicegroup\/index.php\?id\=)([0-9]+)/"; + $content= preg_replace($search, '$@CHOICEGROUPINDEX*$2@$', $content); + + // Link to choicegroup view by moduleid + $search="/(".$base."\/mod\/choicegroup\/view.php\?id\=)([0-9]+)/"; + $content= preg_replace($search, '$@CHOICEGROUPVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/mod/choicegroup/backup/moodle2/backup_choicegroup_settingslib.php b/mod/choicegroup/backup/moodle2/backup_choicegroup_settingslib.php new file mode 100644 index 0000000..b66e78a --- /dev/null +++ b/mod/choicegroup/backup/moodle2/backup_choicegroup_settingslib.php @@ -0,0 +1,27 @@ +<?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/>. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + // This activity has not particular settings but the inherited from the generic + // backup_activity_task so here there isn't any class definition, like the ones + // existing in /backup/moodle2/backup_settingslib.php (activities section) diff --git a/mod/choicegroup/backup/moodle2/backup_choicegroup_stepslib.php b/mod/choicegroup/backup/moodle2/backup_choicegroup_stepslib.php new file mode 100644 index 0000000..5686454 --- /dev/null +++ b/mod/choicegroup/backup/moodle2/backup_choicegroup_stepslib.php @@ -0,0 +1,70 @@ +<?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/>. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Define all the backup steps that will be used by the backup_choicegroup_activity_task + */ + +/** + * Define the complete choicegroup structure for backup, with file and id annotations + */ + +defined('MOODLE_INTERNAL') || die(); + +class backup_choicegroup_activity_structure_step extends backup_activity_structure_step { + + protected function define_structure() { + + // Define each element separated + $choicegroup = new backup_nested_element('choicegroup', array('id'), array( + 'name', 'intro', 'introformat', 'publish', + 'showresults', 'display', 'allowupdate', 'allowunanswered', + 'limitanswers', 'timeopen', 'timeclose', 'timemodified', + 'completionsubmit', 'sortgroupsby')); + + $options = new backup_nested_element('options'); + + $option = new backup_nested_element('option', array('id'), array( + 'groupid', 'maxanswers', 'timemodified')); + + // Build the tree + $choicegroup->add_child($options); + $options->add_child($option); + + // Define sources + $choicegroup->set_source_table('choicegroup', array('id' => backup::VAR_ACTIVITYID)); + + $option->set_source_sql(' + SELECT * + FROM {choicegroup_options} + WHERE choicegroupid = ?', + array(backup::VAR_PARENTID)); + + // Define file annotations + $choicegroup->annotate_files('mod_choicegroup', 'intro', null); // This file area hasn't itemid + + // Return the root element (choicegroup), wrapped into standard activity structure + return $this->prepare_activity_structure($choicegroup); + } +} diff --git a/mod/choicegroup/backup/moodle2/restore_choicegroup_activity_task.class.php b/mod/choicegroup/backup/moodle2/restore_choicegroup_activity_task.class.php new file mode 100644 index 0000000..50f5639 --- /dev/null +++ b/mod/choicegroup/backup/moodle2/restore_choicegroup_activity_task.class.php @@ -0,0 +1,115 @@ +<?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/>. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/choicegroup/backup/moodle2/restore_choicegroup_stepslib.php'); // Because it exists (must) + +/** + * choicegroup restore task that provides all the settings and steps to perform one + * complete restore of the activity + */ +class restore_choicegroup_activity_task extends restore_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + // Choice only has one structure step + $this->add_step(new restore_choicegroup_activity_structure_step('choicegroup_structure', 'choicegroup.xml')); + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder + */ + static public function define_decode_contents() { + $contents = array(); + + $contents[] = new restore_decode_content('choicegroup', array('intro'), 'choicegroup'); + + return $contents; + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder + */ + static public function define_decode_rules() { + $rules = array(); + + $rules[] = new restore_decode_rule('CHOICEGROUPVIEWBYID', '/mod/choicegroup/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('CHOICEGROUPINDEX', '/mod/choicegroup/index.php?id=$1', 'course'); + + return $rules; + + } + + /** + * Define the restore log rules that will be applied + * by the {@link restore_logs_processor} when restoring + * choicegroup logs. It must return one array + * of {@link restore_log_rule} objects + */ + static public function define_restore_log_rules() { + $rules = array(); + + $rules[] = new restore_log_rule('choicegroup', 'add', 'view.php?id={course_module}', '{choicegroup}'); + $rules[] = new restore_log_rule('choicegroup', 'update', 'view.php?id={course_module}', '{choicegroup}'); + $rules[] = new restore_log_rule('choicegroup', 'view', 'view.php?id={course_module}', '{choicegroup}'); + $rules[] = new restore_log_rule('choicegroup', 'choose', 'view.php?id={course_module}', '{choicegroup}'); + $rules[] = new restore_log_rule('choicegroup', 'choose again', 'view.php?id={course_module}', '{choicegroup}'); + $rules[] = new restore_log_rule('choicegroup', 'report', 'report.php?id={course_module}', '{choicegroup}'); + + return $rules; + } + + /** + * Define the restore log rules that will be applied + * by the {@link restore_logs_processor} when restoring + * course logs. It must return one array + * of {@link restore_log_rule} objects + * + * Note this rules are applied when restoring course logs + * by the restore final task, but are defined here at + * activity level. All them are rules not linked to any module instance (cmid = 0) + */ + static public function define_restore_log_rules_for_course() { + $rules = array(); + + // Fix old wrong uses (missing extension) + $rules[] = new restore_log_rule('choicegroup', 'view all', 'index?id={course}', null, + null, null, 'index.php?id={course}'); + $rules[] = new restore_log_rule('choicegroup', 'view all', 'index.php?id={course}', null); + + return $rules; + } +} diff --git a/mod/choicegroup/backup/moodle2/restore_choicegroup_stepslib.php b/mod/choicegroup/backup/moodle2/restore_choicegroup_stepslib.php new file mode 100644 index 0000000..4f8718c --- /dev/null +++ b/mod/choicegroup/backup/moodle2/restore_choicegroup_stepslib.php @@ -0,0 +1,83 @@ +<?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/>. + +/** + * @package moodlecore + * @subpackage backup-moodle2 + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Define all the restore steps that will be used by the restore_choicegroup_activity_task + */ + +/** + * Structure step to restore one choicegroup activity + */ + +defined('MOODLE_INTERNAL') || die(); + +class restore_choicegroup_activity_structure_step extends restore_activity_structure_step { + + protected function define_structure() { + + $paths = array(); + + $paths[] = new restore_path_element('choicegroup', '/activity/choicegroup'); + $paths[] = new restore_path_element('choicegroup_option', '/activity/choicegroup/options/option'); + + // Return the paths wrapped into standard activity structure + return $this->prepare_activity_structure($paths); + } + + protected function process_choicegroup($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + $data->course = $this->get_courseid(); + + $data->timeopen = $this->apply_date_offset($data->timeopen); + $data->timeclose = $this->apply_date_offset($data->timeclose); + $data->timemodified = $this->apply_date_offset($data->timemodified); + + // insert the choicegroup record + $newitemid = $DB->insert_record('choicegroup', $data); + // immediately after inserting "activity" record, call this + $this->apply_activity_instance($newitemid); + } + + protected function process_choicegroup_option($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->choicegroupid = $this->get_new_parentid('choicegroup'); + $data->timemodified = $this->apply_date_offset($data->timemodified); + $data->groupid = $this->get_mappingid('group', $data->groupid); + + $newitemid = $DB->insert_record('choicegroup_options', $data); + $this->set_mapping('choicegroup_option', $oldid, $newitemid); + } + + protected function after_execute() { + // Add choicegroup related files, no need to match by itemname (just internally handled context) + $this->add_related_files('mod_choicegroup', 'intro', null); + } +} diff --git a/mod/choicegroup/classes/event/choice_removed.php b/mod/choicegroup/classes/event/choice_removed.php new file mode 100644 index 0000000..de5e0df --- /dev/null +++ b/mod/choicegroup/classes/event/choice_removed.php @@ -0,0 +1,101 @@ +<?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/>. + +/** + * The mod_choicegroup post created event. + * + * @package mod_choicegroup + * @copyright 2014 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_choicegroup post created event class. + * + * @property-read array $other { + * Extra information about the event. + * + * - int discussionid: The discussion id the post is part of. + * - int choicegroupid: The choicegroup id the post is part of. + * - string choicegrouptype: The type of choicegroup the post is part of. + * } + * + * @package mod_choicegroup + * @since Moodle 2.7 + * @copyright 2014 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class choice_removed extends \core\event\base { + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'groups'; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $a = new \stdClass(); + $a->userid = $this->userid; + $a->contextinstanceid = $this->contextinstanceid; + return get_string('event:removed_desc', 'mod_choicegroup', $a); + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event:removed', 'mod_choicegroup'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/choicegroup/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + // The legacy log table expects a relative path to /mod/choicegroup/. + $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/choicegroup/')); + + return array($this->courseid, 'choicegroup', 'choice removed', $logurl, $this->objectid, $this->contextinstanceid); + } + + +} + diff --git a/mod/choicegroup/classes/event/choice_updated.php b/mod/choicegroup/classes/event/choice_updated.php new file mode 100644 index 0000000..476903f --- /dev/null +++ b/mod/choicegroup/classes/event/choice_updated.php @@ -0,0 +1,101 @@ +<?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/>. + +/** + * The mod_choicegroup post created event. + * + * @package mod_choicegroup + * @copyright 2014 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_choicegroup post created event class. + * + * @property-read array $other { + * Extra information about the event. + * + * - int discussionid: The discussion id the post is part of. + * - int choicegroupid: The choicegroup id the post is part of. + * - string choicegrouptype: The type of choicegroup the post is part of. + * } + * + * @package mod_choicegroup + * @since Moodle 2.7 + * @copyright 2014 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class choice_updated extends \core\event\base { + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'groups'; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $a = new \stdClass(); + $a->userid = $this->userid; + $a->contextinstanceid = $this->contextinstanceid; + return get_string('event:answered_desc', 'mod_choicegroup', $a); + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event:answered', 'mod_choicegroup'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/choicegroup/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + // The legacy log table expects a relative path to /mod/choicegroup/. + $logurl = substr($this->get_url()->out_as_local_url(), strlen('/mod/choicegroup/')); + + return array($this->courseid, 'choicegroup', 'choice updated', $logurl, $this->objectid, $this->contextinstanceid); + } + + +} + diff --git a/mod/choicegroup/classes/event/course_module_instance_list_viewed.php b/mod/choicegroup/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 0000000..3f6d9b8 --- /dev/null +++ b/mod/choicegroup/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,39 @@ +<?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/>. + +/** + * The mod_forum instance list viewed event. + * + * @package mod_forum + * @copyright 2014 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_forum instance list viewed event class. + * + * @package mod_forum + * @since Moodle 2.7 + * @copyright 2014 Dan Poltawski <dan@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { + // No need for any code here as everything is handled by the parent class. +} diff --git a/mod/choicegroup/classes/event/course_module_viewed.php b/mod/choicegroup/classes/event/course_module_viewed.php new file mode 100644 index 0000000..80b1afd --- /dev/null +++ b/mod/choicegroup/classes/event/course_module_viewed.php @@ -0,0 +1,63 @@ +<?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/>. + +/** + * The mod_choicegroup course module viewed event. + * + * @package mod_choicegroup + * @copyright 2014 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\event; + +defined('MOODLE_INTERNAL') || die(); + +class course_module_viewed extends \core\event\course_module_viewed { + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'choicegroup'; + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/choicegroup/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'choicegroup', 'view group choice', 'view.php?id=' . $this->contextinstanceid, + $this->objectid, $this->contextinstanceid); + } + +} + diff --git a/mod/choicegroup/classes/event/report_viewed.php b/mod/choicegroup/classes/event/report_viewed.php new file mode 100644 index 0000000..5e9b02a --- /dev/null +++ b/mod/choicegroup/classes/event/report_viewed.php @@ -0,0 +1,84 @@ +<?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/>. + +/** + * The mod_choicegroup course module viewed event. + * + * @package mod_choicegroup + * @copyright 2014 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\event; + +defined('MOODLE_INTERNAL') || die(); + +class report_viewed extends \core\event\course_module_viewed { + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = 'choicegroup'; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $a = new \stdClass(); + $a->userid = $this->userid; + $a->contextinstanceid = $this->contextinstanceid; + return get_string('event:reportviewed_desc', 'mod_choicegroup', $a); + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('event:reportviewed', 'mod_choicegroup'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/choicegroup/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'choicegroup', 'view group choice report', 'view.php?id=' . $this->contextinstanceid, + $this->objectid, $this->contextinstanceid); + } + +} + diff --git a/mod/choicegroup/classes/external.php b/mod/choicegroup/classes/external.php new file mode 100644 index 0000000..b358510 --- /dev/null +++ b/mod/choicegroup/classes/external.php @@ -0,0 +1,451 @@ +<?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/>. + +/** + * Choice group module external API + * + * @package mod_choicegroup + * @category external + * @copyright 2018 Sara Arjona <sara@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/completionlib.php'); +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/choicegroup/lib.php'); + +/** + * Choice group module external functions + * + * @copyright 2018 Sara Arjona <sara@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_choicegroup_external extends external_api { + + /** + * Describes the parameters for get_choicegroup_options. + * + * @return external_function_parameters + */ + public static function get_choicegroup_options_parameters() { + return new external_function_parameters( + array( + 'choicegroupid' => new external_value(PARAM_INT, 'Choice group instance id'), + 'userid' => new external_value(PARAM_INT, 'User id') + ) + ); + } + + /** + * Returns the options list for the provided choice group instance. + * + * @param int $choicegroupid The choice group id. + * @param int $userid The user id. + * @param boolean $alloptionsdisabled True when all the options should be disabled, because activity is not open or a limit has been reached. + * @return array The choice group options. + */ + public static function get_choicegroup_options($choicegroupid, $userid, $alloptionsdisabled = false) { + global $CFG, $choicegroup_groups; + + $result = array(); + $returnedoptions = array(); + $warnings = array(); + + $params = array( + 'choicegroupid' => $choicegroupid, + 'userid' => $userid + ); + $params = self::validate_parameters(self::get_choicegroup_options_parameters(), $params); + $choicegroup = choicegroup_get_choicegroup($choicegroupid); + $cm = get_coursemodule_from_instance('choicegroup', $choicegroupid); + $context = context_module::instance($cm->id); + + self::validate_context($context); + require_capability('mod/choicegroup:choose', $context); + + $allresponses = choicegroup_get_response_data($choicegroup, $cm); // Big function, approx 6 SQL calls per user + $answers = choicegroup_get_user_answer($choicegroup, $userid, true); + + foreach ($choicegroup->option as $optionid => $text) { + if (isset($text)) { + $option = array(); + $option['id'] = $optionid; + $option['groupid'] = $text; + $option['name'] = $choicegroup_groups[$text]->name; + $option['maxanswers'] = $choicegroup->maxanswers[$optionid]; + $option['displaylayout'] = $choicegroup->display; + + if (isset($allresponses[$text])) { + $option['countanswers'] = count($allresponses[$text]); + } else { + $option['countanswers'] = 0; + } + // Check if the option has been answered previously by the user. + $option['checked'] = false; + if (is_array($answers)) { + foreach($answers as $answer) { + if ($answer && $text == $answer->id) { + $option['checked'] = true; + } + } + } + // Check if the option has to be disabled because the limit has been reached. + $limitreached = $choicegroup->limitanswers && ($option['countanswers'] >= $option['maxanswers']); + $stillnotanswered = $option['checked'] === false; + $option['disabled'] = $alloptionsdisabled; + if ($limitreached && $stillnotanswered) { + $option['disabled'] = true; + $option['name'] .= ' '.get_string('full', 'choicegroup'); + } + + $returnedoptions[] = $option; + } + } + + $result = array(); + $result['options'] = $returnedoptions; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the get_choicegroup_options return value. + * + * @return external_single_structure + */ + public static function get_choicegroup_options_returns() { + + return new external_single_structure( + array( + 'options' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'Option id'), + 'groupid' => new external_value(PARAM_INT, 'Group id'), + 'name' => new external_value(PARAM_RAW, 'Group choice name'), + 'maxanswers' => new external_value(PARAM_INT, 'Maximum number of accepted answers', VALUE_OPTIONAL), + 'displaylayout' => new external_value(PARAM_INT, 'Display layout', VALUE_OPTIONAL), + 'countanswers' => new external_value(PARAM_INT, 'Current number of answers', VALUE_OPTIONAL), + 'checked' => new external_value(PARAM_BOOL, 'Checked', VALUE_OPTIONAL), + 'disabled' => new external_value(PARAM_BOOL, 'Disabled', VALUE_OPTIONAL), + ) + ) + ), + 'warnings' => new external_warnings(), + ) + ); + } + + /** + * Describes the parameters for view_choicegroup. + * + * @return external_function_parameters + */ + public static function view_choicegroup_parameters() { + return new external_function_parameters( + array( + 'choicegroupid' => new external_value(PARAM_INT, 'Choice group instance id') + ) + ); + } + + /** + * Trigger the course module viewed event and update the module completion status. + * + * @param int $choicegroupid The choice group id. + * @return array of warnings and status result + * @throws moodle_exception + */ + public static function view_choicegroup($choicegroupid) { + global $DB; + + $params = array( + 'choicegroupid' => $choicegroupid + ); + $params = self::validate_parameters(self::view_choicegroup_parameters(), $params); + $warnings = array(); + + // Request and permission validation. + $choicegroup = $DB->get_record('choicegroup', array('id' => $params['choicegroupid']), '*', MUST_EXIST); + list($course, $cm) = get_course_and_cm_from_instance($choicegroup, 'choicegroup'); + + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/choicegroup:choose', $context); + + $event = \mod_choicegroup\event\course_module_viewed::create(array( + 'objectid' => $choicegroup->id, + 'context' => $context, + )); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + + $completion = new completion_info($course); + $completion->set_module_viewed($cm); + + $result = array(); + $result['status'] = true; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Returns description of method result value + * + * @return external_description + */ + public static function view_choicegroup_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'Status: true if success'), + 'warnings' => new external_warnings() + ) + ); + } + + /** + * Describes the parameters for submit_response. + * + * @return external_function_parameters + */ + public static function submit_choicegroup_response_parameters() { + return new external_function_parameters ( + array( + 'choicegroupid' => new external_value(PARAM_INT, 'Choice group instance id'), + 'data' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_RAW, 'Data name'), + 'value' => new external_value(PARAM_RAW, 'Data value'), + ) + ), + 'The data to be saved', + VALUE_DEFAULT, + array() + ) + ) + ); + } + + /** + * Returns the options list for the provided choice group instance. + * + * @param int $choicegroupid The choice group id. + * @param array $data The user responses. + * @return array The choice group options. + */ + public static function submit_choicegroup_response($choicegroupid, $data) { + global $CFG, $DB, $USER; + + $warnings = array(); + + $params = array( + 'choicegroupid' => $choicegroupid, + 'data' => $data + ); + + $params = self::validate_parameters(self::submit_choicegroup_response_parameters(), $params); + + if (!$choicegroup = choicegroup_get_choicegroup($choicegroupid)) { + throw new moodle_exception('invalidcoursemodule', 'error'); + } + list($course, $cm) = get_course_and_cm_from_instance($choicegroup, 'choicegroup'); + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/choicegroup:choose', $context); + + $timenow = time(); + if (!empty($choicegroup->timeopen) && ($choicegroup->timeopen > $timenow)) { + throw new moodle_exception('notopenyet', 'choicegroup', '', userdate($choicegroup->timeopen)); + } else if (!empty($choicegroup->timeclose) && ($timenow > $choicegroup->timeclose)) { + throw new moodle_exception('expired', 'choicegroup', '', userdate($choice->timeclose)); + } + + $responses = self::parse_data_to_responses( + $data, + $choicegroup->multipleenrollmentspossible + ); + if (empty($responses)) { + // Update completion state + $completion = new completion_info($course); + if ($completion->is_enabled($cm) && $choicegroup->completionsubmit) { + $completion->update_state($cm, COMPLETION_INCOMPLETE); + } + } + + if (!choicegroup_get_user_answer($choicegroup, $USER) || $choicegroup->allowupdate) { + if ($choicegroup->multipleenrollmentspossible) { + foreach($choicegroup->option as $optionid => $text) { + if (in_array($optionid, $responses)) { + choicegroup_user_submit_response($optionid, $choicegroup, $USER->id, $course, $cm); + } else { + // Remove group selection if selected. + if (groups_is_member($text, $USER->id)) { + $answer_value_group = $DB->get_record('groups', array('id' => $text), 'id, name', MUST_EXIST); + groups_remove_member($answer_value_group->id, $USER->id); + $eventparams = array( + 'context' => $context, + 'objectid' => $choicegroup->id + ); + $event = \mod_choicegroup\event\choice_removed::create($eventparams); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + } + } + } + } else { // !multipleenrollmentspossible + if (count($responses) == 1) { + $responses = reset($responses); + choicegroup_user_submit_response($responses, $choicegroup, $USER->id, $course, $cm); + } + } + } else { + throw new moodle_exception('missingrequiredcapability', 'webservice', '', 'allowupdate'); + } + + $result = array(); + $result['status'] = true; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the submit_response return value. + * + * @return external_single_structure + */ + public static function submit_choicegroup_response_returns() { + + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'Status: true if success'), + 'warnings' => new external_warnings(), + ) + ); + } + + /** + * Extract user responses from the WS data. + * @param array $data The data received from the WS. + * @param boolean $allowmultiple True if more than one response can be selected. + * @return array The optionid from the selected responses. + */ + protected static function parse_data_to_responses($data, $allowmultiple) { + $responses = array(); + foreach($data as $index => $datavalue) { + $name = $datavalue['name']; + $value = $datavalue['value']; + if ($allowmultiple) { + if ($name != 'responses' && $value === 'true') { + $responses[] = substr($name, strrpos($name, '_')+1); + } + } else if ($name === 'responses') { + $responses[] = $value; + break; + } + } + + return $responses; + } + + /** + * Describes the parameters for delete_choicegroup_responses. + * + * @return external_function_parameters + */ + public static function delete_choicegroup_responses_parameters() { + return new external_function_parameters ( + array( + 'choicegroupid' => new external_value(PARAM_INT, 'Choice group instance id'), + ) + ); + } + + /** + * Delete the given submitted responses in a choice group + * + * @param int $choicegroupid The choicegroup instance id + * @return array status information and warnings + * @throws moodle_exception + */ + public static function delete_choicegroup_responses($choicegroupid) { + global $USER, $DB; + + $status = false; + $warnings = array(); + + $params = array( + 'choicegroupid' => $choicegroupid + ); + + $params = self::validate_parameters(self::submit_choicegroup_response_parameters(), $params); + + if (!$choicegroup = choicegroup_get_choicegroup($choicegroupid)) { + throw new moodle_exception('invalidcoursemodule', 'error'); + } + list($course, $cm) = get_course_and_cm_from_instance($choicegroup, 'choicegroup'); + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/choicegroup:choose', $context); + + $timenow = time(); + if (!empty($choicegroup->timeopen) && ($choicegroup->timeopen > $timenow)) { + throw new moodle_exception('notopenyet', 'choicegroup', '', userdate($choicegroup->timeopen)); + } else if (!empty($choicegroup->timeclose) && ($timenow > $choicegroup->timeclose)) { + throw new moodle_exception('expired', 'choicegroup', '', userdate($choice->timeclose)); + } + + $answergiven = choicegroup_get_user_answer($choicegroup, $USER, true); + if (!empty($answergiven)) { + if ($choicegroup->allowupdate && !$choicegroup->multipleenrollmentspossible) { + $params = array('groupid' => reset($answergiven)->id, 'userid' => $USER->id); + $groupmember = $DB->get_record('groups_members', $params, 'id', MUST_EXIST); + $status = choicegroup_delete_responses([$groupmember->id], $choicegroup, $cm, $course); + } else { + throw new moodle_exception('missingrequiredcapability', 'webservice', '', 'allowupdate'); + } + } else { + // User didn't give any answer, so there's no need to delete anything. + $status = true; + } + + $result = array( + 'status' => $status, + 'warnings' => $warnings + ); + return $result; + } + + /** + * Describes the delete_choicegroup_responses return value. + * + * @return external_multiple_structure + */ + public static function delete_choicegroup_responses_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status, True if everything went right'), + 'warnings' => new external_warnings(), + ) + ); + } + +} diff --git a/mod/choicegroup/classes/output/mobile.php b/mod/choicegroup/classes/output/mobile.php new file mode 100644 index 0000000..0bcbb9d --- /dev/null +++ b/mod/choicegroup/classes/output/mobile.php @@ -0,0 +1,154 @@ +<?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/>. + +/** + * Mobile output class for Choice group + * + * @package mod_choicegroup + * @copyright 2018 Sara Arjona <sara@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\output; + +defined('MOODLE_INTERNAL') || die(); + +use context_module; +use mod_choicegroup_external; +use completion_info; + +/** + * Mobile output class for Choice group + * + * @copyright 2018 Sara Arjona <sara@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mobile { + + /** + * Returns the javascript needed to initialize choice group in the app. + * + * @param array $args Arguments from tool_mobile_get_content WS + * @return array javascript + */ + public static function mobile_init($args) { + global $CFG; + + return [ + 'templates' => [], + 'javascript' => file_get_contents($CFG->dirroot . '/mod/choicegroup/mobile/js/init.js'), + ]; + } + + /** + * Returns the choice group course view for the mobile app. + * @param array $args Arguments from tool_mobile_get_content WS + * + * @return array HTML, javascript and otherdata + */ + public static function mobile_course_view($args) { + global $OUTPUT, $USER, $DB, $CFG; + + $args = (object) $args; + + $cm = get_coursemodule_from_id('choicegroup', $args->cmid); + $course = $DB->get_record('course', array('id' => $cm->course)); + + // Capabilities check. + require_login($args->courseid, false, $cm, true, true); + $context = context_module::instance($cm->id); + require_capability('mod/choicegroup:choose', $context); + + // Get choice_options from external. + $choicegroup = choicegroup_get_choicegroup($cm->instance); + $current = choicegroup_get_user_answer($choicegroup, $USER); + + // Check if the activity is open. + $timenow = time(); + + if (!empty($choicegroup->timeopen) && $choicegroup->timeopen > $timenow) { + $choicegroup->open = false; + $choicegroup->message = get_string("notopenyet", "choicegroup", userdate($choicegroup->timeopen)); + } else { + $choicegroup->open = true; + } + if (!empty($choicegroup->timeclose) && $timenow > $choicegroup->timeclose) { + $choicegroup->expired = true; + $choicegroup->message = get_string("expired", "choicegroup", userdate($choicegroup->timeclose)); + } else { + $choicegroup->expired = false; + } + + // The user has made her choice and updates are not allowed or choicegroup is not open. + $choicegroup->answergiven = choicegroup_get_user_answer($choicegroup, $USER->id); + $choicegroup->alloptionsdisabled = (!$choicegroup->open || $choicegroup->expired + || ($choicegroup->answergiven && !$choicegroup->allowupdate) + || !is_enrolled($context, NULL, 'mod/choicegroup:choose') + ); + + // Get choicegroup options from external. + try { + $returnedoptions = mod_choicegroup_external::get_choicegroup_options( + $cm->instance, + $USER->id, + $choicegroup->alloptionsdisabled + ); + $options = array_values($returnedoptions['options']); // Make it mustache compatible. + $responses = array(); + foreach ($options as $option) { + if ($choicegroup->multipleenrollmentspossible) { + $responses['responses_'.$option['id']] = $option['checked']; + } else if ($option['checked']) { + $responses['responses'] = $option['id']; + } + } + } catch (Exception $e) { + $options = array(); + } + + // Format name and intro. + $choicegroup->name = format_string($choicegroup->name); + list($choicegroup->intro, $choicegroup->introformat) = external_format_text( + $choicegroup->intro, + $choicegroup->introformat, + $context->id, + 'mod_choicegroup', + 'intro' + ); + $data = array( + 'cmid' => $cm->id, + 'courseid' => $args->courseid, + 'choicegroup' => $choicegroup, + 'options' => $options + ); + + return array( + 'templates' => array( + array( + 'id' => 'main', + 'html' => $OUTPUT->render_from_template('mod_choicegroup/mobile_view_page', $data), + ), + ), + 'javascript' => file_get_contents($CFG->dirroot . '/mod/choicegroup/mobile/js/courseview.js'), + 'otherdata' => array( + 'data' => json_encode($responses), + 'allowupdate' => $choicegroup->allowupdate ? 1 : 0, + 'multipleenrollmentspossible' => $choicegroup->multipleenrollmentspossible ? 1 : 0, + 'answergiven' => $choicegroup->answergiven ? 1 : 0, + ) + ); + } +} diff --git a/mod/choicegroup/classes/privacy/provider.php b/mod/choicegroup/classes/privacy/provider.php new file mode 100644 index 0000000..8d57be6 --- /dev/null +++ b/mod/choicegroup/classes/privacy/provider.php @@ -0,0 +1,40 @@ +<?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 Subsystem implementation for mod_choicegroup. + * + * @package mod_choicegroup + * @copyright 2018 Nicolas Dunand + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_choicegroup\privacy; + +defined('MOODLE_INTERNAL') || die(); + +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/choicegroup/db/access.php b/mod/choicegroup/db/access.php new file mode 100644 index 0000000..4a2c835 --- /dev/null +++ b/mod/choicegroup/db/access.php @@ -0,0 +1,91 @@ +<?php +// +// Capability definitions for the choicegroup module. +// +// The capabilities are loaded into the database table when the module is +// installed or updated. Whenever the capability definitions are updated, +// the module version number should be bumped up. +// +// The system has four possible values for a capability: +// CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT, and inherit (not set). +// +// +// CAPABILITY NAMING CONVENTION +// +// It is important that capability names are unique. The naming convention +// for capabilities that are specific to modules and blocks is as follows: +// [mod/block]/<plugin_name>:<capabilityname> +// +// component_name should be the same as the directory name of the mod or block. +// +// Core moodle capabilities are defined thus: +// moodle/<capabilityclass>:<capabilityname> +// +// Examples: mod/forum:viewpost +// block/recent_activity:view +// moodle/site:deleteuser +// +// The variable name for the capability definitions array is $capabilities + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'mod/choicegroup:choose' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW + ) + ), + + 'mod/choicegroup:addinstance' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + + 'mod/choicegroup:readresponses' => array( + + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/choicegroup:deleteresponses' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/choicegroup:downloadresponses' => array( + + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ) +); + + diff --git a/mod/choicegroup/db/install.xml b/mod/choicegroup/db/install.xml new file mode 100644 index 0000000..905dfff --- /dev/null +++ b/mod/choicegroup/db/install.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="mod/choicegroup/db" VERSION="20120425" COMMENT="XMLDB file for Moodle mod/choicegroup" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="choicegroup" COMMENT="Available choicegroups are stored here" NEXT="choicegroup_options"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="course"/> + <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="name"/> + <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="course" NEXT="intro"/> + <FIELD NAME="intro" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="name" NEXT="introformat"/> + <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="intro" NEXT="publish"/> + <FIELD NAME="publish" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="introformat" NEXT="multipleenrollmentspossible"/> + <FIELD NAME="multipleenrollmentspossible" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="publish" NEXT="showresults"/> + <FIELD NAME="showresults" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="multipleenrollmentspossible" NEXT="display"/> + <FIELD NAME="display" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="showresults" NEXT="allowupdate"/> + <FIELD NAME="allowupdate" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="display" NEXT="showunanswered"/> + <FIELD NAME="showunanswered" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="allowupdate" NEXT="limitanswers"/> + <FIELD NAME="limitanswers" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="showunanswered" NEXT="timeopen"/> + <FIELD NAME="timeopen" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="limitanswers" NEXT="timeclose"/> + <FIELD NAME="timeclose" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timeopen" NEXT="timemodified"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="timeclose" NEXT="completionsubmit"/> + <FIELD NAME="completionsubmit" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" COMMENT="If this field is set to 1, then the activity will be automatically marked as 'complete' once the user submits their choicegroup." PREVIOUS="timemodified" NEXT="sortgroupsby"/> + <FIELD NAME="sortgroupsby" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" COMMENT="Column used to sort groups." PREVIOUS="completionsubmit"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="course" UNIQUE="false" FIELDS="course"/> + </INDEXES> + </TABLE> + <TABLE NAME="choicegroup_options" COMMENT="available options to choicegroup" PREVIOUS="choicegroup"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="choicegroupid"/> + <FIELD NAME="choicegroupid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="groupid"/> + <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="choicegroupid" NEXT="maxanswers"/> + <FIELD NAME="maxanswers" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="groupid" NEXT="timemodified"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="maxanswers"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="choicegroupid"/> + <KEY NAME="choicegroupid" TYPE="foreign" FIELDS="choicegroupid" REFTABLE="choicegroup" REFFIELDS="id" PREVIOUS="primary"/> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> diff --git a/mod/choicegroup/db/log.php b/mod/choicegroup/db/log.php new file mode 100644 index 0000000..02f76a3 --- /dev/null +++ b/mod/choicegroup/db/log.php @@ -0,0 +1,36 @@ +<?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/>. + +/** + * Definition of log events + * + * @package mod + * @subpackage choicegroup + * @copyright 2010 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$logs = array( + array('module'=>'choicegroup', 'action'=>'view', 'mtable'=>'choicegroup', 'field'=>'name'), + array('module'=>'choicegroup', 'action'=>'update', 'mtable'=>'choicegroup', 'field'=>'name'), + array('module'=>'choicegroup', 'action'=>'add', 'mtable'=>'choicegroup', 'field'=>'name'), + array('module'=>'choicegroup', 'action'=>'report', 'mtable'=>'choicegroup', 'field'=>'name'), + array('module'=>'choicegroup', 'action'=>'choose', 'mtable'=>'choicegroup', 'field'=>'name'), + array('module'=>'choicegroup', 'action'=>'choose again', 'mtable'=>'choicegroup', 'field'=>'name'), +); \ No newline at end of file diff --git a/mod/choicegroup/db/mobile.php b/mod/choicegroup/db/mobile.php new file mode 100644 index 0000000..714d484 --- /dev/null +++ b/mod/choicegroup/db/mobile.php @@ -0,0 +1,61 @@ +<?php +// This file is part of the Choice group module for 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/>. + +/** + * Choice group module capability definition + * + * @package mod_choicegroup + * @copyright 2018 Sara Arjona <sara@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$addons = array( + "mod_choicegroup" => array( + "handlers" => array( // Different places where the add-on will display content. + 'coursechoicegroup' => array( // Handler unique name (can be anything) + 'displaydata' => array( + 'title' => 'pluginname', + 'icon' => $CFG->wwwroot . '/mod/choicegroup/pix/icon.svg', + 'class' => '', + ), + 'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the add-on) + 'method' => 'mobile_course_view', // Main function in \mod_choicegroup\output\mobile + 'init' => 'mobile_init', + 'offlinefunctions' => array( + 'mobile_course_view' => array(), + ), // Function needs caching for offline. + 'styles' => array( + 'url' => $CFG->wwwroot . '/mod/choicegroup/styles_app.css', + 'version' => '0.2' + ), + 'displayrefresh' => false, // Hide default refresh button, a custom one will be used. + ) + ), + 'lang' => array( + array('group', 'moodle'), + array('choice', 'choicegroup'), + array('choicegroupsaved', 'choicegroup'), + array('members/', 'choicegroup'), + array('members/max', 'choicegroup'), + array('modulename', 'choicegroup'), + array('pluginname', 'choicegroup'), + array('removemychoicegroup', 'choicegroup'), + array('savemychoicegroup', 'choicegroup') + ) + ) +); diff --git a/mod/choicegroup/db/services.php b/mod/choicegroup/db/services.php new file mode 100644 index 0000000..9d960ad --- /dev/null +++ b/mod/choicegroup/db/services.php @@ -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/>. + +/** + * Choice group external functions and service definitions. + * + * @package mod_choicegroup + * @category external + * @copyright 2018 Sara Arjona <sara@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = array( + + 'mod_choicegroup_get_choicegroup_options' => array( + 'classname' => 'mod_choicegroup_external', + 'methodname' => 'get_choicegroup_options', + 'description' => 'Retrieve options for a specific choicegroup.', + 'type' => 'read', + 'capabilities' => 'mod/choicegroup:choose', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile') + ), + + 'mod_choicegroup_submit_choicegroup_response' => array( + 'classname' => 'mod_choicegroup_external', + 'methodname' => 'submit_choicegroup_response', + 'description' => 'Submit responses to a specific choicegroup item.', + 'type' => 'write', + 'capabilities' => 'mod/choicegroup:choose', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile') + ), + + 'mod_choicegroup_view_choicegroup' => array( + 'classname' => 'mod_choicegroup_external', + 'methodname' => 'view_choicegroup', + 'description' => 'Trigger the course module viewed event and update the module completion status.', + 'type' => 'write', + 'capabilities' => '', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile') + ), + + 'mod_choicegroup_delete_choicegroup_responses' => array( + 'classname' => 'mod_choicegroup_external', + 'methodname' => 'delete_choicegroup_responses', + 'description' => 'Delete the given submitted responses in a choice group', + 'type' => 'write', + 'capabilities' => 'mod/choicegroup:choose', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile') + ), +); diff --git a/mod/choicegroup/db/upgrade.php b/mod/choicegroup/db/upgrade.php new file mode 100644 index 0000000..88c4c6a --- /dev/null +++ b/mod/choicegroup/db/upgrade.php @@ -0,0 +1,73 @@ +<?php + +// This file keeps track of upgrades to +// the choicegroup module +// +// Sometimes, changes between versions involve +// alterations to database structures and other +// major things that may break installations. +// +// The upgrade function in this file will attempt +// to perform all the necessary actions to upgrade +// your older installation to the current version. +// +// If there's something it cannot do itself, it +// will tell you what you need to do. +// +// The commands in here will all be database-neutral, +// using the methods of database_manager class +// +// Please do not forget to use upgrade_set_timeout() +// before any action that may take longer time to finish. + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_choicegroup_upgrade($oldversion) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); + if ($oldversion < 2013070900) { + + if ($oldversion < 2012042500) { + + /// remove the no longer needed choicegroup_answers DB table + $choicegroup_answers = new xmldb_table('choicegroup_answers'); + $dbman->drop_table($choicegroup_answers); + + /// change the choicegroup_options.text (text) field as choicegroup_options.groupid (int) + $choicegroup_options = new xmldb_table('choicegroup_options'); + $field_text = new xmldb_field('text', XMLDB_TYPE_TEXT, 'small', null, null, null, null, 'choicegroupid'); + $field_groupid = new xmldb_field('groupid', XMLDB_TYPE_INTEGER, '10', null, null, null, '0', 'choicegroupid'); + + $dbman->rename_field($choicegroup_options, $field_text, 'groupid'); + $dbman->change_field_type($choicegroup_options, $field_groupid); + + } + // Define table choicegroup to be created + $table = new xmldb_table('choicegroup'); + + // Adding fields to table choicegroup + $newField = $table->add_field('multipleenrollmentspossible', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + $dbman->add_field($table, $newField); + + + upgrade_mod_savepoint(true, 2013070900, 'choicegroup'); + } + + if ($oldversion < 2015022301) { + $table = new xmldb_table('choicegroup'); + + // Adding field to table choicegroup + $newField = $table->add_field('sortgroupsby', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + if (!$dbman->field_exists($table, $newField)) { + $dbman->add_field($table, $newField); + } + + upgrade_mod_savepoint(true, 2015022301, 'choicegroup'); + } + + return true; +} + + diff --git a/mod/choicegroup/index.php b/mod/choicegroup/index.php new file mode 100644 index 0000000..7fe51c6 --- /dev/null +++ b/mod/choicegroup/index.php @@ -0,0 +1,118 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once("../../config.php"); +require_once("lib.php"); + +$id = required_param('id',PARAM_INT); // course + +$PAGE->set_url('/mod/choicegroup/index.php', array('id'=>$id)); + +if (!$course = $DB->get_record('course', array('id'=>$id))) { + print_error('invalidcourseid'); +} + +require_course_login($course); +$PAGE->set_pagelayout('incourse'); + +$params = array( + 'context' => context_course::instance($course->id) +); +$event = \mod_choicegroup\event\course_module_instance_list_viewed::create($params); +$event->add_record_snapshot('course', $course); +$event->trigger(); + +$strchoicegroup = get_string("modulename", "choicegroup"); +$strchoicegroups = get_string("modulenameplural", "choicegroup"); +$strsectionname = get_string('sectionname', 'format_'.$course->format); +$PAGE->set_title($strchoicegroups); +$PAGE->set_heading($course->fullname); +$PAGE->navbar->add($strchoicegroups); +echo $OUTPUT->header(); + +if (! $choicegroups = get_all_instances_in_course("choicegroup", $course)) { + notice(get_string('thereareno', 'moodle', $strchoicegroups), "../../course/view.php?id=$course->id"); +} + +$usesections = course_format_uses_sections($course->format); +if ($usesections) { + $modinfo = get_fast_modinfo($course->id); + $sections = $modinfo->get_section_info_all(); +} + +$table = new html_table(); + +if ($usesections) { + $table->head = array ($strsectionname, get_string("question"), get_string("answer")); + $table->align = array ("center", "left", "left"); +} else { + $table->head = array (get_string("question"), get_string("answer")); + $table->align = array ("left", "left"); +} + +$currentsection = ""; + +foreach ($choicegroups as $choicegroup) { + $choicegroup_groups = choicegroup_get_groups($choicegroup); + $answer = choicegroup_get_user_answer($choicegroup, $USER->id); + if (!empty($answer->id)) { + $aa = $answer->name; + } else { + $aa = ""; + } + if ($usesections) { + $printsection = ""; + if ($choicegroup->section !== $currentsection) { + if ($choicegroup->section) { + $printsection = get_section_name($course, $sections[$choicegroup->section]); + } + if ($currentsection !== "") { + $table->data[] = 'hr'; + } + $currentsection = $choicegroup->section; + } + } + + //Calculate the href + if (!$choicegroup->visible) { + //Show dimmed if the mod is hidden + $tt_href = "<a class=\"dimmed\" href=\"view.php?id=$choicegroup->coursemodule\">".format_string($choicegroup->name,true)."</a>"; + } else { + //Show normal if the mod is visible + $tt_href = "<a href=\"view.php?id=$choicegroup->coursemodule\">".format_string($choicegroup->name,true)."</a>"; + } + if ($usesections) { + $table->data[] = array ($printsection, $tt_href, $aa); + } else { + $table->data[] = array ($tt_href, $aa); + } +} +echo "<br />"; +echo html_writer::table($table); + +echo $OUTPUT->footer(); + diff --git a/mod/choicegroup/javascript.js b/mod/choicegroup/javascript.js new file mode 100644 index 0000000..9b5cf35 --- /dev/null +++ b/mod/choicegroup/javascript.js @@ -0,0 +1,75 @@ +// 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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +var NDY = YUI().use("node", function(Y) { + var choicegroup_memberdisplay_click = function(e) { + + var names = Y.all('div.choicegroups-membersnames'), + btnShowHide = Y.all('a.choicegroup-memberdisplay'); + + btnShowHide.toggleClass('hidden'); + names.toggleClass('hidden'); + + // Fix for Chrome where focus is not returned to the link after it is toggled. + if (document.getElementsByClassName) { + var elements = document.getElementsByClassName('choicegroup-membershow'); + if (elements[0].classList.contains('hidden')) { + elements = document.getElementsByClassName('choicegroup-memberhide'); + } + elements[0].focus(); + } + e.preventDefault(); + + }; + Y.on("click", choicegroup_memberdisplay_click, "a.choicegroup-memberdisplay"); + + var choicegroup_descriptiondisplay_click = function(e) { + + var names = Y.all('div.choicegroups-descriptions'), + btnShowHide = Y.all('a.choicegroup-descriptiondisplay'); + + btnShowHide.toggleClass('hidden'); + names.toggleClass('hidden'); + + // Fix for Chrome where focus is not returned to the link after it is toggled. + if (document.getElementsByClassName) { + var elements = document.getElementsByClassName('choicegroup-descriptionshow'); + if (elements[0].classList.contains('hidden')) { + elements = document.getElementsByClassName('choicegroup-descriptionhide'); + } + elements[0].focus(); + } + e.preventDefault(); + + }; + Y.on("click", choicegroup_descriptiondisplay_click, "a.choicegroup-descriptiondisplay"); + Y.delegate('click', function () { + Y.one(".modchoicegroupsumbit").hide(); + }, Y.config.doc, "table.choicegroups input[id^='choiceid_'][type='radio'][checked]", this); + Y.delegate('click', function () { + Y.one(".modchoicegroupsumbit").show(); + }, Y.config.doc, "table.choicegroups input[id^='choiceid_'][type='radio']:not([checked])", this); +}); diff --git a/mod/choicegroup/lang/en/choicegroup.php b/mod/choicegroup/lang/en/choicegroup.php new file mode 100644 index 0000000..7684465 --- /dev/null +++ b/mod/choicegroup/lang/en/choicegroup.php @@ -0,0 +1,165 @@ +<?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/>. + +/** + * Strings for component 'choice', language 'en', branch 'MOODLE_20_STABLE' + * + * @package choice + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['addmorechoices'] = 'Add more choices'; +$string['allowupdate'] = 'Allow choice to be updated'; +$string['answered'] = 'Answered'; +$string['completionsubmit'] = 'Show as complete when user makes a choice'; +$string['defaultsettings'] = 'Default settings'; +$string['displayhorizontal'] = 'Display horizontally'; +$string['displaymode'] = 'Display mode'; +$string['displayvertical'] = 'Display vertically'; +$string['expired'] = 'Sorry, this activity closed on {$a} and is no longer available'; +$string['fillinatleastoneoption'] = 'You need to provide at least one possible answer.'; +$string['fillinatleasttwooptions'] = 'You need to provide at least two possible answers.'; +$string['full'] = '(Full)'; +$string['havetologin'] = 'You have to log in before you can submit your choice'; +$string['choice'] = 'Choice'; +$string['choicegroupclose'] = 'Until'; +$string['choicegroup:deleteresponses'] = 'Delete responses'; +$string['choicegroup:downloadresponses'] = 'Download responses'; +$string['choicegroupfull'] = 'This group choice is full and there are no available places.'; +$string['choicegroup:choose'] = 'Record a choice'; +$string['choicegroupname'] = 'Group choice name'; +$string['choicegroupopen'] = 'Open'; +$string['choicegroupoptions'] = 'Choice options'; +$string['choicegroupoptions_help'] = 'Here is where you specify which groups participants can choose from. + +The list on the left displays all available groups and groupings. To add one or several groups, select these from the list and click "Add". To add all groups from a grouping, select the grouping and click "Add". + +The selected groups appear on the list on the right. + +To remove any groups from the selection, select them from the list on the right and click "Remove".'; +$string['limitanswers_help'] = 'This option allows you to limit the number of participants that can select each choice option. When the limit is reached then no-one else can select that option. + +If limits are disabled then any number of participants can select each of the options.'; +$string['choicegroup:addinstance'] = 'Add a new group choice activity'; +$string['choicegroup:readresponses'] = 'Read responses'; +$string['choicegroupsaved'] = 'Your choice has been saved'; +$string['choicetext'] = 'Choice text'; +$string['chooseaction'] = 'Choose an action ...'; +$string['choosegroup'] = 'Choose a group'; +$string['createdate'] = 'Group creation date'; +$string['limit'] = 'Limit'; +$string['limitanswers'] = 'Limit the number of responses allowed'; +$string['modulename'] = 'Group choice'; +$string['modulename_help'] = 'The Group Choice module allows students to enrol themselves in a group within a course. The teacher can select which groups students can choose from and the maximum number of students allowed in each group.'; +$string['modulename_link'] = 'mod/choicegroup/view'; +$string['modulenameplural'] = 'Group choices'; +$string['mustchooseone'] = 'You must choose an answer before saving. Nothing was saved.'; +$string['noguestchoose'] = 'Sorry, guests are not allowed to make choices.'; +$string['noresultsviewable'] = 'The results are not currently viewable.'; +$string['neverresultsviewable'] = 'The results are not viewable.'; +$string['name'] = 'Name'; +$string['afterresultsviewable'] = 'The results will be visible after you have made your choice.'; +$string['notyetresultsviewable'] = 'The results will be visible after this activity has closed.'; +$string['notanswered'] = 'Not answered yet'; +$string['notenrolledchoose'] = 'Sorry, only enrolled users are allowed to make choices.'; +$string['notopenyet'] = 'Sorry, this activity is not available until {$a}'; +$string['option'] = 'Group'; +$string['pluginadministration'] = 'Choice administration'; +$string['pluginname'] = 'Group choice'; +$string['privacy'] = 'Privacy of results'; +$string['publish'] = 'Publish results'; +$string['publishafteranswer'] = 'Show results to students after they answer'; +$string['publishafterclose'] = 'Show results to students only after the choice is closed'; +$string['publishalways'] = 'Always show results to students'; +$string['publishanonymous'] = 'Publish anonymous results, do not show student names'; +$string['publishnames'] = 'Publish full results, showing names and their choices'; +$string['publishnot'] = 'Do not publish results to students'; +$string['removemychoicegroup'] = 'Remove my choice'; +$string['removeresponses'] = 'Remove all responses'; +$string['responses'] = 'Responses'; +$string['responsesto'] = 'Responses to {$a}'; +$string['savemychoicegroup'] = 'Save my choice'; +$string['showunanswered'] = 'Show column for unanswered'; +$string['spaceleft'] = 'space available'; +$string['spacesleft'] = 'spaces available'; +$string['systemdefault_date'] = 'System Default (currently Group creation date)'; +$string['systemdefault_name'] = 'System Default (currently Name)'; +$string['taken'] = 'Taken'; +$string['timerestrict'] = 'Restrict answering to this time period'; +$string['viewallresponses'] = 'View {$a} responses'; +$string['byparticipants'] = 'by {$a} participants'; +$string['withselected'] = 'With selected'; +$string['yourselection'] = 'Your selection'; +$string['skipresultgraph'] = 'Skip result graph'; +$string['sortgroupsby'] = 'Sort groups by'; +$string['moveselectedusersto'] = 'Move selected users to...'; +$string['numberofuser'] = 'The number of users'; +$string['groupdoesntexist'] = 'Some of the specified groups don\'t exist within this course. The teacher should create the necessary groups and/or modify this activity.'; +$string['samegroupused'] = 'The same group can not be used several times.'; + +$string['members/max'] = 'Members / Capacity'; +$string['members/'] = 'Members'; +$string['groupmembers'] = 'Group members'; +$string['page-mod-choice-x'] = 'Any Group choice module page'; +$string['showdescription'] = 'Show descriptions'; +$string['hidedescription'] = 'Hide descriptions'; +$string['generallimitation'] = 'General limitation'; +$string['applytoallgroups'] = 'Apply to all groups'; +$string['pleasesetgroups'] = 'Please create at least one group in this course.'; +$string['nogroupincourse'] = 'No groups defined in course.'; +$string['pleasesetonegroupor'] = 'Please create at least one group in this course.<br /><br /> +<ul> +<li><a href="{$a->linkgroups}">manage course groups</a></li> +<li><a href="{$a->linkcourse}">get back to the course</a></li> +</ul>'; +$string['pleaseselectonegroup'] = 'Please select at least one group to chose from.'; + +$string['multipleenrollmentspossible'] = 'Allow enrollment to multiple groups'; +$string['and'] = 'and'; +$string['event:answered'] = 'Choice made'; +$string['event:answered_desc'] = 'The user with id \'{$a->userid}\' has chosen a group in the group choice with the course module id \'{$a->contextinstanceid}\'.'; +$string['event:removed'] = 'Choice removed'; +$string['event:removed_desc'] = 'The user with id \'{$a->userid}\' has removed his choice in the group choice with the course module id \'{$a->contextinstanceid}\'.'; +$string['event:reportviewed'] = 'Report viewed'; +$string['event:reportviewed_desc'] = 'The user with id \'{$a->userid}\' has viewed the report for the group choice activity with the course module id \'{$a->contextinstanceid}\'.'; +$string['groupsheader'] = "Groups"; +$string['the_value_you_entered_is_not_a_number'] = "The value you entered is not a number."; +$string['add_groupings'] = "Add Groupings"; +$string['add_grouping'] = "Add Grouping"; +$string['add_groups'] = "Add Groups"; +$string['del_groups'] = "Remove Groups"; +$string['del_group'] = "Remove Group"; +$string['add_group'] = "Add Group"; +$string['add'] = "Add"; +$string['del'] = "Remove"; +$string['set_limit_for_group'] = "Limit For "; +$string['available_groups'] = 'Available Groups'; +$string['selected_groups'] = 'Selected Groups'; +$string['char_bullet_collapsed'] = '►'; +$string['char_bullet_expanded'] = '▼'; +$string['char_limitui_parenthesis_start'] = '⦗'; +$string['char_limitui_parenthesis_end'] = '⦘'; +$string['expand_all_groupings'] = 'Expand All Groupings'; +$string['collapse_all_groupings'] = 'Collapse All Groupings'; +$string['double_click_grouping_legend'] = 'Double click on a grouping to expand/collapse individually.'; +$string['double_click_group_legend'] = 'Double click on a group to add it.'; +$string['privacy:metadata'] = 'The Group Choice plugin does not store any personal data. All user data is stored by the group component of Moodle core (core_group).'; +$string['showgroupmembers'] = 'Show Group Members'; +$string['hidegroupmembers'] = 'Hide Group Members'; + + diff --git a/mod/choicegroup/lib.php b/mod/choicegroup/lib.php new file mode 100644 index 0000000..62bb679 --- /dev/null +++ b/mod/choicegroup/lib.php @@ -0,0 +1,1106 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** @global int $CHOICEGROUP_COLUMN_HEIGHT */ +global $CHOICEGROUP_COLUMN_HEIGHT; +$CHOICEGROUP_COLUMN_HEIGHT = 300; + +/** @global int $CHOICEGROUP_COLUMN_WIDTH */ +global $CHOICEGROUP_COLUMN_WIDTH; +$CHOICEGROUP_COLUMN_WIDTH = 300; + +define('CHOICEGROUP_PUBLISH_ANONYMOUS', '0'); +define('CHOICEGROUP_PUBLISH_NAMES', '1'); +define('CHOICEGROUP_PUBLISH_DEFAULT', '1'); + +define('CHOICEGROUP_SHOWRESULTS_NOT', '0'); +define('CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER', '1'); +define('CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE', '2'); +define('CHOICEGROUP_SHOWRESULTS_ALWAYS', '3'); +define('CHOICEGROUP_SHOWRESULTS_DEFAULT', '3'); + +define('CHOICEGROUP_DISPLAY_HORIZONTAL', '0'); +define('CHOICEGROUP_DISPLAY_VERTICAL', '1'); + +define('CHOICEGROUP_SORTGROUPS_SYSTEMDEFAULT', '0'); +define('CHOICEGROUP_SORTGROUPS_CREATEDATE', '1'); +define('CHOICEGROUP_SORTGROUPS_NAME', '2'); + +/** @global array $CHOICEGROUP_PUBLISH */ +global $CHOICEGROUP_PUBLISH; +$CHOICEGROUP_PUBLISH = array (CHOICEGROUP_PUBLISH_ANONYMOUS => get_string('publishanonymous', 'choicegroup'), + CHOICEGROUP_PUBLISH_NAMES => get_string('publishnames', 'choicegroup')); + +/** @global array $CHOICEGROUP_SHOWRESULTS */ +global $CHOICEGROUP_SHOWRESULTS; +$CHOICEGROUP_SHOWRESULTS = array (CHOICEGROUP_SHOWRESULTS_NOT => get_string('publishnot', 'choicegroup'), + CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER => get_string('publishafteranswer', 'choicegroup'), + CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE => get_string('publishafterclose', 'choicegroup'), + CHOICEGROUP_SHOWRESULTS_ALWAYS => get_string('publishalways', 'choicegroup')); + +/** @global array $CHOICEGROUP_DISPLAY */ +global $CHOICEGROUP_DISPLAY; +$CHOICEGROUP_DISPLAY = array (CHOICEGROUP_DISPLAY_HORIZONTAL => get_string('displayhorizontal', 'choicegroup'), + CHOICEGROUP_DISPLAY_VERTICAL => get_string('displayvertical','choicegroup')); + +require_once($CFG->dirroot.'/group/lib.php'); + +/// Standard functions ///////////////////////////////////////////////////////// + +/** + * @global object + * @param object $course + * @param object $user + * @param object $mod + * @param object $choicegroup + * @return object|null + */ +function choicegroup_user_outline($course, $user, $mod, $choicegroup) { + if ($groupmembership = choicegroup_get_user_answer($choicegroup, $user)) { // if user has answered + $result = new stdClass(); + $result->info = "'".format_string($groupmembership->name)."'"; + $result->time = $groupmembership->timeuseradded; + return $result; + } + return null; +} + +/** + * + */ +function choicegroup_get_user_answer($choicegroup, $user, $returnArray = false, $refresh = false) { + global $DB, $choicegroup_groups; + + static $user_answers = array(); + + if (is_numeric($user)) { + $userid = $user; + } + else { + $userid = $user->id; + } + + if (!$refresh and isset($user_answers[$userid])) { + if ($returnArray === true) { + return $user_answers[$userid]; + } else { + return $user_answers[$userid][0]; + } + } else { + $user_answers = array(); + } + if(!is_array($choicegroup_groups) || !count($choicegroup_groups)){ + $choicegroup_groups = choicegroup_get_groups($choicegroup); + + } + + $groupids = array(); + foreach ($choicegroup_groups as $group) { + if (is_numeric($group->id)) { + $groupids[] = $group->id; + } + } + if ($groupids) { + $params1 = array($userid); + list($insql, $params2) = $DB->get_in_or_equal($groupids); + $params = array_merge($params1, $params2); + $groupmemberships = $DB->get_records_sql('SELECT * FROM {groups_members} WHERE userid = ? AND groupid '.$insql, $params); + $groups = array(); + foreach ($groupmemberships as $groupmembership) { + $group = $choicegroup_groups[$groupmembership->groupid]; + $group->timeuseradded = $groupmembership->timeadded; + $groups[] = $group; + } + if (count($groups) > 0) { + $user_answers[$userid] = $groups; + if ($returnArray === true) { + return $groups; + } else { + return $groups[0]; + } + } + } + return false; + +} + +/** + * @global object + * @param object $course + * @param object $user + * @param object $mod + * @param object $choicegroup + * @return string|void + */ +function choicegroup_user_complete($course, $user, $mod, $choicegroup) { + if ($groupmembership = choicegroup_get_user_answer($choicegroup, $user)) { // if user has answered + $result = new stdClass(); + $result->info = "'".format_string($groupmembership->name)."'"; + $result->time = $groupmembership->timeuseradded; + echo get_string("answered", "choicegroup").": $result->info. ".get_string("updated", '', userdate($result->time)); + } else { + print_string("notanswered", "choicegroup"); + } +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will create a new instance and return the id number + * of the new instance. + * + * @global object + * @param object $choicegroup + * @return int + */ +function choicegroup_add_instance($choicegroup) { + global $DB; + + $choicegroup->timemodified = time(); + + if (empty($choicegroup->timerestrict)) { + $choicegroup->timeopen = 0; + $choicegroup->timeclose = 0; + } + + //insert answers + $choicegroup->id = $DB->insert_record("choicegroup", $choicegroup); + + // deserialize the selected groups + + $groupIDs = explode(';', $choicegroup->serializedselectedgroups); + $groupIDs = array_diff( $groupIDs, array( '' ) ); + + foreach ($groupIDs as $groupID) { + $groupID = trim($groupID); + if (isset($groupID) && $groupID != '') { + $option = new stdClass(); + $option->groupid = $groupID; + $option->choicegroupid = $choicegroup->id; + $property = 'group_' . $groupID . '_limit'; + if (isset($choicegroup->$property)) { + $option->maxanswers = $choicegroup->$property; + } + $option->timemodified = time(); + $DB->insert_record("choicegroup_options", $option); + } + } + + if (class_exists('\core_completion\api')) { + $completiontimeexpected = !empty($choicegroup->completionexpected) ? $choicegroup->completionexpected : null; + \core_completion\api::update_completion_date_event($choicegroup->coursemodule, 'choicegroup', $choicegroup->id, $completiontimeexpected); + } + + + return $choicegroup->id; +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will update an existing instance with new data. + * + * @global object + * @param object $choicegroup + * @return bool + */ +function choicegroup_update_instance($choicegroup) { + global $DB; + + $choicegroup->id = $choicegroup->instance; + $choicegroup->timemodified = time(); + + + if (empty($choicegroup->timerestrict)) { + $choicegroup->timeopen = 0; + $choicegroup->timeclose = 0; + } + + if (empty($choicegroup->multipleenrollmentspossible)) { + $choicegroup->multipleenrollmentspossible = 0; + } + + + // deserialize the selected groups + + $groupIDs = explode(';', $choicegroup->serializedselectedgroups); + $groupIDs = array_diff( $groupIDs, array( '' ) ); + + // prepare pre-existing selected groups from database + + if (!($preExistingGroups = $DB->get_records("choicegroup_options", array("choicegroupid" => $choicegroup->id), "id"))) { + return false; + } + + // walk through form-selected groups + foreach ($groupIDs as $groupID) { + $groupID = trim($groupID); + if (isset($groupID) && $groupID != '') { + $option = new stdClass(); + $option->groupid = $groupID; + $option->choicegroupid = $choicegroup->id; + $property = 'group_' . $groupID . '_limit'; + if (isset($choicegroup->$property)) { + $option->maxanswers = $choicegroup->$property; + } + $option->timemodified = time(); + // Find out if this selection already exists + foreach ($preExistingGroups as $key => $preExistingGroup) { + if ($option->groupid == $preExistingGroup->groupid) { + // match found, so instead of creating a new record we should merely update a pre-existing record + $option->id = $preExistingGroup->id; + $DB->update_record("choicegroup_options", $option); + // remove the element from the array to not deal with it later + unset($preExistingGroups[$key]); + continue 2; // continue the big loop + } + } + $DB->insert_record("choicegroup_options", $option); + } + + } + // remove all remaining pre-existing groups which did not appear in the form (and are thus assumed to have been deleted) + foreach ($preExistingGroups as $preExistingGroup) { + $DB->delete_records("choicegroup_options", array("id"=>$preExistingGroup->id)); + } + + if (class_exists('\core_completion\api')) { + $completiontimeexpected = !empty($choicegroup->completionexpected) ? $choicegroup->completionexpected : null; + \core_completion\api::update_completion_date_event($choicegroup->coursemodule, 'choicegroup', $choicegroup->id, $completiontimeexpected); + } + + + return $DB->update_record('choicegroup', $choicegroup); + +} + +/** + * @global object + * @param object $choicegroup + * @param object $user + * @param object $coursemodule + * @param array $allresponses + * @return array + */ +function choicegroup_prepare_options($choicegroup, $user, $coursemodule, $allresponses) { + + $cdisplay = array('options'=>array()); + + $cdisplay['limitanswers'] = true; + $context = context_module::instance($coursemodule->id); + $answers = choicegroup_get_user_answer($choicegroup, $user, true, true); + + if (!isset($choicegroup->option)) { + $choicegroup->option = []; + } + foreach ($choicegroup->option as $optionid => $text) { + if (isset($text)) { //make sure there are no dud entries in the db with blank text values. + $option = new stdClass; + $option->attributes = new stdClass; + $option->attributes->value = $optionid; + $option->groupid = $text; + $option->maxanswers = $choicegroup->maxanswers[$optionid]; + $option->displaylayout = $choicegroup->display; + + if (isset($allresponses[$text])) { + $option->countanswers = count($allresponses[$text]); + } else { + $option->countanswers = 0; + } + if (is_array($answers)) { + foreach($answers as $answer) { + if ($answer && $text == $answer->id) { + $option->attributes->checked = true; + } + } + } + if ( $choicegroup->limitanswers && ($option->countanswers >= $option->maxanswers) && empty($option->attributes->checked)) { + $option->attributes->disabled = true; + } + $cdisplay['options'][] = $option; + } + } + + $cdisplay['hascapability'] = is_enrolled($context, null, 'mod/choicegroup:choose'); //only enrolled users are allowed to make a choicegroup + + if ($choicegroup->allowupdate && is_array($answers)) { + $cdisplay['allowupdate'] = true; + } + + return $cdisplay; +} + +/** + * @global object + * @param int $formanswer + * @param object $choicegroup + * @param int $userid + * @param object $course Course object + * @param object $cm + */ +function choicegroup_user_submit_response($formanswer, $choicegroup, $userid, $course, $cm) { + global $DB, $CFG; + require_once($CFG->libdir.'/completionlib.php'); + + $context = context_module::instance($cm->id); + $eventparams = array( + 'context' => $context, + 'objectid' => $choicegroup->id + ); + + $selected_option = $DB->get_record('choicegroup_options', array('id' => $formanswer)); + + $current = choicegroup_get_user_answer($choicegroup, $userid); + if ($current) { + $currentgroup = $DB->get_record('groups', array('id' => $current->id), 'id,name', MUST_EXIST); + } + $selectedgroup = $DB->get_record('groups', array('id' => $selected_option->groupid), 'id,name', MUST_EXIST); + + $countanswers=0; + groups_add_member($selected_option->groupid, $userid); + $groupmember_added = true; + if ($choicegroup->limitanswers) { + $groupmember = $DB->get_record('groups_members', array('groupid' => $selected_option->groupid, 'userid'=>$userid)); + $select_count = 'groupid='.$selected_option->groupid.' and id<='.$groupmember->id; + $countanswers = $DB->count_records_select('groups_members', $select_count); + $maxans = $choicegroup->maxanswers[$formanswer]; + if ($countanswers > $maxans) { + groups_remove_member($selected_option->groupid, $userid); + $groupmember_added = false; + } + } + if ($groupmember_added) { + if ($current) { + if (!($choicegroup->multipleenrollmentspossible == 1)) { + if ($selected_option->groupid != $current->id) { + if (groups_is_member($current->id, $userid)) { + groups_remove_member($current->id, $userid); +// $eventparams['groupname'] = $currentgroup->name; + $event = \mod_choicegroup\event\choice_removed::create($eventparams); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + } + } + } + } else { + // Update completion state + $completion = new completion_info($course); + if ($completion->is_enabled($cm) && $choicegroup->completionsubmit) { + $completion->update_state($cm, COMPLETION_COMPLETE); + } +// $eventparams['groupname'] = $selectedgroup->name; + $event = \mod_choicegroup\event\choice_updated::create($eventparams); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + } + } else { + if (!$current || !($current->id==$selected_option->groupid)) { //check to see if current choicegroup already selected - if not display error + print_error('choicegroupfull', 'choicegroup', $CFG->wwwroot.'/mod/choicegroup/view.php?id='.$cm->id); + } + } +} + +/** + * @param object $choicegroup + * @param array $allresponses + * @param object $cm + * @return void Output is echo'd + */ +function choicegroup_show_reportlink($choicegroup, $allresponses, $cm) { + $responsecount = 0; + $respondents = array(); + foreach($allresponses as $optionid => $userlist) { + if ($optionid) { + $responsecount += count($userlist); + if ($choicegroup->multipleenrollmentspossible) { + foreach ($userlist as $user) { + if (!in_array($user->id, $respondents)) { + $respondents[] = $user->id; + } + } + } + } + } + echo '<div class="reportlink"><a href="report.php?id='.$cm->id.'">'.get_string("viewallresponses", "choicegroup", $responsecount); + if ($choicegroup->multipleenrollmentspossible == 1) { + echo ' ' . get_string("byparticipants", "choicegroup", count($respondents)); + } + echo '</a></div>'; +} + +/** + * @global object + * @param object $choicegroup + * @param object $course + * @param object $coursemodule + * @param array $allresponses + + * * @param bool $allresponses + * @return object + */ +function prepare_choicegroup_show_results($choicegroup, $course, $cm, $allresponses, $forcepublish=false) { + global $CFG, $FULLSCRIPT, $PAGE, $OUTPUT; + + $display = clone($choicegroup); + $display->coursemoduleid = $cm->id; + $display->courseid = $course->id; +//debugging('<pre>'.print_r($choicegroup->option, true).'</pre>', DEBUG_DEVELOPER); +//debugging('<pre>'.print_r($allresponses, true).'</pre>', DEBUG_DEVELOPER); + + //overwrite options value; + $display->options = array(); + $totaluser = 0; + foreach ($choicegroup->option as $optionid => $groupid) { + $display->options[$optionid] = new stdClass; + $display->options[$optionid]->groupid = $groupid; + $display->options[$optionid]->maxanswer = $choicegroup->maxanswers[$optionid]; + + if (array_key_exists($groupid, $allresponses)) { + $display->options[$optionid]->user = $allresponses[$groupid]; + foreach ($display->options[$optionid]->user as $user){ + $user->grpsmemberid = array_search(array($groupid, $user->id), $choicegroup->grpmemberid); + } + $totaluser += count($allresponses[$groupid]); + } + } + if ($choicegroup->showunanswered) { + $display->options[0]->user = $allresponses[0]; + } + unset($display->option); + unset($display->maxanswers); + + $display->numberofuser = $totaluser; + $context = context_module::instance($cm->id); + $display->viewresponsecapability = has_capability('mod/choicegroup:readresponses', $context); + $display->deleterepsonsecapability = has_capability('mod/choicegroup:deleteresponses',$context); + $display->fullnamecapability = has_capability('moodle/site:viewfullnames', $context); + + if (empty($allresponses)) { + echo $OUTPUT->heading(get_string("nousersyet")); + return false; + } + + + $totalresponsecount = 0; + foreach ($allresponses as $optionid => $userlist) { + if ($choicegroup->showunanswered || $optionid) { + $totalresponsecount += count($userlist); + } + } + + $context = context_module::instance($cm->id); + + $hascapfullnames = has_capability('moodle/site:viewfullnames', $context); + + $viewresponses = has_capability('mod/choicegroup:readresponses', $context); + switch ($forcepublish) { + case CHOICEGROUP_PUBLISH_NAMES: + echo '<div id="tablecontainer">'; + if ($viewresponses) { + echo '<form id="attemptsform" method="post" action="'.$FULLSCRIPT.'" onsubmit="var menu = document.getElementById(\'menuaction\'); return (menu.options[menu.selectedIndex].value == \'delete\' ? \''.addslashes_js(get_string('deleteattemptcheck','quiz')).'\' : true);">'; + echo '<div>'; + echo '<input type="hidden" name="id" value="'.$cm->id.'" />'; + echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />'; + echo '<input type="hidden" name="mode" value="overview" />'; + } + + echo "<table cellpadding=\"5\" cellspacing=\"10\" class=\"results names\">"; + echo "<tr>"; + + $columncount = array(); // number of votes in each column + if ($choicegroup->showunanswered) { + $columncount[0] = 0; + echo "<th class=\"col0 header\" scope=\"col\">"; + print_string('notanswered', 'choicegroup'); + echo "</th>"; + } + $count = 1; + foreach ($choicegroup->option as $optionid => $optiontext) { + $columncount[$optionid] = 0; // init counters + echo "<th class=\"col$count header\" scope=\"col\">"; + echo format_string($optiontext); + echo "</th>"; + $count++; + } + echo "</tr><tr>"; + + if ($choicegroup->showunanswered) { + echo "<td class=\"col$count data\" >"; + // added empty row so that when the next iteration is empty, + // we do not get <table></table> error from w3c validator + // MDL-7861 + echo "<table class=\"choicegroupresponse\"><tr><td></td></tr>"; + if (!empty($allresponses[0])) { + foreach ($allresponses[0] as $user) { + echo "<tr>"; + echo "<td class=\"picture\">"; + echo $OUTPUT->user_picture($user, array('courseid'=>$course->id)); + echo "</td><td class=\"fullname\">"; + echo "<a href=\"$CFG->wwwroot/user/view.php?id=$user->id&course=$course->id\">"; + echo fullname($user, $hascapfullnames); + echo "</a>"; + echo "</td></tr>"; + } + } + echo "</table></td>"; + } + $count = 1; + foreach ($choicegroup->option as $optionid => $optiontext) { + echo '<td class="col'.$count.' data" >'; + + // added empty row so that when the next iteration is empty, + // we do not get <table></table> error from w3c validator + // MDL-7861 + echo '<table class="choicegroupresponse"><tr><td></td></tr>'; + if (isset($allresponses[$optionid])) { + foreach ($allresponses[$optionid] as $user) { + $columncount[$optionid] += 1; + echo '<tr><td class="attemptcell">'; + if ($viewresponses and has_capability('mod/choicegroup:deleteresponses',$context)) { + echo '<input type="checkbox" name="userid[]" value="'. $user->id. '" />'; + } + echo '</td><td class="picture">'; + echo $OUTPUT->user_picture($user, array('courseid'=>$course->id)); + echo '</td><td class="fullname">'; + echo "<a href=\"$CFG->wwwroot/user/view.php?id=$user->id&course=$course->id\">"; + echo fullname($user, $hascapfullnames); + echo '</a>'; + echo '</td></tr>'; + } + } + $count++; + echo '</table></td>'; + } + echo "</tr><tr>"; + $count = 1; + + if ($choicegroup->showunanswered) { + echo "<td></td>"; + } + + foreach ($choicegroup->option as $optionid => $optiontext) { + echo "<td align=\"center\" class=\"col$count count\">"; + if ($choicegroup->limitanswers) { + echo get_string("taken", "choicegroup").":"; + echo $columncount[$optionid]; + echo "<br/>"; + echo get_string("limit", "choicegroup").":"; + echo $choicegroup->maxanswers[$optionid]; + } else { + if (isset($columncount[$optionid])) { + echo $columncount[$optionid]; + } + } + echo "</td>"; + $count++; + } + echo "</tr>"; + + /// Print "Select all" etc. + if ($viewresponses and has_capability('mod/choicegroup:deleteresponses',$context)) { + echo '<tr><td></td><td>'; + echo '<a href="javascript:select_all_in(\'DIV\',null,\'tablecontainer\');">'.get_string('selectall').'</a> / '; + echo '<a href="javascript:deselect_all_in(\'DIV\',null,\'tablecontainer\');">'.get_string('deselectall').'</a> '; + echo ' '; + echo html_writer::label(get_string('withselected', 'choicegroup'), 'menuaction'); + echo html_writer::select(array('delete' => get_string('delete')), 'action', '', array(''=>get_string('withselectedusers')), array('id'=>'menuaction')); + $PAGE->requires->js_init_call('M.util.init_select_autosubmit', array('attemptsform', 'menuaction', '')); + echo '<noscript id="noscriptmenuaction" style="display:inline">'; + echo '<div>'; + echo '<input type="submit" value="'.get_string('go').'" /></div></noscript>'; + echo '</td><td></td></tr>'; + } + + echo "</table></div>"; + if ($viewresponses) { + echo "</form></div>"; + } + break; + } + return $display; +} + +/** + * @global object + * @param array $grpsmemberids + * @param object $choicegroup Choice main table row + * @param object $cm Course-module object + * @param object $course Course object + * @return bool + */ +function choicegroup_delete_responses($grpsmemberids, $choicegroup, $cm, $course) { + global $CFG, $DB; + require_once($CFG->libdir.'/completionlib.php'); + + if(!is_array($grpsmemberids) || empty($grpsmemberids)) { + return false; + } + + foreach($grpsmemberids as $num => $grpsmemberid) { + if(empty($grpsmemberid)) { + unset($grpsmemberids[$num]); + } + } + + $context = context_module::instance($cm->id); + $completion = new completion_info($course); + $eventparams = array( + 'context' => $context, + 'objectid' => $choicegroup->id + ); + + foreach($grpsmemberids as $grpsmemberid) { + $groupsmember = $DB->get_record('groups_members', array('id'=>$grpsmemberid), '*', MUST_EXIST); + $userid = $groupsmember->userid; + $groupid = $groupsmember->groupid; + $currentgroup = $DB->get_record('groups', array('id' => $groupid), 'id,name', MUST_EXIST); + if (groups_is_member($groupid, $userid)) { + groups_remove_member($groupid, $userid); + $event = \mod_choicegroup\event\choice_removed::create($eventparams); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + } + // Update completion state + $current = choicegroup_get_user_answer($choicegroup, $userid, false, true); + if ($current === false && $completion->is_enabled($cm) && $choicegroup->completionsubmit) { + $completion->update_state($cm, COMPLETION_INCOMPLETE, $userid); + } + } + return true; +} + + +/** + * Given an ID of an instance of this module, + * this function will permanently delete the instance + * and any data that depends on it. + * + * @global object + * @param int $id + * @return bool + */ +function choicegroup_delete_instance($id) { + global $DB; + + if (! $choicegroup = $DB->get_record("choicegroup", array("id"=>"$id"))) { + return false; + } + + $result = true; + + if (! $DB->delete_records("choicegroup_options", array("choicegroupid"=>"$choicegroup->id"))) { + $result = false; + } + + if (! $DB->delete_records("choicegroup", array("id"=>"$choicegroup->id"))) { + $result = false; + } + + return $result; +} + +/** + * Returns text string which is the answer that matches the id + * + * @global object + * @param object $choicegroup + * @param int $id + * @return string + */ +function choicegroup_get_option_text($choicegroup, $id) { + global $DB; + + if ($result = $DB->get_record('groups', array('id' => $id))) { + return $result->name; + } else { + return get_string("notanswered", "choicegroup"); + } +} + +/* + * Returns DB records of groups used by the choicegroup activity + * + * @global object + * @param object $choicegroup + * @return array + */ +function choicegroup_get_groups($choicegroup) { + global $DB; + + static $groups = array(); + + if (count($groups)) { + return $groups; + } + + if (is_numeric($choicegroup)) { + $choicegroupid = $choicegroup; + } + else { + $choicegroupid = $choicegroup->id; + } + + $groups = array(); + $options = $DB->get_records('choicegroup_options', array('choicegroupid' => $choicegroupid)); + foreach ($options as $option) { + if ($group = $DB->get_record('groups', array('id' => $option->groupid))) + $groups[$group->id] = $group; + } + return $groups; +} + +/** + * Gets a full choicegroup record + * + * @global object + * @param int $choicegroupid + * @return object|bool The choicegroup or false + */ +function choicegroup_get_choicegroup($choicegroupid) { + global $DB; + + if ($choicegroup = $DB->get_record("choicegroup", array("id" => $choicegroupid))) { + $sortcolumn = choicegroup_get_sort_column($choicegroup); + + $params = array( + 'choicegroupid' => $choicegroupid + ); + + $grpfilter = ''; + if (($groupid = optional_param('group', 0, PARAM_INT)) != 0) { + $params['groupid'] = $groupid; + $grpfilter = "AND grp_o.groupid = :groupid"; + } + + $sql = "SELECT grp_m.id grpmemberid, grp_m.userid, grp_o.id, grp_o.groupid, grp_o.maxanswers + FROM {groups} grp + INNER JOIN {choicegroup_options} grp_o on grp.id = grp_o.groupid + LEFT JOIN {groups_members} grp_m on grp_m.groupid = grp_o.groupid + WHERE grp_o.choicegroupid = :choicegroupid $grpfilter + ORDER BY $sortcolumn ASC"; + + $rs = $DB->get_recordset_sql($sql, $params); + + foreach ($rs as $option) { + $choicegroup->option[$option->id] = $option->groupid; + $choicegroup->grpmemberid[$option->grpmemberid] = array($option->groupid, $option->userid); + $choicegroup->maxanswers[$option->id] = $option->maxanswers; + } + + $rs->close(); + + return $choicegroup; + } + return false; +} + +function choicegroup_get_sort_column($choicegroup) { + if ($choicegroup->sortgroupsby == CHOICEGROUP_SORTGROUPS_SYSTEMDEFAULT) { + $sortcolumn = get_config('choicegroup', 'sortgroupsby'); + } else { + $sortcolumn = $choicegroup->sortgroupsby; + } + + switch ($sortcolumn) { + case CHOICEGROUP_SORTGROUPS_CREATEDATE: + return 'timecreated'; + case CHOICEGROUP_SORTGROUPS_NAME: + return 'name'; + default: + return 'timecreated'; + } +} + +/** + * @return array + */ +function choicegroup_get_view_actions() { + return array('view','view all','report'); +} + +/** + * @return array + */ +function choicegroup_get_post_actions() { + return array('choose','choose again'); +} + + +/** + * Implementation of the function for printing the form elements that control + * whether the course reset functionality affects the choicegroup. + * + * @param object $mform form passed by reference + */ +function choicegroup_reset_course_form_definition(&$mform) { + $mform->addElement('header', 'choicegroupheader', get_string('modulenameplural', 'choicegroup')); + $mform->addElement('advcheckbox', 'reset_choicegroup', get_string('removeresponses','choicegroup')); +} + +/** + * Course reset form defaults. + * + * @return array + */ +function choicegroup_reset_course_form_defaults($course) { + return array('reset_choicegroup'=>1); +} + +/** + * @global object + * @global object + * @global object + * @uses CONTEXT_MODULE + * @param object $choicegroup + * @param object $cm + * @return array + */ +function choicegroup_get_response_data($choicegroup, $cm) { + // Initialise the returned array, which is a matrix: $allresponses[responseid][userid] = responseobject. + static $allresponses = array(); + + if (count($allresponses)) { + return $allresponses; + } + + // First get all the users who have access here. + // To start with we assume they are all "unanswered" then move them later. + $ctx = \context_module::instance($cm->id); + $users = get_enrolled_users($ctx, 'mod/choicegroup:choose', 0, user_picture::fields('u', array('idnumber')), 'u.lastname ASC,u.firstname ASC'); + if ($users) { + $modinfo = get_fast_modinfo($cm->course); + $cminfo = $modinfo->get_cm($cm->id); + $availability = new \core_availability\info_module($cminfo); + $users = $availability->filter_user_list($users); + } + + $allresponses[0] = $users; + + $responses = choicegroup_get_responses($choicegroup, $ctx); + foreach ($responses as $response){ + if (isset($users[$response->userid])) { + $allresponses[$response->groupid][$response->userid] = clone $users[$response->userid]; + $allresponses[$response->groupid][$response->userid]->timemodified = $response->timeadded; + + unset($allresponses[0][$response->userid]); + } + } + return $allresponses; +} + +/* Return an array with the options selected of users of the $choicegroup + * + * @param object $choicegroup choicegroup record + * @param object $cm course module object + * @return array of selected options by all users +*/ +function choicegroup_get_responses($choicegroup, $cm){ + + global $DB; + + if (is_numeric($choicegroup)) { + $choicegroupid = $choicegroup; + } else { + $choicegroupid = $choicegroup->id; + } + + $params1 = array('choicegroupid'=>$choicegroupid); + list($esql, $params2) = get_enrolled_sql($cm, 'mod/choicegroup:choose', 0); + $params = array_merge($params1, $params2); + + $sql = 'SELECT gm.* FROM {user} u JOIN ('.$esql.') je ON je.id = u.id + JOIN {groups_members} gm ON gm.userid = u.id AND groupid IN ( + SELECT groupid FROM {choicegroup_options} WHERE choicegroupid=:choicegroupid) + WHERE u.deleted = 0 ORDER BY u.lastname ASC,u.firstname ASC'; + + return $DB->get_records_sql($sql, $params); +} + +/** + * Returns all other caps used in module + * + * @return array + */ +function choicegroup_get_extra_capabilities() { + return array('moodle/site:accessallgroups'); +} + +/** + * @uses FEATURE_GROUPS + * @uses FEATURE_GROUPINGS + * @uses FEATURE_GROUPMEMBERSONLY + * @uses FEATURE_MOD_INTRO + * @uses FEATURE_COMPLETION_TRACKS_VIEWS + * @uses FEATURE_GRADE_HAS_GRADE + * @uses FEATURE_GRADE_OUTCOMES + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed True if module supports feature, null if doesn't know + */ +function choicegroup_supports($feature) { + switch($feature) { + case FEATURE_GROUPS: return true; + case FEATURE_GROUPINGS: return true; + case FEATURE_GROUPMEMBERSONLY: return true; + case FEATURE_MOD_INTRO: return true; + case FEATURE_COMPLETION_TRACKS_VIEWS: return true; + case FEATURE_COMPLETION_HAS_RULES: return true; + case FEATURE_GRADE_HAS_GRADE: return false; + case FEATURE_GRADE_OUTCOMES: return false; + case FEATURE_BACKUP_MOODLE2: return true; + case FEATURE_SHOW_DESCRIPTION: return true; + + default: return null; + } +} + +/** + * Adds module specific settings to the settings block + * + * @param settings_navigation $settings The settings navigation object + * @param navigation_node $choicegroupnode The node to add module settings to + */ +function choicegroup_extend_settings_navigation(settings_navigation $settings, navigation_node $choicegroupnode) { + global $PAGE; + + if (has_capability('mod/choicegroup:readresponses', $PAGE->cm->context)) { + + $groupmode = groups_get_activity_groupmode($PAGE->cm); + if ($groupmode) { + groups_get_activity_group($PAGE->cm, true); + } + if (!$choicegroup = choicegroup_get_choicegroup($PAGE->cm->instance)) { + print_error('invalidcoursemodule'); + return false; + } + $allresponses = choicegroup_get_response_data($choicegroup, $PAGE->cm, $groupmode); // Big function, approx 6 SQL calls per user + + $responsecount = 0; + $respondents = array(); + foreach($allresponses as $optionid => $userlist) { + if ($optionid) { + $responsecount += count($userlist); + if ($choicegroup->multipleenrollmentspossible) { + foreach ($userlist as $user) { + if (!in_array($user->id, $respondents)) { + $respondents[] = $user->id; + } + } + } + } + } + $viewallresponsestext = get_string("viewallresponses", "choicegroup", $responsecount); + if ($choicegroup->multipleenrollmentspossible == 1) { + $viewallresponsestext .= ' ' . get_string("byparticipants", "choicegroup", count($respondents)); + } + $choicegroupnode->add($viewallresponsestext, new moodle_url('/mod/choicegroup/report.php', array('id'=>$PAGE->cm->id))); + } +} + +/** + * Obtains the automatic completion state for this choicegroup based on any conditions + * in forum settings. + * + * @param object $course Course + * @param object $cm Course-module + * @param int $userid User ID + * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) + * @return bool True if completed, false if not, $type if conditions not set. + */ +function choicegroup_get_completion_state($course, $cm, $userid, $type) { + global $DB; + + // Get choicegroup details + $choicegroup = $DB->get_record('choicegroup', array('id'=>$cm->instance), '*', MUST_EXIST); + + // If completion option is enabled, evaluate it and return true/false + if($choicegroup->completionsubmit) { + $useranswer = choicegroup_get_user_answer($choicegroup, $userid); + return $useranswer !== false; + } else { + // Completion option is not enabled so just return $type + return $type; + } +} + + +/** + * Return a list of page types + * @param string $pagetype current page type + * @param stdClass $parentcontext Block's parent context + * @param stdClass $currentcontext Current context of block + */ +function choicegroup_page_type_list($pagetype, $parentcontext, $currentcontext) { + $module_pagetype = array('mod-choicegroup-*'=>get_string('page-mod-choicegroup-x', 'choice')); + return $module_pagetype; +} + + +function choicegroup_get_sort_options() { + return array ( + CHOICEGROUP_SORTGROUPS_CREATEDATE => get_string('createdate', 'choicegroup'), + CHOICEGROUP_SORTGROUPS_NAME => get_string('name', 'choicegroup') + ); +} + + +/** + * This function receives a calendar event and returns the action associated with it, or null if there is none. + * + * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event + * is not displayed on the block. + * + * @param calendar_event $event + * @param \core_calendar\action_factory $factory + * @return \core_calendar\local\event\entities\action_interface|null + */ +function mod_choicegroup_core_calendar_provide_event_action(calendar_event $event, + \core_calendar\action_factory $factory) { + $cm = get_fast_modinfo($event->courseid)->instances['choicegroup'][$event->instance]; + + $completion = new \completion_info($cm->get_course()); + + $completiondata = $completion->get_data($cm, false); + + if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { + return null; + } + + return $factory->create_instance( + get_string('view'), + new \moodle_url('/mod/choicegroup/view.php', ['id' => $cm->id]), + 1, + true + ); +} + diff --git a/mod/choicegroup/mobile/js/courseview.js b/mod/choicegroup/mobile/js/courseview.js new file mode 100644 index 0000000..b87fddc --- /dev/null +++ b/mod/choicegroup/mobile/js/courseview.js @@ -0,0 +1,240 @@ +// 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/>. + +/** + * This file is part of the Moodle apps support for the choicegroup plugin. + * Defines the function to be used from the mobile course view template. + * + * @copyright 2019 Dani Palou <dpalou@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +var that = this; +var allowOffline = this.CoreConfigConstants.versioncode > 3800; // In 3.8.0 and older plugins couldn't add DB schemas. +var multipleEnrol = this.CONTENT_OTHERDATA.multipleenrollmentspossible; + +if (Array.isArray(this.CONTENT_OTHERDATA.data) && this.CONTENT_OTHERDATA.data.length == 0) { + // When there are no responses we receive an empty array instead of an empty object. Fix it. + this.CONTENT_OTHERDATA.data = {}; +} + +var originalData = this.CoreUtilsProvider.clone(this.CONTENT_OTHERDATA.data); + +/** + * Send responses to the site. + */ +this.submitResponses = function() { + var promise; + + if (!that.CONTENT_OTHERDATA.allowupdate) { + // Ask the user to confirm. + that.CoreDomUtilsProvider.showConfirm(that.TranslateService.instant('core.areyousure')); + } else { + // No need to confirm. + promise = Promise.resolve(); + } + + promise.then(function() { + // Submit the responses now. + var modal = that.CoreDomUtilsProvider.showModalLoading('core.sending', true); + var data = that.CoreUtilsProvider.objectToArrayOfObjects(that.CONTENT_OTHERDATA.data, 'name', 'value'); + + if (multipleEnrol) { + // In multiple enrol, the WS expects to receive 'true' as a string instead of 1 or 0. + data.forEach(function(entry) { + entry.value = String(entry.value); + }); + } + + that.choiceGroupProvider.submitResponses(that.module.instance, that.module.name, that.courseId, that.module.id, data, + allowOffline).then(function(online) { + + // Responses have been sent to server or stored to be sent later. + that.CoreDomUtilsProvider.showToast(that.TranslateService.instant('plugin.mod_choicegroup.choicegroupsaved')); + + if (online) { + // Check completion since it could be configured to complete once the user answers the choice. + that.CoreCourseProvider.checkModuleCompletion(that.courseId, that.module.completiondata); + + // Data has been sent, refresh the content. + return that.refreshContent(true); + } else { + // Data stored in offline. + return that.loadOfflineData(); + } + + }).catch((message) => { + that.CoreDomUtilsProvider.showErrorModalDefault(message, 'Error submitting responses.', true); + }).finally(() => { + modal.dismiss(); + }); + }).catch(() => { + // User cancelled, ignore. + }); +}; + +/** + * Delete the responses. Only if multiple enrol is not allowed. + */ +this.deleteResponses = function() { + var modal = that.CoreDomUtilsProvider.showModalLoading('core.sending', true); + + that.choiceGroupProvider.deleteResponses(that.module.instance, that.module.name, that.courseId, that.module.id, allowOffline) + .then(function(online) { + + // Responses have been sent to server or stored to be sent later. + that.CoreDomUtilsProvider.showToast(that.TranslateService.instant('plugin.mod_choicegroup.choicegroupsaved')); + + if (online) { + // Data has been sent, refresh the content. + return that.refreshContent(true); + } else { + // Data stored in offline. + return that.loadOfflineData(); + } + + }).catch((message) => { + that.CoreDomUtilsProvider.showErrorModalDefault(message, 'Error deleting responses.', true); + }).finally(() => { + modal.dismiss(); + }); +}; + +/** + * Check if the activity has offline data to be sent. + * + * @return Promise resolved when done. + */ +this.loadOfflineData = function() { + // Get the offline response if it exists. + return that.choiceGroupOffline.getResponse(that.module.instance).then(function(response) { + that.hasOffline = true; + + if (response.deleting) { + // Uncheck selected option. Delete is only possible if there is no multiple enrolment. + delete that.CONTENT_OTHERDATA.data.responses; + that.showDelete = false; + } else { + // Load the offline options into the model. + that.CONTENT_OTHERDATA.data = {}; + + response.data.forEach(function(entry) { + that.CONTENT_OTHERDATA.data[entry.name] = entry.value; + }); + + that.showDelete = !multipleEnrol; // Show delete if there is offline data and is not multiple enrol. + } + }).catch(function() { + // Offline data not found. Use the original data. + that.hasOffline = false; + that.showDelete = that.CONTENT_OTHERDATA.answergiven; + that.CONTENT_OTHERDATA.data = that.CoreUtilsProvider.clone(originalData); + }); +} + +/** + * Tries to synchronize the activity. + * + * @param showErrors If show errors to the user of hide them. + * @param done Function to call when done. + * @return Promise resolved with true if sync succeed, or false if failed. + */ +this.synchronize = function(showErrors, done) { + that.refreshIcon = 'spinner'; + that.syncIcon = 'spinner'; + + // Try to synchronize the group choice. + return that.choiceGroupSync.syncChoiceGroup(that.module.instance).then(function(result) { + if (result.warnings && result.warnings.length) { + that.CoreDomUtilsProvider.showErrorModal(result.warnings[0]); + } + + return result.updated; + }).catch(function(error) { + if (showErrors) { + that.CoreDomUtilsProvider.showErrorModalDefault(error, 'core.errorsync', true); + } + + return false; + }).then(function(updated) { + if (updated) { + // Data has been sent, fetch the content (WS data has already been updated in the sync process). + return that.fetchContent(false); + } + + // Check if the group choice has offline data. + return that.loadOfflineData(); + }).finally(function() { + done && done(); + that.refreshIcon = 'refresh'; + that.syncIcon = 'sync'; + }); +}; + +/** + * Refresh data. + * + * @param done Function to call when done. + * @return Promise resolved when done. + */ +this.doRefresh = function(done) { + that.refreshIcon = 'spinner'; + that.syncIcon = 'spinner'; + + return that.refreshContent(false).finally(function() { + done && done(); + that.refreshIcon = 'refresh'; + that.syncIcon = 'sync'; + }); +}; + +this.moduleName = this.TranslateService.instant('plugin.mod_choicegroup.modulename'); +this.isOnline = this.CoreAppProvider.isOnline(); + +// Refresh online status when changes. +var onlineObserver = this.Network.onchange().subscribe(function() { + that.isOnline = that.CoreAppProvider.isOnline(); +}); + +var syncObserver; + +if (allowOffline) { + // Try to synchronize the choice. + this.synchronize(false).finally(function() { + that.loaded = true; + }); + + // Update the view if the group choice is synchronized automatically. + syncObserver = this.CoreEventsProvider.on(this.choiceGroupSync.AUTO_SYNCED, function(data) { + if (data.choiceGroupId == that.module.instance) { + // This group choice has been synchronized, fetch the content (WS data has already been updated in the sync process). + return that.fetchContent(false); + } + }, this.CoreSitesProvider.getCurrentSiteId()); +} else { + // No offline allowed, just display the data. + this.loaded = true; + that.refreshIcon = 'refresh'; + that.hasOffline = false; + that.showDelete = that.CONTENT_OTHERDATA.answergiven; +} + +/** + * Component being destroyed. + */ +this.ngOnDestroy = function() { + onlineObserver && onlineObserver.unsubscribe(); + syncObserver && syncObserver.off(); +}; diff --git a/mod/choicegroup/mobile/js/init.js b/mod/choicegroup/mobile/js/init.js new file mode 100644 index 0000000..30875bc --- /dev/null +++ b/mod/choicegroup/mobile/js/init.js @@ -0,0 +1,600 @@ +// 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/>. + +/** + * This file is part of the Moodle apps support for the choicegroup plugin. + * Defines some "providers" in the app init process so they can be used by all group choices. + * + * @copyright 2019 Dani Palou <dpalou@moodle.com> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +var that = this; + +/** + * Offline provider. + */ + +var CHOICEGROUP_TABLE = 'addon_mod_choicegroup_responses'; + +// Define the database tables. +var siteSchema = { + name: 'AddonModChoiceGroupOfflineProvider', + version: 1, + onlyCurrentSite: true, + tables: [ + { + name: CHOICEGROUP_TABLE, + columns: [ + { + name: 'choicegroupid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'name', + type: 'TEXT' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'cmid', + type: 'INTEGER' + }, + { + name: 'data', + type: 'TEXT' + }, + { + name: 'deleting', + type: 'INTEGER' + }, + { + name: 'timecreated', + type: 'INTEGER' + } + ] + } + ] +}; + +/** + * Class to handle offline group choices. + */ +function AddonModChoiceGroupOfflineProvider() { + // Register the schema so the tables are created. + that.CoreSitesProvider.registerSiteSchema(siteSchema); +} + +/** + * Delete a response stored in DB. + * + * @param id Group choice ID to remove. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ +AddonModChoiceGroupOfflineProvider.prototype.deleteResponse = function(id, siteId) { + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + + return site.getDb().deleteRecords(CHOICEGROUP_TABLE, {choicegroupid: id}); + }); +}; + +/** + * Get all offline responses. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with responses. + */ +AddonModChoiceGroupOfflineProvider.prototype.getResponses = function(siteId) { + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + return site.getDb().getRecords(CHOICEGROUP_TABLE).then(function(records) { + // Parse the data of each record. + records.forEach(function(record) { + record.data = that.CoreTextUtilsProvider.parseJSON(record.data, []); + }); + + return records; + }); + }); +}; + +/** + * Check if there are offline responses to send. + * + * @param id Group choice ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: true if has offline answers, false otherwise. + */ +AddonModChoiceGroupOfflineProvider.prototype.hasResponse = function(id, siteId) { + return this.getResponse(id, siteId).then(function(response) { + return !!response.choicegroupid; + }).catch(function() { + // No offline data found, return false. + return false; + }); +}; + +/** + * Get an offline response. + * + * @param id Group choice ID to get. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the stored data. + */ +AddonModChoiceGroupOfflineProvider.prototype.getResponse = function(id, siteId) { + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + + return site.getDb().getRecord(CHOICEGROUP_TABLE, {choicegroupid: id}).then(function(record) { + // Parse the data. + record.data = that.CoreTextUtilsProvider.parseJSON(record.data, []); + + return record; + }); + }); +}; + +/** + * Store a response to a group choice. + * + * @param id Group choice ID. + * @param name Group choice name. + * @param courseId Course ID the group choice belongs to. + * @param cmId Course module ID. + * @param data List of selected options. + * @param deleting If true, the user is deleting responses, if false, submitting. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is successfully stored. + */ +AddonModChoiceGroupOfflineProvider.prototype.saveResponses = function(id, name, courseId, cmId, data, deleting, siteId) { + data = data || []; + + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + var entry = { + choicegroupid: id, + name: name, + courseid: courseId, + cmid: cmId, + data: JSON.stringify(data), + deleting: deleting ? 1 : 0, + timecreated: Date.now() + }; + + return site.getDb().insertRecord(CHOICEGROUP_TABLE, entry); + }); +}; + +var choiceGroupOffline = new AddonModChoiceGroupOfflineProvider(); + +/** + * Group choice provider. + */ + +/** + * Class to handle group choices. + */ +function AddonModChoiceGroupProvider() { } + +/** + * Delete responses from a group choice. + * + * @param id Group choice ID to remove. + * @param name The group choice name. + * @param courseId Course ID the group choice belongs to. + * @param cmId Course module ID. + * @param allowOffline Whether to allow storing the data in offline. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: true if deleted in server, false if stored in offline. Rejected if failure. + */ +AddonModChoiceGroupProvider.prototype.deleteResponses = function(id, name, courseId, cmId, allowOffline, siteId) { + siteId = siteId || that.CoreSitesProvider.getCurrentSiteId(); + + var self = this; + + // Convenience function to store the delete to be synchronized later. + var storeOffline = function() { + return choiceGroupOffline.saveResponses(id, name, courseId, cmId, undefined, true, siteId).then(function() { + return false; + }); + }; + + if (!that.CoreAppProvider.isOnline() && allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + // If there's already some data to be sent to the server, discard it first. + return choiceGroupOffline.deleteResponse(id, siteId).catch(function() { + // Nothing was stored already. + }).then(function() { + // Now try to delete the responses in the server. + return self.deleteResponsesOnline(id, siteId).then(function() { + return true; + }).catch(function(error) { + if (!allowOffline || that.CoreUtilsProvider.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + }); +}; + +/** + * Delete responses from a group choice. It will fail if offline or cannot connect. + * + * @param id Group choice ID to remove. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ +AddonModChoiceGroupProvider.prototype.deleteResponsesOnline = function(id, siteId) { + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + var params = { + choicegroupid: id + }; + + return site.write('mod_choicegroup_delete_choicegroup_responses', params).then(function(response) { + + if (!response || response.status === false) { + // Couldn't delete the responses. Reject the promise. + var error = response && response.warnings && response.warnings[0] ? + response.warnings[0] : that.CoreUtilsProvider.createFakeWSError(''); + + return Promise.reject(error); + } + }); + }); +}; + +/** + * Send the responses to a group choice. + * + * @param id Group choice ID to submit. + * @param name The group choice name. + * @param courseId Course ID the group choice belongs to. + * @param cmId Course module ID. + * @param data The responses to send. + * @param allowOffline Whether to allow storing the data in offline. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: true if responses sent to server, false if stored in offline. Rejected if failure. + */ +AddonModChoiceGroupProvider.prototype.submitResponses = function(id, name, courseId, cmId, data, allowOffline, siteId) { + siteId = siteId || that.CoreSitesProvider.getCurrentSiteId(); + + var self = this; + + // Convenience function to store the delete to be synchronized later. + var storeOffline = function() { + return choiceGroupOffline.saveResponses(id, name, courseId, cmId, data, false, siteId).then(function() { + return false; + }); + }; + + if (!that.CoreAppProvider.isOnline() && allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + // If there's already some data to be sent to the server, discard it first. + return choiceGroupOffline.deleteResponse(id, siteId).catch(function() { + // Nothing was stored already. + }).then(function() { + // Now try to delete the responses in the server. + return self.submitResponsesOnline(id, data, siteId).then(function() { + return true; + }).catch(function(error) { + if (!allowOffline || that.CoreUtilsProvider.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + }); +}; + +/** + * Send responses from a group choice to Moodle. It will fail if offline or cannot connect. + * + * @param id Group choice ID to submit. + * @param data The responses to send. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ +AddonModChoiceGroupProvider.prototype.submitResponsesOnline = function(id, data, siteId) { + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + var params = { + choicegroupid: id, + data: data + }; + + return site.write('mod_choicegroup_submit_choicegroup_response', params).then(function(response) { + + if (!response || response.status === false) { + // Couldn't delete the responses. Reject the promise. + var error = response && response.warnings && response.warnings[0] ? + response.warnings[0] : that.CoreUtilsProvider.createFakeWSError(''); + + return Promise.reject(error); + } + }); + }); +}; + +var choiceGroupProvider = new AddonModChoiceGroupProvider(); + +/** + * Group choice sync provider. + */ + +/** + * Class to handle group choice sync. + */ +function AddonModChoiceGroupSyncProvider() { + // Inherit from sync base provider. + that.CoreSyncBaseProvider.call(this, 'AddonModChoiceGroupSyncProvider', that.CoreLoggerProvider, that.CoreSitesProvider, + that.CoreAppProvider, that.CoreSyncProvider, that.CoreTextUtilsProvider, that.TranslateService, + that.CoreTimeUtilsProvider); + + this.AUTO_SYNCED = 'addon_mod_choicegroup_autom_synced'; +} + +AddonModChoiceGroupSyncProvider.prototype = Object.create(this.CoreSyncBaseProvider.prototype); +AddonModChoiceGroupSyncProvider.prototype.constructor = AddonModChoiceGroupSyncProvider; + +/** + * Try to synchronize all the group choices in a certain site or in all sites. + * + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ +AddonModChoiceGroupSyncProvider.prototype.syncAllChoiceGroups = function(force) { + return this.syncOnSites('group choices', this.syncAllChoiceGroupsFunc.bind(this), [force], + that.CoreSitesProvider.getCurrentSiteId()); +}; + +/** + * Sync all pending group choices on a site. + * + * @param siteId Site ID to sync. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ +AddonModChoiceGroupSyncProvider.prototype.syncAllChoiceGroupsFunc = function(siteId, force) { + var self = this; + + return choiceGroupOffline.getResponses(siteId).then(function(responses) { + // Sync all responses. + var promises = responses.map(function(response) { + var promise = force ? self.syncChoiceGroup(response.choicegroupid, siteId) : + self.syncChoiceGroupIfNeeded(response.choicegroupid, siteId); + + return promise.then(function(result) { + if (result && result.updated) { + // Sync successful, send event. + that.CoreEventsProvider.trigger(self.AUTO_SYNCED, { + choiceGroupId: response.choicegroupid, + warnings: result.warnings + }, siteId); + } + }); + }); + + return Promise.all(promises); + }); +}; + +/** + * Sync a group choice only if a certain time has passed since the last time. + * + * @param id Group choice ID to be synced. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the group choice is synced or it doesn't need to be synced. + */ +AddonModChoiceGroupSyncProvider.prototype.syncChoiceGroupIfNeeded = function(id, siteId) { + var self = this; + + return this.isSyncNeeded(id, siteId).then(function(needed) { + if (needed) { + return self.syncChoiceGroup(id, siteId); + } + }); +}; + +/** + * Synchronize a group choice. + * + * @param id Group choice ID to be synced. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ +AddonModChoiceGroupSyncProvider.prototype.syncChoiceGroup = function(id, siteId) { + var self = this; + + return that.CoreSitesProvider.getSite(siteId).then(function(site) { + siteId = site.getId(); + + if (self.isSyncing(id, siteId)) { + // There's already a sync ongoing for this group choice, return the promise. + return self.getOngoingSync(id, siteId); + } + + self.logger.debug('Try to sync group choice ' + id); + + var courseId; + var cmId; + var result = { + warnings: [], + updated: false + }; + + // Get the data to synchronize. + return choiceGroupOffline.getResponse(id, siteId).catch(function() { + // No offline data found, return empty object. + return {}; + }).then(function(data) { + if (!data.choicegroupid) { + // Nothing to sync. + return; + } + + if (!that.CoreAppProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + courseId = data.courseid; + cmId = data.cmid; + + // Send the responses. + var promise; + + if (data.deleting) { + // The user has deleted his responses. + promise = choiceGroupProvider.deleteResponsesOnline(id, siteId); + } else { + // The user has added a response. + promise = choiceGroupProvider.submitResponsesOnline(id, data.data, siteId); + } + + return promise.then(function() { + // Success sending the data. Delete the data stored. + result.updated = true; + + return choiceGroupOffline.deleteResponse(id, siteId); + }).catch(function(error) { + if (that.CoreUtilsProvider.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. Delete them. + result.updated = true; + + return choiceGroupOffline.deleteResponse(id, siteId).then(function() { + // Responses deleted, add a warning. + result.warnings.push(that.TranslateService.instant('core.warningofflinedatadeleted', { + component: that.TranslateService.instant('plugin.mod_choicegroup.modulename'), + name: data.name, + error: that.CoreTextUtilsProvider.getErrorMessageFromError(error) + })); + }); + } + + // Couldn't connect to server, reject. + return Promise.reject(error); + }); + }).then(function() { + if (result.updated) { + // Data has been sent to server, refresh the data. + var args = { + courseid: courseId, + cmid: cmId + }; + var preSets = { + getFromCache: false, + emergencyCache: false + }; + + return that.CoreSitePluginsProvider.getContent('mod_choicegroup', 'mobile_course_view', args, preSets) + .catch(function() { + // Ignore errors. + }); + } + }).then(function() { + // Sync finished, set sync time. + return self.setSyncTime(id, siteId); + }).then(function() { + // All done, return the result. + return result; + }); + + return self.addOngoingSync(id, syncPromise, siteId); + }); +}; + +var choiceGroupSync = new AddonModChoiceGroupSyncProvider(); + +/** + * Group choice sync handler. It will be registered in the cron delegate. + */ + +/** + * Handler to trigger group choice sync. + */ +function AddonModChoiceGroupSyncCronHandler() { + this.name = 'AddonModChoiceGroupSyncCronHandler'; +} + +/** + * Execute the process. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ +AddonModChoiceGroupSyncCronHandler.prototype.execute = function(siteId, force) { + // Only allow synchronizing current site. + if (!siteId || siteId == that.CoreSitesProvider.getCurrentSiteId()) { + return choiceGroupSync.syncAllChoiceGroups(force); + } +}; + +/** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ +AddonModChoiceGroupSyncCronHandler.prototype.getInterval = function() { + return choiceGroupSync.syncInterval; +}; + +/** + * Link handler to treat links to a group choice. + */ + +/** + * Handler to treat links to the index page. + */ +function AddonModChoiceGroupLinkHandler() { + that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, 'AddonModChoiceGroup', 'choicegroup'); + + this.name = "AddonModChoiceGroupLinkHandler"; +} + +AddonModChoiceGroupLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype); +AddonModChoiceGroupLinkHandler.prototype.constructor = AddonModChoiceGroupLinkHandler; + +// Register the sync handler. Wait a bit to make sure the DB tables are created. +setTimeout(function() { + that.CoreCronDelegate.register(new AddonModChoiceGroupSyncCronHandler()); +}, 500); + +// Register the link handler. +this.CoreContentLinksDelegate.registerHandler(new AddonModChoiceGroupLinkHandler()); + +var result = { + choiceGroupProvider: choiceGroupProvider, + choiceGroupOffline: choiceGroupOffline, +}; + +if (this.CoreConfigConstants.versioncode > 3800) { + // 3.8.0 and older versions of the app have a bug when returning classes with Angular dependencies. + // Only return the sync provider if the version is newer. + result.choiceGroupSync = choiceGroupSync; +} + +result; diff --git a/mod/choicegroup/mod_form.php b/mod/choicegroup/mod_form.php new file mode 100644 index 0000000..0d70574 --- /dev/null +++ b/mod/choicegroup/mod_form.php @@ -0,0 +1,304 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once ($CFG->dirroot.'/course/moodleform_mod.php'); + +class mod_choicegroup_mod_form extends moodleform_mod { + + function definition() { + global $CFG, $CHOICEGROUP_SHOWRESULTS, $CHOICEGROUP_PUBLISH, $CHOICEGROUP_DISPLAY, $DB, $COURSE, $PAGE; + + $mform =& $this->_form; + + //------------------------------------------------------------------------------- + $mform->addElement('header', 'general', get_string('general', 'form')); + + $mform->addElement('text', 'name', get_string('choicegroupname', 'choicegroup'), array('size'=>'64')); + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + $mform->addRule('name', null, 'required', null, 'client'); + + if (method_exists($this, 'standard_intro_elements')) { + $this->standard_intro_elements(get_string('description')); + } else { + $this->add_intro_editor(true, get_string('description')); + } + + //------------------------------------------------------------------------------- + + + // ------------------------- + // Fetch data from database + // ------------------------- + $groups = array(); + $db_groups = $DB->get_records('groups', array('courseid' => $COURSE->id)); + foreach ($db_groups as $group) { + $groups[$group->id] = new stdClass(); + $groups[$group->id]->name = format_string($group->name); + $groups[$group->id]->mentioned = false; + $groups[$group->id]->id = $group->id; + } + + if (count($db_groups) < 1) { + $a = new stdClass(); + $a->linkgroups = $CFG->wwwroot . '/group/index.php?id=' . $COURSE->id; + $a->linkcourse = $CFG->wwwroot . '//course/view.php?id=' . $COURSE->id; + $message = get_string('pleasesetonegroupor', 'choicegroup', $a); + \core\notification::add($message, \core\notification::WARNING); + print_error('nogroupincourse', 'choicegroup', new moodle_url('/course/view.php?id=' . $COURSE->id), $a); + } + + $db_groupings = $DB->get_records('groupings', array('courseid' => $COURSE->id)); + $groupings = array(); + if ($db_groupings) { + foreach ($db_groupings as $grouping) { + $groupings[$grouping->id] = new stdClass(); + $groupings[$grouping->id]->name = $grouping->name; + } + + list($sqlin, $inparams) = $DB->get_in_or_equal(array_keys($groupings)); + $db_groupings_groups = $DB->get_records_select('groupings_groups', 'groupingid '.$sqlin, $inparams); + + foreach ($db_groupings_groups as $grouping_group_link) { + $groupings[$grouping_group_link->groupingid]->linkedGroupsIDs[] = $grouping_group_link->groupid; + } + } + // ------------------------- + // ------------------------- + + // ------------------------- + // Continue generating form + // ------------------------- + $mform->addElement('header', 'miscellaneoussettingshdr', get_string('miscellaneoussettings', 'form')); + $mform->setExpanded('miscellaneoussettingshdr'); + $mform->addElement('checkbox', 'multipleenrollmentspossible', get_string('multipleenrollmentspossible', 'choicegroup')); + + $mform->addElement('select', 'showresults', get_string("publish", "choicegroup"), $CHOICEGROUP_SHOWRESULTS); + $mform->setDefault('showresults', CHOICEGROUP_SHOWRESULTS_DEFAULT); + + $mform->addElement('select', 'publish', get_string("privacy", "choicegroup"), $CHOICEGROUP_PUBLISH, CHOICEGROUP_PUBLISH_DEFAULT); + $mform->setDefault('publish', CHOICEGROUP_PUBLISH_DEFAULT); + $mform->disabledIf('publish', 'showresults', 'eq', 0); + + $mform->addElement('selectyesno', 'allowupdate', get_string("allowupdate", "choicegroup")); + + $mform->addElement('selectyesno', 'showunanswered', get_string("showunanswered", "choicegroup")); + + $menuoptions = array(); + $menuoptions[0] = get_string('disable'); + $menuoptions[1] = get_string('enable'); + $mform->addElement('select', 'limitanswers', get_string('limitanswers', 'choicegroup'), $menuoptions); + $mform->addHelpButton('limitanswers', 'limitanswers', 'choicegroup'); + + $mform->addElement('text', 'generallimitation', get_string('generallimitation', 'choicegroup'), array('size' => '6')); + $mform->setType('generallimitation', PARAM_INT); + $mform->disabledIf('generallimitation', 'limitanswers', 'neq', 1); + $mform->addRule('generallimitation', get_string('error'), 'numeric', 'extraruledata', 'client', false, false); + $mform->setDefault('generallimitation', 0); + $mform->addElement('button', 'setlimit', get_string('applytoallgroups', 'choicegroup')); + $mform->disabledIf('setlimit', 'limitanswers', 'neq', 1); + + + // ------------------------- + // Generate the groups section of the form + // ------------------------- + + + $mform->addElement('header', 'groups', get_string('groupsheader', 'choicegroup')); + $mform->addElement('html', '<fieldset class="clearfix"> + <div class="fcontainer clearfix"> + <div id="fitem_id_option_0" class="fitem fitem_fselect "> + <div class="fitemtitle"><label for="id_option_0">'.get_string('groupsheader', 'choicegroup').'</label><span class="helptooltip"><a href="'. $CFG->wwwroot .'/help.php?component=choicegroup&identifier=choicegroupoptions&lang='.current_language().'" title="'.get_string('choicegroupoptions_help', 'choicegroup').'" aria-haspopup="true" target="_blank"><img src="'.$CFG->wwwroot.'/theme/image.php?theme='.$PAGE->theme->name.'&component=core&image=help" alt="'.get_string('choicegroupoptions_help', 'choicegroup').'" class="iconhelp"></a></span></div><div class="felement fselect"> + <div class="tablecontainer"> + <table><tr><th>'.get_string('available_groups', 'choicegroup').'</th><th> </th><th>'.get_string('selected_groups', 'choicegroup').'</th><th> </th></tr><tr><td style="vertical-align: top">'); + + $mform->addElement('html','<select id="availablegroups" name="availableGroups" multiple size=10 style="width:200px">'); + foreach ($groupings as $groupingID => $grouping) { + // find all linked groups to this grouping + if (isset($grouping->linkedGroupsIDs) && count($grouping->linkedGroupsIDs) > 1) { // grouping has more than 2 items, thus we should display it (otherwise it would be clearer to display only that single group alone) + $mform->addElement('html', '<option value="'.$groupingID.'" style="font-weight: bold" class="grouping">'.get_string('char_bullet_expanded', 'choicegroup').$grouping->name.'</option>'); + foreach ($grouping->linkedGroupsIDs as $linkedGroupID) { + if (isset($groups[$linkedGroupID])) { + $mform->addElement('html', '<option value="'.$linkedGroupID.'" class="group nested"> '.$groups[$linkedGroupID]->name.'</option>'); + $groups[$linkedGroupID]->mentioned = true; + } + } + } + } + foreach ($groups as $group) { + if ($group->mentioned === false) { + $mform->addElement('html', '<option value="'.$group->id.'" class="group toplevel">'.format_string($group->name).'</option>'); + } + } + $mform->addElement('html', '</select><br><button name="expandButton" type="button" disabled id="expandButton" class="btn btn-secondary">' . get_string('expand_all_groupings', 'choicegroup') . + '</button><button name="collapseButton" type="button" disabled id="collapseButton" class="btn btn-secondary">' . get_string('collapse_all_groupings', 'choicegroup') . + '</button><br>' . get_string('double_click_grouping_legend', 'choicegroup') . '<br>' . get_string('double_click_group_legend', 'choicegroup')); + + + + + + + $mform->addElement('html' ,' + </td><td><button id="addGroupButton" name="add" type="button" disabled class="btn btn-secondary">' . get_string('add', 'choicegroup') . + '</button><div><button name="remove" type="button" disabled id="removeGroupButton" class="btn btn-secondary">' . get_string('del', 'choicegroup') . '</button></div></td>'); + $mform->addElement('html','<td style="vertical-align: top"><select id="id_selectedGroups" name="selectedGroups" multiple size=10 style="width:200px"></select></td>'); + + $mform->addElement('html','<td><div><div id="fitem_id_limit_0" class="fitem fitem_ftext" style="display:none"><div class=""><label for="id_limit_0" id="label_for_limit_ui">'.get_string('set_limit_for_group', 'choicegroup').'</label></div><div class="ftext"> + <input class="mod-choicegroup-limit-input" type="text" value="0" id="ui_limit_input" disabled="disabled"></div></div></div></td></tr></table></div> + </div></div> + + </div> + </fieldset>'); + + $mform->setExpanded('groups'); + + foreach ($groups as $group) { + $mform->addElement('hidden', 'group_' . $group->id . '_limit', '', array('id' => 'group_' . $group->id . '_limit', 'class' => 'limit_input_node')); + $mform->setType('group_' . $group->id . '_limit', PARAM_RAW); + } + + + $serializedselectedgroupsValue = ''; + if (isset($this->_instance) && $this->_instance != '') { + // this is presumably edit mode, try to fill in the data for javascript + $cg = choicegroup_get_choicegroup($this->_instance); + foreach ($cg->option as $optionID => $groupID) { + $serializedselectedgroupsValue .= ';' . $groupID; + $mform->setDefault('group_' . $groupID . '_limit', $cg->maxanswers[$optionID]); + } + + } + + + $mform->addElement('hidden', 'serializedselectedgroups', $serializedselectedgroupsValue, array('id' => 'serializedselectedgroups')); + $mform->setType('serializedselectedgroups', PARAM_RAW); + + switch (get_config('choicegroup', 'sortgroupsby')) { + case CHOICEGROUP_SORTGROUPS_CREATEDATE: + $systemdefault = array(CHOICEGROUP_SORTGROUPS_SYSTEMDEFAULT => get_string('systemdefault_date', 'choicegroup')); + break; + case CHOICEGROUP_SORTGROUPS_NAME: + $systemdefault = array(CHOICEGROUP_SORTGROUPS_SYSTEMDEFAULT => get_string('systemdefault_name', 'choicegroup')); + break; + } + + $options = array_merge($systemdefault, choicegroup_get_sort_options()); + $mform->addElement('select', 'sortgroupsby', get_string('sortgroupsby', 'choicegroup'), $options); + $mform->setDefault('sortgroupsby', CHOICEGROUP_SORTGROUPS_SYSTEMDEFAULT); + + // ------------------------- + // Go on the with the remainder of the form + // ------------------------- + + + //------------------------------------------------------------------------------- + $mform->addElement('header', 'timerestricthdr', get_string('timerestrict', 'choicegroup')); + $mform->addElement('checkbox', 'timerestrict', get_string('timerestrict', 'choicegroup')); + + $mform->addElement('date_time_selector', 'timeopen', get_string("choicegroupopen", "choicegroup")); + $mform->disabledIf('timeopen', 'timerestrict'); + + $mform->addElement('date_time_selector', 'timeclose', get_string("choicegroupclose", "choicegroup")); + $mform->disabledIf('timeclose', 'timerestrict'); + + //------------------------------------------------------------------------------- + $this->standard_coursemodule_elements(); + //------------------------------------------------------------------------------- + $this->add_action_buttons(); +} + +function data_preprocessing(&$default_values){ + global $DB; + $this->js_call(); + + if (empty($default_values['timeopen'])) { + $default_values['timerestrict'] = 0; + } else { + $default_values['timerestrict'] = 1; + } + + } + + function validation($data, $files) { + $errors = parent::validation($data, $files); + + $groupIDs = explode(';', $data['serializedselectedgroups']); + $groupIDs = array_diff( $groupIDs, array( '' ) ); + + if (array_key_exists('multipleenrollmentspossible', $data) && $data['multipleenrollmentspossible'] === '1') { + if (count($groupIDs) < 1) { + $errors['groups'] = get_string('fillinatleastoneoption', 'choicegroup'); + } + } else { + if (count($groupIDs) < 1) { + $errors['groups'] = get_string('fillinatleastoneoption', 'choicegroup'); + } + } + + + return $errors; + } + + function get_data() { + $data = parent::get_data(); + if (!$data) { + return false; + } + // Set up completion section even if checkbox is not ticked + if (empty($data->completionsection)) { + $data->completionsection=0; + } + return $data; + } + + function add_completion_rules() { + $mform =& $this->_form; + + $mform->addElement('checkbox', 'completionsubmit', '', get_string('completionsubmit', 'choicegroup')); + return array('completionsubmit'); + } + + function completion_rule_enabled($data) { + return !empty($data['completionsubmit']); + } + + public function js_call() { + global $PAGE; + $params = [$this->_form->getAttribute('id')]; + $PAGE->requires->yui_module('moodle-mod_choicegroup-form', 'Y.Moodle.mod_choicegroup.form.init', $params); + foreach (array_keys(get_string_manager()->load_component_strings('choicegroup', current_language())) as $string) { + $PAGE->requires->string_for_js($string, 'choicegroup'); + } + } + +} + diff --git a/mod/choicegroup/pix/column.png b/mod/choicegroup/pix/column.png new file mode 100644 index 0000000000000000000000000000000000000000..88fbae915b92d13a1699d279279d25ccc0578690 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0y~yU@&B0U@&52W?*1Ae{A7L1_lPU0G|+71_p-z|Nk$& zIsYyL1B0-qi(`mHcyh+00ER_uZGlw*7Z|cjqcar)UgR(^Ffe$!`njxgN@xNANOl^t literal 0 HcmV?d00001 diff --git a/mod/choicegroup/pix/icon.gif b/mod/choicegroup/pix/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..327422e6d48b1d9235d88b6d9f2ae73173a31ad4 GIT binary patch literal 115 zcmZ?wbhEHb6krfw_{hSLX3Q`%&G^jBGzJC+#h)y!3=Av`It&a93?O+1W*3iLHisv) zD#(=tg;;!_CHT}bSYQ^*0=~&hS7hzl#=Ym%(ro5tHHIzI&n{1V+2)eW`?6Rj(Z&Dl U&Cjecf=nN0#fGftVPUWa0BC+Dl>h($ literal 0 HcmV?d00001 diff --git a/mod/choicegroup/pix/icon.svg b/mod/choicegroup/pix/icon.svg new file mode 100644 index 0000000..ceaf924 --- /dev/null +++ b/mod/choicegroup/pix/icon.svg @@ -0,0 +1,430 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In --> + +<svg + xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + width="24px" + height="24px" + viewBox="0 0 24 24" + style="overflow:visible;enable-background:new 0 0 24 24;" + xml:space="preserve" + preserveAspectRatio="xMinYMid meet" + id="svg2" + inkscape:version="0.48.4 r9939" + sodipodi:docname="Group_Choice.svg"><metadata + id="metadata75"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1440" + inkscape:window-height="878" + id="namedview73" + showgrid="false" + inkscape:zoom="9.8333333" + inkscape:cx="-4.1694915" + inkscape:cy="12" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /> +<defs + id="defs4"> +<linearGradient + y2="23.9487" + x2="11.8569" + y1="1.8521" + x1="11.8569" + gradientUnits="userSpaceOnUse" + id="SVGID_6_-1"><stop + id="stop3188" + style="stop-color:#DB6D17" + offset="0" /><stop + id="stop3190" + style="stop-color:#BF3B08" + offset="1" /><a:midPointStop + style="stop-color:#DB6D17" + offset="0" /><a:midPointStop + style="stop-color:#DB6D17" + offset="0.5" /><a:midPointStop + style="stop-color:#BF3B08" + offset="1" /></linearGradient><linearGradient + y2="22.383301" + x2="9.5522003" + y1="9.0078001" + x1="9.5522003" + gradientUnits="userSpaceOnUse" + id="SVGID_7_-7"><stop + id="stop3197" + style="stop-color:#F6A55E" + offset="0" /><stop + id="stop3199" + style="stop-color:#EA5B03" + offset="1" /><a:midPointStop + style="stop-color:#F6A55E" + offset="0" /><a:midPointStop + style="stop-color:#F6A55E" + offset="0.5" /><a:midPointStop + style="stop-color:#EA5B03" + offset="1" /></linearGradient><linearGradient + y2="20.8174" + x2="7.2484999" + y1="14.7998" + x1="7.2484999" + gradientUnits="userSpaceOnUse" + id="SVGID_8_-4"><stop + id="stop3206" + style="stop-color:#F17219" + offset="0" /><stop + id="stop3208" + style="stop-color:#EA5B03" + offset="1" /><a:midPointStop + style="stop-color:#F17219" + offset="0" /><a:midPointStop + style="stop-color:#F17219" + offset="0.5" /><a:midPointStop + style="stop-color:#EA5B03" + offset="1" /></linearGradient><linearGradient + id="SVGID_1_-9" + gradientUnits="userSpaceOnUse" + x1="7.8237" + y1="0" + x2="7.8237" + y2="23.3262"><stop + offset="0" + style="stop-color:#76A1F0" + id="stop4136" /><stop + offset="1" + style="stop-color:#6B90D5" + id="stop4138" /><a:midPointStop + offset="0" + style="stop-color:#76A1F0" /><a:midPointStop + offset="0.5" + style="stop-color:#76A1F0" /><a:midPointStop + offset="1" + style="stop-color:#6B90D5" /></linearGradient><linearGradient + id="SVGID_2_-4" + gradientUnits="userSpaceOnUse" + x1="7.8237" + y1="1" + x2="7.8237" + y2="22.3262" + gradientTransform="translate(3.9966107,-0.15847424)"><stop + offset="0" + style="stop-color:#BBE0F7" + id="stop4143" /><stop + offset="1" + style="stop-color:#82B4FB" + id="stop4145" /><a:midPointStop + offset="0" + style="stop-color:#BBE0F7" /><a:midPointStop + offset="0.5" + style="stop-color:#BBE0F7" /><a:midPointStop + offset="1" + style="stop-color:#82B4FB" /></linearGradient><linearGradient + id="SVGID_3_-8" + gradientUnits="userSpaceOnUse" + x1="7.8237" + y1="2.0985999" + x2="7.8237" + y2="21.3262" + gradientTransform="translate(3.9966107,-0.15847424)"><stop + offset="0" + style="stop-color:#95BFF8" + id="stop4150" /><stop + offset="0.5569" + style="stop-color:#84ADEF" + id="stop4152" /><stop + offset="1" + style="stop-color:#7CA4EB" + id="stop4154" /><a:midPointStop + offset="0" + style="stop-color:#95BFF8" /><a:midPointStop + offset="0.4" + style="stop-color:#95BFF8" /><a:midPointStop + offset="1" + style="stop-color:#7CA4EB" /></linearGradient><linearGradient + y2="23.3262" + x2="7.8237" + y1="0" + x1="7.8237" + gradientUnits="userSpaceOnUse" + id="linearGradient4216" + xlink:href="#SVGID_1_-9" + inkscape:collect="always" + gradientTransform="translate(3.9966107,-0.15847424)" /></defs> +<linearGradient + id="SVGID_1_" + gradientUnits="userSpaceOnUse" + x1="7.9995" + y1="7.9868" + x2="7.9995" + y2="24.001"> + <stop + offset="0" + style="stop-color:#db6d17;stop-opacity:1;" + id="stop7" /> + <stop + offset="1" + style="stop-color:#bf3b08;stop-opacity:1;" + id="stop9" /> + <a:midPointStop + offset="0" + style="stop-color:#F0A829" /> + <a:midPointStop + offset="0.5" + style="stop-color:#F0A829" /> + <a:midPointStop + offset="1" + style="stop-color:#C7671A" /> +</linearGradient> +<path + style="fill:url(#SVGID_1_);" + d="M3,19.2l-3,1.6V24h16v-0.4c0-0.5-0.5-1.2-1-1.5l-6-3.1c-0.5-0.3-0.6-0.8-0.3-1.2 c0,0,1.6-2,1.6-4.2C10.4,10.5,8.4,8,6,8c-2.4,0-4.4,2.6-4.4,5.7c0,2.1,1.6,4.2,1.6,4.2C3.6,18.3,3.5,18.9,3,19.2z" + id="path11" /> +<linearGradient + id="SVGID_2_" + gradientUnits="userSpaceOnUse" + x1="7.7212" + y1="8.9868" + x2="7.7212" + y2="23.001"> + <stop + offset="0" + style="stop-color:#f6a55e;stop-opacity:1;" + id="stop14" /> + <stop + offset="1" + style="stop-color:#ea5b03;stop-opacity:1;" + id="stop16" /> + <a:midPointStop + offset="0" + style="stop-color:#FFEBA8" /> + <a:midPointStop + offset="0.5" + style="stop-color:#FFEBA8" /> + <a:midPointStop + offset="1" + style="stop-color:#F8BE27" /> +</linearGradient> +<path + style="fill:url(#SVGID_2_);" + d="M1,23v-1.7L3.5,20c0.5-0.3,0.8-0.7,1-1.2c0.1-0.5,0-1.1-0.4-1.5c0,0-1.4-1.8-1.4-3.6 C2.6,11.1,4.2,9,6,9s3.4,2.1,3.4,4.7c0,1.7-1.4,3.5-1.4,3.6c-0.3,0.4-0.5,1-0.4,1.5c0.1,0.5,0.5,1,1,1.2l5.9,3H1z" + id="path18" /> +<linearGradient + id="SVGID_3_" + gradientUnits="userSpaceOnUse" + x1="6.1343" + y1="9.9868" + x2="6.1343" + y2="22.001"> + <stop + offset="0" + style="stop-color:#f17219;stop-opacity:1;" + id="stop21" /> + <stop + offset="1" + style="stop-color:#ea5b03;stop-opacity:1;" + id="stop23" /> + <a:midPointStop + offset="0" + style="stop-color:#FFC30F" /> + <a:midPointStop + offset="0.5" + style="stop-color:#FFC30F" /> + <a:midPointStop + offset="1" + style="stop-color:#F5AE0D" /> +</linearGradient> +<path + style="fill:url(#SVGID_3_);" + d="M2,22L2,22l1.9-1.1c0.8-0.4,1.3-1.1,1.5-1.9c0.2-0.8,0-1.7-0.6-2.3c-0.3-0.4-1.2-1.8-1.2-3 c0-2,1.1-3.7,2.4-3.7s2.4,1.7,2.4,3.7c0,1.1-0.9,2.5-1.2,3c-0.5,0.7-0.7,1.5-0.5,2.3c0.2,0.8,0.7,1.5,1.5,1.9l2.2,1.1H2z" + id="path25" /> +<linearGradient + id="SVGID_4_" + gradientUnits="userSpaceOnUse" + x1="16" + y1="7.9868" + x2="16" + y2="24.001"> + <stop + offset="0" + style="stop-color:#90c50e;stop-opacity:1;" + id="stop28" /> + <stop + offset="1" + style="stop-color:#70a034;stop-opacity:1;" + id="stop30" /> + <a:midPointStop + offset="0" + style="stop-color:#8D470D" /> + <a:midPointStop + offset="0.5" + style="stop-color:#8D470D" /> + <a:midPointStop + offset="1" + style="stop-color:#7C3D09" /> +</linearGradient> +<path + style="fill:url(#SVGID_4_);" + d="M24,24v-3.4l-3-1.5c-0.5-0.3-0.6-0.8-0.3-1.2c0,0,1.6-2,1.6-4.2c0-3.2-1.9-5.7-4.4-5.7 c-2.4,0-4.4,2.6-4.4,5.7c0,2.1,1.6,4.2,1.6,4.2c0.3,0.4,0.2,1-0.3,1.3l-6,3.2c-0.5,0.3-1,0.9-1,1.5V24H24z" + id="path32" /> +<linearGradient + id="SVGID_5_" + gradientUnits="userSpaceOnUse" + x1="16.4121" + y1="8.9868" + x2="16.4121" + y2="23.001"> + <stop + offset="0" + style="stop-color:#d9f991;stop-opacity:1;" + id="stop35" /> + <stop + offset="1" + style="stop-color:#b1dd4b;stop-opacity:1;" + id="stop37" /> + <a:midPointStop + offset="0" + style="stop-color:#D58738" /> + <a:midPointStop + offset="0.5" + style="stop-color:#D58738" /> + <a:midPointStop + offset="1" + style="stop-color:#AB551F" /> +</linearGradient> +<path + style="fill:url(#SVGID_5_);" + d="M9.8,23l5.7-3c0.5-0.3,0.8-0.7,1-1.2c0.1-0.5,0-1.1-0.4-1.5c0,0-1.4-1.8-1.4-3.6 c0-2.6,1.5-4.7,3.4-4.7s3.4,2.1,3.4,4.7c0,1.8-1.4,3.6-1.4,3.6c-0.3,0.4-0.5,1-0.4,1.5s0.5,1,1,1.2l2.4,1.2V23H9.8z" + id="path39" /> +<linearGradient + id="SVGID_6_" + gradientUnits="userSpaceOnUse" + x1="17.9424" + y1="9.9868" + x2="17.9424" + y2="22.001"> + <stop + offset="0" + style="stop-color:#b3e73a;stop-opacity:1;" + id="stop42" /> + <stop + offset="1" + style="stop-color:#90c61d;stop-opacity:1;" + id="stop44" /> + <a:midPointStop + offset="0" + style="stop-color:#D0813A" /> + <a:midPointStop + offset="0.5" + style="stop-color:#D0813A" /> + <a:midPointStop + offset="1" + style="stop-color:#AF551D" /> +</linearGradient> +<path + style="fill:url(#SVGID_6_);" + d="M13.9,22l2.1-1.1c0.8-0.4,1.3-1.1,1.5-1.9c0.2-0.8,0-1.7-0.6-2.3c-0.3-0.4-1.2-1.8-1.2-3 c0-2,1.1-3.7,2.4-3.7s2.4,1.7,2.4,3.7c0,1.2-0.9,2.5-1.2,3c-0.5,0.7-0.7,1.5-0.6,2.3c0.2,0.8,0.7,1.5,1.5,1.9l1.9,1V22H13.9z" + id="path46" /> +<linearGradient + id="SVGID_7_" + gradientUnits="userSpaceOnUse" + x1="7.4507" + y1="0" + x2="7.4507" + y2="12.9043"> + <stop + offset="0" + style="stop-color:#76A1F0" + id="stop49" /> + <stop + offset="1" + style="stop-color:#6B90D5" + id="stop51" /> + <a:midPointStop + offset="0" + style="stop-color:#76A1F0" /> + <a:midPointStop + offset="0.5" + style="stop-color:#76A1F0" /> + <a:midPointStop + offset="1" + style="stop-color:#6B90D5" /> +</linearGradient> + +<linearGradient + id="SVGID_8_" + gradientUnits="userSpaceOnUse" + x1="7.4507" + y1="1" + x2="7.4507" + y2="11.2168"> + <stop + offset="0" + style="stop-color:#BBE0F7" + id="stop56" /> + <stop + offset="1" + style="stop-color:#82B4FB" + id="stop58" /> + <a:midPointStop + offset="0" + style="stop-color:#BBE0F7" /> + <a:midPointStop + offset="0.5" + style="stop-color:#BBE0F7" /> + <a:midPointStop + offset="1" + style="stop-color:#82B4FB" /> +</linearGradient> + +<linearGradient + id="SVGID_9_" + gradientUnits="userSpaceOnUse" + x1="7.4507" + y1="2" + x2="7.4507" + y2="9.9097"> + <stop + offset="0" + style="stop-color:#95BFF8" + id="stop63" /> + <stop + offset="0.5569" + style="stop-color:#84ADEF" + id="stop65" /> + <stop + offset="1" + style="stop-color:#7CA4EB" + id="stop67" /> + <a:midPointStop + offset="0" + style="stop-color:#95BFF8" /> + <a:midPointStop + offset="0.4" + style="stop-color:#95BFF8" /> + <a:midPointStop + offset="1" + style="stop-color:#7CA4EB" /> +</linearGradient> + + +</svg> \ No newline at end of file diff --git a/mod/choicegroup/pix/row.png b/mod/choicegroup/pix/row.png new file mode 100644 index 0000000000000000000000000000000000000000..d082724c452f38f899cd65ee38a16fc1a2220b2f GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0y~yU@&4}U@+uhVqjnpKbh;nz`(#*9OUlAu<o49O9lo8 z&H|6fVg?3oVGw3ym^DX&fq{X&#M9T6{T4eDn=-%e;pqkp3=BG+E{-7)hu>Z{WMp9A zU{<(p|7FdC=ghJmw;AUhbukb$5cFa3VG(8&K6qp|tK=uC_s7#OTQe{)FnGH9xvX<a GXaWGbXC~DE literal 0 HcmV?d00001 diff --git a/mod/choicegroup/renderer.php b/mod/choicegroup/renderer.php new file mode 100644 index 0000000..c8f1887 --- /dev/null +++ b/mod/choicegroup/renderer.php @@ -0,0 +1,421 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +define ('CHOICEGROUP_DISPLAY_HORIZONTAL_LAYOUT', 0); +define ('CHOICEGROUP_DISPLAY_VERTICAL_LAYOUT', 1); + +class mod_choicegroup_renderer extends plugin_renderer_base { + + /** + * @param $options + * @param $coursemoduleid + * @param bool $vertical + * @param bool $publish + * @param bool $limitanswers + * @param bool $showresults + * @param bool $current + * @param bool $choicegroupopen + * @param bool $disabled + * @param bool $multipleenrollmentspossible + * + * @return string + */ + public function display_options($options, $coursemoduleid, $vertical = true, $publish = false, $limitanswers = false, $showresults = false, $current = false, $choicegroupopen = false, $disabled = false, $multipleenrollmentspossible = false) { + global $DB, $PAGE, $choicegroup_groups, $choicegroup_users; + + $target = new moodle_url('/mod/choicegroup/view.php'); + $attributes = array('method'=>'POST', 'action'=>$target, 'class'=> 'tableform'); + + $html = html_writer::start_tag('form', $attributes); + $html .= html_writer::start_tag('div', array('class'=>'tablecontainer')); + $html .= html_writer::start_tag('table', array('class'=>'choicegroups' )); + + $html .= html_writer::start_tag('tr'); + $html .= html_writer::tag('th', get_string('choice', 'choicegroup'), array('class'=>'width10')); + + $group = get_string('group').' '; + $group .= html_writer::tag('a', get_string('showdescription', 'choicegroup'), array('role' => 'button','class' => 'choicegroup-descriptiondisplay choicegroup-descriptionshow btn btn-secondary ml-1', 'href' => '#')); + $html .= html_writer::tag('th', $group, array('class'=>'width40')); + + if ( $showresults == CHOICEGROUP_SHOWRESULTS_ALWAYS or + ($showresults == CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER and $current) or + ($showresults == CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE and !$choicegroupopen)) { + if ($limitanswers) { + $html .= html_writer::tag('th', get_string('members/max', 'choicegroup'), array('class'=>'width10')); + } + else { + $html .= html_writer::tag('th', get_string('members/', 'choicegroup'), array('class'=>'width10')); + } + if ($publish == CHOICEGROUP_PUBLISH_NAMES) { + $membersdisplay_html = html_writer::tag('a', get_string('showgroupmembers','mod_choicegroup'), array('role' => 'button','class' => 'choicegroup-memberdisplay choicegroup-membershow btn btn-secondary ml-1', 'href' => '#')); + $html .= html_writer::tag('th', get_string('groupmembers', 'choicegroup') .' '. $membersdisplay_html, array('class'=>'width40')); + } + } + $html .= html_writer::end_tag('tr'); + + $availableoption = count($options['options']); + if ($multipleenrollmentspossible == 1) { + $i=0; + $answer_to_groupid_mappings = ''; + } + $initiallyHideSubmitButton = false; + foreach ($options['options'] as $option) { + $group = (isset($choicegroup_groups[$option->groupid])) ? ($choicegroup_groups[$option->groupid]) : (false); + if (!$group) { + $colspan = 2; + if ( $showresults == CHOICEGROUP_SHOWRESULTS_ALWAYS or ($showresults == CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER and $current) or ($showresults == CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE and !$choicegroupopen)) { + $colspan++; + if ($publish == CHOICEGROUP_PUBLISH_NAMES) { + $colspan++; + } + } + $cell = html_writer::tag('td', get_string('groupdoesntexist', 'choicegroup'), array('colspan' => $colspan)); + $html .= html_writer::tag('tr', $cell); + break; + } + $html .= html_writer::start_tag('tr', array('class'=>'option')); + $html .= html_writer::start_tag('td', array('class'=>'center')); + + if ($multipleenrollmentspossible == 1) { + $option->attributes->name = 'answer_'.$i; + $option->attributes->type = 'checkbox'; + $answer_to_groupid_mappings .= '<input type="hidden" name="answer_'.$i.'_groupid" value="'.$option->groupid.'">'; + $i++; + } else { + $option->attributes->name = 'answer'; + $option->attributes->type = 'radio'; + if (array_key_exists('attributes', $option) && array_key_exists('checked', $option->attributes) && $option->attributes->checked == true) { + $initiallyHideSubmitButton = true; + } + } + + $labeltext = html_writer::tag('label', format_string($group->name), array('for' => 'choiceid_' . $option->attributes->value)); + $group_members = $DB->get_records('groups_members', array('groupid' => $group->id)); + $group_members_names = array(); + foreach ($group_members as $group_member) { + $group_user = (isset($choicegroup_users[$group_member->userid])) ? ($choicegroup_users[$group_member->userid]) : ($DB->get_record('user', array('id' => $group_member->userid))); + $group_members_names[] = $group_user->lastname . ', ' . $group_user->firstname; + } + sort($group_members_names); + if (!empty($option->attributes->disabled) || ($limitanswers && sizeof($group_members) >= $option->maxanswers) && empty($option->attributes->checked)) { + $labeltext .= ' ' . html_writer::tag('em', get_string('full', 'choicegroup')); + $option->attributes->disabled=true; + $availableoption--; + } + $context = \context_course::instance($group->courseid); + $labeltext .= html_writer::tag('div', format_text(file_rewrite_pluginfile_urls($group->description, + 'pluginfile.php', + $context->id, + 'group', + 'description', + $group->id)), + array('class' => 'choicegroups-descriptions hidden')); + if ($disabled) { + $option->attributes->disabled=true; + } + $attributes = (array) $option->attributes; + $attributes['id'] = 'choiceid_' . $option->attributes->value; + $html .= html_writer::empty_tag('input', $attributes); + $html .= html_writer::end_tag('td'); + $html .= html_writer::tag('td', $labeltext); + + + if ( $showresults == CHOICEGROUP_SHOWRESULTS_ALWAYS or + ($showresults == CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER and $current) or + ($showresults == CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE and !$choicegroupopen)) { + + $maxanswers = ($limitanswers) ? (' / '.$option->maxanswers) : (''); + $html .= html_writer::tag('td', sizeof($group_members_names).$maxanswers, array('class' => 'center')); + if ($publish == CHOICEGROUP_PUBLISH_NAMES) { + $group_members_html = html_writer::tag('div', implode('<br />', $group_members_names), array('class' => 'choicegroups-membersnames hidden', 'id' => 'choicegroup_'.$option->attributes->value)); + $html .= html_writer::tag('td', $group_members_html, array('class' => 'center')); + } + } + $html .= html_writer::end_tag('tr'); + } + $html .= html_writer::end_tag('table'); + $html .= html_writer::end_tag('div'); + if ($multipleenrollmentspossible == 1) { + $html .= '<input type="hidden" name="number_of_groups" value="'.$i.'">' . $answer_to_groupid_mappings; + } + $html .= html_writer::tag('div', '', array('class'=>'clearfloat')); + $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey())); + $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'id', 'value'=>$coursemoduleid)); + + if (!empty($options['hascapability']) && ($options['hascapability'])) { + if ($availableoption < 1) { + $html .= html_writer::tag('p', get_string('choicegroupfull', 'choicegroup')); + } else { + if (!$disabled) { + $html .= html_writer::empty_tag('input', array( + 'type'=>'submit', + 'value'=>get_string('savemychoicegroup','choicegroup'), + 'class'=>'btn btn-primary', + 'style' => $initiallyHideSubmitButton?'display: none':'' + )); + } + } + + if (!empty($options['allowupdate']) && ($options['allowupdate']) && !($multipleenrollmentspossible == 1) && !$disabled) { + $url = new moodle_url('view.php', array('id'=>$coursemoduleid, 'action'=>'delchoicegroup', 'sesskey'=>sesskey())); + $html .= ' ' . html_writer::link($url, get_string('removemychoicegroup','choicegroup')); + } + } elseif (!isloggedin() || isguestuser()) { // Only display message if user is not logged in or is a guest user. + $html .= ' '.html_writer::tag('p', get_string('havetologin', 'choicegroup')); + } + + $html .= html_writer::end_tag('form'); + + return $html; + } + + /** + * Returns HTML to display choicegroups result + * @param object $choicegroups + * @param bool $forcepublish + * @return string + */ + public function display_result($choicegroups, $forcepublish = false) { + if (empty($forcepublish)) { //allow the publish setting to be overridden + $forcepublish = $choicegroups->publish; + } + + $displaylayout = ($choicegroups) ? ($choicegroups->display) : (CHOICEGROUP_DISPLAY_HORIZONTAL); + + if ($forcepublish) { //CHOICEGROUP_PUBLISH_NAMES + return $this->display_publish_name_vertical($choicegroups); + } else { //CHOICEGROUP_PUBLISH_ANONYMOUS'; + if ($displaylayout == CHOICEGROUP_DISPLAY_HORIZONTAL_LAYOUT) { + return $this->display_publish_anonymous_horizontal($choicegroups); + } + return $this->display_publish_anonymous_vertical($choicegroups); + } + } + + /** + * Returns HTML to display choicegroups result + * @param object $choicegroups + * @param bool $forcepublish + * @return string + */ + public function display_publish_name_vertical($choicegroups) { + global $PAGE; + global $DB; + global $context; + + if (!has_capability('mod/choicegroup:downloadresponses', $context)) { + return; // only the (editing)teacher can see the diagram + } + if (!$choicegroups) { + return; // no answers yet, so don't bother + } + + $html =''; + $html .= html_writer::tag('h3',format_string(get_string("responses", "choicegroup"))); + + $attributes = array('method'=>'POST'); + $attributes['action'] = new moodle_url($PAGE->url); + $attributes['id'] = 'attemptsform'; + $attributes['class'] = 'tableform'; + + if ($choicegroups->viewresponsecapability) { + $html .= html_writer::start_tag('form', $attributes); + $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'id', 'value'=> $choicegroups->coursemoduleid)); + $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=> sesskey())); + $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'mode', 'value'=>'overview')); + } + + $table = new html_table(); + $table->cellpadding = 0; + $table->cellspacing = 0; + $table->attributes['class'] = 'results names '; + $table->tablealign = 'center'; + $table->data = array(); + + $count = 0; + ksort($choicegroups->options); + + $columns = array(); + foreach ($choicegroups->options as $optionid => $options) { + $coldata = ''; + if ($choicegroups->showunanswered && $optionid == 0) { + $coldata .= html_writer::tag('div', format_string(get_string('notanswered', 'choicegroup')), array('class'=>'option')); + } else if ($optionid > 0) { + $coldata .= html_writer::tag('div', format_string(choicegroup_get_option_text($choicegroups, $choicegroups->options[$optionid]->groupid)), array('class'=>'option')); + } + $numberofuser = 0; + if (!empty($options->user) && count($options->user) > 0) { + $numberofuser = count($options->user); + } + + $coldata .= html_writer::tag('div', ' ('.$numberofuser. ')', array('class'=>'numberofuser', 'title' => get_string('numberofuser', 'choicegroup'))); + $columns[] = $coldata; + } + + $table->head = $columns; + + $coldata = ''; + $columns = array(); + foreach ($choicegroups->options as $optionid => $options) { + $coldata = ''; + if ($choicegroups->showunanswered || $optionid > 0) { + if (!empty($options->user)) { + foreach ($options->user as $user) { + $data = ''; + if (empty($user->imagealt)){ + $user->imagealt = ''; + } + + if ($choicegroups->viewresponsecapability && $choicegroups->deleterepsonsecapability && $optionid > 0) { + $attemptaction = html_writer::checkbox('grpsmemberid[]', $user->grpsmemberid,''); + $data .= html_writer::tag('div', $attemptaction, array('class'=>'attemptaction')); + } + $userimage = $this->output->user_picture($user, array('courseid'=>$choicegroups->courseid)); + $data .= html_writer::tag('div', $userimage, array('class'=>'image')); + + $userlink = new moodle_url('/user/view.php', array('id'=>$user->id,'course'=>$choicegroups->courseid)); + $name = html_writer::tag('a', fullname($user, $choicegroups->fullnamecapability), array('href'=>$userlink, 'class'=>'username')); + $data .= html_writer::tag('div', $name, array('class'=>'fullname')); + $data .= html_writer::tag('div','', array('class'=>'clearfloat')); + $coldata .= html_writer::tag('div', $data, array('class'=>'user')); + } + } + } + + $columns[] = $coldata; + $count++; + } + + $table->data[] = $columns; + foreach ($columns as $d) { + $table->colclasses[] = 'data'; + } + $html .= html_writer::tag('div', html_writer::table($table), array('class'=>'response tablecontainer')); + + $actiondata = ''; + if ($choicegroups->viewresponsecapability && $choicegroups->deleterepsonsecapability) { + $selecturl = new moodle_url('#'); + $actiondata .= html_writer::start_div('selectallnone'); + $actiondata .= html_writer::link($selecturl, get_string('selectall'), ['data-select-info' => true]) . ' / '; + + $actiondata .= html_writer::link($selecturl, get_string('deselectall'), ['data-select-info' => false]); + $actiondata .= html_writer::end_div(); + $actiondata .= html_writer::tag('label', ' ' . get_string('withselected', 'choice') . ' ', array('for'=>'menuaction', 'class' => 'mr-1')); + + $actionurl = new moodle_url($PAGE->url, array('sesskey'=>sesskey(), 'action'=>'delete_confirmation()')); + $select = new single_select($actionurl, 'action', array('delete'=>get_string('delete')), null, array(''=>get_string('chooseaction', 'choicegroup')), 'attemptsform'); + + $PAGE->requires->js_call_amd('mod_choicegroup/select_all_choices', 'init'); + $actiondata .= $this->output->render($select); + } + $html .= html_writer::tag('div', $actiondata, array('class'=>'responseaction')); + + if ($choicegroups->viewresponsecapability) { + $html .= html_writer::end_tag('form'); + } + + return $html; + } + + + /** + * Returns HTML to display choicegroups result + * @param object $choicegroups + * @return string + */ + public function display_publish_anonymous_horizontal($choicegroups) { + global $context, $DB, $CHOICEGROUP_COLUMN_WIDTH; + + if (!has_capability('mod/choicegroup:downloadresponses', $context)) { + return; // only the (editing)teacher can see the diagram + } + + $table = new html_table(); + $table->cellpadding = 5; + $table->cellspacing = 0; + $table->attributes['class'] = 'results anonymous '; + $table->data = array(); + + $count = 0; + ksort($choicegroups->options); + + $rows = array(); + foreach ($choicegroups->options as $optionid => $options) { + $numberofuser = 0; + $graphcell = new html_table_cell(); + if (!empty($options->user)) { + $numberofuser = count($options->user); + } + + $width = 0; + $percentageamount = 0; + $columndata = ''; + if($choicegroups->numberofuser > 0) { + $width = ($CHOICEGROUP_COLUMN_WIDTH * ((float)$numberofuser / (float)$choicegroups->numberofuser)); + $percentageamount = ((float)$numberofuser/(float)$choicegroups->numberofuser)*100.0; + } + $displaydiagram = html_writer::tag('img','', array('style'=>'height:50px; width:'.$width.'px', 'alt'=>'', 'src'=>$this->output->pix_url('row', 'choicegroup'))); + + $skiplink = html_writer::tag('a', get_string('skipresultgraph', 'choicegroup'), array('href'=>'#skipresultgraph'. $optionid, 'class'=>'skip-block')); + $skiphandler = html_writer::tag('span', '', array('class'=>'skip-block-to', 'id'=>'skipresultgraph'.$optionid)); + + $graphcell->text = $skiplink . $displaydiagram . $skiphandler; + $graphcell->attributes = array('class'=>'graph horizontal'); + + $datacell = new html_table_cell(); + if ($choicegroups->showunanswered && $optionid == 0) { + $columndata .= html_writer::tag('div', format_string(get_string('notanswered', 'choicegroup')), array('class'=>'option')); + } else if ($optionid > 0) { + $columndata .= html_writer::tag('div', format_string(choicegroup_get_option_text($choicegroups, $choicegroups->options[$optionid]->groupid)), array('class'=>'option')); + } + $columndata .= html_writer::tag('div', ' ('.$numberofuser.')', array('title'=> get_string('numberofuser', 'choicegroup'), 'class'=>'numberofuser')); + + if($choicegroups->numberofuser > 0) { + $percentageamount = ((float)$numberofuser/(float)$choicegroups->numberofuser)*100.0; + } + $columndata .= html_writer::tag('div', format_float($percentageamount,1). '%', array('class'=>'percentage')); + + $datacell->text = $columndata; + $datacell->attributes = array('class'=>'header'); + + $row = new html_table_row(); + $row->cells = array($datacell, $graphcell); + $rows[] = $row; + } + + $table->data = $rows; + + $html = ''; + $header = html_writer::tag('h3',format_string(get_string("responses", "choicegroup"))); + $html .= html_writer::tag('div', $header, array('class'=>'responseheader')); + $html .= html_writer::table($table); + + return $html; + } + +} diff --git a/mod/choicegroup/report.php b/mod/choicegroup/report.php new file mode 100644 index 0000000..d11e0e4 --- /dev/null +++ b/mod/choicegroup/report.php @@ -0,0 +1,304 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../../config.php"); +require_once("lib.php"); + +$id = required_param('id', PARAM_INT); //moduleid +$format = optional_param('format', CHOICEGROUP_PUBLISH_NAMES, PARAM_INT); +$download = optional_param('download', '', PARAM_ALPHA); +$action = optional_param('action', '', PARAM_ALPHA); +$grpsmemberids = optional_param_array('grpsmemberid', array(), PARAM_INT); //get array of responses to delete. + +$url = new moodle_url('/mod/choicegroup/report.php', array('id'=>$id)); +if ($format !== CHOICEGROUP_PUBLISH_NAMES) { + $url->param('format', $format); +} +if ($download !== '') { + $url->param('download', $download); +} +if ($action !== '') { + $url->param('action', $action); +} +$PAGE->set_url($url); + +if (! $cm = get_coursemodule_from_id('choicegroup', $id)) { + print_error("invalidcoursemodule"); +} + +if (! $course = $DB->get_record("course", array("id" => $cm->course))) { + print_error("coursemisconf"); +} + +require_login($course->id, false, $cm); + +$context = context_module::instance($cm->id); + +require_capability('mod/choicegroup:readresponses', $context); + +if (!$choicegroup = choicegroup_get_choicegroup($cm->instance)) { + print_error('invalidcoursemodule'); +} + +$strchoicegroup = get_string("modulename", "choicegroup"); +$strchoicegroups = get_string("modulenameplural", "choicegroup"); +$strresponses = get_string("responses", "choicegroup"); + +$eventparams = array( + 'context' => $context, + 'objectid' => $choicegroup->id +); +$event = \mod_choicegroup\event\report_viewed::create($eventparams); +$event->add_record_snapshot('course_modules', $cm); +$event->add_record_snapshot('course', $course); +$event->add_record_snapshot('choicegroup', $choicegroup); +$event->trigger(); + +if (data_submitted() && $action == 'delete' && has_capability('mod/choicegroup:deleteresponses',$context) && confirm_sesskey()) { + choicegroup_delete_responses($grpsmemberids, $choicegroup, $cm, $course); //delete responses. + redirect("report.php?id=$cm->id"); +} + +if (!$download) { + $PAGE->navbar->add($strresponses); + $PAGE->set_title(format_string($choicegroup->name).": $strresponses"); + $PAGE->set_heading(format_string($course->fullname)); + echo $OUTPUT->header(); + echo $OUTPUT->heading(format_string($choicegroup->name)); + /// Check to see if groups are being used in this choicegroup + $groupmode = groups_get_activity_groupmode($cm); + if ($groupmode) { + groups_get_activity_group($cm, true); + groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/choicegroup/report.php?id='.$id); + } +} else { + $groupmode = groups_get_activity_groupmode($cm); + $groups = choicegroup_get_groups($choicegroup); + $groups_ids = array(); + foreach($groups as $group) { + $groups_ids[] = $group->id; + } +} +$users = choicegroup_get_response_data($choicegroup, $cm, $groupmode); + +if ($download == "ods" && has_capability('mod/choicegroup:downloadresponses', $context)) { + require_once("$CFG->libdir/odslib.class.php"); + +/// Calculate file name + $filename = clean_filename("$course->shortname ".strip_tags(format_string($choicegroup->name,true))).'.ods'; +/// Creating a workbook + $workbook = new MoodleODSWorkbook("-"); +/// Send HTTP headers + $workbook->send($filename); +/// Creating the first worksheet + $myxls = $workbook->add_worksheet($strresponses); + +/// Print names of all the fields + $myxls->write_string(0,0,get_string("lastname")); + $myxls->write_string(0,1,get_string("firstname")); + $myxls->write_string(0,2,get_string("idnumber")); + $myxls->write_string(0,3,get_string("email")); + $myxls->write_string(0,4,get_string("group")); + $myxls->write_string(0,5,get_string("choice","choicegroup")); + +/// generate the data for the body of the spreadsheet + $i=0; + $row=1; + if ($users) { + $displayed = array(); + foreach ($users as $option => $userid) { + foreach($userid as $user) { + if (in_array($user->id, $displayed)) { + continue; + } + $displayed[] = $user->id; + $myxls->write_string($row,0,$user->lastname); + $myxls->write_string($row,1,$user->firstname); + $studentid=(!empty($user->idnumber) ? $user->idnumber : " "); + $myxls->write_string($row,2,$studentid); + $myxls->write_string($row,3,$user->email); + $ug2 = array(); + if ($usergrps = groups_get_all_groups($course->id, $user->id)) { + foreach ($groups_ids as $gid) { + if (array_key_exists($gid, $usergrps)) { + $ug2[] = format_string($usergrps[$gid]->name); + } + } + } + $myxls->write_string($row, 4, implode(', ', $ug2)); + $row++; + $pos=5; + } + } + } + /// Close the workbook + $workbook->close(); + + exit; +} + +//print spreadsheet if one is asked for: +if ($download == "xls" && has_capability('mod/choicegroup:downloadresponses', $context)) { + require_once("$CFG->libdir/excellib.class.php"); + +/// Calculate file name + $filename = clean_filename("$course->shortname ".strip_tags(format_string($choicegroup->name,true))).'.xls'; +/// Creating a workbook + $workbook = new MoodleExcelWorkbook("-"); +/// Send HTTP headers + $workbook->send($filename); +/// Creating the first worksheet + // assigning by reference gives this: Strict standards: Only variables should be assigned by reference in /data_1/www/html/moodle/moodle/mod/choicegroup/report.php on line 157 + // removed the ampersand. + $myxls = $workbook->add_worksheet($strresponses); +/// Print names of all the fields + $myxls->write_string(0,0,get_string("lastname")); + $myxls->write_string(0,1,get_string("firstname")); + $myxls->write_string(0,2,get_string("idnumber")); + $myxls->write_string(0,3,get_string("email")); + $myxls->write_string(0,4,get_string("group")); + $myxls->write_string(0,5,get_string("choice","choicegroup")); + + +/// generate the data for the body of the spreadsheet + $i=0; + $row=1; + if ($users) { + $displayed = array(); + foreach ($users as $option => $userid) { + foreach($userid as $user) { + if (in_array($user->id, $displayed)) { + continue; + } + $displayed[] = $user->id; + $myxls->write_string($row,0,$user->lastname); + $myxls->write_string($row,1,$user->firstname); + $studentid=(!empty($user->idnumber) ? $user->idnumber : " "); + $myxls->write_string($row,2,$studentid); + $myxls->write_string($row,3,$user->email); + $ug2 = array(); + if ($usergrps = groups_get_all_groups($course->id, $user->id)) { + foreach ($groups_ids as $gid) { + if (array_key_exists($gid, $usergrps)) { + $ug2[] = format_string($usergrps[$gid]->name); + } + } + } + $myxls->write_string($row, 4, implode(', ', $ug2)); + $row++; + } + } + $pos=5; + } + /// Close the workbook + $workbook->close(); + exit; +} + +// print text file +if ($download == "txt" && has_capability('mod/choicegroup:downloadresponses', $context)) { + $filename = clean_filename("$course->shortname ".strip_tags(format_string($choicegroup->name,true))).'.txt'; + + header("Content-Type: application/download\n"); + header("Content-Disposition: attachment; filename=\"$filename\""); + header("Expires: 0"); + header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); + header("Pragma: public"); + + /// Print names of all the fields + + echo get_string("firstname")."\t".get_string("lastname") . "\t". get_string("idnumber") . "\t"; + echo get_string("email") . "\t"; + echo get_string("group"). "\t"; + echo get_string("choice","choicegroup"). "\n"; + + /// generate the data for the body of the spreadsheet + $i=0; + if ($users) { + $displayed = array(); + foreach ($users as $option => $userid) { + foreach($userid as $user) { + if (in_array($user->id, $displayed)) { + continue; + } + $displayed[] = $user->id; + echo $user->lastname; + echo "\t".$user->firstname; + $studentid = " "; + if (!empty($user->idnumber)) { + $studentid = $user->idnumber; + } + echo "\t". $studentid."\t"; + echo $user->email . "\t"; + $ug2 = array(); + if ($usergrps = groups_get_all_groups($course->id, $user->id)) { + foreach ($groups_ids as $gid) { + if (array_key_exists($gid, $usergrps)) { + $ug2[] = format_string($usergrps[$gid]->name); + } + } + } + echo implode(', ', $ug2) . "\t"; + echo "\n"; + } + } + } + exit; +} +// Show those who haven't answered the question. +if (!empty($choicegroup->showunanswered)) { + $choicegroup->option[0] = get_string('notanswered', 'choicegroup'); + $choicegroup->maxanswers[0] = 0; +} + +$results = prepare_choicegroup_show_results($choicegroup, $course, $cm, $users); +$renderer = $PAGE->get_renderer('mod_choicegroup'); +echo $renderer->display_result($results, has_capability('mod/choicegroup:readresponses', $context)); + +//now give links for downloading spreadsheets. +if (!empty($users) && has_capability('mod/choicegroup:downloadresponses',$context)) { + $downloadoptions = array(); + $options = array(); + $options["id"] = "$cm->id"; + $options["download"] = "ods"; + $button = $OUTPUT->single_button(new moodle_url("report.php", $options), get_string("downloadods")); + $downloadoptions[] = html_writer::tag('li', $button, array('class'=>'reportoption mt-1')); + + $options["download"] = "xls"; + $button = $OUTPUT->single_button(new moodle_url("report.php", $options), get_string("downloadexcel")); + $downloadoptions[] = html_writer::tag('li', $button, array('class'=>'reportoption mt-1')); + + $options["download"] = "txt"; + $button = $OUTPUT->single_button(new moodle_url("report.php", $options), get_string("downloadtext")); + $downloadoptions[] = html_writer::tag('li', $button, array('class'=>'reportoption mt-1')); + + $downloadlist = html_writer::tag('ul', implode('', $downloadoptions)); + $downloadlist .= html_writer::tag('div', '', array('class'=>'clearfloat')); + echo html_writer::tag('div',$downloadlist, array('class'=>'downloadreport')); +} + +echo $OUTPUT->footer(); + diff --git a/mod/choicegroup/settings.php b/mod/choicegroup/settings.php new file mode 100644 index 0000000..e6a5d71 --- /dev/null +++ b/mod/choicegroup/settings.php @@ -0,0 +1,36 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot . '/mod/choicegroup/lib.php'); + +if ($ADMIN->fulltree) { + $settings->add(new admin_setting_heading('defaults', get_string('defaultsettings', 'choicegroup'), '')); + + $options = choicegroup_get_sort_options(); + $settings->add(new admin_setting_configselect('choicegroup/sortgroupsby', get_string('sortgroupsby', 'choicegroup'), '', CHOICEGROUP_SORTGROUPS_CREATEDATE, $options)); +} diff --git a/mod/choicegroup/styles.css b/mod/choicegroup/styles.css new file mode 100644 index 0000000..53340cd --- /dev/null +++ b/mod/choicegroup/styles.css @@ -0,0 +1,274 @@ +.path-mod-choicegroup .results { + border-collapse: separate; +} + +.path-mod-choicegroup .results .data { + vertical-align: top; + white-space: nowrap; +} + +.path-mod-choicegroup .button { + text-align: center; +} + +.path-mod-choicegroup .attemptcell { + width: 5px; + white-space: nowrap; +} + +.path-mod-choicegroup .anonymous, +.path-mod-choicegroup div.downloadreport ul { + list-style: none; + margin-left: 1em; +} + +.path-mod-choicegroup .choicegroupresponse { + width: 100%; +} + +.path-mod-choicegroup .choicegroupresponse .picture { + width: 10px; + white-space: nowrap; +} + +.path-mod-choicegroup .choicegroupresponse .fullname { + width: 100%; + white-space: nowrap; +} + +.path-mod-choicegroup .responseheader { + width: 100%; + text-align: center; + margin-top: 10px; +} + +.path-mod-choicegroup .choicegroups .option label { + vertical-align: top; +} + +.path-mod-choicegroup .choicegroups .option input { + vertical-align: middle; +} + +.path-mod-choicegroup .horizontal, +.path-mod-choicegroup .horizontal .choicegroups .option { + padding-right: 20px; + display: inline; + white-space: normal; +} + +.path-mod-choicegroup .horizontal .choicegroups .button { + margin-top: 10px; +} + +.path-mod-choicegroup ul.choicegroups li { + list-style: none; +} + +.path-mod-choicegroup .results { + text-align: center; +} + +.path-mod-choicegroup .results.anonymous .graph.horizontal { + vertical-align: middle; + text-align: left; + width: 70%; +} + +.path-mod-choicegroup .results.anonymous .graph.vertical, +.path-mod-choicegroup .cell { + vertical-align: bottom; + text-align: center; +} + +.path-mod-choicegroup .results.names .header { + width: 10%; + white-space: normal; +} + +.path-mod-choicegroup .results.names .cell { + vertical-align: top; + text-align: left; +} + +.path-mod-choicegroup .results.names .user, +.path-mod-choicegroup #yourselection { + padding: 5px; +} + +.path-mod-choicegroup .results.names .user .attemptaction, +.path-mod-choicegroup .results.names .user .image, +.path-mod-choicegroup .results.names .user .fullname { + float: left; +} + +.path-mod-choicegroup .results.names .user .fullname { + padding-left: 5px; +} + +.path-mod-choicegroup .results .data.header { + width: 10%; +} + +.path-mod-choicegroup .responseaction { + text-align: center; +} + +.path-mod-choicegroup .results .option { + white-space: normal; +} + +.path-mod-choicegroup .results .option, +.path-mod-choicegroup .results .numberofuser, +.path-mod-choicegroup .results .percentage { + font-weight: bold; + font-size: 108%; +} + +#page-mod-choicegroup-report .downloadreport { + text-align: center; +} + +#page-mod-choicegroup-report .downloadreport ul { + overflow: auto; + min-width: 80%; + max-width: 100%; + display: inline-block; + margin-left: 0; + margin-right: auto; + padding-left: 0; + text-align: left; +} + +#page-mod-choicegroup-report .downloadreport ul li { + list-style: none; +} + +.path-mod-choicegroup .clearfloat { + float: none; + clear: both; +} + +/** + * Override for RTL layout + */ +.path-mod-choicegroup.dir-rtl .horizontal .choicegroups .option { + padding-right: 0px; + padding-left: 20px; + float: right; +} + +.path-mod-choicegroup.dir-rtl .results.anonymous .graph.horizontal { + text-align: right; +} + +.path-mod-choicegroup.dir-rtl .results.anonymous { + text-align: center; +} + +.path-mod-choicegroup.dir-rtl .results.names .cell { + text-align: right; +} + +.path-mod-choicegroup.dir-rtl .results.names .user .attemptaction, +.path-mod-choicegroup.dir-rtl .results.names .user .image, +.path-mod-choicegroup.dir-rtl .results.names .user .fullname, +.path-mod-choicegroup.dir-rtl .results.names .user .fullname { + padding-left: 0px; + padding-right: 5px; +} + +.path-mod-choicegroup.dir-rtl .downloadreport { + margin-left: 0; + text-align: center; +} + +#page-mod-choicegroup-view.dir-rtl .reportlink { + text-align: left; +} + +.path-mod-choicegroup div.tablecontainer { + overflow: auto; + border: 1px #ccc solid; + min-width: 80%; + max-width: 100%; + display: inline-block; + margin-left: auto; + margin-right: auto; + margin-bottom: 1rem; + text-align: left; +} + +.path-mod-choicegroup.dir-rtl div.tablecontainer { + text-align: right; +} + +.path-mod-choicegroup div.border { + border: 1px #ccc solid; +} + +.path-mod-choicegroup div.tablecontainer table { + width: 100%; +} + +.path-mod-choicegroup td { + border-top: 1px #ccc solid; +} + +.path-mod-choicegroup th, .path-mod-choicegroup td { + padding: 10px 15px; +} + +.path-mod-choicegroup td.center { + text-align: center; +} + +.path-mod-choicegroup div.choicegroup-memberdisplay { + width: 12px; + height: 12px; + line-height: 12px; + cursor: pointer; + text-align: center; + display: block; + border: 1px #999 solid; + margin: 0px auto; +} + +.path-mod-choicegroup table.choicegroups { + margin: 20px 0; +} + +.path-mod-choicegroup a.choicegroup-memberdisplay, +.path-mod-choicegroup a.choicegroup-descriptiondisplay { + display: inline-block; +} + +.path-mod-choicegroup div.choicegroups-membersnames.hidden, +.path-mod-choicegroup div.choicegroups-descriptions.hidden, +.path-mod-choicegroup a.choicegroup-memberdisplay.hidden, +.path-mod-choicegroup a.choicegroup-descriptiondisplay.hidden { + display: none; +} + +.path-mod-choicegroup .results td { + min-width: 120px; +} + +.path-mod-choicegroup .choicegroups-descriptions { + border-top: 1px #ccc dotted; + margin-top: 5px; + padding-top: 5px; +} + +.path-mod-choicegroup .tableform { + text-align: center +} + +/* Styles to format the Choices table. */ +.path-mod-choicegroup .width10 { + width: 10%; +} + +.path-mod-choicegroup .width40 { + width: 40%; +} diff --git a/mod/choicegroup/styles_app.css b/mod/choicegroup/styles_app.css new file mode 100644 index 0000000..23be5bf --- /dev/null +++ b/mod/choicegroup/styles_app.css @@ -0,0 +1,3 @@ +.bold { + font-weight: bold; +} \ No newline at end of file diff --git a/mod/choicegroup/templates/mobile_view_page.mustache b/mod/choicegroup/templates/mobile_view_page.mustache new file mode 100644 index 0000000..9c75626 --- /dev/null +++ b/mod/choicegroup/templates/mobile_view_page.mustache @@ -0,0 +1,215 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_choicegroup/mobile_view_page + + Template for the mobile view page. + + Classes required for JS: + - + + Data attributes required for JS: + - + + Context variables required for this template: + * cmid + * courseid + * choicegroup + * options + + Example context (json): + { + "cmid": "62", + "courseid": "3", + "choicegroup": { + "id": "4", + "course": "3", + "name": "Group choice activity", + "intro": "<p>Select your group</p>", + "introformat": "1", + "publish": "1", + "multipleenrollmentspossible": "1", + "showresults": "3", + "display": "0", + "allowupdate": "0", + "showunanswered": "0", + "limitanswers": "1", + "timeopen": "0", + "timeclose": "0", + "timemodified": "1528114222", + "completionsubmit": "0", + "sortgroupsby": "0", + "option": { + "10": "3", + "11": "4", + "12": "5" + }, + "grpmemberid": { + "6": [ + "3", + "2" + ], + }, + "maxanswers": { + "12": "1", + "11": "2", + "10": "2" + }, + "open": true, + "expired": false, + "alloptionsdisabled": false + }, + "options": [ + { + "id": 10, + "groupid": "3", + "name": "Group 1", + "maxanswers": "1", + "displaylayout": "0", + "countanswers": 2, + "checked": false, + "disabled": true + }, + { + "id": 11, + "groupid": "4", + "name": "Group 2", + "maxanswers": "2", + "displaylayout": "0", + "countanswers": 1, + "checked": true, + "disabled": false + }, + { + "id": 12, + "groupid": "5", + "name": "Group 3", + "maxanswers": "2", + "displaylayout": "0", + "countanswers": 0, + "checked": false, + "disabled": false + } + ] + } +}} +{{=<% %>=}} +<!-- Add options to the context menu. --> +<core-navbar-buttons end> + <core-context-menu> + <core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh($event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item> + + <core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="650" [content]="'core.settings.synchronizenow' | translate" (action)="synchronize(true, $event)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> + </core-context-menu> +</core-navbar-buttons> + +<core-loading [hideUntil]="loaded"> + <core-course-module-description description="<% choicegroup.intro %>" component="mod_choicegroup" componentId="<% cmid %>"></core-course-module-description> + + <!-- Choice done in offline but not synchronized --> + <ion-card class="core-warning-card" icon-start *ngIf="hasOffline"> + <ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + </ion-card> + + <%# choicegroup.message %> + <ion-list> + <ion-item> + <% choicegroup.message %> + </ion-item> + </ion-list> + <%/ choicegroup.message %> + + <%# choicegroup.open %> + <form id="savemychoice"> + + <ion-grid> + <ion-row> + <ion-col class="bold"> + {{ 'plugin.mod_choicegroup.group' | translate }} + </ion-col> + <ion-col col-3 class="bold" justify-content-center align-items-center text-center> + <%^ choicegroup.limitanswers %> + {{ 'plugin.mod_choicegroup.members/' | translate }} + <%/ choicegroup.limitanswers %> + <%# choicegroup.limitanswers %> + {{ 'plugin.mod_choicegroup.members/max' | translate }} + <%/ choicegroup.limitanswers %> + </ion-col> + </ion-row> + + <%^ choicegroup.multipleenrollmentspossible %> + <ion-list radio-group [(ngModel)]="CONTENT_OTHERDATA.data.responses" name="responses"> + <%/ choicegroup.multipleenrollmentspossible %> + <%# choicegroup.multipleenrollmentspossible %> + <ion-list> + <%/ choicegroup.multipleenrollmentspossible %> + <%# options %> + <ion-row> + <ion-col> + <ion-item> + <ion-label><% name %></ion-label> + <%^ choicegroup.multipleenrollmentspossible %> + <ion-radio <%# checked %>checked="true"<%/ checked %> <%# disabled %>disabled="true"<%/ disabled %> value="<% id %>"></ion-radio> + <%/ choicegroup.multipleenrollmentspossible %> + <%# choicegroup.multipleenrollmentspossible %> + <ion-checkbox item-right + [(ngModel)]="CONTENT_OTHERDATA.data.responses_<% id %>" name="responses_<% id %>" + <%# checked %>checked="true"<%/ checked %> + <%# disabled %>disabled="true"<%/ disabled %> + value="<% id %>"> + </ion-checkbox> + <%/ choicegroup.multipleenrollmentspossible %> + </ion-item> + </ion-col> + + <ion-col col-3 justify-content-center align-items-center text-center> + <% countanswers %> + <%# choicegroup.limitanswers %> / <% maxanswers %> <%/ choicegroup.limitanswers %> + </ion-col> + </ion-row> + <%/ options %> + </ion-list> + </ion-grid> + + <%^ choicegroup.expired %> + <%^ choicegroup.alloptionsdisabled %> + <ion-list> + <ion-item> + <button ion-button block type="submit" (click)="submitResponses()"> + {{ 'plugin.mod_choicegroup.savemychoicegroup' | translate }} + </button> + </ion-item> + + <%^ choicegroup.multipleenrollmentspossible %> + <%# choicegroup.allowupdate %> + <ion-item *ngIf="showDelete"> + <button ion-button block outline color="danger" type="button" (click)="deleteResponses()"> + <ion-icon name="trash"></ion-icon> + {{ 'plugin.mod_choicegroup.removemychoicegroup' | translate }} + </button> + </ion-item> + <%/ choicegroup.allowupdate %> + <%/ choicegroup.multipleenrollmentspossible %> + </ion-list> + <%/ choicegroup.alloptionsdisabled %> + <%/ choicegroup.expired %> + </form> + + <!-- Call log WS when the template is loaded. --> + <span core-site-plugins-call-ws-on-load name="mod_choicegroup_view_choicegroup" [params]="{choicegroupid: <% choicegroup.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}"></span> + <%/ choicegroup.open %> +</core-loading> diff --git a/mod/choicegroup/version.php b/mod/choicegroup/version.php new file mode 100644 index 0000000..0e2916b --- /dev/null +++ b/mod/choicegroup/version.php @@ -0,0 +1,36 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013-2015 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020121900; +$plugin->requires = 2018051700; // Moodle 3.5 +$plugin->maturity = MATURITY_STABLE; +$plugin->release = '1.21 for Moodle 3.5-3.10 (Build: 2020121900)'; + +$plugin->component = 'mod_choicegroup'; +$plugin->cron = 0; + diff --git a/mod/choicegroup/view.php b/mod/choicegroup/view.php new file mode 100644 index 0000000..3df9423 --- /dev/null +++ b/mod/choicegroup/view.php @@ -0,0 +1,277 @@ +<?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/>. + +/** + * Version information + * + * @package mod + * @subpackage choicegroup + * @copyright 2013 Université de Lausanne + * @author Nicolas Dunand <Nicolas.Dunand@unil.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../../config.php"); +require_once("lib.php"); +require_once($CFG->dirroot.'/group/lib.php'); +require_once($CFG->libdir . '/completionlib.php'); + +$id = required_param('id', PARAM_INT); // Course Module ID +$action = optional_param('action', '', PARAM_ALPHA); +$userids = optional_param_array('userid', array(), PARAM_INT); // array of attempt ids for delete action +$notify = optional_param('notify', '', PARAM_ALPHA); + +$url = new moodle_url('/mod/choicegroup/view.php', array('id'=>$id)); +if ($action !== '') { + $url->param('action', $action); +} +$PAGE->set_url($url); + +if (! $cm = get_coursemodule_from_id('choicegroup', $id)) { + print_error('invalidcoursemodule'); +} + +if (! $course = $DB->get_record("course", array("id" => $cm->course))) { + print_error('coursemisconf'); +} + +require_login($course, false, $cm); +$PAGE->requires->js_call_amd('mod_choicegroup/choicegroupdatadisplay', 'init'); +if (!$choicegroup = choicegroup_get_choicegroup($cm->instance)) { + print_error('invalidcoursemodule'); +} +$choicegroup_groups = choicegroup_get_groups($choicegroup); +$choicegroup_users = array(); + +$strchoicegroup = get_string('modulename', 'choicegroup'); +$strchoicegroups = get_string('modulenameplural', 'choicegroup'); + +if (!$context = context_module::instance($cm->id)) { + print_error('badcontext'); +} + +$eventparams = array( + 'context' => $context, + 'objectid' => $choicegroup->id +); + +$current = choicegroup_get_user_answer($choicegroup, $USER); +if ($action == 'delchoicegroup' and confirm_sesskey() and is_enrolled($context, null, 'mod/choicegroup:choose') and $choicegroup->allowupdate and !($choicegroup->timeclose and (time() > $choicegroup->timeclose))) { + // user wants to delete his own choice: + if ($current !== false) { + if (groups_is_member($current->id, $USER->id)) { + $currentgroup = $DB->get_record('groups', array('id' => $current->id), 'id,name', MUST_EXIST); + groups_remove_member($current->id, $USER->id); + $event = \mod_choicegroup\event\choice_removed::create($eventparams); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + } + $current = choicegroup_get_user_answer($choicegroup, $USER, false, true); + // Update completion state + $completion = new completion_info($course); + if ($completion->is_enabled($cm) && $choicegroup->completionsubmit) { + $completion->update_state($cm, COMPLETION_INCOMPLETE); + } + } +} + +$PAGE->set_title(format_string($choicegroup->name)); +$PAGE->set_heading($course->fullname); + +/// Mark as viewed +$completion=new completion_info($course); +$completion->set_module_viewed($cm); + +/// Submit any new data if there is any +if (data_submitted() && is_enrolled($context, null, 'mod/choicegroup:choose') && confirm_sesskey()) { + + if ($choicegroup->multipleenrollmentspossible == 1) { + $number_of_groups = optional_param('number_of_groups', '', PARAM_INT); + + for ($i = 0; $i < $number_of_groups; $i++) { + $answer_value = optional_param('answer_' . $i, '', PARAM_INT); + if ($answer_value != '') { + choicegroup_user_submit_response($answer_value, $choicegroup, $USER->id, $course, $cm); + } else { + $answer_value_group_id = optional_param('answer_'.$i.'_groupid', '', PARAM_INT); + if (groups_is_member($answer_value_group_id, $USER->id)) { + $answer_value_group = $DB->get_record('groups', array('id' => $answer_value_group_id), 'id,name', MUST_EXIST); + groups_remove_member($answer_value_group_id, $USER->id); + $event = \mod_choicegroup\event\choice_removed::create($eventparams); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('choicegroup', $choicegroup); + $event->trigger(); + } + } + } + + + } else { // multipleenrollmentspossible != 1 + + $timenow = time(); + if (has_capability('mod/choicegroup:deleteresponses', $context)) { + if ($action == 'delete') { //some responses need to be deleted + choicegroup_delete_responses($userids, $choicegroup, $cm, $course); //delete responses. + redirect("view.php?id=$cm->id"); + } + } + + $answer = optional_param('answer', '', PARAM_INT); + + if (empty($answer)) { + redirect(new moodle_url('/mod/choicegroup/view.php', + array('id' => $cm->id, 'notify' => 'mustchooseone', 'sesskey' => sesskey()))); + } else { + choicegroup_user_submit_response($answer, $choicegroup, $USER->id, $course, $cm); + redirect(new moodle_url('/mod/choicegroup/view.php', + array('id' => $cm->id, 'notify' => 'choicegroupsaved', 'sesskey' => sesskey()))); + } + } +} + + +/// Display the choicegroup and possibly results + + +$event = \mod_choicegroup\event\course_module_viewed::create($eventparams); +$event->add_record_snapshot('course_modules', $cm); +$event->add_record_snapshot('course', $course); +$event->add_record_snapshot('choicegroup', $choicegroup); +$event->trigger(); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($choicegroup->name)); + +if ($notify and confirm_sesskey()) { + if ($notify === 'choicegroupsaved') { + echo $OUTPUT->notification(get_string('choicegroupsaved', 'choicegroup'), 'notifysuccess'); + } else if ($notify === 'mustchooseone') { + echo $OUTPUT->notification(get_string('mustchooseone', 'choicegroup'), 'notifyproblem'); + } +} + +/// Check to see if groups are being used in this choicegroup +$groupmode = groups_get_activity_groupmode($cm); + +if ($groupmode) { + groups_get_activity_group($cm, true); + groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/choicegroup/view.php?id='.$id); +} + +$allresponses = choicegroup_get_response_data($choicegroup, $cm); // Big function, approx 6 SQL calls per user + + +if (has_capability('mod/choicegroup:readresponses', $context)) { + choicegroup_show_reportlink($choicegroup, $allresponses, $cm); +} + +echo '<div class="clearer"></div>'; + +if ($choicegroup->intro) { + echo $OUTPUT->box(format_module_intro('choicegroup', $choicegroup, $cm->id), 'generalbox', 'intro'); +} + +//if user has already made a selection, and they are not allowed to update it, show their selected answer. +if (isloggedin() && ($current !== false) ) { + if ($choicegroup->multipleenrollmentspossible == 1) { + $currents = choicegroup_get_user_answer($choicegroup, $USER, true, true); + + $names = array(); + if (is_array($currents)) { + foreach ($currents as $current) { + $names[] = format_string($current->name); + } + } + $formatted_names = join(' '.get_string("and", "choicegroup").' ', array_filter(array_merge(array(join(', ', array_slice($names, 0, -1))), array_slice($names, -1)))); + echo $OUTPUT->box(get_string("yourselection", "choicegroup", userdate($choicegroup->timeopen)).": ".$formatted_names, 'generalbox', 'yourselection'); + + } else { + echo $OUTPUT->box(get_string("yourselection", "choicegroup", userdate($choicegroup->timeopen)).": ".format_string($current->name), 'generalbox', 'yourselection'); + } +} + +/// Print the form +$choicegroupopen = true; +$timenow = time(); +if ($choicegroup->timeclose !=0) { + if ($choicegroup->timeopen > $timenow ) { + echo $OUTPUT->box(get_string("notopenyet", "choicegroup", userdate($choicegroup->timeopen)), "generalbox notopenyet"); + echo $OUTPUT->footer(); + exit; + } else if ($timenow > $choicegroup->timeclose) { + echo $OUTPUT->box(get_string("expired", "choicegroup", userdate($choicegroup->timeclose)), "generalbox expired"); + $choicegroupopen = false; + } +} + +$options = choicegroup_prepare_options($choicegroup, $USER, $cm, $allresponses); +$renderer = $PAGE->get_renderer('mod_choicegroup'); +if ( (!$current or $choicegroup->allowupdate) and $choicegroupopen and is_enrolled($context, null, 'mod/choicegroup:choose')) { +// They haven't made their choicegroup yet or updates allowed and choicegroup is open + + echo $renderer->display_options($options, $cm->id, $choicegroup->display, $choicegroup->publish, $choicegroup->limitanswers, $choicegroup->showresults, $current, $choicegroupopen, false, $choicegroup->multipleenrollmentspossible); +} else { + // form can not be updated + echo $renderer->display_options($options, $cm->id, $choicegroup->display, $choicegroup->publish, $choicegroup->limitanswers, $choicegroup->showresults, $current, $choicegroupopen, true, $choicegroup->multipleenrollmentspossible); +} +$choicegroupformshown = true; + +$sitecontext = context_system::instance(); + +if (isguestuser()) { + // Guest account + echo $OUTPUT->confirm(get_string('noguestchoose', 'choicegroup').'<br /><br />'.get_string('liketologin'), + get_login_url(), new moodle_url('/course/view.php', array('id'=>$course->id))); +} else if (!is_enrolled($context)) { + // Only people enrolled can make a choicegroup + $SESSION->wantsurl = $FULLME; + $SESSION->enrolcancel = (!empty($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : ''; + + $coursecontext = context_course::instance($course->id); + $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); + + echo $OUTPUT->box_start('generalbox', 'notice'); + echo '<p class="center">'. get_string('notenrolledchoose', 'choicegroup') .'</p>'; + echo $OUTPUT->container_start('continuebutton'); + echo $OUTPUT->single_button(new moodle_url('/enrol/index.php?', array('id'=>$course->id)), get_string('enrolme', 'core_enrol', $courseshortname)); + echo $OUTPUT->container_end(); + echo $OUTPUT->box_end(); + +} + +// print the results at the bottom of the screen +if ( $choicegroup->showresults == CHOICEGROUP_SHOWRESULTS_ALWAYS or + ($choicegroup->showresults == CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER and $current) or + ($choicegroup->showresults == CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE and !$choicegroupopen)) { +} +else if ($choicegroup->showresults == CHOICEGROUP_SHOWRESULTS_NOT) { + echo $OUTPUT->box(get_string('neverresultsviewable', 'choicegroup')); +} +else if ($choicegroup->showresults == CHOICEGROUP_SHOWRESULTS_AFTER_ANSWER && !$current) { + echo $OUTPUT->box(get_string('afterresultsviewable', 'choicegroup')); +} +else if ($choicegroup->showresults == CHOICEGROUP_SHOWRESULTS_AFTER_CLOSE and $choicegroupopen) { + echo $OUTPUT->box(get_string('notyetresultsviewable', 'choicegroup')); +} +else if (!$choicegroupformshown) { + echo $OUTPUT->box(get_string('noresultsviewable', 'choicegroup')); +} + +echo $OUTPUT->footer(); + diff --git a/mod/choicegroup/yui/form/form.js b/mod/choicegroup/yui/form/form.js new file mode 100644 index 0000000..e93ca61 --- /dev/null +++ b/mod/choicegroup/yui/form/form.js @@ -0,0 +1,429 @@ +/** + * This is JavaScript code that handles drawing on mouse events and painting pre-existing drawings. + * @package qtype + * @subpackage freehanddrawing + * @copyright ETHZ LET <jacob.shapiro@let.ethz.ch> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +YUI.add('moodle-mod_choicegroup-form', function(Y) { + var CSS = { + }, + SELECTORS = { + AVAILABLE_GRPS_SELECT: '#availablegroups', + AVAILABLE_GRPS_SELECT_OPTIONS: "select[id='availablegroups'] option", + SELECTED_GRPS_SELECT: '#id_selectedGroups', + ADD_GRP_BTN: '#addGroupButton', + DEL_GRP_BTN: '#removeGroupButton', + LIMIT_UI_INPUT: '#ui_limit_input', + LIMIT_UI_DIV: '#fitem_id_limit_0', + LIMIT_UI_LABEL: '#label_for_limit_ui', + APPLY_LIMIT_TO_ALL_GRPS_BTN: '#id_setlimit', + ENABLE_DISABLE_LIMITING_SELECT: '#id_limitanswers', + EXPAND_ALL_GRPNGS_BTN: '#expandButton', + COLLAPSE_ALL_GRPNGS_BTN: '#collapseButton', + SERIALIZED_SELECTED_GRPS_LIST: '#serializedselectedgroups', + GLOBAL_LIMIT_INPUT: '#id_generallimitation', + HIDDEN_LIMIT_INPUTS: 'input.limit_input_node', + }; + Y.namespace('Moodle.mod_choicegroup.form'); + Y.Moodle.mod_choicegroup.form = { + init: function(formid) { + + // ------------------------------- + // Global Variables + // ------------------------------- + + var CHAR_LIMITUI_PAR_LEFT = M.util.get_string('char_limitui_parenthesis_start', 'choicegroup'); + var CHAR_LIMITUI_PAR_RIGHT = M.util.get_string('char_limitui_parenthesis_end', 'choicegroup'); + var CHAR_SELECT_BULLET_COLLAPSED = M.util.get_string('char_bullet_collapsed', 'choicegroup'); + var CHAR_SELECT_BULLET_EXPANDED = M.util.get_string('char_bullet_expanded', 'choicegroup'); + + + var availableGroupsNode = Y.one(SELECTORS.AVAILABLE_GRPS_SELECT); + var addGroupButtonNode = Y.one(SELECTORS.ADD_GRP_BTN); + var selectedGroupsNode = Y.one(SELECTORS.SELECTED_GRPS_SELECT); + var removeGroupButtonNode = Y.one(SELECTORS.DEL_GRP_BTN); + var formNode = Y.one('#' + formid); + var uiInputLimitNode = Y.one(SELECTORS.LIMIT_UI_INPUT); + var applyLimitToAllGroupsButtonNode = Y.one(SELECTORS.APPLY_LIMIT_TO_ALL_GRPS_BTN); + var limitAnswersSelectNode = Y.one(SELECTORS.ENABLE_DISABLE_LIMITING_SELECT); + var limitInputUIDIVNode = Y.one(SELECTORS.LIMIT_UI_DIV); + var expandButtonNode = Y.one(SELECTORS.EXPAND_ALL_GRPNGS_BTN); + var collapseButtonNode = Y.one(SELECTORS.COLLAPSE_ALL_GRPNGS_BTN); + var serializedSelectedGroupsListNode = Y.one(SELECTORS.SERIALIZED_SELECTED_GRPS_LIST); + + var groupingsNodesContainer = new Array(); + + // -------------------------------- + // Global Functions + // -------------------------------- + + + function removeElementFromArray(ar, from, to) { + var rest = ar.slice((to || from) + 1 || ar.length); + ar.length = from < 0 ? ar.length + from : from; + return ar.push.apply(ar, rest); + } + + function getInputLimitNodeOfSelectedGroupNode(n) { + return Y.one('#group_' + n.get('value') + '_limit'); + } + + function cleanSelectedGroupsList() { + var optionsNodes = Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option"); + optionsNodes.each(function(optNode) { + if (optNode.get('parentNode') != null) { + optNode.setContent(optNode.getContent().replace(/ /gi,'')); + optionsNodes.each(function(opt2Node){ + if ((opt2Node != optNode) && (opt2Node.get('value') == optNode.get('value'))) { + opt2Node.remove(); + } + }); + } + }); + } + + function addOptionNodeToSelectedGroupsList(optNode) { + if (optNode.hasClass('grouping') == true) { + // check if option is collapsed + if (((typeof groupingsNodesContainer[optNode.get('value')]) == 'undefined') || ( groupingsNodesContainer[optNode.get('value')].length == 0)) { + // it is expanded, take nodes from UI + // This is a grouping, so instead of adding this item we actually need to add everything underneath it + var sib = optNode.next(); // sib means sibling, as in, the next element in the DOM tree + while (sib && sib.hasClass('nested') && sib.hasClass('group')) { + // add sib + selectedGroupsNode.append(sib.cloneNode(true)); + // go to next node + sib = sib.next(); + } + } else { + // yes it IS collapsed, need to take the nodes from the container rather than from the UI + groupingsNodesContainer[optNode.get('value')].forEach(function (underlyingGroupNode) { + selectedGroupsNode.append(underlyingGroupNode.cloneNode(true)); + }); + } + } else { + selectedGroupsNode.append(optNode.cloneNode(true)); + } + if (limitAnswersSelectNode.get('value') == '1') { + updateLimitUIOfAllSelectedGroups(); + } + } + + function updateGroupLimit(e) { + var selectedOptionsNodes = Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option:checked"); + // get value of input box + var limit = uiInputLimitNode.get('value'); + selectedOptionsNodes.each(function(optNode) { + getInputLimitNodeOfSelectedGroupNode(optNode).set('value', limit); + updateLimitUIOfSelectedGroup(optNode); + }); + } + + function collapseGrouping(groupingNode) { + // Change the text of this <option> so that it is marked as collapsed: + groupingNode.set('text', CHAR_SELECT_BULLET_COLLAPSED + groupingNode.get('text').substring(1)); + var sib = groupingNode.next(); // sib means sibling, as in, the next element in the DOM tree + while (sib && sib.hasClass('nested') && sib.hasClass('group')) { + // save this node somewhere first + if (typeof groupingsNodesContainer[groupingNode.get('value')] == 'undefined') { + groupingsNodesContainer[groupingNode.get('value')] = new Array(); + } + groupingsNodesContainer[groupingNode.get('value')].push(sib.cloneNode(true)); + // save the next node before removing the current one + var nextSibling = sib.next(); + sib.remove(); + // go to next node + sib = nextSibling; + } + } + + function expandGrouping(groupingNode) { + // Change the text of this <option> so that it is marked as collapsed: + groupingNode.set('text', CHAR_SELECT_BULLET_EXPANDED + groupingNode.get('text').substring(1)); + var nextOpt = groupingNode.next(); + if (typeof groupingsNodesContainer[groupingNode.get('value')] != 'undefined') { + groupingsNodesContainer[groupingNode.get('value')].forEach(function(underlyingGroupNode) { + if (typeof nextOpt != 'undefined') { + availableGroupsNode.insertBefore(underlyingGroupNode, nextOpt); + } else { + availableGroupsNode.appendChild(underlyingGroupNode); + } + }); + groupingsNodesContainer[groupingNode.get('value')] = new Array(); + } + + + } + + function collapseAllGroupings() { + var availableOptionsNodes = Y.all(SELECTORS.AVAILABLE_GRPS_SELECT + " option"); + availableOptionsNodes.each(function(optNode) { + if (optNode.hasClass('grouping') == true) { + collapseGrouping(optNode); + } + }); + } + + function expandAllGroupings() { + var availableOptionsNodes = Y.all(SELECTORS.AVAILABLE_GRPS_SELECT + " option"); + availableOptionsNodes.each(function(optNode) { + if (optNode.hasClass('grouping') == true) { + expandGrouping(optNode); + } + }); + } + + function getGroupNameWithoutLimitText(groupNode) { + var indexOfLimitUIText = groupNode.get('text').indexOf(' ' + CHAR_LIMITUI_PAR_LEFT); + if (indexOfLimitUIText !== -1) { + return groupNode.get('text').substring(0, indexOfLimitUIText); + } else { + return groupNode.get('text'); + } + } + function clearLimitUIFromSelectedGroup(groupNode) { + groupNode.set('text', getGroupNameWithoutLimitText(groupNode)); + } + + function updateLimitUIOfSelectedGroup(groupNode) { + groupNode.set('text', getGroupNameWithoutLimitText(groupNode) + ' ' + CHAR_LIMITUI_PAR_LEFT + getInputLimitNodeOfSelectedGroupNode(groupNode).get('value') + CHAR_LIMITUI_PAR_RIGHT); + } + + function updateLimitUIOfAllSelectedGroups() { + Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option").each(function(optNode) { updateLimitUIOfSelectedGroup(optNode); }); + } + + function clearLimitUIFromAllSelectedGroups() { + Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option").each(function(optNode) { clearLimitUIFromSelectedGroup(optNode); }); + } + + function expandOrCollapseGrouping(groupingNode) { + if (((typeof groupingsNodesContainer[groupingNode.get('value')]) == 'undefined') || ( groupingsNodesContainer[groupingNode.get('value')].length == 0)) { + collapseGrouping(groupingNode); + expandButtonNode.set('disabled', false); + } else { + expandGrouping(groupingNode); + collapseButtonNode.set('disabled', false); + } + } + + getTextWidth = function(text, font) { + // Thanks for http://stackoverflow.com/a/21015393/3430277 + // re-use canvas object for better performance + var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); + var context = canvas.getContext("2d"); + context.font = font; + var metrics = context.measureText(text); + return metrics.width; + }; + + function wasFirstCharacterClicked(e, n) { + // Thanks for http://stackoverflow.com/a/21015393/3430277 + // e is the event, n is the node to check + var style = window.getComputedStyle(n.getDOMNode(), null).getPropertyValue('font'); + if ((e.pageX - e.currentTarget.getX()) <= getTextWidth(n.get('text').charAt(0),style)) { + return true; + } + return false; + } + + // -------------------------------- + // this code happens on form load + // -------------------------------- + if (serializedSelectedGroupsListNode.get('value') != '') { + var selectedGroups = serializedSelectedGroupsListNode.get('value').split(';'); + selectedGroups = selectedGroups.filter(function(n) {return n != '';}); + var availableOptionsNodes = Y.all(SELECTORS.AVAILABLE_GRPS_SELECT + " option"); + availableOptionsNodes.each(function(optNode) { + selectedGroups.forEach(function (selectedGroup) { + if (selectedGroup == optNode.get('value')) { + addOptionNodeToSelectedGroupsList(optNode); + } + }); + }); + cleanSelectedGroupsList(); + } + + + // Collapse all groupings on load + collapseAllGroupings(); + expandButtonNode.set('disabled', false); + // If necessary update their limit information + if (limitAnswersSelectNode.get('value') == '1') { // limiting is enabled, show limit box + updateLimitUIOfAllSelectedGroups(); + } + + // ------------------------------- + // ------------------------------- + + + + + + + // --------------------------------- + // Setup UI Bindings (on load) + // --------------------------------- + + + Y.one('#expandButton').on('click', function(e) { + expandAllGroupings(); + expandButtonNode.set('disabled', true); + collapseButtonNode.set('disabled', false); + + }); + Y.one('#collapseButton').on('click', function(e) { + collapseAllGroupings(); + collapseButtonNode.set('disabled', true); + expandButtonNode.set('disabled', false); + + }); + + + // On click fill in the limit in every field + applyLimitToAllGroupsButtonNode.on('click', function (e) { + // Get the value string + var generalLimitValue = Y.one(SELECTORS.GLOBAL_LIMIT_INPUT).get('value'); + // Make sure we've got an integer value + generalLimitValue = parseInt(generalLimitValue); + if (!isNaN(generalLimitValue)) { + var limitInputNodes = Y.all(SELECTORS.HIDDEN_LIMIT_INPUTS); + limitInputNodes.each(function(n) { n.set('value', generalLimitValue); }); + } else { + alert(M.util.get_string('the_value_you_entered_is_not_a_number', 'choicegroup')); + } + updateLimitUIOfAllSelectedGroups(); + }); + + + + + formNode.on('submit', function(e) { + var selectedOptionsNodes = Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option"); + if (selectedOptionsNodes.size() < 1 && !window.skipClientValidation) { + alert(M.util.get_string('pleaseselectonegroup', 'choicegroup')); + e.preventDefault(); + e.stopPropagation(); + } + var serializedSelection = ''; + selectedOptionsNodes.each(function(optNode) { serializedSelection += ';' + optNode.get('value'); }); + serializedSelectedGroupsListNode.set('value', serializedSelection); + + }); + + + availableGroupsNode.on('click', function(e) { + var selectedOptionsNodes = Y.all(SELECTORS.AVAILABLE_GRPS_SELECT + " option:checked"); + if (selectedOptionsNodes.size() >= 2) { + var allGroupings = true; + selectedOptionsNodes.each(function(optNode){ + if (optNode.hasClass('grouping') == false) { + allGroupings = false; + } + }); + if (allGroupings) { + addGroupButtonNode.setContent(M.util.get_string('add_groupings', 'choicegroup')); + } else { + addGroupButtonNode.setContent(M.util.get_string('add_groups', 'choicegroup')); + } + addGroupButtonNode.set('disabled', false); + + } else if (selectedOptionsNodes.size() >= 1) { + var firstNode = selectedOptionsNodes.item(0); + if (firstNode.hasClass('grouping')) { + addGroupButtonNode.setContent(M.util.get_string('add_grouping', 'choicegroup')); + if (wasFirstCharacterClicked(e, firstNode)) { + expandOrCollapseGrouping(firstNode); + } + + } else { + addGroupButtonNode.setContent(M.util.get_string('add_group', 'choicegroup')); + } + addGroupButtonNode.set('disabled', false); + + } else { + addGroupButtonNode.set('disabled', true); + addGroupButtonNode.setContent(M.util.get_string('add', 'choicegroup')); + } + + }); + Y.delegate('dblclick', function(e) { + if (e.currentTarget.hasClass('grouping') == true) { + expandOrCollapseGrouping(e.currentTarget); + } else { + addOptionNodeToSelectedGroupsList(e.currentTarget); + cleanSelectedGroupsList(); + } + + + }, Y.config.doc, SELECTORS.AVAILABLE_GRPS_SELECT_OPTIONS, this); + + selectedGroupsNode.on('click', function(e) { + var selectedOptionsNodes = Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option:checked"); + if (selectedOptionsNodes.size() >= 2) { + removeGroupButtonNode.setContent(M.util.get_string('del_groups', 'choicegroup')); + removeGroupButtonNode.set('disabled', false); + uiInputLimitNode.set('disabled', true); + //uiInputLimitNode.set('value', 'multiple values'); + limitInputUIDIVNode.hide(); + + } else if (selectedOptionsNodes.size() >= 1) { + removeGroupButtonNode.setContent(M.util.get_string('del_group', 'choicegroup')); + removeGroupButtonNode.set('disabled', false); + uiInputLimitNode.set('disabled', false); + uiInputLimitNode.set('value', getInputLimitNodeOfSelectedGroupNode(selectedOptionsNodes.item(0)).get('value')); + Y.one(SELECTORS.LIMIT_UI_LABEL).set('text', M.util.get_string('set_limit_for_group', 'choicegroup') + getGroupNameWithoutLimitText(selectedOptionsNodes.item(0)) + ":"); + if (limitAnswersSelectNode.get('value') == '1') { // limiting is enabled, show limit box + limitInputUIDIVNode.show(); + } + + + } else { + removeGroupButtonNode.set('disabled', true); + removeGroupButtonNode.setContent(M.util.get_string('del', 'choicegroup')); + uiInputLimitNode.set('disabled', true); + limitInputUIDIVNode.hide(); + } + + }); + + uiInputLimitNode.on('change', function(e) { updateGroupLimit(e); }); + uiInputLimitNode.on('blur', function(e) { updateGroupLimit(e); }); + + + addGroupButtonNode.on('click', function(e) { + var selectedOptionsNodes = Y.all(SELECTORS.AVAILABLE_GRPS_SELECT + " option:checked"); + selectedOptionsNodes.each(function(optNode) { addOptionNodeToSelectedGroupsList(optNode); }); + cleanSelectedGroupsList(); + }); + removeGroupButtonNode.on('click', function(e) { + var selectedOptionsNodes = Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option:checked"); + selectedOptionsNodes.each(function(optNode) { + optNode.remove(); + + }); + }); + + limitAnswersSelectNode.on('change', function(e) { + if (limitAnswersSelectNode.get('value') == '1') { // limiting is enabled, show limit box + var selectedOptionsNodes = Y.all(SELECTORS.SELECTED_GRPS_SELECT + " option:checked"); + if (selectedOptionsNodes.size() == 1) { + limitInputUIDIVNode.show(); + } + updateLimitUIOfAllSelectedGroups(); + + } else { // limiting is disabled + limitInputUIDIVNode.hide(); + clearLimitUIFromAllSelectedGroups(); + } + + }); + + + }, + + + }; +}, '@VERSION@', {requires: ['node', 'event'] }); diff --git a/mod/publication/.gitlab-ci.yml b/mod/publication/.gitlab-ci.yml new file mode 100644 index 0000000..32d0b13 --- /dev/null +++ b/mod/publication/.gitlab-ci.yml @@ -0,0 +1,67 @@ +cache: + paths: + - .composer/cache + +variables: + TRAVIS_BUILD_DIR: "$CI_PROJECT_DIR" + MUSTACHE_IGNORE_NAMES: "email_html_body.mustache, email_html.mustache, email_text.mustache" + COMPOSER_HOME: "$CI_PROJECT_DIR/.composer/cache" + DOCKER_HOST: tcp://localhost:2375 + DOCKER_TLS_CERTDIR: "" + +.postgres: + before_script: + # Wait until database is ready + - timeout 3m bash -c 'echo -e "DB...\c"; until </dev/tcp/127.0.0.1/5432; do echo -e ".\c"; sleep 1; done; echo "ok";' 2> /dev/null + - cd ../.. + - moodle-plugin-ci install --moodle="moodle-upstream-core" --db-user=moodleci --db-pass=moodleing --db-host=127.0.0.1 + services: + - postgres:11 + - docker:dind + + variables: + DB: "pgsql" + POSTGRES_USER: "moodleci" + POSTGRES_PASSWORD: "moodleing" + +.mariadb: + before_script: + # Wait until database is ready + - timeout 3m bash -c 'echo -e "DB...\c"; until </dev/tcp/127.0.0.1/3306; do echo -e ".\c"; sleep 1; done; echo "ok";' 2> /dev/null + - cd ../.. + - moodle-plugin-ci install --moodle="moodle-upstream-core" --db-user=root --db-pass=superrootpass --db-host=127.0.0.1 + services: + - mariadb:10.4-bionic + - docker:dind + + variables: + DB: "mariadb" + MYSQL_ROOT_PASSWORD: "superrootpass" + +.job_template: &job_definition + script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + #- moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit + - moodle-plugin-ci behat + +code_checker_pgsql: + extends: .postgres + <<: *job_definition + image: amcdev/moodle-plugin-ci:7.3-docker-node-14.15.0 + variables: + MOODLE_BRANCH: "MOODLE_310_STABLE" + +code_checker_mariadb: + extends: .mariadb + <<: *job_definition + image: amcdev/moodle-plugin-ci:7.3-docker-node-14.15.0 + variables: + MOODLE_BRANCH: "MOODLE_310_STABLE" \ No newline at end of file diff --git a/mod/publication/CHANGELOG.txt b/mod/publication/CHANGELOG.txt new file mode 100644 index 0000000..a636ac8 --- /dev/null +++ b/mod/publication/CHANGELOG.txt @@ -0,0 +1,213 @@ +CHANGELOG +========= + +3.10.0 (2020-11-11) +------------------ +* Moodle 3.10.0 compatible version +* [FIXED] GitHub #38 - merge Pull request #39 by Christian Wolters - Omit manager in recieveteachernotification cap +* [FIXED] GitHub #41 - remove hardcoded "not approved" + +3.9.0 (2020-06-15) +------------------ +* Moodle 3.9.0 compatible version + +3.8.1 (2020-02-18) +------------------ + +* [BUG] #6594 uploaded files are now approved automatically, if specified in the settings +* [BUG] #6380 pagination now fixed to predefined values (all/10/20/50/100) + + +3.8.0 (2020-01-05) +------------------ + +* [BUG] #6492 fixed bug which caused fatal error when opening reports / dates +* [FEATURE] #4641 table is now sortable by approval selection +* [FIXED] #6381 Fix typo in receiveteachernotification langstring + + +3.7.1 (2019-10-04) +------------------ + +* [FIXED] #6295 Fix typo in receiveteachernotification capability and add langstring for it +* [FIXED] #6333 Images are now shown in the description +* [FIXED] #6250 Fix typo in 'extended' [github pull #32 @germanvaleroelizondo] +* [FIXED] #6209 space added between save approval and revert button +* [FEATURE] #6208 approval select boxes are now left of column instead of center +* [FIXED] #6205 list of files and approval select boxes are now aligned + + +3.7.0 (2019-08-07) +------------------ + +* [BUG] #6207 publication layout bug after changing approval fixed +* [BUG] #6206 notifications with groups fixed +* [FEATURE] #6126 added support for report-editdates +* [FEATURE] #4995 notification on file-status change for student +* [FEATURE] #4730 notification on file-upload for teacher +* [FEATURE] #4731 extended logs + + +3.6.1 (2019-05-15) +------------------ + +* [FIXED] #6015 remove setting to show id +* [FIXED] #6035 auto-reload on groupfilter-change + + +3.6.0 (2019-02-01) +------------------ + +* Moodle 3.6 compatible version +* [FIXED] #5951 remove strong-tags in labels due to flexbox not displaying spaces around them +* [FIXED] #6014 fix help icon related to column status being displayed the column before +* [FEATURE] #5835 context locking now affects publication too (some capabilities were defined as reading instead of writing)! +* [FEATURE] #5758 add new core_userlist_provider methods to privacy provider +* [FEATURE] #6025 privacy API updates (#5758) are now covered by unit tests +* [UPDATE] #5638 update .travis.yml +* [UPDATE] #5952 update accepted file types help text and label +* [UPDATE] #6029 update README.md +* [CHANGED] #5638 strip leading slashes from namespaces and use statements +* [CHANGED] #6025 reorganized unit tests + + +3.5.0 (2018-07-18) +------------------ + +* Moodle 3.5 compatible version +* [CHANGED] #5134 removed german lang file from repository +* [FEATURE] #5386 implemented privacy API + + +3.4.0 (2017-12-13) +------------------ + +* Moodle 3.4 compatible version +* [CHANGED] #4849 use moodleform's hideIf-Method instead of custom JS +* [CHANGED] #4621 prepend user's respectively group's name to onlinetext-file-downloads +* [CHANGED] #4851 reformatted many lines of code (PHPDoc comments, coding style, properties, variables, etc.) +* [CHANGED] added a label to filemanager for uploading files due to future behat tests using it + + +3.3.2 (2017-11-16) +------------------ + +* [FIXED] #4914 error caused for users without capability "mod/publication:upload" by not instantiated files table + + +3.3.1 (2017-09-04) +------------------ + +* [FIXED] #4675 integrated fix for granting extension causing fatal errors (thanks @raad https://github.com/raad) +* [FIXED] warning in "grant extension"-form due to no due date being used +* [FIXED] #4686 fixed template JSON not validating and template HTML as well as template coding style +* [FIXED] fixed/added some PHPDoc comments +* [CHANGED] #4685 updated travis.yml to use moodle-plugin-ci version 2 and run behat tests in firefox and chrome + + +3.3.0 (2017-08-10) +------------------ + +* Moodle 3.3 compatible version +* [FEATURE] #3926 add preview for imported onlinetext-submissions +* [CHANGED] #4432 updated filetype restrictions to work like mod_assign's implementation +* [CHANGED] #3824 show group approval mode elements only when needed +* [CHANGED] #4432 update filetype restrictions to support either extensions or mime types (see mod_assign update) +* [CHANGED] #3905 improve message output if assign to import from has been deleted +* [CHANGED] #4276 fixed upper/lower-case mix of 'student folder' in english language file +* [FIXED] #4647 missing param for group table if no group's in course +* [FIXED] added missing SVG file for questionmark icon +* [FIXED] exception due to no total files in table +* several other small fixes and improvements + + +3.2.2 (2016-04-19) +------------------ + +* [FIXED] #4409 fixed file permission check for one's own files in teamsubmissions for standard group +* [CHANGED] #4276 fix inconsistent plugin naming in english lang strings +* [CHANGED] added local PHPUnit config file + + +3.2.1 (2016-03-23) +------------------ + +* [FIXED] #4368 fixed GROUP BY in SQL causing problems with postgres +* [CHANGED] #4292 added missing PHPDoc comments and fixed code checker warnings and unified some duplicated code in a static method +* [CHANGED] fixed .travis.yml to check against MOODLE_32_STABLE + + +3.2.0 (2016-12-05) +------------------ + +* Moodle 3.2 compatible version + + +3.1.1 (2016-12-05) +------------------ + +* [FEATURE] #3589 Make name and description searchable +* [FEATURE] #3302 add support for importing online-text-submissions (incl. embeded files) +* [FEATURE] #3856 add support for importing team-submissions (incl. online-text- and file-submissions) + + +3.1.0 (2016-06-22) +------------------ + +* Moodle 3.1 compatible version +* [FEATURE] #3237 Sync Assignment-Submissions automatically + + +3.0.1 (2016-06-30) +------------------ + +* [FIXED] #3315 Typo causing warning about undefined variable + + +3.0.0 (2016-05-11) +------------------ + +* Moodle 3.0 compatible version +* [CHANGED] PHP 7 compatibility +* [CHANGED] #3134 Reformatted parts of code (code checker issues) +* [FIXED] #3171 Fix files not being restored correctly + + +2.9.2 (2016-05-12) +------------------ + +* [FIXED] #3171 Problems with restored files not being shown + + +2.9.1 (2016-03-04) +------------------ + +* [FIXED] #3107 German lang strings + + +2.9.0 (2016-01-20) +------------------ + +* Moodle 2.9 compatible version +* [CHANGED] #2495 Replace javascript with AMD modules based on JQuery instead of YUI +* [FIXED] Language strings (fix typos, termini, etc.) +* [FIXED] #2737 Capability publication:upload for submit button +* [FIXED] #2777 Uninitialized variable corrupting ZIP files with debugging enabled +* [FIXED] #2886 Disable assignments with teamsubmissions enabled in publication until + team submissions are supported +* [FIXED] #2875 Usage of fullname function (don't override fullname format anymore) +* [REMOVED] #2495 Unused settings and deprecate unused lang strings +* [REMOVED] Unused cron setting from version.php +* [REMOVED] Unused code + + +2.8.0 (2015-06-24) +------------------ + +* Moodle 2.8 compatible version + + +2.7 (2014-11-30) +---------------- + +* First release for Moodle 2.7 diff --git a/mod/publication/README.md b/mod/publication/README.md new file mode 100644 index 0000000..59e0060 --- /dev/null +++ b/mod/publication/README.md @@ -0,0 +1,102 @@ +[](https://travis-ci.org/academic-moodle-cooperation/moodle-mod_publication) + +Student Folder Module +===================== + +This file is part of the mod_publication plugin for Moodle - <http://moodle.org/> + +*Author:* Hannes Laimer, Philipp Hager, Andreas Windbichler + +*Copyright:* 2014 [Academic Moodle Cooperation](http://www.academic-moodle-cooperation.org) + +*License:* [GNU GPL v3 or later](http://www.gnu.org/copyleft/gpl.html) + + +Description +----------- + +With the Student Folder module students can upload documents which can be made visible by teachers. +This facilitates publication of student's documents in a course and improves exchange of knowledge. + + +Example +------- + +The student folder has two features: on the one hand participants can upload their documents in the +student folder which can be made visible for other students immediately after the upload or after +the teacher's approval. On the other hand it is possible to import documents from the activity +assignment. Here teachers can decide which documents should be visible to all participants or name +individual students to release documents. + + +Requirements +------------ + +The plugin is available for Moodle 2.7+. This version is for Moodle 3.10. + + +Installation +------------ + +* Copy the module code directly to the mod/publication directory. + +* Log into Moodle as administrator. + +* Open the administration area (http://your-moodle-site/admin) to start the installation + automatically. + + +Admin Settings +-------------- + +An administrator can adjust the default settings for the student folder instance-wide in the +general settings page. There he can specify the following: + +* Require activity description +* Obtain approval +* Approved by default +* Maximum number of attachments +* Maximum attachment size +* Hide ID-Number + + +Documentation +------------- + +You can find a cheat sheet for the plugin on the [AMC +website](https://www.academic-moodle-cooperation.org/en/module/studentfolder/) and a video tutorial +in german only in the [AMC YouTube Channel](https://www.youtube.com/c/AMCAcademicMoodleCooperation). + + +Bug Reports / Support +--------------------- + +We try our best to deliver bug-free plugins, but we can not test the plugin for every platform, +database, PHP and Moodle version. If you find any bug please report it on +[GitHub](https://github.com/academic-moodle-cooperation/moodle-mod_publication/issues). Please +provide a detailed bug description, including the plugin and Moodle version and, if applicable, a +screenshot. + +You may also file a request for enhancement on GitHub. If we consider the request generally useful +and if it can be implemented with reasonable effort we might implement it in a future version. + +You may also post general questions on the plugin on GitHub, but note that we do not have the +resources to provide detailed support. + + +License +------- + +This plugin 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. + +The plugin 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 with Moodle. If not, see +<http://www.gnu.org/licenses/>. + + +Good luck and have fun! diff --git a/mod/publication/amd/build/alignrows.min.js b/mod/publication/amd/build/alignrows.min.js new file mode 100644 index 0000000..6382b3b --- /dev/null +++ b/mod/publication/amd/build/alignrows.min.js @@ -0,0 +1,2 @@ +define ("mod_publication/alignrows",["jquery"],function(a){var b=new function Alignrows(){this.cmid=0};b.initializer=function(){a("#attempts").ready(function(){var b=a("#attempts > tbody > tr > td > table > tbody > tr > td"),c=Math.max.apply(null,b.map(function(){return a(this).height()}).get());b.height(c).css("vertical-align","middle");a(".permissionstable > tbody > tr > td").removeClass("c0")})};return b}); +//# sourceMappingURL=alignrows.min.js.map diff --git a/mod/publication/amd/build/alignrows.min.js.map b/mod/publication/amd/build/alignrows.min.js.map new file mode 100644 index 0000000..790ab24 --- /dev/null +++ b/mod/publication/amd/build/alignrows.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/alignrows.js"],"names":["define","$","instance","Alignrows","cmid","initializer","ready","alltds","maxHeight","Math","max","apply","map","height","get","css","removeClass"],"mappings":"AA2BAA,OAAM,6BAAC,CAAC,QAAD,CAAD,CAAa,SAASC,CAAT,CAAY,IAUvBC,CAAAA,CAAQ,CAAG,GAJC,SAAZC,CAAAA,SAAY,EAAW,CACvB,KAAKC,IAAL,CAAY,CACf,CAR0B,CAW3BF,CAAQ,CAACG,WAAT,CAAuB,UAAW,CAC9BJ,CAAC,CAAC,WAAD,CAAD,CAAeK,KAAf,CAAqB,UAAY,IACzBC,CAAAA,CAAM,CAAGN,CAAC,CAAC,uDAAD,CADe,CAEzBO,CAAS,CAAGC,IAAI,CAACC,GAAL,CAASC,KAAT,CAAe,IAAf,CAAqBJ,CAAM,CAACK,GAAP,CAAW,UAAY,CACxD,MAAOX,CAAAA,CAAC,CAAC,IAAD,CAAD,CAAQY,MAAR,EACV,CAFoC,EAElCC,GAFkC,EAArB,CAFa,CAK7BP,CAAM,CAACM,MAAP,CAAcL,CAAd,EAAyBO,GAAzB,CAA6B,gBAA7B,CAA+C,QAA/C,EACAd,CAAC,CAAC,qCAAD,CAAD,CAAyCe,WAAzC,CAAqD,IAArD,CACH,CAPD,CAQH,CATD,CAUA,MAAOd,CAAAA,CACV,CAtBK,CAAN","sourcesContent":["// This file is part of mod_grouptool for Moodle - http://moodle.org/\n//\n// It is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// It is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript to align rows\n *\n * @package mod_publication\n * @author Hannes Laimer\n * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n /**\n * @module mod_publication/alignrows\n */\ndefine(['jquery'], function($) {\n\n /**\n * @constructor\n * @alias module:mod_publication/alignrows\n */\n var Alignrows = function() {\n this.cmid = 0;\n };\n\n var instance = new Alignrows();\n instance.initializer = function() {\n $(\"#attempts\").ready(function () {\n var alltds = $(\"#attempts > tbody > tr > td > table > tbody > tr > td\");\n var maxHeight = Math.max.apply(null, alltds.map(function () {\n return $(this).height();\n }).get());\n alltds.height(maxHeight).css('vertical-align', 'middle');\n $(\".permissionstable > tbody > tr > td\").removeClass('c0');\n });\n };\n return instance;\n});"],"file":"alignrows.min.js"} \ No newline at end of file diff --git a/mod/publication/amd/build/filesform.min.js b/mod/publication/amd/build/filesform.min.js new file mode 100644 index 0000000..0722419 --- /dev/null +++ b/mod/publication/amd/build/filesform.min.js @@ -0,0 +1,2 @@ +define ("mod_publication/filesform",["jquery","core/log"],function(a,b){var c=new function Filesform(){this.form=a("#fastg");this.menuaction=a("#menuaction");this.usersel=a(".userselection")};c.initializer=function(){b.info("Initialize filesform JS!","mod_publication");c.form.on("submit",function(){if("zipusers"===c.menuaction.val()){setTimeout(function(){c.usersel.prop("checked",!1)},100)}})};return c}); +//# sourceMappingURL=filesform.min.js.map diff --git a/mod/publication/amd/build/filesform.min.js.map b/mod/publication/amd/build/filesform.min.js.map new file mode 100644 index 0000000..3cb7aeb --- /dev/null +++ b/mod/publication/amd/build/filesform.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/filesform.js"],"names":["define","$","log","instance","Filesform","form","menuaction","usersel","initializer","info","on","val","setTimeout","prop"],"mappings":"AA6BAA,OAAM,6BAAC,CAAC,QAAD,CAAW,UAAX,CAAD,CAAyB,SAASC,CAAT,CAAYC,CAAZ,CAAiB,IAYxCC,CAAAA,CAAQ,CAAG,GANC,SAAZC,CAAAA,SAAY,EAAW,CACvB,KAAKC,IAAL,CAAYJ,CAAC,CAAC,QAAD,CAAb,CACA,KAAKK,UAAL,CAAkBL,CAAC,CAAC,aAAD,CAAnB,CACA,KAAKM,OAAL,CAAeN,CAAC,CAAC,gBAAD,CACnB,CAV2C,CAc5CE,CAAQ,CAACK,WAAT,CAAuB,UAAW,CAC9BN,CAAG,CAACO,IAAJ,CAAS,0BAAT,CAAqC,iBAArC,EACAN,CAAQ,CAACE,IAAT,CAAcK,EAAd,CAAiB,QAAjB,CAA2B,UAAW,CAClC,GAAkC,UAA9B,GAAAP,CAAQ,CAACG,UAAT,CAAoBK,GAApB,EAAJ,CAA8C,CAC1CC,UAAU,CAAC,UAAW,CAClBT,CAAQ,CAACI,OAAT,CAAiBM,IAAjB,CAAsB,SAAtB,IACH,CAFS,CAEP,GAFO,CAGb,CACJ,CAND,CAOH,CATD,CAWA,MAAOV,CAAAA,CACV,CA1BK,CAAN","sourcesContent":["// This file is part of mod_publication for Moodle - http://moodle.org/\n//\n// It is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// It is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Resets checked checkboxes after ZIP file was loaded!\n *\n * @package mod_publication\n * @author Philipp Hager\n * @author Hannes Laimer\n * @author Andreas Windbichler\n * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module mod_publication/filesform\n */\ndefine(['jquery', 'core/log'], function($, log) {\n\n /**\n * @constructor\n * @alias module:mod_publication/modform\n */\n var Filesform = function() {\n this.form = $('#fastg');\n this.menuaction = $('#menuaction');\n this.usersel = $('.userselection');\n };\n\n var instance = new Filesform();\n\n instance.initializer = function() {\n log.info('Initialize filesform JS!', 'mod_publication');\n instance.form.on('submit', function() {\n if (instance.menuaction.val() === 'zipusers') {\n setTimeout(function() {\n instance.usersel.prop('checked', false);\n }, 100);\n }\n });\n };\n\n return instance;\n});\n"],"file":"filesform.min.js"} \ No newline at end of file diff --git a/mod/publication/amd/build/groupapprovalstatus.min.js b/mod/publication/amd/build/groupapprovalstatus.min.js new file mode 100644 index 0000000..333176a --- /dev/null +++ b/mod/publication/amd/build/groupapprovalstatus.min.js @@ -0,0 +1,2 @@ +define ("mod_publication/groupapprovalstatus",["jquery","core/modal_factory","core/str","core/templates","core/log"],function(a,b,c,d,e){var f=new function Groupapprovalstatus(){this.id=""};f.initializer=function(g){f.id=g.id;f.mode=g.mode;e.info("Initialize groupapprovalstatus JS!","mod_publication");if(!f.modal){f.modalpromise=b.create({type:b.types.DEFAULT,body:"..."})}c.get_string("filedetails","mod_publication").done(function(b){e.info("Done loading strings...","mod_publication");f.modalpromise.done(function(c){e.info("Done preparing modal","mod_publication");f.modal=c;a(".path-mod-publication .statustable .approvaldetails *").click(function(c){c.stopPropagation();var e=a(c.target),g=e.parent(),h;try{h=g.data("approved")}catch(a){h=[]}var i;try{i=g.data("rejected")}catch(a){i=[]}var j;try{j=g.data("pending")}catch(a){j=[]}var k;try{k=b+" "+g.data("filename")}catch(a){k=b}var l;try{l=g.data("status")}catch(a){l={approved:!1,rejected:!1,pending:!1}}var m={id:f.id,mode:f.mode,status:l,approved:h,rejected:i,pending:j},n=d.render("mod_publication/approvaltooltip",m);n.done(function(a){f.modal.setTitle(k);f.modal.setBody(a);f.modal.show()}).fail(function(a){f.modal.setBody(a.message);f.modal.show()})});a(".path-mod-publication .statustable .approvaldetails").fadeIn("slow")})}).fail(function(a){e.error("Error getting strings: "+a,"mod_publication")})};return f}); +//# sourceMappingURL=groupapprovalstatus.min.js.map diff --git a/mod/publication/amd/build/groupapprovalstatus.min.js.map b/mod/publication/amd/build/groupapprovalstatus.min.js.map new file mode 100644 index 0000000..92ef498 --- /dev/null +++ b/mod/publication/amd/build/groupapprovalstatus.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/groupapprovalstatus.js"],"names":["define","$","ModalFactory","str","templates","log","instance","Groupapprovalstatus","id","initializer","config","mode","info","modal","modalpromise","create","type","types","DEFAULT","body","get_string","done","s","click","e","stopPropagation","element","target","dataelement","parent","approved","data","ex","rejected","pending","filename","stat","context","status","promise","render","source","setTitle","setBody","show","fail","message","fadeIn","error"],"mappings":"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","sourcesContent":["// This file is part of mod_publication for Moodle - http://moodle.org/\n//\n// It is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// It is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * JS showing detailed infos about user's approval status for group approvals in a modal window\n *\n * @package mod_publication\n * @author Philipp Hager\n * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module mod_publication/groupapprovalstatus\n */\ndefine(['jquery', 'core/modal_factory', 'core/str', 'core/templates', 'core/log'], function($, ModalFactory, str, templates, log) {\n\n /**\n * @constructor\n * @alias module:mod_publication/groupapprovalstatus\n */\n var Groupapprovalstatus = function() {\n this.id = '';\n };\n\n var instance = new Groupapprovalstatus();\n\n /**\n * Initialises the JavaScript for publication's group approval status tooltips\n *\n *\n * @param {Object} config The configuration\n */\n instance.initializer = function(config) {\n instance.id = config.id;\n instance.mode = config.mode;\n\n log.info('Initialize groupapprovalstatus JS!', 'mod_publication');\n\n // Prepare modal object!\n if (!instance.modal) {\n instance.modalpromise = ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n body: '...'\n });\n }\n\n str.get_string('filedetails', 'mod_publication').done(function(s) {\n log.info('Done loading strings...', 'mod_publication');\n instance.modalpromise.done(function(modal) {\n log.info('Done preparing modal', 'mod_publication');\n instance.modal = modal;\n $('.path-mod-publication .statustable .approvaldetails *').click(function(e) {\n e.stopPropagation();\n var element = $(e.target);\n\n var dataelement = element.parent();\n\n var approved;\n try {\n approved = dataelement.data('approved');\n } catch (ex) {\n approved = [];\n }\n\n var rejected;\n try {\n rejected = dataelement.data('rejected');\n } catch (ex) {\n rejected = [];\n }\n\n var pending;\n try {\n pending = dataelement.data('pending');\n } catch (ex) {\n pending = [];\n }\n\n var filename;\n try {\n filename = s + ' ' + dataelement.data('filename');\n } catch (ex) {\n filename = s;\n }\n\n var stat;\n try {\n stat = dataelement.data('status');\n } catch (ex) {\n stat = {\n approved: false,\n rejected: false,\n pending: false\n };\n }\n\n var context = {\n id: instance.id,\n mode: instance.mode,\n status: stat,\n approved: approved,\n rejected: rejected,\n pending: pending\n };\n\n // This will call the function to load and render our template.\n var promise = templates.render('mod_publication/approvaltooltip', context);\n\n // How we deal with promise objects is by adding callbacks.\n promise.done(function(source) {\n // Here eventually I have my compiled template, and any javascript that it generated.\n instance.modal.setTitle(filename);\n instance.modal.setBody(source);\n instance.modal.show();\n }).fail(function(ex) {\n // Deal with this exception (I recommend core/notify exception function for this).\n instance.modal.setBody(ex.message);\n instance.modal.show();\n });\n });\n // Everything is prepared, fade the symbols in!\n $('.path-mod-publication .statustable .approvaldetails').fadeIn('slow');\n });\n }).fail(function(ex) {\n log.error('Error getting strings: ' + ex, 'mod_publication');\n });\n };\n\n return instance;\n});\n"],"file":"groupapprovalstatus.min.js"} \ No newline at end of file diff --git a/mod/publication/amd/build/onlinetextpreview.min.js b/mod/publication/amd/build/onlinetextpreview.min.js new file mode 100644 index 0000000..decbfcf --- /dev/null +++ b/mod/publication/amd/build/onlinetextpreview.min.js @@ -0,0 +1,2 @@ +define ("mod_publication/onlinetextpreview",["jquery","core/modal_factory","core/str","core/ajax","core/log","core/notification"],function(a,b,c,d,e,f){var g=new function Onlinetextpreview(){this.cmid=""};g.initializer=function(h){g.cmid=h.cmid;e.info("Initialize onlinetextpreview JS!","mod_publication");if(!g.modal){g.modalpromise=b.create({type:b.types.DEFAULT,large:!0})}c.get_strings([{key:"preview",component:"core"},{key:"onlinetextfilename",component:"assignsubmission_onlinetext"},{key:"from",component:"core"}]).done(function(b){e.info("Done loading strings...","mod_publication");g.modalpromise.done(function(c){e.info("Done preparing modal","mod_publication");g.modal=c;a(".path-mod-publication table.publications .onlinetextpreview *").click(function(c){c.stopPropagation();c.preventDefault();var e=a(c.target),h=e.parent(),i;try{i=h.data("itemid")}catch(a){f.exception(a)}d.call([{methodname:"mod_publication_get_onlinetextpreview",args:{itemid:i,cmid:g.cmid},done:function done(a){var c="";if(h.data("itemname").length){c=" "+b[2].toLowerCase()+" "+h.data("itemname")}g.modal.setTitle(b[0]+" "+b[1]+c);g.modal.setBody(a);g.modal.show()},fail:function fail(a){f.exception(a)}}])})})}).fail(function(a){e.error("Error getting strings: "+a,"mod_publication")})};return g}); +//# sourceMappingURL=onlinetextpreview.min.js.map diff --git a/mod/publication/amd/build/onlinetextpreview.min.js.map b/mod/publication/amd/build/onlinetextpreview.min.js.map new file mode 100644 index 0000000..f22690a --- /dev/null +++ b/mod/publication/amd/build/onlinetextpreview.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/onlinetextpreview.js"],"names":["define","$","ModalFactory","str","ajax","log","notification","instance","Onlinetextpreview","cmid","initializer","config","info","modal","modalpromise","create","type","types","DEFAULT","large","get_strings","key","component","done","s","click","e","stopPropagation","preventDefault","element","target","dataelement","parent","itemid","data","ex","exception","call","methodname","args","itemname","length","toLowerCase","setTitle","setBody","show","fail","error"],"mappings":"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","sourcesContent":["// This file is part of mod_publication for Moodle - http://moodle.org/\n//\n// It is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// It is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * JS showing detailed infos about user's approval status for group approvals in a modal window\n *\n * @package mod_publication\n * @author Philipp Hager\n * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module mod_publication/onlinetextpreview\n */\ndefine(['jquery', 'core/modal_factory', 'core/str', 'core/ajax', 'core/log', 'core/notification'], function($,\n ModalFactory, str, ajax, log, notification) {\n\n /**\n * @constructor\n * @alias module:mod_publication/Onlinetextpreview\n */\n var Onlinetextpreview = function() {\n this.cmid = '';\n };\n\n var instance = new Onlinetextpreview();\n\n /**\n * Initialises the JavaScript for publication's group approval status tooltips\n *\n *\n * @param {Object} config The configuration\n */\n instance.initializer = function(config) {\n instance.cmid = config.cmid;\n\n log.info('Initialize onlinetextpreview JS!', 'mod_publication');\n\n // Prepare modal object!\n if (!instance.modal) {\n instance.modalpromise = ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n large: true\n });\n }\n\n str.get_strings([\n {key: 'preview', component: 'core'},\n {key: 'onlinetextfilename', component: 'assignsubmission_onlinetext'},\n {key: 'from', component: 'core'}\n ]).done(function(s) {\n log.info('Done loading strings...', 'mod_publication');\n instance.modalpromise.done(function(modal) {\n log.info('Done preparing modal', 'mod_publication');\n instance.modal = modal;\n $('.path-mod-publication table.publications .onlinetextpreview *').click(function(e) {\n e.stopPropagation();\n e.preventDefault();\n var element = $(e.target);\n\n var dataelement = element.parent();\n\n var itemid;\n try {\n itemid = dataelement.data('itemid');\n } catch (ex) {\n notification.exception(ex);\n }\n\n ajax.call([\n {\n methodname: 'mod_publication_get_onlinetextpreview',\n args: {itemid: itemid, cmid: instance.cmid},\n done: function(data) {\n var itemname = '';\n if (dataelement.data('itemname').length) {\n itemname = ' ' + s[2].toLowerCase() + ' ' + dataelement.data('itemname');\n }\n instance.modal.setTitle(s[0] + ' ' + s[1] + itemname);\n instance.modal.setBody(data);\n instance.modal.show();\n },\n fail: function(ex) {\n notification.exception(ex);\n }\n }\n ]);\n });\n });\n }).fail(function(ex) {\n log.error('Error getting strings: ' + ex, 'mod_publication');\n });\n };\n\n return instance;\n});\n"],"file":"onlinetextpreview.min.js"} \ No newline at end of file diff --git a/mod/publication/amd/src/alignrows.js b/mod/publication/amd/src/alignrows.js new file mode 100644 index 0000000..c8fbef4 --- /dev/null +++ b/mod/publication/amd/src/alignrows.js @@ -0,0 +1,50 @@ +// This file is part of mod_grouptool for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Javascript to align rows + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + /** + * @module mod_publication/alignrows + */ +define(['jquery'], function($) { + + /** + * @constructor + * @alias module:mod_publication/alignrows + */ + var Alignrows = function() { + this.cmid = 0; + }; + + var instance = new Alignrows(); + instance.initializer = function() { + $("#attempts").ready(function () { + var alltds = $("#attempts > tbody > tr > td > table > tbody > tr > td"); + var maxHeight = Math.max.apply(null, alltds.map(function () { + return $(this).height(); + }).get()); + alltds.height(maxHeight).css('vertical-align', 'middle'); + $(".permissionstable > tbody > tr > td").removeClass('c0'); + }); + }; + return instance; +}); \ No newline at end of file diff --git a/mod/publication/amd/src/filesform.js b/mod/publication/amd/src/filesform.js new file mode 100644 index 0000000..7ef970c --- /dev/null +++ b/mod/publication/amd/src/filesform.js @@ -0,0 +1,56 @@ +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Resets checked checkboxes after ZIP file was loaded! + * + * @package mod_publication + * @author Philipp Hager + * @author Hannes Laimer + * @author Andreas Windbichler + * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * @module mod_publication/filesform + */ +define(['jquery', 'core/log'], function($, log) { + + /** + * @constructor + * @alias module:mod_publication/modform + */ + var Filesform = function() { + this.form = $('#fastg'); + this.menuaction = $('#menuaction'); + this.usersel = $('.userselection'); + }; + + var instance = new Filesform(); + + instance.initializer = function() { + log.info('Initialize filesform JS!', 'mod_publication'); + instance.form.on('submit', function() { + if (instance.menuaction.val() === 'zipusers') { + setTimeout(function() { + instance.usersel.prop('checked', false); + }, 100); + } + }); + }; + + return instance; +}); diff --git a/mod/publication/amd/src/groupapprovalstatus.js b/mod/publication/amd/src/groupapprovalstatus.js new file mode 100644 index 0000000..d130978 --- /dev/null +++ b/mod/publication/amd/src/groupapprovalstatus.js @@ -0,0 +1,143 @@ +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * JS showing detailed infos about user's approval status for group approvals in a modal window + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * @module mod_publication/groupapprovalstatus + */ +define(['jquery', 'core/modal_factory', 'core/str', 'core/templates', 'core/log'], function($, ModalFactory, str, templates, log) { + + /** + * @constructor + * @alias module:mod_publication/groupapprovalstatus + */ + var Groupapprovalstatus = function() { + this.id = ''; + }; + + var instance = new Groupapprovalstatus(); + + /** + * Initialises the JavaScript for publication's group approval status tooltips + * + * + * @param {Object} config The configuration + */ + instance.initializer = function(config) { + instance.id = config.id; + instance.mode = config.mode; + + log.info('Initialize groupapprovalstatus JS!', 'mod_publication'); + + // Prepare modal object! + if (!instance.modal) { + instance.modalpromise = ModalFactory.create({ + type: ModalFactory.types.DEFAULT, + body: '...' + }); + } + + str.get_string('filedetails', 'mod_publication').done(function(s) { + log.info('Done loading strings...', 'mod_publication'); + instance.modalpromise.done(function(modal) { + log.info('Done preparing modal', 'mod_publication'); + instance.modal = modal; + $('.path-mod-publication .statustable .approvaldetails *').click(function(e) { + e.stopPropagation(); + var element = $(e.target); + + var dataelement = element.parent(); + + var approved; + try { + approved = dataelement.data('approved'); + } catch (ex) { + approved = []; + } + + var rejected; + try { + rejected = dataelement.data('rejected'); + } catch (ex) { + rejected = []; + } + + var pending; + try { + pending = dataelement.data('pending'); + } catch (ex) { + pending = []; + } + + var filename; + try { + filename = s + ' ' + dataelement.data('filename'); + } catch (ex) { + filename = s; + } + + var stat; + try { + stat = dataelement.data('status'); + } catch (ex) { + stat = { + approved: false, + rejected: false, + pending: false + }; + } + + var context = { + id: instance.id, + mode: instance.mode, + status: stat, + approved: approved, + rejected: rejected, + pending: pending + }; + + // This will call the function to load and render our template. + var promise = templates.render('mod_publication/approvaltooltip', context); + + // How we deal with promise objects is by adding callbacks. + promise.done(function(source) { + // Here eventually I have my compiled template, and any javascript that it generated. + instance.modal.setTitle(filename); + instance.modal.setBody(source); + instance.modal.show(); + }).fail(function(ex) { + // Deal with this exception (I recommend core/notify exception function for this). + instance.modal.setBody(ex.message); + instance.modal.show(); + }); + }); + // Everything is prepared, fade the symbols in! + $('.path-mod-publication .statustable .approvaldetails').fadeIn('slow'); + }); + }).fail(function(ex) { + log.error('Error getting strings: ' + ex, 'mod_publication'); + }); + }; + + return instance; +}); diff --git a/mod/publication/amd/src/onlinetextpreview.js b/mod/publication/amd/src/onlinetextpreview.js new file mode 100644 index 0000000..40ed389 --- /dev/null +++ b/mod/publication/amd/src/onlinetextpreview.js @@ -0,0 +1,109 @@ +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * JS showing detailed infos about user's approval status for group approvals in a modal window + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2020 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * @module mod_publication/onlinetextpreview + */ +define(['jquery', 'core/modal_factory', 'core/str', 'core/ajax', 'core/log', 'core/notification'], function($, + ModalFactory, str, ajax, log, notification) { + + /** + * @constructor + * @alias module:mod_publication/Onlinetextpreview + */ + var Onlinetextpreview = function() { + this.cmid = ''; + }; + + var instance = new Onlinetextpreview(); + + /** + * Initialises the JavaScript for publication's group approval status tooltips + * + * + * @param {Object} config The configuration + */ + instance.initializer = function(config) { + instance.cmid = config.cmid; + + log.info('Initialize onlinetextpreview JS!', 'mod_publication'); + + // Prepare modal object! + if (!instance.modal) { + instance.modalpromise = ModalFactory.create({ + type: ModalFactory.types.DEFAULT, + large: true + }); + } + + str.get_strings([ + {key: 'preview', component: 'core'}, + {key: 'onlinetextfilename', component: 'assignsubmission_onlinetext'}, + {key: 'from', component: 'core'} + ]).done(function(s) { + log.info('Done loading strings...', 'mod_publication'); + instance.modalpromise.done(function(modal) { + log.info('Done preparing modal', 'mod_publication'); + instance.modal = modal; + $('.path-mod-publication table.publications .onlinetextpreview *').click(function(e) { + e.stopPropagation(); + e.preventDefault(); + var element = $(e.target); + + var dataelement = element.parent(); + + var itemid; + try { + itemid = dataelement.data('itemid'); + } catch (ex) { + notification.exception(ex); + } + + ajax.call([ + { + methodname: 'mod_publication_get_onlinetextpreview', + args: {itemid: itemid, cmid: instance.cmid}, + done: function(data) { + var itemname = ''; + if (dataelement.data('itemname').length) { + itemname = ' ' + s[2].toLowerCase() + ' ' + dataelement.data('itemname'); + } + instance.modal.setTitle(s[0] + ' ' + s[1] + itemname); + instance.modal.setBody(data); + instance.modal.show(); + }, + fail: function(ex) { + notification.exception(ex); + } + } + ]); + }); + }); + }).fail(function(ex) { + log.error('Error getting strings: ' + ex, 'mod_publication'); + }); + }; + + return instance; +}); diff --git a/mod/publication/backup/moodle2/backup_publication_activity_task.class.php b/mod/publication/backup/moodle2/backup_publication_activity_task.class.php new file mode 100644 index 0000000..f2b0480 --- /dev/null +++ b/mod/publication/backup/moodle2/backup_publication_activity_task.class.php @@ -0,0 +1,80 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * backup/moodle2/backup_publication_activity_task.class.php + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/mod/publication/backup/moodle2/backup_publication_stepslib.php'); + +/** + * Class contains backup steps definition + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_publication_activity_task extends backup_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + $this->add_step(new backup_publication_activity_structure_step('publication_structure', 'publication.xml')); + } + + /** + * Code the transformations to perform in the activity in + * order to get transportable (encoded) links + * + * @param string $content + * @return string + */ + static public function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot, "/"); + + $search = "/(" . $base . "\/mod\/publication\/index.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@PUBLICATIONINDEX*$2@$', $content); + + $search = "/(" . $base . "\/mod\/publication\/view.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@PUBLICATIONVIEWBYID*$2@$', $content); + + return $content; + } + +} + diff --git a/mod/publication/backup/moodle2/backup_publication_stepslib.php b/mod/publication/backup/moodle2/backup_publication_stepslib.php new file mode 100644 index 0000000..c640166 --- /dev/null +++ b/mod/publication/backup/moodle2/backup_publication_stepslib.php @@ -0,0 +1,121 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * backup/moodle2/backup_publication_stepslieb.php + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class used to design mod_publications data structure to back up + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_publication_activity_structure_step extends backup_activity_structure_step { + + /** + * Define the structure for the publication activity + * + * @return backup_nested_element + */ + protected function define_structure() { + + // To know if we are including userinfo. + $userinfo = $this->get_setting_value('userinfo'); + + // Define each element separated. + $publication = new backup_nested_element('publication', ['id'], [ + 'name', + 'intro', + 'introformat', + 'alwaysshowdescription', + 'duedate', + 'allowsubmissionsfromdate', + 'timemodified', + 'cutoffdate', + 'mode', + 'importfrom', + 'obtainstudentapproval', + 'maxfiles', + 'maxbytes', + 'allowedfiletypes', + 'obtainteacherapproval', + 'notifyteacher', + 'notifystudents' + ]); + + $extduedates = new backup_nested_element('extduedates'); + + $extduedate = new backup_nested_element('extduedate', ['id'], [ + 'userid', + 'publication', + 'extensionduedate' + ]); + + $files = new backup_nested_element('files'); + + $file = new backup_nested_element('file', ['id'], [ + 'userid', + 'timecreated', + 'fileid', + 'filename', + 'contenthash', + 'type', + 'teacherapproval', + 'studentapproval' + ]); + + // Define sources. + $publication->set_source_table('publication', ['id' => backup::VAR_ACTIVITYID]); + + if ($userinfo) { + // Build the tree. + $publication->add_child($extduedates); + $extduedates->add_child($extduedate); + $publication->add_child($files); + $files->add_child($file); + + $extduedate->set_source_table('publication_extduedates', ['publication' => backup::VAR_PARENTID]); + + $file->set_source_table('publication_file', ['publication' => backup::VAR_PARENTID]); + + $file->annotate_files('mod_publication', 'attachment', null); + + // Define id annotations. + $extduedate->annotate_ids('user', 'userid'); + $file->annotate_ids('user', 'userid'); + + // Define file annotations. + // This file area hasn't itemid. + $publication->annotate_files('mod_publication', 'attachment', null); + } + + // Return the root element (publication), wrapped into standard activity structure. + + return $this->prepare_activity_structure($publication); + } +} diff --git a/mod/publication/backup/moodle2/restore_publication_activity_task.class.php b/mod/publication/backup/moodle2/restore_publication_activity_task.class.php new file mode 100644 index 0000000..fc96caf --- /dev/null +++ b/mod/publication/backup/moodle2/restore_publication_activity_task.class.php @@ -0,0 +1,129 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * backup/moodle2/restore_publication_activity_task.class.php + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/mod/publication/backup/moodle2/restore_publication_stepslib.php'); + +/** + * Class to define restoration activity data structure + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_publication_activity_task extends restore_activity_task { + + /** + * Define (add) particular settings this activity can have. + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have. + */ + protected function define_my_steps() { + // Assignment only has one structure step. + $this->add_step(new restore_publication_activity_structure_step('publication_structure', 'publication.xml')); + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder. + * + * @return array + */ + static public function define_decode_contents() { + $contents = []; + + $contents[] = new restore_decode_content('publication', ['intro'], 'publication'); + + return $contents; + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder. + * + * @return array of restore_decode_rule + */ + static public function define_decode_rules() { + $rules = []; + + $rules[] = new restore_decode_rule('PUBLICATIONVIEWBYID', + '/mod/publication/view.php?id=$1', + 'course_module'); + $rules[] = new restore_decode_rule('PUBLICATIONINDEX', + '/mod/publication/index.php?id=$1', + 'course_module'); + + return $rules; + + } + + /** + * Define the restore log rules that will be applied + * by the {@link restore_logs_processor} when restoring + * assign logs. It must return one array + * of {@link restore_log_rule} objects. + * + * @return array of restore_log_rule + */ + static public function define_restore_log_rules() { + $rules = []; + + $rules[] = new restore_log_rule('publication', 'add', 'view.php?id={course_module}', '{publication}'); + $rules[] = new restore_log_rule('publication', 'update', 'view.php?id={course_module}', '{publication}'); + $rules[] = new restore_log_rule('publication', 'view', 'view.php?id={course_module}', '{publication}'); + + return $rules; + } + + /** + * Define the restore log rules that will be applied + * by the {@link restore_logs_processor} when restoring + * course logs. It must return one array + * of {@link restore_log_rule} objects + * + * Note this rules are applied when restoring course logs + * by the restore final task, but are defined here at + * activity level. All them are rules not linked to any module instance (cmid = 0) + * + * @return array + */ + static public function define_restore_log_rules_for_course() { + $rules = []; + + return $rules; + } + +} diff --git a/mod/publication/backup/moodle2/restore_publication_stepslib.php b/mod/publication/backup/moodle2/restore_publication_stepslib.php new file mode 100644 index 0000000..e2d7315 --- /dev/null +++ b/mod/publication/backup/moodle2/restore_publication_stepslib.php @@ -0,0 +1,219 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * backup/moodle2/restore_publication_stepslib.php + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class performing all restore structure steps for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_publication_activity_structure_step extends restore_activity_structure_step { + + /** + * Define the structure of the restore workflow. + * + * @return restore_path_element $structure + */ + protected function define_structure() { + + $paths = []; + // To know if we are including userinfo. + $userinfo = $this->get_setting_value('userinfo'); + + // Define each element separated. + $paths[] = new restore_path_element('publication', '/activity/publication'); + if ($userinfo) { + $files = new restore_path_element('publication_file', + '/activity/publication/files/file'); + $paths[] = $files; + + $extduedates = new restore_path_element('publication_extduedates', + '/activity/publication/extduedates/extduedate'); + + $paths[] = $extduedates; + } + + return $this->prepare_activity_structure($paths); + } + + /** + * Process an assign restore. + * + * @param object $data The data in object form + * @return void + */ + protected function process_publication($data) { + global $DB; + + $data = (object)$data; + $data->course = $this->get_courseid(); + + $data->allowsubmissionsfromdate = $this->apply_date_offset($data->allowsubmissionsfromdate); + $data->duedate = $this->apply_date_offset($data->duedate); + + if (!isset($data->cutoffdate)) { + $data->cutoffdate = 0; + } + + if (!empty($data->preventlatesubmissions)) { + $data->cutoffdate = $data->duedate; + } else { + $data->cutoffdate = $this->apply_date_offset($data->cutoffdate); + } + + // Delete importfrom after restore. + $data->importfrom = -1; + + // Convert pre v3.3 file-type-restrictions to the new format! + if (!empty($data->allowedfiletypes) && preg_match('/^([\.A-Za-z0-9]+([ ]*[,][ ]*[\.A-Za-z0-9]+)*)$/', + $data->allowedfiletypes)) { + $allowedfiletypes = preg_split('([ ]*[,][ ]*)', $data->allowedfiletypes); + array_walk($allowedfiletypes, function (&$type) { + if ((strpos($type, '.') === false) || (strpos($type, '.') !== 0)) { + $type = '.' . $type; + } + }); + $data->allowedfiletypes = implode('; ', $allowedfiletypes); + } + + $newitemid = $DB->insert_record('publication', $data); + + $this->apply_activity_instance($newitemid); + } + + /** + * Process a submission restore + * + * @param object $data The data in object form + * @return void + */ + protected function process_publication_file($data) { + global $DB; + + $data = (object)$data; + + $data->publication = $this->get_new_parentid('publication'); + + $data->timecreated = $this->apply_date_offset($data->timecreated); + if ($data->userid > 0) { + $data->userid = $this->get_mappingid('user', $data->userid); + } + + $DB->insert_record('publication_file', $data); + + // Note - the old contextid is required in order to be able to restore files stored in + // sub plugin file areas attached to the submissionid. + } + + /** + * Process a user_flags restore + * + * @param object $data The data in object form + * @return void + */ + protected function process_publication_extduedates($data) { + global $DB; + + $data = (object)$data; + + $data->publication = $this->get_new_parentid('publication'); + + $data->userid = $this->get_mappingid('user', $data->userid); + if (!empty($data->extensionduedate)) { + $data->extensionduedate = $this->apply_date_offset($data->extensionduedate); + } else { + $data->extensionduedate = 0; + } + // Flags mailed and locked need no translation on restore. + + $DB->insert_record('publication_extduedates', $data); + } + + /** + * Once the database tables have been fully restored, restore the files + * + * @return void + */ + protected function after_execute() { + $this->add_related_files('mod_publication', 'attachment', null); + } + + /** + * Proceses to execute after the restoration, handles links to restored files + * + */ + protected function after_restore() { + global $DB; + + // Get set new fileids after restoring. + + $pubid = $this->get_new_parentid('publication'); + + $coursemodule = get_coursemodule_from_instance('publication', $pubid); + + $context = context_module::instance($coursemodule->id); + + $contextid = $context->id; + + $fs = get_file_storage(); + $files = $fs->get_area_files($contextid, 'mod_publication', 'attachment'); + + foreach ($files as $file) { + $contingencies = [ + 'publication' => $pubid, + // We need to look for the new user ID if there is one! + 'userid' => $this->get_mappingid('user', $file->get_itemid(), $file->get_itemid()), + 'filename' => $file->get_filename() + ]; + $DB->set_field('publication_file', 'fileid', $file->get_id(), $contingencies); + } + + // Now we correct the itemids of the files! + $rs = $DB->get_recordset('publication_file', ['publication' => $pubid]); + foreach ($rs as $record) { + $file = $fs->get_file_by_id($record->fileid); + if ($file->get_itemid() != $record->userid) { + $dataobject = (object)['id' => $record->fileid, 'itemid' => $record->userid]; + $DB->update_record('files', $dataobject); + } + } + $rs->close(); + + // And we correct the directories! + $rs = $DB->get_recordset('files', ['contextid' => $contextid, 'component' => 'mod_publication', 'filename' => '.']); + foreach ($rs as $record) { + $record->itemid = $this->get_mappingid('user', $record->itemid, $record->itemid); // We may need to update user ID! + $DB->update_record('files', $record); + } + $rs->close(); + + } +} diff --git a/mod/publication/classes/event/course_module_instance_list_viewed.php b/mod/publication/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 0000000..f2d12b7 --- /dev/null +++ b/mod/publication/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,40 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a list of mod_publications in a course being viewed + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * All instances in the course have been viewed in this event + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { +} \ No newline at end of file diff --git a/mod/publication/classes/event/course_module_viewed.php b/mod/publication/classes/event/course_module_viewed.php new file mode 100644 index 0000000..1014368 --- /dev/null +++ b/mod/publication/classes/event/course_module_viewed.php @@ -0,0 +1,48 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a single mod_publication being viewed + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Course module has been viewed for this event + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_viewed extends \core\event\course_module_viewed { + /** + * Init event objecttable + */ + protected function init() { + $this->data['objecttable'] = 'publication'; + parent::init(); + } + // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well. +} \ No newline at end of file diff --git a/mod/publication/classes/event/publication_approval_changed.php b/mod/publication/classes/event/publication_approval_changed.php new file mode 100644 index 0000000..9f0c5f5 --- /dev/null +++ b/mod/publication/classes/event/publication_approval_changed.php @@ -0,0 +1,124 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a single mod_publication being viewed + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Approval has changed for this event + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication_approval_changed extends \core\event\base { + /** + * Init event objecttable + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['objecttable'] = 'publication_file'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Logs approval changes + * @param \stdClass $cm + * @param object $do + * @return \core\event\base + * @throws \coding_exception + * + */ + public static function approval_changed(\stdClass $cm, $do) { + // Trigger overview event. + $event = self::create(array( + 'objectid' => $do->publication, + 'context' => \context_module::instance($cm->id), + 'relateduserid' => $do->reluser, + 'other' => (Array)$do + )); + return $event; + } + // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well. + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "Approval for file with id '".$this->data['other']['fileid'] + ."' in publication with id '" .$this->data['other']['publication'] + ."' has been changed to '".$this->data['other']['approval'] + ."' by the user with id '" .$this->data['other']['userid']."'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventpublicationapprovalchanged', 'publication'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + $moduleid = get_coursemodule_from_instance('publication', $this->data['other']['publication'])->id; + return new \moodle_url("/mod/publication/view.php", array('id' => $moduleid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'publication', 'approval changed '.$this->data['other']['approval'], $this->get_url(), + $this->data['other']['publication'], $this->contextinstanceid); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + // Make sure this class is never used without proper object details. + if (empty($this->objectid) || empty($this->objecttable)) { + throw new \coding_exception('The registration_created event must define objectid and object table.'); + } + // Make sure the context level is set to module. + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} \ No newline at end of file diff --git a/mod/publication/classes/event/publication_duedate_extended.php b/mod/publication/classes/event/publication_duedate_extended.php new file mode 100644 index 0000000..2ad4d80 --- /dev/null +++ b/mod/publication/classes/event/publication_duedate_extended.php @@ -0,0 +1,122 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a single mod_publication being viewed + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * Duedate was extended for this event + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication_duedate_extended extends \core\event\base { + /** + * Init event objecttable + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['objecttable'] = 'publication_file'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Logs due-date extension + * @param \stdClass $cm + * @param object $do + * @return \core\event\base + * @throws \coding_exception + */ + public static function duedate_extended(\stdClass $cm, $do) { + // Trigger overview event. + $event = self::create(array( + 'objectid' => (int)$do['publication'], + 'context' => \context_module::instance($cm->id), + 'relateduserid' => null, + 'other' => (Array)$do + )); + return $event; + } + // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well. + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The due-date of the publication with id '".$this->data['other']['publication']."' was extended to " + .date_format_string($this->data['other']['extensionduedate'], "%d.%m.%Y")." by the user with id '" + .$this->data['other']['userid']."'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventpublicationduedateextended', 'publication'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + $moduleid = get_coursemodule_from_instance('publication', $this->data['other']['publication'])->id; + return new \moodle_url("/mod/publication/view.php", array('id' => $moduleid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'publication', 'duedate extended '.$this->data['other']['extensionduedate'], $this->get_url(), + $this->data['other']['publication'], $this->contextinstanceid); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + // Make sure this class is never used without proper object details. + if (empty($this->objectid) || empty($this->objecttable)) { + throw new \coding_exception('The registration_created event must define objectid and object table.'); + } + // Make sure the context level is set to module. + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} \ No newline at end of file diff --git a/mod/publication/classes/event/publication_file_deleted.php b/mod/publication/classes/event/publication_file_deleted.php new file mode 100644 index 0000000..e599467 --- /dev/null +++ b/mod/publication/classes/event/publication_file_deleted.php @@ -0,0 +1,125 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a single mod_publication being viewed + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * A file was deleted for this event + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication_file_deleted extends \core\event\base { + /** + * Init event objecttable + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['objecttable'] = 'publication_file'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Logs deletion of publication + * @param \stdClass $cm + * @param object $do additional data + * @return \core\event\base + * @throws \coding_exception + */ + public static function create_from_object(\stdClass $cm, $do) { + // Trigger overview event. + $event = self::create(array( + 'objectid' => $do->id, + 'context' => \context_module::instance($cm->id), + 'relateduserid' => $do->userid, + 'other' => (Array)$do + )); + return $event; + } + // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well. + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '".$this->data['other']['userid']."' deleted a file with id '".$this->data['other']['id']. + "' in publication with id '".$this->data['other']['publication']."'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventpublicationfiledeleted', 'publication'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + $moduleid = get_coursemodule_from_instance('publication', $this->data['other']['publication'])->id; + return new \moodle_url("/mod/publication/view.php", array('id' => $moduleid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'publication', 'file uploaded', $this->get_url(), + $this->data['other']['publication'], $this->contextinstanceid); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + // Make sure this class is never used without proper object details. + if (empty($this->objectid) || empty($this->objecttable)) { + throw new \coding_exception('The registration_created event must define objectid and object table.'); + } + // Make sure the context level is set to module. + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + + if (empty($this->data['relateduserid'])) { + throw new \coding_exception('Related user has to be set!'); + } + } +} \ No newline at end of file diff --git a/mod/publication/classes/event/publication_file_imported.php b/mod/publication/classes/event/publication_file_imported.php new file mode 100644 index 0000000..9c30f2c --- /dev/null +++ b/mod/publication/classes/event/publication_file_imported.php @@ -0,0 +1,122 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a single mod_publication being viewed + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * A file was deleted for this event + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication_file_imported extends \core\event\base { + /** + * Init event objecttable + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['objecttable'] = 'publication_file'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Logs that a file was imported + * @param \stdClass $cm + * @param object $do + * @return \core\event\base + * @throws \coding_exception + */ + public static function file_added(\stdClass $cm, $do) { + // Trigger overview event. + $event = self::create(array( + 'objectid' => (int)$do->publication, + 'context' => \context_module::instance($cm->id), + 'relateduserid' => $do->userid, + 'other' => (Array)$do + )); + return $event; + } + // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well. + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The ".$this->data['other']['typ']." with id '".$this->data['other']['itemid']. + "' added a file with id '".$this->data['other']['fileid']. + "' which was imported to publication with id '".$this->data['other']['publication']."'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventpublicationfileimported', 'publication'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + $moduleid = get_coursemodule_from_instance('publication', $this->data['other']['publication'])->id; + return new \moodle_url("/mod/publication/view.php", array('id' => $moduleid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'publication', 'file imported '.$this->data['other']['typ'], $this->get_url(), + $this->data['other']['publication'], $this->contextinstanceid); + } + + /** + * Custom validation. + * + * @throws \coding_exception + * @return void + */ + protected function validate_data() { + parent::validate_data(); + // Make sure this class is never used without proper object details. + if (empty($this->objectid) || empty($this->objecttable)) { + throw new \coding_exception('The registration_created event must define objectid and object table.'); + } + // Make sure the context level is set to module. + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} \ No newline at end of file diff --git a/mod/publication/classes/event/publication_file_uploaded.php b/mod/publication/classes/event/publication_file_uploaded.php new file mode 100644 index 0000000..6485430 --- /dev/null +++ b/mod/publication/classes/event/publication_file_uploaded.php @@ -0,0 +1,105 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains event class for a single mod_publication being viewed + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\event; +use phpDocumentor\Reflection\Types\Object_; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A file was uploaded for this event + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication_file_uploaded extends \core\event\base { + /** + * Init event objecttable + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['objecttable'] = 'publication_file'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Logs that a file was uploaded + * @param \stdClass $cm + * @param object $dobj + * @return \core\event\base + * @throws \coding_exception + */ + public static function create_from_object(\stdClass $cm, $dobj) { + // Trigger overview event. + $event = self::create(array( + 'objectid' => $dobj->id, + 'context' => \context_module::instance($cm->id), + 'relateduserid' => $dobj->userid, + 'other' => (Array)$dobj + )); + return $event; + } + // You might need to override get_url() and get_legacy_log_data() if view mode needs to be stored as well. + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '".$this->data['other']['userid']."' uploaded a new file with id '".$this->data['other']['id']. + "' to publication with id '".$this->data['other']['publication']."'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventpublicationfileuploaded', 'publication'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + $moduleid = get_coursemodule_from_instance('publication', $this->data['other']['publication'])->id; + return new \moodle_url("/mod/publication/view.php", array('id' => $moduleid)); + } + + /** + * Return the legacy event log data. + * + * @return array|null + */ + protected function get_legacy_logdata() { + return array($this->courseid, 'publication', 'file uploaded', $this->get_url(), + $this->data['other']['publication'], $this->contextinstanceid); + } +} \ No newline at end of file diff --git a/mod/publication/classes/local/allfilestable/base.php b/mod/publication/classes/local/allfilestable/base.php new file mode 100644 index 0000000..f643430 --- /dev/null +++ b/mod/publication/classes/local/allfilestable/base.php @@ -0,0 +1,711 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Base class for classes listing all files imported or uploaded + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\allfilestable; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Base class for tables showing all (public) files (upload or import) + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class base extends \table_sql { + /** @var \publication publication object */ + protected $publication = null; + /** @var \context_module context instance object */ + protected $context; + /** @var \stdClass coursemodule object */ + protected $cm = null; + /** @var \file_storage file storage */ + protected $fs = null; + /** @var \stored_file[] files */ + protected $files = null; + /** @var \stored_file[] resource-files */ + protected $resources = null; + /** @var int current itemid for files array */ + protected $itemid = null; + /** @var int amount of files in table, get's counted during formating of the rows! */ + protected $totalfiles = null; + /** @var string[] of cached itemnames */ + protected $itemnames = []; + + /** @var int activity's groupmode */ + protected $groupmode = 0; + /** @var int current group if group mode is active */ + protected $currentgroup = 0; + /** @var string valid pix-icon */ + protected $valid = ''; + /** @var string questionmark pix-icon */ + protected $questionmark = ''; + /** @var string invalid pix-icon */ + protected $invalid = ''; + /** @var string student visible pix-icon */ + protected $studvisibleyes = ''; + /** @var string student not visible pix-icon */ + protected $studvisibleno = ''; + /** @var string[] select box options */ + protected $options = []; + /** @var int[] $users */ + protected $users = []; + + /** + * constructor + * + * @param string $uniqueid a string identifying this table.Used as a key in session vars. + * It gets set automatically with the helper methods! + * @param \publication $publication publication object + */ + public function __construct($uniqueid, \publication $publication) { + global $CFG, $OUTPUT; + + parent::__construct($uniqueid); + + $this->fs = get_file_storage(); + $this->publication = $publication; + $this->cm = get_coursemodule_from_instance('publication', $publication->get_instance()->id, 0, false, MUST_EXIST); + $this->context = \context_module::instance($this->cm->id); + $this->groupmode = groups_get_activity_groupmode($this->cm); + $this->currentgroup = groups_get_activity_group($this->cm, true); + + list($columns, $headers, $helpicons) = $this->get_columns(); + $this->define_columns($columns); + $this->define_headers($headers); + $this->define_help_for_headers($helpicons); + + $this->define_baseurl($CFG->wwwroot . '/mod/publication/view.php?id=' . $this->cm->id . '&currentgroup=' . + $this->currentgroup); + + $this->sortable(true, 'lastname'); // Sorted by lastname by default. + $this->collapsible(true); + $this->initialbars(true); + + $this->column_suppress('picture'); + $this->column_suppress('fullname'); + $this->column_suppress('group'); + + $this->column_class('fullname', 'fullname'); + $this->column_class('timemodified', 'timemodified'); + + $this->set_attribute('cellspacing', '0'); + $this->set_attribute('id', 'attempts'); + $this->set_attribute('class', 'publications'); + $this->set_attribute('width', '100%'); + + $this->no_sorting('studentapproval'); + $this->no_sorting('selection'); + + $this->no_sorting('visibleforstudents'); + + $this->init_sql(); + + // Save status of table(s) persistent as user preference! + $this->is_persistent(true); + + $this->valid = $OUTPUT->pix_icon('i/valid', get_string('student_approved', 'publication')); + $this->questionmark = $OUTPUT->pix_icon('questionmark', get_string('student_pending', 'publication'), 'mod_publication'); + $this->invalid = $OUTPUT->pix_icon('i/invalid', get_string('student_rejected', 'publication')); + + $this->studvisibleyes = $OUTPUT->pix_icon('i/valid', get_string('visibleforstudents_yes', 'publication')); + $this->studvisibleno = $OUTPUT->pix_icon('i/invalid', get_string('visibleforstudents_no', 'publication')); + + $this->options = [ + 1 => get_string('yes'), + 2 => get_string('no') + ]; + } + + /** + * This function is not part of the public api. + */ + public function print_nothing_to_display() { + global $OUTPUT; + + // Render button to allow user to reset table preferences. + echo $this->render_reset_button(); + + $this->print_initials_bar(); + + echo $OUTPUT->box(get_string('nothing_to_show_users', 'publication'), 'font-italic'); + } + + /** + * Return all columns, column-headers and helpicons for this table + * + * @return array Array with column names, column headers and help icons + */ + protected function get_columns() { + $selectallnone = \html_writer::checkbox('selectallnone', false, false, '', [ + 'id' => 'selectallnone', + 'onClick' => 'toggle_userselection()' + ]); + + $columns = ['selection', 'picture', 'fullname']; + $headers = [$selectallnone, '', get_string('fullnameuser')]; + $helpicons = [null, null, null]; + + $useridentity = get_extra_user_fields($this->context); + foreach ($useridentity as $cur) { + if (has_capability('mod/publication:approve', $this->context)) { + $columns[] = $cur; + $headers[] = ($cur == 'phone1') ? get_string('phone') : get_string($cur); + $helpicons[] = null; + } + } + + $columns[] = 'timemodified'; + $headers[] = get_string('lastmodified'); + $helpicons[] = null; + + // Import and upload tables will enhance this list! Import from teamassignments will overwrite it! + return [$columns, $headers, $helpicons]; + } + + /** + * Setter for users property + * + * @param int[] $users + */ + protected function set_users($users) { + $this->users = $users; + } + + /** + * Sets the predefined SQL for this table + */ + protected function init_sql() { + global $DB; + + $params = []; + $ufields = \user_picture::fields('u'); + $useridentityfields = get_extra_user_fields_sql($this->context, 'u'); + + $fields = $ufields . ' ' . $useridentityfields . ', u.username, + COUNT(*) filecount, + SUM(files.studentapproval) AS studentapproval, + SUM(files.teacherapproval) AS teacherapproval, + MAX(files.timecreated) AS timemodified '; + + // Also filters out users according to set activitygroupmode & current activitygroup! + $users = $this->publication->get_users(); + list($sqluserids, $userparams) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'user'); + $params = $params + $userparams + ['publication' => $this->cm->instance]; + + $from = '{user} u ' . + 'LEFT JOIN {publication_file} files ON u.id = files.userid AND files.publication = :publication '; + + $where = "u.id " . $sqluserids; + $groupby = $ufields . ' ' . $useridentityfields . ', u.username '; + + $this->set_sql($fields, $from, $where, $params, $groupby); + $this->set_count_sql("SELECT COUNT(u.id) FROM " . $from . " WHERE " . $where, $params); + + } + + /** + * Set the sql to query the db. Query will be : + * SELECT $fields FROM $from WHERE $where + * Of course you can use sub-queries, JOINS etc. by putting them in the + * appropriate clause of the query. + * + * @param string $fields fields to fetch (SQL snippet) + * @param string $from from where to fetch (SQL snippet) + * @param string $where where conditions for SQL query (SQL snippet) + * @param array $params (optional) params for query + * @param string $groupby (optional) groupby clause (SQL snippet) + */ + public function set_sql($fields, $from, $where, array $params = null, $groupby = '') { + parent::set_sql($fields, $from, $where, $params); + $this->sql->groupby = $groupby; + } + + /** + * Query the db. Store results in the table object for use by build_table. We had to override, due to group by clause! + * + * @param int $pagesize size of page for paginated displayed table. + * @param bool $useinitialsbar do you want to use the initials bar. Bar + * will only be used if there is a fullname column defined for the table. + */ + public function query_db($pagesize, $useinitialsbar = true) { + global $DB; + if (!$this->is_downloading()) { + if ($this->countsql === null) { + $this->countsql = 'SELECT COUNT(1) FROM ' . $this->sql->from . ' WHERE ' . $this->sql->where; + $this->countparams = $this->sql->params; + } + $grandtotal = $DB->count_records_sql($this->countsql, $this->countparams); + if ($useinitialsbar && !$this->is_downloading()) { + $this->initialbars($grandtotal > $pagesize); + } + + list($wsql, $wparams) = $this->get_sql_where(); + if ($wsql) { + $this->countsql .= ' AND ' . $wsql; + $this->countparams = array_merge($this->countparams, $wparams); + + $this->sql->where .= ' AND ' . $wsql; + $this->sql->params = array_merge($this->sql->params, $wparams); + + $total = $DB->count_records_sql($this->countsql, $this->countparams); + } else { + $total = $grandtotal; + } + + $this->pagesize($pagesize, $total); + } + + // Fetch the attempts! + $sort = $this->get_sql_sort(); + if ($sort) { + $sort = "ORDER BY $sort"; + } + $sql = "SELECT DISTINCT {$this->sql->fields} + FROM {$this->sql->from} + WHERE {$this->sql->where} + " . ($this->sql->groupby ? "GROUP BY {$this->sql->groupby}" : "") . " + {$sort}"; + if (!$this->is_downloading()) { + $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); + } else { + $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); + } + } + + /** + * Returns all files to be displayed for this itemid (=userid or groupid) + * + * @param int $itemid User or group ID to fetch files for + * @return array Array with itemid, files-array and resources-array as items + */ + public function get_files($itemid) { + if (($itemid === $this->itemid) && (($this->files !== null) || ($this->resources !== null))) { + // We cache just the current files, to use less memory! + return [$this->itemid, $this->files, $this->resources]; + } + + $contextid = $this->publication->get_context()->id; + $filearea = 'attachment'; + + $this->itemid = $itemid; + $this->files = []; + $this->resources = []; + + $files = $this->fs->get_area_files($contextid, 'mod_publication', $filearea, $this->itemid, 'timemodified', false); + + foreach ($files as $file) { + if ($file->get_filepath() == '/resources/') { + $this->resources[] = $file; + } else { + $this->files[] = $file; + } + } + + return [$this->itemid, $this->files, $this->resources]; + } + + /** + * Returns the amount of files displayed in this table! + */ + public function totalfiles() { + if ($this->totalfiles !== null) { + return $this->totalfiles; + } else { + return 0; + } + } + + /** + * Method wraps string with span-element including data attributes containing detailed group approval data! + * Is implemented/overwritten where needed! + * + * @param string $symbol string/html-snippet to wrap element around + * @param \stored_file $file file to fetch details for + */ + protected function add_details_tooltip(&$symbol, \stored_file $file) { + // This method does nothing here! + } + + /** + * Method returns online-text-preview where it's needed! + * Is implemented/overwritten where needed! + * + * @param int $itemid user ID or group ID + * @param int $fileid file ID + * @return string link to onlinetext-preview + */ + protected function add_onlinetext_preview($itemid, $fileid) { + global $DB, $OUTPUT; + // This method does nothing here! + // Get file data/record! + $conditions = [ + 'publication' => $this->cm->instance, + 'userid' => $itemid, + 'fileid' => $fileid, + 'type' => PUBLICATION_MODE_ONLINETEXT + ]; + if (!$DB->record_exists('publication_file', $conditions)) { + return ''; + } + + $itemname = $this->get_itemname($itemid); + + $url = new \moodle_url('/mod/publication/onlinepreview.php', [ + 'id' => $this->cm->id, + 'itemid' => $itemid, + 'itemname' => $itemname + ]); + + $detailsattr = [ + 'class' => 'onlinetextpreview', + 'data-itemid' => $itemid, + 'data-itemname' => $itemname + ]; + $symbol = \html_writer::tag('span', $OUTPUT->pix_icon('i/preview', get_string('preview')), $detailsattr); + + return \html_writer::link($url, $symbol, ['target' => '_blank']); + } + + /** + * Caches and returns itemnames for given itemids + * + * @param int $itemid + * @return string Itemname + */ + protected function get_itemname($itemid) { + global $DB; + + if (!array_key_exists($itemid, $this->itemnames)) { + $user = $DB->get_record('user', ['id' => $itemid]); + $this->itemnames[$itemid] = fullname($user); + } + + return $this->itemnames[$itemid]; + } + + /*************************************************************** + *** COLUMN OUTPUT METHODS ************************************* + **************************************************************/ + + /** + * This function is called for each data row to allow processing of the + * XXX value. + * + * @param object $values Contains object with all the values of record. + * @return string Return XXX. + */ + public function col_selection($values) { + // If the data is being downloaded than we don't want to show HTML. + if ($this->is_downloading()) { + return ''; + } else { + return \html_writer::checkbox('selectedeuser[' . $values->id . ']', 'selected', false, null, + ['class' => 'userselection']); + } + } + + /** + * This function is called for each data row to allow processing of the + * user's name with link and optional extension date. + * + * @param object $values Contains object with all the values of record. + * @return string Return user fullname. + */ + public function col_fullname($values) { + // Saves DB access in \mod_publication\local\allfilestable::get_itemname()! + if (!array_key_exists($values->id, $this->itemnames)) { + $this->itemnames[$values->id] = fullname($values); + } + + $extension = $this->publication->user_extensionduedate($values->id); + if ($extension) { + if (has_capability('mod/publication:grantextension', $this->context) || + has_capability('mod/publication:approve', $this->context)) { + $extensiontxt = \html_writer::empty_tag('br') . "\n" . + get_string('extensionto', 'publication') . ': ' . userdate($extension); + } else { + $extensiontxt = ''; + } + } else { + $extensiontxt = ''; + } + + if ($this->is_downloading()) { + return strip_tags(parent::col_fullname($values) . $extensiontxt); + } else { + return parent::col_fullname($values) . $extensiontxt; + } + } + + + /** + * This function is called for each data row to allow processing of the + * group. Also caches group name in itemnames for onlinetext-preview! + * + * @param object $values Contains object with all the values of record. + * @return string Return group's name. + */ + public function col_groupname($values) { + // Saves DB access in \mod_publication\local\allfilestable::get_itemname()! + if (!array_key_exists($values->id, $this->itemnames)) { + $this->itemnames[$values->id] = $values->groupname; + } + + return $values->groupname; + } + + /** + * This function is called for each data row to allow processing of the + * user picture. + * + * @param object $values Contains object with all the values of record. + * @return string Return user picture markup. + */ + public function col_picture($values) { + global $OUTPUT; + // If the data is being downloaded than we don't want to show HTML. + if ($this->is_downloading()) { + return ''; + } else { + return $OUTPUT->user_picture($values); + } + } + + /** + * This function is called for each data row to allow processing of the + * user's groups. + * + * @param object $values Contains object with all the values of record. + * @return string Return user groups. + */ + public function col_groups($values) { + $groups = groups_get_all_groups($this->publication->get_instance()->course, $values->id, 0, 'g.name'); + if (!empty($groups)) { + $values->groups = ''; + foreach ($groups as $group) { + if ($values->groups != '') { + $values->groups .= ', '; + } + $values->groups .= $group->name; + } + if ($this->is_downloading()) { + return $values->groups; + } else { + return \html_writer::tag('div', $values->groups, ['id' => 'gr' . $values->id]); + } + } else if ($this->is_downloading()) { + return ''; + } else { + return \html_writer::tag('div', '-', ['id' => 'gr' . $values->id]); + } + } + + /** + * This function is called for each data row to allow processing of the + * user's submission time. + * + * @param object $values Contains object with all the values of record. + * @return string Return user time of submission. + */ + public function col_timemodified($values) { + global $OUTPUT; + + list(, $files, ) = $this->get_files($values->id); + + $filetable = new \html_table(); + $filetable->attributes = ['class' => 'filetable']; + + foreach ($files as $file) { + if (has_capability('mod/publication:approve', $this->context) + || $this->publication->has_filepermission($file->get_id())) { + $filerow = []; + $filerow[] = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file)); + + $url = new \moodle_url('/mod/publication/view.php', ['id' => $this->cm->id, 'download' => $file->get_id()]); + $filerow[] = \html_writer::link($url, $file->get_filename()) . + $this->add_onlinetext_preview($values->id, $file->get_id()); + + $filetable->data[] = $filerow; + } + } + + if ($this->totalfiles === null) { + $this->totalfiles = 0; + } + if (count($filetable->data) > 0) { + $lastmodified = \html_writer::table($filetable); + $lastmodified .= \html_writer::span(userdate($values->timemodified), "timemodified"); + $this->totalfiles += count($filetable->data); + } else { + $lastmodified = get_string('nofiles', 'publication'); + } + + // TODO: download without tags? + return $lastmodified; + } + + /** + * This function is called for each data row to allow processing of the + * file status. + * + * @param object $values Contains object with all the values of record. + * @return string Return user time of submission. + */ + public function col_studentapproval($values) { + list(, $files, ) = $this->get_files($values->id); + + $table = new \html_table(); + $table->attributes = ['class' => 'statustable']; + + foreach ($files as $file) { + if (has_capability('mod/publication:approve', $this->context) + || $this->publication->has_filepermission($file->get_id())) { + switch ($this->publication->student_approval($file)) { + case 2: + $symbol = $this->valid; + break; + case 1: + $symbol = $this->invalid; + break; + default: + $symbol = $this->questionmark; + } + $this->add_details_tooltip($symbol, $file); + $table->data[] = [$symbol]; + } + } + + if (count($table->data) > 0) { + return \html_writer::table($table); + } else { + return ''; + } + } + + /** + * This function is called for each data row to allow processing of the + * file permission. + * + * @param object $values Contains object with all the values of record. + * @return string Return user time of submission. + */ + public function col_teacherapproval($values) { + + list(, $files, ) = $this->get_files($values->id); + + $table = new \html_table(); + $table->attributes = ['class' => 'permissionstable']; + + foreach ($files as $file) { + if ($this->publication->has_filepermission($file->get_id()) + || has_capability('mod/publication:approve', $this->context)) { + + $checked = $this->publication->teacher_approval($file); + // Null if none found, DB-entry otherwise! + // TODO change that conversions and queue the real values! Everywhere! + $checked = ($checked === false || $checked === null) ? "" : $checked; + + $sel = \html_writer::select($this->options, 'files[' . $file->get_id() . ']', (string)$checked); + $table->data[] = [$sel]; + } + } + + if (count($table->data) > 0) { + return \html_writer::table($table); + } else { + return ''; + } + } + + /** + * This function is called for each data row to allow processing of the + * file visibility. + * + * @param object $values Contains object with all the values of record. + * @return string Return user time of submission. + */ + public function col_visibleforstudents($values) { + list(, $files, ) = $this->get_files($values->id); + + $table = new \html_table(); + $table->attributes = ['class' => 'statustable']; + + foreach ($files as $file) { + if ($this->publication->has_filepermission($file->get_id())) { + $table->data[] = [$this->studvisibleyes]; + } else { + $table->data[] = [$this->studvisibleno]; + } + } + + // TODO: download without tags? + if (count($table->data) > 0) { + return \html_writer::table($table); + } else { + return ''; + } + } + + /** + * This function is called for each data row to allow processing of + * columns which do not have a *_cols function. + * + * @param string $colname Name of current column + * @param object $values Values of the current row + * @return string return processed value. + */ + public function other_cols($colname, $values) { + // Process user identity fields! + $useridentity = get_extra_user_fields($this->context); + if ($colname === 'phone') { + $colname = 'phone1'; + } + if (in_array($colname, $useridentity)) { + if (!empty($values->$colname)) { + if ($this->is_downloading()) { + return $values->$colname; + } else { + return \html_writer::tag('div', $values->$colname, ['id' => 'u' . $colname . $values->id]); + } + } else { + if ($this->is_downloading()) { + return '-'; + } else { + return \html_writer::tag('div', '-', ['id' => 'u' . $colname . $values->id]); + } + } + } + + return $values->$colname; + } +} diff --git a/mod/publication/classes/local/allfilestable/group.php b/mod/publication/classes/local/allfilestable/group.php new file mode 100644 index 0000000..69eb30e --- /dev/null +++ b/mod/publication/classes/local/allfilestable/group.php @@ -0,0 +1,279 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains class for files table listing all files for imported teamsubmissions + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\allfilestable; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table showing my group files + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group extends base { + /** @var int grouping id for assign's team submissions */ + protected $groupingid = 0; + /** @var bool if a group membership is required by assign's team submission */ + protected $requiregroup = 0; + /** @var \stdClass course module object of assign to import from */ + protected $assigncm = null; + /** @var \context_module context instance of assign to import from */ + protected $assigncontext = null; + + /** + * Sets the predefined SQL for this table + */ + protected function init_sql() { + global $DB; + + $params = []; + + $fields = "g.id, g.name AS groupname, NULL AS groupmembers, COUNT(*) AS filecount, + SUM(files.studentapproval) AS studentapproval, NULL AS teacherapproval, MAX(files.timecreated) AS timemodified "; + + $groups = $this->publication->get_groups($this->groupingid); + if (count($groups) > 0) { + list($sqlgroupids, $groupparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'group'); + $params = $params + $groupparams + ['publication' => $this->cm->instance]; + } else { + $sqlgroupids = " = :group "; + $params = $params + ['group' => -1, 'publication' => $this->cm->instance]; + } + + if ($this->requiregroup || !count($this->publication->get_submissionmembers(0))) { + $grouptable = '{groups} g '; + } else { + // If no group is required by assign to submit, we have to include all users without group as group 0 - standard group! + $grouptable = " ( SELECT 0 AS id, :stdname AS name + UNION ALL + SELECT {groups}.id, {groups}.name AS name + FROM {groups}) AS g "; + $params['stdname'] = get_string('defaultteam', 'assign'); + } + + $from = $grouptable . " LEFT JOIN {publication_file} files ON g.id = files.userid AND files.publication = :publication "; + + $where = "g.id " . $sqlgroupids; + $groupby = " g.id, groupname, groupmembers, teacherapproval "; + + $this->set_sql($fields, $from, $where, $params, $groupby); + $this->set_count_sql("SELECT COUNT(g.id) FROM " . $from . " WHERE " . $where, $params); + + } + + /** + * constructor + * + * @param string $uniqueid a string identifying this table.Used as a key in session vars. + * It gets set automatically with the helper methods! + * @param \publication $publication publication object + */ + public function __construct($uniqueid, \publication $publication) { + global $DB, $PAGE; + + $assignid = $publication->get_instance()->importfrom; + $this->groupingid = $DB->get_field('assign', 'teamsubmissiongroupingid', ['id' => $assignid]); + $this->requiregroup = $publication->requiregroup(); + $this->assigncm = get_coursemodule_from_instance('assign', $assignid, $publication->get_instance()->course); + $this->assigncontext = \context_module::instance($this->assigncm->id); + + parent::__construct($uniqueid, $publication); + + $this->sortable(true, 'groupname'); // Sorted by group by default. + $this->no_sorting('groupmembers'); + + // Init JS! + $params = new \stdClass(); + $params->id = $uniqueid; + switch ($publication->get_instance()->groupapproval) { + case PUBLICATION_APPROVAL_ALL; + $params->mode = get_string('groupapprovalmode_all', 'mod_publication'); + break; + case PUBLICATION_APPROVAL_SINGLE: + $params->mode = get_string('groupapprovalmode_single', 'mod_publication'); + break; + } + + $PAGE->requires->js_call_amd('mod_publication/groupapprovalstatus', 'initializer', [$params]); + + $params = new \stdClass(); + $cm = get_coursemodule_from_instance('publication', $publication->get_instance()->id); + $params->cmid = $cm->id; + $PAGE->requires->js_call_amd('mod_publication/onlinetextpreview', 'initializer', [$params]); + } + + /** + * This function is not part of the public api. + */ + public function print_nothing_to_display() { + global $OUTPUT; + + // Render button to allow user to reset table preferences. + echo $this->render_reset_button(); + + $this->print_initials_bar(); + + echo $OUTPUT->box(get_string('nothing_to_show_groups', 'publication'), 'font-italic'); + } + + /** + * Return all columns, column-headers and helpicons for this table + * + * @return array Array with column names, column headers and help icons + */ + protected function get_columns() { + $selectallnone = \html_writer::checkbox('selectallnone', false, false, '', [ + 'id' => 'selectallnone', + 'onClick' => 'toggle_userselection()' + ]); + + $columns = ['selection', 'groupname', 'groupmembers', 'timemodified']; + $headers = [$selectallnone, get_string('group'), get_string('groupmembers'), get_string('lastmodified')]; + $helpicons = [null, null, null, null]; + + if (has_capability('mod/publication:approve', $this->context)) { + if ($this->publication->get_instance()->obtainstudentapproval) { + $columns[] = 'studentapproval'; + $headers[] = get_string('studentapproval', 'publication'); + $helpicons[] = new \help_icon('studentapproval', 'publication'); + } + $columns[] = 'teacherapproval'; + if ($this->publication->get_instance()->obtainstudentapproval) { + $headers[] = get_string('obtainstudentapproval', 'publication'); + } else { + $headers[] = get_string('teacherapproval', 'publication'); + } + $helpicons[] = null; + + $columns[] = 'visibleforstudents'; + $headers[] = get_string('visibleforstudents', 'publication'); + $helpicons[] = null; + } + + // Import and upload tables will enhance this list! Import from teamassignments will overwrite it! + return [$columns, $headers, $helpicons]; + } + + /** + * Display members of the group + * + * @param object $values Contains object with all the values of record. + * @return string Return groups members. + */ + public function col_groupmembers($values) { + $cell = ''; + + $groupmembers = $this->publication->get_submissionmembers($values->id); + + if (!count($groupmembers)) { + return $cell; + } + + foreach ($groupmembers as $cur) { + $cell .= \html_writer::tag('div', parent::col_fullname($cur)); + } + + return $cell; + } + + /** + * Method wraps string with span-element including data attributes containing detailed group approval data! + * + * @param string $symbol string/html-snippet to wrap element around + * @param \stored_file $file file to fetch details for + */ + protected function add_details_tooltip(&$symbol, \stored_file $file) { + global $DB, $OUTPUT; + + $pubfileid = $DB->get_field('publication_file', 'id', [ + 'publication' => $this->publication->get_instance()->id, + 'fileid' => $file->get_id() + ]); + list(, $approvaldetails) = $this->publication->group_approval($pubfileid); + + $approved = []; + $rejected = []; + $pending = []; + foreach ($approvaldetails as $cur) { + if (empty($cur->approvaltime)) { + $cur->approvaltime = '-'; + } else { + $cur->approvaltime = userdate($cur->approvaltime, get_string('strftimedatetime')); + } + if ($cur->approval === null) { + $pending[] = ['name' => fullname($cur), 'time' => '-'];; + } else if ($cur->approval == 0) { + $rejected[] = ['name' => fullname($cur), 'time' => $cur->approvaltime]; + } else if ($cur->approval == 1) { + $approved[] = ['name' => fullname($cur), 'time' => $cur->approvaltime]; + } + } + + $status = new \stdClass(); + $status->approved = false; + $status->rejected = false; + $status->pending = false; + switch ($this->publication->student_approval($file)) { + case 2: + $status->approved = true; + break; + case 1: + $status->rejected = true; + break; + default: + $status->pending = true; + } + + $detailsattr = [ + 'class' => 'approvaldetails', + 'data-pending' => json_encode($pending), + 'data-approved' => json_encode($approved), + 'data-rejected' => json_encode($rejected), + 'data-filename' => $file->get_filename(), + 'data-status' => json_encode($status) + ]; + + $symbol = $symbol . \html_writer::tag('span', $OUTPUT->pix_icon('i/preview', get_string('show_details', 'publication')), + $detailsattr); + + } + + /** + * Caches and returns itemnames for given itemids + * + * @param int $itemid + * @return string Itemname + */ + protected function get_itemname($itemid) { + if (!array_key_exists($itemid, $this->itemnames)) { + $this->itemnames[$itemid] = groups_get_group_name($itemid); + } + + return $this->itemnames[$itemid]; + } +} diff --git a/mod/publication/classes/local/allfilestable/import.php b/mod/publication/classes/local/allfilestable/import.php new file mode 100644 index 0000000..e2c75b6 --- /dev/null +++ b/mod/publication/classes/local/allfilestable/import.php @@ -0,0 +1,86 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains class for files table listing all files in import mode + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\allfilestable; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table showing all imported files + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import extends base { + /** + * constructor + * + * @param string $uniqueid a string identifying this table.Used as a key in session vars. + * It gets set automatically with the helper methods! + * @param \publication $publication publication object + */ + public function __construct($uniqueid, \publication $publication) { + global $PAGE; + + parent::__construct($uniqueid, $publication); + + $params = new \stdClass(); + $cm = get_coursemodule_from_instance('publication', $publication->get_instance()->id); + $params->cmid = $cm->id; + $PAGE->requires->js_call_amd('mod_publication/onlinetextpreview', 'initializer', [$params]); + } + + /** + * Return all columns, column-headers and helpicons for this table + * + * @return array Array with column names, column headers and help icons + */ + public function get_columns() { + list($columns, $headers, $helpicons) = parent::get_columns(); + + if (has_capability('mod/publication:approve', $this->context)) { + if ($this->publication->get_instance()->obtainstudentapproval) { + $columns[] = 'studentapproval'; + $headers[] = get_string('studentapproval', 'publication'); + $helpicons[] = new \help_icon('studentapproval', 'publication'); + } + $columns[] = 'teacherapproval'; + if ($this->publication->get_instance()->obtainstudentapproval) { + $headers[] = get_string('obtainstudentapproval', 'publication'); + } else { + $headers[] = get_string('teacherapproval', 'publication'); + } + $helpicons[] = null; + + $columns[] = 'visibleforstudents'; + $headers[] = get_string('visibleforstudents', 'publication'); + $helpicons[] = null; + } + + return [$columns, $headers, $helpicons]; + } +} diff --git a/mod/publication/classes/local/allfilestable/upload.php b/mod/publication/classes/local/allfilestable/upload.php new file mode 100644 index 0000000..4823af2 --- /dev/null +++ b/mod/publication/classes/local/allfilestable/upload.php @@ -0,0 +1,71 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains class for files table listing all files in upload mode + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\allfilestable; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table showing all uploaded files + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upload extends base { + /** + * Return all columns, column-headers and helpicons for this table + * + * @return array Array with column names, column headers and help icons + */ + public function get_columns() { + list($columns, $headers, $helpicons) = parent::get_columns(); + + if (has_capability('mod/publication:approve', $this->context)) { + $columns[] = 'teacherapproval'; + $headers[] = get_string('teacherapproval', 'publication'); + $helpicons[] = null; + + $columns[] = 'visibleforstudents'; + $headers[] = get_string('visibleforstudents', 'publication'); + $helpicons[] = null; + } + + return [$columns, $headers, $helpicons]; + } + + /** + * Method is not needed here and has to return ''! + * + * @param int $itemid user ID or group ID + * @param int $fileid file ID + * @return string empty string + */ + protected function add_onlinetext_preview($itemid, $fileid) { + // This method does nothing here! + return ''; + } +} diff --git a/mod/publication/classes/local/filestable/base.php b/mod/publication/classes/local/filestable/base.php new file mode 100644 index 0000000..b4385c2 --- /dev/null +++ b/mod/publication/classes/local/filestable/base.php @@ -0,0 +1,169 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Base class for tables showing files related to me (uploaded by me, imported from me or my group and options to approve them) + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\filestable; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Base class for tables showing my files or group files (upload or import) + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class base extends \html_table { + /** @var \publication publication object */ + protected $publication = null; + /** @var \file_storage file storage object */ + protected $fs = null; + /** @var \stored_file[] array of stored_file objects */ + protected $files = null; + /** @var \stored_file[] array of stored_file objects used in onlinetexts */ + protected $resources = null; + /** @var bool whether or not changes of approval are still possible */ + protected $changepossible = false; + /** @var string[] select options */ + protected $options = []; + + /** + * constructor + * + * @param \publication $publication publication object + */ + public function __construct(\publication $publication) { + parent::__construct(); + + $this->publication = $publication; + + $this->fs = get_file_storage(); + } + + /** + * Initialize the table (get the files, style table, prepare options used for approval-selects, add files to table, etc.) + * + * @return int amount of files in table + */ + public function init() { + $files = $this->get_files(); + + if ((!$files || count($files) == 0) && has_capability('mod/publication:upload', $this->publication->get_context())) { + return 0; + } + + if (!isset($this->attributes)) { + $this->attributes = ['class' => 'coloredrows']; + } else if (!isset($this->attributes['class'])) { + $this->attributes['class'] = 'coloredrows'; + } else { + $this->attributes['class'] .= ' coloredrows'; + } + + $this->options = []; + $this->options[2] = get_string('student_approve', 'publication'); + $this->options[1] = get_string('student_reject', 'publication'); + + if (empty($files) || count($files) == 0) { + return 0; + } + + foreach ($files as $file) { + $this->data[] = $this->add_file($file); + } + + return count($this->data); + } + + /** + * Add a single file to the table + * + * @param \stored_file $file Stored file instance + * @return string[] Array of table cell contents + */ + public function add_file(\stored_file $file) { + global $OUTPUT; + + $data = []; + $data[] = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file)); + + $dlurl = new \moodle_url('/mod/publication/view.php', [ + 'id' => $this->publication->get_coursemodule()->id, + 'download' => $file->get_id() + ]); + $data[] = \html_writer::link($dlurl, $file->get_filename()); + + // The specific data will be added in the child-classes! + + return $data; + } + + /** + * Get all files, in which the current user is involved + * + * @return \stored_file[] array of stored_files indexed by pathanmehash + */ + public function get_files() { + global $USER; + + if ($this->files !== null) { + return $this->files; + } + + $contextid = $this->publication->get_context()->id; + $filearea = 'attachment'; + // User ID for regular instances, group id for assignments with teamsubmission! + $itemid = $USER->id; + + $files = $this->fs->get_area_files($contextid, 'mod_publication', $filearea, $itemid, 'timemodified', false); + + foreach ($files as $file) { + if ($file->get_filepath() == '/resources/') { + $this->resources[] = $file; + } else { + $this->files[] = $file; + } + } + + return $this->files; + } + + /** + * Returns if it's possible to change the approval + * + * @return bool + */ + public function changepossible() { + return ($this->changepossible ? true : false) && has_capability('mod/publication:upload', + $this->publication->get_context()); + } + +} diff --git a/mod/publication/classes/local/filestable/group.php b/mod/publication/classes/local/filestable/group.php new file mode 100644 index 0000000..24bce26 --- /dev/null +++ b/mod/publication/classes/local/filestable/group.php @@ -0,0 +1,158 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains class for files table listing files imported from one's group(s) (and options for approving them) + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\filestable; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table showing my group files + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class group extends base { + /** @var int $groupingid saves the team-assignments submission grouping id */ + protected $groupingid = 0; + + /** + * Add a single file to the table + * + * @param \stored_file $file Stored file instance + * @return string[] Array of table cell contents + */ + public function add_file(\stored_file $file) { + global $USER, $DB; + + // The common columns! + $data = parent::add_file($file); + + // Now add the specific data to the table! + $teacherapproval = $this->publication->teacher_approval($file); + if ($teacherapproval && $this->publication->get_instance()->obtainstudentapproval) { + $pubfileid = $DB->get_field('publication_file', 'id', [ + 'publication' => $this->publication->get_instance()->id, + 'fileid' => $file->get_id() + ]); + list($studentapproval, $approvaldetails) = $this->publication->group_approval($pubfileid); + if ($this->publication->is_open() + && (!key_exists($USER->id, $approvaldetails) || ($approvaldetails[$USER->id]->approval === null))) { + $this->changepossible = true; + if (!key_exists($USER->id, $approvaldetails)) { + $checked = 0; + } else { + $checked = $approvaldetails[$USER->id]->approval === null ? 0 : $approvaldetails[$USER->id]->approval + 1; + } + $data[] = \html_writer::select($this->options, 'studentapproval[' . $file->get_id() . ']', $checked); + } else { + if ($studentapproval === null) { + $data[] = get_string('student_pending', 'publication'); + } else if ($studentapproval) { + $data[] = get_string('student_approved', 'publication'); + } else { + $rejected = []; + $pending = []; + foreach ($approvaldetails as $cur) { + if ($cur->approval === 0) { + $rejected[] = fullname($cur); + } else if ($cur->approval === null) { + $pending[] = fullname($cur); + } + } + if (count($rejected) > 0) { + $rejected = get_string('rejected', 'publication') . ': ' . implode(', ', $rejected); + } else if ($this->publication->get_instance()->groupapproval == PUBLICATION_APPROVAL_ALL) { + if (count($pending) > 0) { + $rejected = get_string('pending', 'publication') . ': ' . implode(', ', $pending); + } else { + $rejected = ''; + } + } else { + $rejected = ''; + } + $data[] = \html_writer::tag('span', get_string('student_rejected', 'publication'), + ['title' => $rejected]); + } + } + } else { + switch ($teacherapproval) { + case 1: + $data[] = get_string('teacher_approved', 'publication'); + break; + default: + $data[] = get_string('student_pending', 'publication'); + } + } + + return $data; + } + + /** + * Get all files, in which the current user's groups are involved + * + * @return \stored_file[] array of stored_files indexed by pathanmehash + */ + public function get_files() { + global $USER, $DB; + + if ($this->files !== null) { + return $this->files; + } + + $contextid = $this->publication->get_context()->id; + $filearea = 'attachment'; + + /* OK, assign is a little bit inconsistent with implementation and doc-comments, it states it will return false for user's + * group if there's no group or multiple groups, instead it uses just the first group it finds for the user! + * So if assign doesn't behave that exact, we just use all users groups (except there's a groupingid set for submission! */ + $assignid = $this->publication->get_instance()->importfrom; + $this->groupingid = $DB->get_field('assign', 'teamsubmissiongroupingid', ['id' => $assignid]); + $groups = groups_get_all_groups($this->publication->get_instance()->course, $USER->id, $this->groupingid); + if (empty($groups)) { + // Users without group membership get assigned group id 0! + $groups = []; + $groups[0] = new \stdClass(); + $groups[0]->id = 0; + } + + foreach ($groups as $group) { + $itemid = $group->id; + + $files = $this->fs->get_area_files($contextid, 'mod_publication', $filearea, $itemid, 'timemodified', false); + + foreach ($files as $file) { + if ($file->get_filepath() == '/resources/') { + $this->resources[] = $file; + } else { + $this->files[] = $file; + } + } + } + + return $this->files; + } +} diff --git a/mod/publication/classes/local/filestable/import.php b/mod/publication/classes/local/filestable/import.php new file mode 100644 index 0000000..24e46aa --- /dev/null +++ b/mod/publication/classes/local/filestable/import.php @@ -0,0 +1,80 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains class for files table listing files imported from oneself (and options for approving them) + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\filestable; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table showing my imported files + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import extends base { + /** + * Add a single file to the table + * + * @param \stored_file $file Stored file instance + * @return string[] Array of table cell contents + */ + public function add_file(\stored_file $file) { + // The common columns! + $data = parent::add_file($file); + + // Now add the specific data to the table! + $teacherapproval = $this->publication->teacher_approval($file); + if ($teacherapproval && $this->publication->get_instance()->obtainstudentapproval) { + $studentapproval = $this->publication->student_approval($file); + if ($this->publication->is_open() && $studentapproval == 0) { + $this->changepossible = true; + $data[] = \html_writer::select($this->options, 'studentapproval[' . $file->get_id() . ']', $studentapproval); + } else { + switch ($studentapproval) { + case 2: + $data[] = get_string('student_approved', 'publication'); + break; + case 1: + $data[] = get_string('student_rejected', 'publication'); + break; + default: + $data[] = get_string('student_pending', 'publication'); + } + } + } else { + switch ($teacherapproval) { + case 1: + $data[] = get_string('teacher_approved', 'publication'); + break; + default: + $data[] = get_string('student_pending', 'publication'); + } + } + + return $data; + } +} diff --git a/mod/publication/classes/local/filestable/upload.php b/mod/publication/classes/local/filestable/upload.php new file mode 100644 index 0000000..cedb0d0 --- /dev/null +++ b/mod/publication/classes/local/filestable/upload.php @@ -0,0 +1,73 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains class for files table listing files uploaded by oneself (and options for approving them) + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\filestable; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Table showing my uploaded files + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upload extends base { + /** + * Add a single file to the table + * + * @param \stored_file $file Stored file instance + * @return string[] Array of table cell contents + */ + public function add_file(\stored_file $file) { + // The common columns! + $data = parent::add_file($file); + + // Now add the specific data to the table! + $teacherapproval = $this->publication->teacher_approval($file); + if ($this->publication->get_instance()->obtainteacherapproval) { + // Teacher has to approve: show all status. + if (is_null($teacherapproval)) { + $data[] = get_string('hidden', 'publication') . ' (' . get_string('teacher_pending', 'publication') . ')'; + } else if ($teacherapproval == 1) { + $data[] = get_string('visible', 'publication'); + } else { + $data[] = get_string('hidden', 'publication') . ' (' . get_string('teacher_rejected', 'publication') . ')'; + } + } else { + // Teacher doenst have to approve: only show when rejected. + if (is_null($teacherapproval)) { + $data[] = get_string('visible', 'publication'); + } else if ($teacherapproval == 1) { + $data[] = get_string('visible', 'publication'); + } else { + $data[] = get_string('hidden', 'publication') . ' (' . get_string('teacher_rejected', 'publication') . ')'; + } + } + + return $data; + } +} diff --git a/mod/publication/classes/local/tests/base.php b/mod/publication/classes/local/tests/base.php new file mode 100644 index 0000000..8c17207 --- /dev/null +++ b/mod/publication/classes/local/tests/base.php @@ -0,0 +1,217 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Base class with common logic for some unit tests. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\tests; + +use advanced_testcase; +use stdClass; +use mod_assign_testable_assign; +use context_module; +use mod_assign_test_generator; + +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); // It must be included from a Moodle page! +} + +// Make sure the code being tested is accessible. +global $CFG; +require_once($CFG->dirroot . '/mod/publication/locallib.php'); // Include the code to test! +require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); // Include assign's generator helper trait! + +/** + * This base class contains common logic for tests. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base extends advanced_testcase { + // Import assign's generator helper trait to be able to easily add assign instances and submissions! + use mod_assign_test_generator { + create_instance as public create_assign; + add_submission as public; + } + + /** Default number of students to create */ + const DEFAULT_STUDENT_COUNT = 50; + /** Default number of teachers to create */ + const DEFAULT_TEACHER_COUNT = 6; + /** Default number of editing teachers to create */ + const DEFAULT_EDITING_TEACHER_COUNT = 6; + /** Number of timestamps to create */ + const DEFAULT_TIMESTAMP_COUNT = 6; + /** Number of groups to create */ + const GROUP_COUNT = 6; + + /** @var stdClass $course New course created to hold the assignments */ + protected $course = null; + + /** @var array $teachers List of DEFAULT_TEACHER_COUNT teachers in the course */ + protected $teachers = null; + + /** @var array $editingteachers List of DEFAULT_EDITING_TEACHER_COUNT editing teachers in the course */ + protected $editingteachers = null; + + /** @var array $students List of DEFAULT_STUDENT_COUNT students in the course */ + protected $students = null; + + /** @var array $groups List of 10 groups in the course */ + protected $groups = null; + + /** @var array $timestamps List of 10 different timestamps */ + protected $timestamps = null; + + /** + * Setup function - we will create a course and add an tmt instance to it. + */ + protected function setUp():void { + global $DB; + + $this->resetAfterTest(true); + + $this->course = self::getDataGenerator()->create_course(); + $this->teachers = []; + for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) { + array_push($this->teachers, self::getDataGenerator()->create_user()); + } + + $this->editingteachers = []; + for ($i = 0; $i < self::DEFAULT_EDITING_TEACHER_COUNT; $i++) { + array_push($this->editingteachers, self::getDataGenerator()->create_user()); + } + + $this->students = []; + for ($i = 0; $i < self::DEFAULT_STUDENT_COUNT; $i++) { + array_push($this->students, self::getDataGenerator()->create_user()); + } + + $this->groups = []; + for ($i = 0; $i < self::GROUP_COUNT; $i++) { + array_push($this->groups, self::getDataGenerator()->create_group(['courseid' => $this->course->id])); + } + + $this->timestamps = []; + for ($i = 0; $i < self::DEFAULT_TIMESTAMP_COUNT; $i++) { + $hour = rand(0, 23); + $minute = rand(0, 60); + $second = rand(0, 60); + $month = rand(1, 12); + $day = rand(0, 31); + $year = rand(1980, date('Y')); + array_push($this->timestamps, mktime($hour, $minute, $second, $month, $day, $year)); + } + + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + foreach ($this->teachers as $i => $teacher) { + self::getDataGenerator()->enrol_user($teacher->id, + $this->course->id, + $teacherrole->id); + groups_add_member($this->groups[$i % self::GROUP_COUNT], $teacher); + } + + $editingteacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); + foreach ($this->editingteachers as $i => $editingteacher) { + self::getDataGenerator()->enrol_user($editingteacher->id, + $this->course->id, + $editingteacherrole->id); + groups_add_member($this->groups[$i % self::GROUP_COUNT], $editingteacher); + } + + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + foreach ($this->students as $i => $student) { + self::getDataGenerator()->enrol_user($student->id, + $this->course->id, + $studentrole->id); + groups_add_member($this->groups[$i % self::GROUP_COUNT], $student); + } + + // Make sure to run these tests as (editing)teacher, due to students getting no table if no files are present! + self::setUser($this->editingteachers[0]); + } + + /** + * Convenience function to create a testable instance of a publication instance. + * + * @param array $params Array of parameters to pass to the generator + * @return publication Testable wrapper around the publication class. + */ + protected function create_instance($params = []) { + $generator = self::getDataGenerator()->get_plugin_generator('mod_publication'); + if (!isset($params['course'])) { + $params['course'] = $this->course->id; + } + $instance = $generator->create_instance($params); + $cm = get_coursemodule_from_instance('publication', $instance->id); + $context = context_module::instance($cm->id); + + return new publication($cm, $this->course, $context); + } + + /** + * Simulate a file upload + * + * @param int $userid + * @param int $publicationid + * @param string $filename + * @param string $content + * @return bool|int + * @throws \coding_exception + * @throws \dml_exception + * @throws \file_exception + * @throws \stored_file_creation_exception + */ + protected function create_upload($userid, $publicationid, $filename, $content) { + global $DB; + + $cm = get_coursemodule_from_instance('publication', $publicationid); + $context = context_module::instance($cm->id); + $fs = get_file_storage(); + + // We gotta create a new one! + $filerecord = (object)[ + 'contextid' => $context->id, + 'component' => 'mod_publication', + 'filearea' => 'attachment', + 'itemid' => $userid, + 'userid' => $userid, + 'filename' => $filename, + 'filepath' => '/', + ]; + $file = $fs->create_file_from_string($filerecord, $content); + + $dataobject = new stdClass(); + $dataobject->publication = $publicationid; + $dataobject->userid = $userid; + $dataobject->timecreated = $file->get_timecreated(); + $dataobject->fileid = $file->get_id(); + $dataobject->studentapproval = 1; // Upload always means user approves. + $dataobject->filename = $file->get_filename(); + $dataobject->type = PUBLICATION_MODE_UPLOAD; + + return $DB->insert_record('publication_file', $dataobject); + } +} + diff --git a/mod/publication/classes/local/tests/publication.php b/mod/publication/classes/local/tests/publication.php new file mode 100644 index 0000000..236b7ef --- /dev/null +++ b/mod/publication/classes/local/tests/publication.php @@ -0,0 +1,178 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * subclass of mod_publication to publish all testable methods! + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_publication\local\tests; + +use stdClass; +use coding_exception; +use dml_exception; +use required_capability_exception; +use Exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Test subclass that makes all the protected methods we want to test public. + * + * Please ignore the nasty code in here just catching all kind of exceptions and then throwing them again, it's just to shut up + * code-checker about "unnecessary method overrides" which we need to make the methods under test publicly available! + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication extends \publication { + /** + * Additional method to get publication record. + * + * @return stdClass Instances database record + */ + public function get_publication() { + return $this->instance; + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid active-group-id to register/queue user to + * @param int $userid user to register/queue + * @param bool $previewonly optional don't act, just return a preview + * @return string status message + * @throws coding_exception + * @throws dml_exception + * @throws required_capability_exception + */ + public function testable_register_in_agrp($agrpid, $userid=0, $previewonly=false) { + return parent::register_in_agrp($agrpid, $userid, $previewonly); + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid active-group-id to unregister/unqueue user from + * @param int $userid user to unregister/unqueue + * @param bool $previewonly (optional) don't act, just return a preview + * @return string $message if everything went right + * @throws coding_exception + * @throws dml_exception + * @throws required_capability_exception + */ + public function testable_unregister_from_agrp($agrpid, $userid=0, $previewonly=false) { + return parent::unregister_from_agrp($agrpid, $userid, $previewonly); + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid ID of the active group + * @param int $userid ID of user to queue or null (then $USER->id is used) + * @return bool whether or not user qualifies for a group change + * @throws Exception + */ + public function testable_qualifies_for_groupchange($agrpid, $userid) { + return parent::qualifies_for_groupchange($agrpid, $userid); + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid ID of the active group + * @param int $userid (optional) ID of user to queue or null (then $USER->id is used) + * @param stdClass $message (optional) cached data for the language strings + * @param int $oldagrpid (optional) ID of former active group + * @return string status message + * @throws coding_exception + * @throws dml_exception + * @throws exceedgroupqueuelimit + * @throws exceeduserqueuelimit + * @throws exceeduserreglimit + * @throws registration + * @throws regpresent + * @throws required_capability_exception + */ + public function testable_can_change_group($agrpid, $userid=0, $message=null, $oldagrpid = null) { + return parent::can_change_group($agrpid, $userid, $message, $oldagrpid); + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid ID of active group to change to + * @param int $userid (optional) ID of user to change group for or null ($USER->id is used). + * @param stdClass $message (optional) prepared message object containing username and groupname or null. + * @param int $oldagrpid (optional) ID of former active group + * @return string success message + * @throws coding_exception + * @throws dml_exception + * @throws exceedgroupqueuelimit + * @throws exceeduserqueuelimit + * @throws exceeduserreglimit + * @throws registration + * @throws regpresent + * @throws required_capability_exception + */ + public function testable_change_group($agrpid, $userid = null, $message = null, $oldagrpid = null) { + return parent::change_group($agrpid, $userid, $message, $oldagrpid); + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid + * @param int $userid + * @param stdClass $message + * @return string + * @throws coding_exception + * @throws dml_exception + * @throws required_capability_exception + */ + public function testable_add_registration($agrpid, $userid, $message) { + return parent::add_registration($agrpid, $userid, $message); + } + + /** + * Override method to be available for testing! + * + * @param int $agrpid + * @param int $userid + * @param stdClass $message + * @return string + * @throws coding_exception + * @throws dml_exception + * @throws required_capability_exception + */ + public function testable_add_queue_entry($agrpid, $userid, $message) { + return parent::add_queue_entry($agrpid, $userid, $message); + } + + /** + * Get context module. + * + * @return \context_module + */ + public function get_context() { + return $this->context; + } +} diff --git a/mod/publication/classes/observer.php b/mod/publication/classes/observer.php new file mode 100644 index 0000000..9f1dde0 --- /dev/null +++ b/mod/publication/classes/observer.php @@ -0,0 +1,203 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * observer.php + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_publication; + +use core\notification; +use mod_assign\event\assessable_submitted; +use publication; +use stdClass; + +defined('MOODLE_INTERNAL') || die; + +/** + * mod_grouptool\observer handles events due to changes in moodle core which affect grouptool + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class observer { + /** + * \mod_assign\event\assessable_submitted + * + * @param \mod_assign\event\assessable_submitted $e Event object containing useful data + * @return bool true if success + */ + public static function import_assessable(assessable_submitted $e) { + global $DB, $CFG, $OUTPUT; + + // Keep other page calls slimmed down! + require_once($CFG->dirroot . '/mod/publication/locallib.php'); + + // We have the submission ID, so first we fetch the corresponding submission, assign, etc.! + $assign = $e->get_assign(); + $assignid = $assign->get_course_module()->instance; + $submission = $DB->get_record($e->objecttable, ['id' => $e->objectid]); + + if (!empty($assign->get_instance()->teamsubmission) && !empty($submission->userid)) { + /* If the userid is set, we can skip here... the files and texts are in the submission with groupid set + or groupid 0 for users without group! */ + return true; + } + + $assignmoduleid = $DB->get_field('modules', 'id', ['name' => 'assign']); + $assigncm = $DB->get_record('course_modules', [ + 'course' => $assign->get_course()->id, + 'module' => $assignmoduleid, + 'instance' => $assignid + ]); + $assigncontext = \context_module::instance($assigncm->id); + + $sql = "SELECT pub.id, pub.course + FROM {publication} pub + WHERE (pub.mode = ?) AND (pub.importfrom = ?) AND (pub.autoimport = 1)"; + $params = [\PUBLICATION_MODE_IMPORT, $assignid]; + if (!$publications = $DB->get_records_sql($sql, $params)) { + return true; + } + + $subfilerecords = $DB->get_records('assignsubmission_file', [ + 'assignment' => $assignid, + 'submission' => $submission->id + ]); + $fs = get_file_storage(); + + $assignfileids = []; + $assignfiles = []; + $itemid = empty($assign->get_instance()->teamsubmission) ? $submission->userid : $submission->groupid; + $importtype = empty($assign->get_instance()->teamsubmission) ? 'user' : 'group'; + + foreach ($publications as $curpub) { + $cm = get_coursemodule_from_instance('publication', $curpub->id, 0, false, MUST_EXIST); + $context = \context_module::instance($cm->id); + foreach ($subfilerecords as $record) { + if ($assignfileids == []) { + $files = $fs->get_area_files($assigncontext->id, + "assignsubmission_file", + "submission_files", + $record->submission, + "id", + false); + + foreach ($files as $file) { + $assignfiles[$file->get_id()] = $file; + $assignfileids[$file->get_id()] = $file->get_id(); + } + } + + $conditions = []; + $conditions['publication'] = $curpub->id; + $conditions['userid'] = $itemid; + // We look for regular imported files here! + $conditions['type'] = PUBLICATION_MODE_IMPORT; + + $oldpubfiles = $DB->get_records('publication_file', $conditions); + + foreach ($oldpubfiles as $oldpubfile) { + + if (in_array($oldpubfile->filesourceid, $assignfileids)) { + // File was in assign and is still there. + unset($assignfileids[$oldpubfile->filesourceid]); + + } else { + // File has been removed from assign. + // Remove from publication (file and db entry). + if ($file = $fs->get_file_by_id($oldpubfile->fileid)) { + $file->delete(); + } + + $conditions['id'] = $oldpubfile->id; + $dataobject = $DB->get_record('publication_file', ['id' => $conditions['id']]); + $dataobject->typ = $importtype; + $dataobject->itemid = $itemid; + \mod_publication\event\publication_file_deleted::create_from_object($cm, $dataobject)->trigger(); + $DB->delete_records('publication_file', $conditions); + } + } + + // Add new files to publication. + foreach ($assignfileids as $assignfileid) { + $newfilerecord = new \stdClass(); + $newfilerecord->contextid = $context->id; + $newfilerecord->component = 'mod_publication'; + $newfilerecord->filearea = 'attachment'; + $newfilerecord->itemid = $itemid; + + try { + if ($fs->file_exists($newfilerecord->contextid, + $newfilerecord->component, + $newfilerecord->filearea, + $newfilerecord->itemid, + $assignfiles[$assignfileid]->get_filepath(), + $assignfiles[$assignfileid]->get_filename())) { + notification::info($OUTPUT->box('File existed, skipped creation!', 'generalbox')); + $newfile = $fs->get_file($newfilerecord->contextid, + $newfilerecord->component, + $newfilerecord->filearea, + $newfilerecord->itemid, + $assignfiles[$assignfileid]->get_filepath(), + $assignfiles[$assignfileid]->get_filename()); + } else { + $newfile = $fs->create_file_from_storedfile($newfilerecord, $assignfiles[$assignfileid]); + } + + $dataobject = new \stdClass(); + $dataobject->publication = $curpub->id; + $dataobject->userid = $itemid; + $dataobject->timecreated = time(); + $dataobject->fileid = $newfile->get_id(); + $dataobject->filesourceid = $assignfileid; + $dataobject->filename = $newfile->get_filename(); + $dataobject->contenthash = "666"; + $dataobject->type = \PUBLICATION_MODE_IMPORT; + $DB->insert_record('publication_file', $dataobject); + $dataobject->typ = $importtype; + $dataobject->itemid = $itemid; + \mod_publication\event\publication_file_imported::file_added($cm, $dataobject)->trigger(); + + $publication = new publication($cm); + if ($publication->get_instance()->notifyteacher) { + publication::send_teacher_notification_uploaded($cm, $newfile, null, $publication); + } + + } catch (\Exception $ex) { + // File could not be copied, maybe it does allready exist. + // Should not happen. + notification::error($OUTPUT->box($ex->getMessage(), 'generalbox')); + } + } + + } + + // And now the same for online texts! + \publication::update_assign_onlinetext($assigncm, $assigncontext, $curpub->id, $context->id, $submission->id); + + } + + return true; + } + +} diff --git a/mod/publication/classes/privacy/provider.php b/mod/publication/classes/privacy/provider.php new file mode 100644 index 0000000..308cf3e --- /dev/null +++ b/mod/publication/classes/privacy/provider.php @@ -0,0 +1,727 @@ +<?php +// This file is part of mod_publication for 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 mod_publication + * @author Philipp Hager + * @copyright 2018 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_publication\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\metadata\provider as metadataprovider; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\plugin\provider as pluginprovider; +use core_privacy\local\request\user_preference_provider as preference_provider; +use core_privacy\local\request\writer; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\helper; +use core_privacy\local\request\core_userlist_provider; +use core_privacy\local\request\userlist; +use core_privacy\local\request\approved_userlist; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +/** + * Privacy class for requesting user data. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2018 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements metadataprovider, pluginprovider, preference_provider, core_userlist_provider { + /** + * Provides meta data that is stored about a user with mod_publication + * + * @param collection $collection A collection of meta data items to be added to. + * @return collection Returns the collection of metadata. + */ + public static function get_metadata(collection $collection): collection { + $publicationextduedates = [ + 'userid' => 'privacy:metadata:userid', + 'extensionduedate' => 'privacy:metadata:extensionduedate', + ]; + $publicationfile = [ + 'userid' => 'privacy:metadata:userid', + 'timecreated' => 'privacy:metadata:timecreated', + 'fileid' => 'privacy:metadata:fileid', + 'filesourceid' => 'privacy:metadata:fileid', + 'filename' => 'privacy:metadata:filename', + 'contenthash' => 'privacy:metadata:contenthash', + 'type' => 'privacy:metadata:type', + 'teacherapproval' => 'privacy:metadata:teacherapproval', + 'studentapproval' => 'privacy:metadata:studentapproval' + ]; + $publicationgroupapproval = [ + 'fileid' => 'privacy:metadata:fileid', + 'userid' => 'privacy:metadata:userid', + 'approval' => 'privacy:metadata:approval', + 'timemodified' => 'privacy:metadata:timemodified' + ]; + + $collection->add_database_table('publication_extduedates', $publicationextduedates, 'privacy:metadata:extduedates'); + $collection->add_database_table('publication_file', $publicationfile, 'privacy:metadata:files'); + $collection->add_database_table('publication_groupapproval', $publicationgroupapproval, 'privacy:metadata:groupapproval'); + + $collection->add_user_preference('publication_perpage', 'privacy:metadata:publicationperpage'); + + // Link to subplugins. + $collection->add_subsystem_link('core_files', [], 'privacy:metadata:publicationfileexplanation'); + + return $collection; + } + + /** + * Returns all of the contexts that has information relating to the userid. + * + * @param int $userid The user ID. + * @return contextlist an object with the contexts related to a userid. + */ + public static function get_contexts_for_userid(int $userid): contextlist { + global $DB; + + $params = [ + 'modulename' => 'publication', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + 'guserid' => $userid, + 'extuserid' => $userid, + 'fuserid' => $userid + ]; + + $enroled = enrol_get_all_users_courses($userid); + if (!empty($enroled)) { + $enroled = array_keys($enroled); + } else { + $enroled = [-1]; + } + list($enrolsql, $enrolparams) = $DB->get_in_or_equal($enroled, SQL_PARAMS_NAMED, 'enro'); + $params = $params + $enrolparams; + + /* The where clause is quite interesting here, because we have to differentiate + * if there was a mod_assign to import from with teamsubmission enabled or not. + * If we imported from an assign instance with teamsubmissions the userid-fields often contains the group's id. + * If we uploaded or imported from a none-teamsubmission-assign instance we have the userid fields populated with user's + * ids. + * Did I also mention the possibility of imports from teamsubmission-assigns which won't prevent users without groups be + * counted as special "standard group"? We also consider these here! + * + * I know it's not the best design, but when implementing the teamsubmission-imports we had not much time to add another + * field to reference group-ids. + * TODO: split {publication_file}.userid to a userid and a groupid field or rename it at least to itemid! */ + $sql = " + SELECT DISTINCT ctx.id + FROM {course_modules} cm + JOIN {modules} m ON cm.module = m.id AND m.name = :modulename + JOIN {publication} p ON cm.instance = p.id + JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel +LEFT JOIN {publication_extduedates} ext ON p.id = ext.publication +LEFT JOIN {publication_file} f ON p.id = f.publication +LEFT JOIN {publication_groupapproval} ga ON f.id = ga.fileid +LEFT JOIN {assign} a ON p.importfrom = a.id +LEFT JOIN {groups} g ON g.courseid = p.course +LEFT JOIN {groups_members} gm ON g.id = gm.groupid AND gm.userid = :guserid + WHERE ((p.importfrom > 0 AND a.teamsubmission > 0) + AND ((gm.userid = :userid AND (ext.userid = gm.groupid OR f.userid = gm.groupid)) + OR (gm.userid IS NULL AND f.userid = 0 AND a.preventsubmissionnotingroup = 0 AND g.courseid $enrolsql))) + OR ((p.importfrom <= 0 OR a.teamsubmission = 0) AND (ext.userid = :extuserid OR f.userid = :fuserid))"; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $params = [ + 'modulename' => 'publication', + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + 'upload' => PUBLICATION_MODE_UPLOAD + ]; + + // Get all who uploaded/have files imported! + // First get all regular uploads. + $sql = "SELECT f.userid + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {publication} p ON p.id = cm.instance + JOIN {publication_file} f ON p.id = f.publication + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel AND p.mode = :upload"; + $userlist->add_from_sql('userid', $sql, $params); + + unset($params['upload']); + $params['import'] = PUBLICATION_MODE_IMPORT; + // Second get all imported file's users. + $sql = "SELECT gm.userid + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {publication} p ON p.id = cm.instance + JOIN {publication_file} f ON p.id = f.publication + LEFT JOIN {assign} a ON p.importfrom = a.id + LEFT JOIN {groups} g ON g.courseid = p.course AND f.userid = g.id + LEFT JOIN {groups_members} gm ON g.id = gm.groupid + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel AND p.mode = :import + AND (p.importfrom > 0 AND a.teamsubmission > 0)"; + $userlist->add_from_sql('userid', $sql, $params); + $sql = "SELECT f.userid + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {publication} p ON p.id = cm.instance + JOIN {publication_file} f ON p.id = f.publication + LEFT JOIN {assign} a ON p.importfrom = a.id + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel AND p.mode = :import + AND (p.importfrom > 0 AND a.teamsubmission = 0)"; + $userlist->add_from_sql('userid', $sql, $params); + // TODO: std-Group-Members may be missing here! + + // Get all who got an extension! + $sql = "SELECT e.userid + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {publication} p ON p.id = cm.instance + JOIN {publication_extduedates} e ON p.id = e.publication + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; + $userlist->add_from_sql('userid', $sql, $params); + + // Get all who gave (group) approval! + $sql = "SELECT ga.userid + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {publication} p ON p.id = cm.instance + JOIN {publication_file} f ON p.id = f.publication + JOIN {publication_groupapproval} ga ON p.id = ga.fileid + WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + + if ($context->contextlevel == CONTEXT_MODULE) { + // Apparently we can't trust anything that comes via the context. + // Go go mega query to find out it we have an checkmark context that matches an existing checkmark. + $sql = "SELECT p.id + FROM {publication} p + JOIN {course_modules} cm ON p.id = cm.instance AND p.course = cm.course + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule + WHERE ctx.id = :contextid"; + $params = ['modulename' => 'publication', 'contextmodule' => CONTEXT_MODULE, 'contextid' => $context->id]; + $id = $DB->get_field_sql($sql, $params); + // If we have an id over zero then we can proceed. + if ($id > 0) { + $userids = $userlist->get_userids(); + if (count($userids) <= 0) { + return; + } + + $fs = get_file_storage(); + + list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'usr'); + + // Delete users' files, extended due dates and groupapprovals for this publication! + $DB->delete_records_select('publication_extduedates', "publication = :id AND userid ".$usersql, + ['id' => $id] + $userparams); + $files = $DB->get_records_select('publication_file', "publication = :id AND userid ".$usersql, + ['id' => $id] + $userparams); + + if ($files) { + $fileids = array_keys($files); + foreach ($files as $cur) { + $file = $fs->get_file_by_id($cur->fileid); + $file->delete(); + } + list($filesql, $fileparams) = $DB->get_in_or_equal($fileids, SQL_PARAMS_NAMED, 'file'); + $DB->delete_records_select('publication_groupapproval', "(fileid $filesql) AND (userid ".$usersql.")", + $fileparams + $userparams); + $DB->delete_records_list('publication_file', 'id', $fileids); + } + + } + } + } + + + /** + * Write out the user data filtered by contexts. + * + * + * @param approved_contextlist $contextlist contexts that we are writing data out from. + * @throws \dml_exception + * @throws \coding_exception + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + $contexts = $contextlist->get_contexts(); + + if (empty($contexts)) { + return; + } + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT + c.id AS contextid, + p.*, + cm.id AS cmid + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid + JOIN {publication} p ON p.id = cm.instance + WHERE c.id {$contextsql}"; + + // Keep a mapping of publicationid to contextid. + $mappings = []; + + $publications = $DB->get_records_sql($sql, $contextparams); + + $user = $contextlist->get_user(); + + foreach ($publications as $publication) { + $context = \context_module::instance($publication->cmid); + $mappings[$publication->id] = $publication->contextid; + + // Check that the context is a module context. + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + + $publicationdata = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + + $cm = get_coursemodule_from_instance('publication', $publication->id); + + $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + $publication = new \publication($cm, $course, $context); + + writer::with_context($context)->export_data([], $publicationdata); + + /* We don't differentiate between roles, if we have data about the user, we give it freely ;) - no sensible + * information here! */ + + static::export_user_preferences($user->id); + static::export_extensions($context, $publication, $user); + static::export_files($context, $publication, $user, []); + } + } + + /** + * Stores the user preferences related to mod_publication. + * + * @param int $userid The user ID that we want the preferences for. + * @throws \dml_exception + * @throws \coding_exception + */ + public static function export_user_preferences(int $userid) { + $context = \context_system::instance(); + $value = get_user_preferences('publication_perpage', null, $userid); + if ($value !== null) { + writer::with_context($context)->export_user_preference('mod_publication', 'publication_perpage', $value, + get_string('privacy:metadata:publicationperpage', 'mod_publication')); + } + } + + /** + * Export overrides for this assignment. + * + * @param \context $context Context + * @param \publication $pub The publication object. + * @param \stdClass $user The user object. + * @throws \coding_exception + */ + public static function export_extensions(\context $context, \publication $pub, \stdClass $user) { + $ext = $pub->user_extensionduedate($user->id); + // Overrides returns an array with data in it, but an override with actual data will have the assign ID set. + if ($ext > 0) { + $data = (object)[get_string('privacy:extensionduedate', 'mod_publication') => transform::datetime($ext)]; + writer::with_context($context)->export_data([], $data); + } + } + + /** + * Fetches all of the user's files and adds them to the export + * + * @param \context_module $context + * @param \publication $pub + * @param \stdClass $user + * @param array $path Current directory path that we are exporting to. + * @throws \dml_exception + * @throws \coding_exception + */ + protected static function export_files(\context_module $context, \publication $pub, \stdClass $user, array $path) { + global $DB; + + $groupimports = false; + $emptygroup = false; + if (($pub->get_instance()->mode == PUBLICATION_MODE_IMPORT) && ($pub->get_instance()->importfrom > 0)) { + $assign = $DB->get_record('assign', ['id' => $pub->get_instance()->importfrom], + 'name, teamsubmission, preventsubmissionnotingroup'); + $groupimports = $assign->teamsubmission; + if ($groupimports && !$assign->preventsubmissionnotingroup) { + $groups = groups_get_user_groups($pub->get_instance()->course, $user->id); + $emptygroup = ($groups === [0 => []]) ? true : false; + } + } + + if ($groupimports) { + // Imported files are saved under group's ID! + if (!$emptygroup) { + $rs = $DB->get_recordset_sql(" + SELECT f.* + FROM {publication} p + JOIN {publication_file} f ON p.id = f.publication + JOIN {groups} g ON g.courseid = p.course + JOIN {groups_members} gm ON g.id = gm.groupid AND gm.userid = :userid AND f.userid = gm.groupid + WHERE p.id = :publication", [ + 'publication' => $pub->get_instance()->id, + 'userid' => $user->id + ]); + } else { + $rs = $DB->get_recordset("publication_file", [ + 'publication' => $pub->get_instance()->id, + 'userid' => 0 + ]); + } + } else { + // Imported and uploaded files are saved with user's ID! + $rs = $DB->get_recordset_sql("SELECT f.* + FROM {publication} p + JOIN {publication_file} f ON p.id = f.publication + WHERE p.id = :publication AND f.userid = :userid", [ + 'publication' => $pub->get_instance()->id, + 'userid' => $user->id + ]); + } + + foreach ($rs as $cur) { + $filepath = array_merge($path, [get_string('privacy:path:files', 'mod_publication'), $cur->filename]); + switch ($cur->type) { + case PUBLICATION_MODE_ONLINETEXT: + static::export_onlinetext($context, $cur, $filepath); + break; + default: + static::export_file($context, $cur, $filepath); + break; + } + } + + if ($groupimports) { + static::export_groupapprovals($context, $pub, $user, $path); + } + } + + /** + * Exports an uploaded/imported file! + * + * @param \context_module $context + * @param \stdClass $file + * @param array $path + * @throws \coding_exception + */ + protected static function export_file(\context_module $context, \stdClass $file, array $path) { + // Export file! + static $fs = null; + + if ($fs === null) { + $fs = new \file_storage(); + } + + $fsfile = $fs->get_file_by_id($file->fileid); + static::export_file_metadata($context, $file, $path); + writer::with_context($context)->export_custom_file($path, $fsfile->get_filename(), $fsfile->get_content()); + } + + /** + * Adds the metadata of an imported/uploaded file to the export! + * + * @param \context_module $context + * @param \stdClass $file + * @param array $path + * @throws \coding_exception + */ + protected static function export_file_metadata(\context_module $context, \stdClass $file, array $path) { + // Export file's metadata! + $export = (object)[ + 'timecreated' => transform::datetime($file->timecreated), + 'filename' => $file->filename, + 'contenthash' => $file->contenthash, + 'teacherapproval' => transform::yesno($file->teacherapproval), + 'studentapproval' => transform::yesno($file->studentapproval) + ]; + switch ($file->type) { + case PUBLICATION_MODE_IMPORT: + $export->type = get_string('privacy:type:import', 'publication'); + break; + case PUBLICATION_MODE_UPLOAD: + $export->type = get_string('privacy:type:upload', 'publication'); + break; + case PUBLICATION_MODE_ONLINETEXT: + $export->type = get_string('privacy:type:onlinetext', 'publication'); + break; + } + + writer::with_context($context)->export_data($path, (object)$export); + } + + /** + * Adds an imported onlinetext and resources to export! + * + * @param \context_module $context + * @param \stdClass $file + * @param array $path + * @throws \coding_exception + */ + protected static function export_onlinetext(\context_module $context, \stdClass $file, array $path) { + // Export file! + static $fs = null; + + if ($fs === null) { + $fs = new \file_storage(); + } + + $fsfile = $fs->get_file_by_id($file->fileid); + + static::export_file_metadata($context, $file, $path); + writer::with_context($context)->export_custom_file($path, $fsfile->get_filename(), $fsfile->get_content()); + + /* + * Export resources! + * We won't use writer::with_context($context)->export_area_files() due to us only needing a subdirectory! + */ + $resources = $fs->get_directory_files($context->id, + 'mod_publication', + 'attachment', + $fsfile->get_itemid(), + '/resources/', + true, + false); + if (count($resources) > 0) { + foreach ($resources as $cur) { + writer::with_context($context)->export_custom_file(array_merge($path, [ + get_string('privacy:path:resources', 'mod_publication') + ]), $cur->get_filename(), $cur->get_content()); + } + } + } + + /** + * Fetches all of the user's group approvals and adds them to the export + * + * @param \context $context + * @param \publication $pub + * @param \stdClass $user + * @param array $path Current directory path that we are exporting to. + * @throws \dml_exception + */ + protected static function export_groupapprovals(\context $context, \publication $pub, \stdClass $user, array $path) { + global $DB; + + // Fetch all approvals! + $rs = $DB->get_recordset_sql("SELECT ga.id, f.filename, ga.userid, ga.approval, ga.timecreated, ga.timemodified, + f.userid AS groupid + FROM {publication_groupapproval} ga + JOIN {publication_file} f ON ga.fileid = f.id + WHERE ga.userid = :userid AND f.publication = :publication", [ + 'userid' => $user->id, + 'publication' => $pub->get_instance()->id + ]); + + foreach ($rs as $cur) { + static::export_groupapproval($context, $cur, $path); + } + + $rs->close(); + } + + /** + * Formats and then exports the user's approval data. + * + * @param \context $context + * @param \stdClass $approval + * @param array $path Current directory path that we are exporting to. + */ + protected static function export_groupapproval(\context $context, \stdClass $approval, array $path) { + $approvaldata = (object)[ + 'filename' => $approval->filename, + 'approval' => transform::yesno($approval->approval), + 'groupid' => $approval->groupid, + 'timecreated' => transform::datetime($approval->timecreated), + 'timemodified' => transform::datetime($approval->timemodified) + ]; + + writer::with_context($context)->export_data($path, $approvaldata); + } + + /** + * Delete all use data which matches the specified context. + * + * @param \context $context The module context. + * @throws \dml_exception + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if ($context->contextlevel == CONTEXT_MODULE) { + $fs = new \file_storage(); + + // Apparently we can't trust anything that comes via the context. + // Go go mega query to find out it we have an assign context that matches an existing assignment. + $sql = "SELECT p.id + FROM {publication} p + JOIN {course_modules} cm ON p.id = cm.instance AND p.course = cm.course + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule + WHERE ctx.id = :contextid"; + $params = ['modulename' => 'publication', 'contextmodule' => CONTEXT_MODULE, 'contextid' => $context->id]; + $id = $DB->get_field_sql($sql, $params); + // If we have a count over zero then we can proceed. + if ($id > 0) { + // Get all publication files and group approvals to delete them! + if ($files = $DB->get_records('publication_file', ['publication' => $id])) { + $fileids = array_keys($files); + + // Go through all files and delete files and resources in filespace! + foreach ($files as $cur) { + $fs->delete_area_files($context->id, 'mod_publication', 'attachment', $cur->userid); + } + + $DB->delete_records_list('publication_groupapproval', 'fileid', $fileids); + $DB->delete_records_list('publication_file', 'id', $fileids); + } + + $DB->delete_records('publication_extduedates', ['publication' => $id]); + } + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + * @throws \dml_exception + * @throws \coding_exception + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + $user = $contextlist->get_user(); + $fs = new \file_storage(); + + $contextids = $contextlist->get_contextids(); + + if (empty($contextids) || $contextids === []) { + return; + } + + list($ctxsql, $ctxparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'ctx'); + + // Apparently we can't trust anything that comes via the context. + // Go go mega query to find out it we have an assign context that matches an existing assignment. + $sql = "SELECT ctx.id AS ctxid, p.* + FROM {publication} p + JOIN {course_modules} cm ON p.id = cm.instance AND p.course = cm.course + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule + WHERE ctx.id ".$ctxsql; + $params = ['modulename' => 'publication', 'contextmodule' => CONTEXT_MODULE]; + + if (!$records = $DB->get_records_sql($sql, $params + $ctxparams)) { + return; + } + + foreach ($contextlist as $context) { + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + + $pub = $records[$context->id]; + + $teams = false; + $emptygroup = false; + if ($pub->mode === PUBLICATION_MODE_IMPORT) { + $assign = $DB->get_record('assign', ['id' => $pub->importfrom]); + $teams = $assign->teamsubmission; + $usergroups = groups_get_user_groups($pub->course, $user->id); + $emptygroup = $teams && !$assign->preventsubmissionnotingroup && ($usergroups === [0 => []]); + } + + if ($emptygroup) { + $files = $DB->get_records('publication_file', ['publication' => $pub->id, 'userid' => 0]); + } else if (!$teams) { + $files = $DB->get_records('publication_file', ['publication' => $pub->id, 'userid' => $user->id]); + } else { + $files = []; + + $usergroups = groups_get_all_groups($pub->course, $user->id); + foreach (array_keys($usergroups) as $grpid) { + $files = $files + $DB->get_records('publication_file', ['publication' => $pub->id, 'userid' => $grpid]); + } + } + + if ($files) { + $fileids = array_keys($files); + + // Go through all files and delete files and resources in filespace! + foreach ($files as $cur) { + if (!$teams) { + $fs->delete_area_files($context->id, 'mod_publication', 'attachment', $cur->userid); + } else { + groups_remove_member($cur->userid, $user->id); + } + } + + list($filesql, $fileparams) = $DB->get_in_or_equal($fileids, SQL_PARAMS_NAMED, 'file'); + $DB->delete_records_select('publication_groupapproval', 'userid = :userid AND fileid '.$filesql, + ['userid' => $user->id] + $fileparams); + if (!$teams) { + $DB->delete_records_list('publication_file', 'id', $fileids); + } + } + + $DB->delete_records('publication_extduedates', ['publication' => $pub->id, 'userid' => $user->id]); + } + } +} diff --git a/mod/publication/classes/report_editdates_integration.php b/mod/publication/classes/report_editdates_integration.php new file mode 100644 index 0000000..7a7386d --- /dev/null +++ b/mod/publication/classes/report_editdates_integration.php @@ -0,0 +1,104 @@ +<?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/>. + +/** + * Contains class for report-editdates support + * + * @package mod_publication + * @author Hannes Laimer + * @author Andreas Krieger + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Class needed for report-editdates support + * + * @package mod_publication + * @author Hannes Laimer + * @author Andreas Krieger + * @copyright 2019 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_report_editdates_integration +extends report_editdates_mod_date_extractor { + + /** + * mod_publication_report_editdates_integration constructor. + * @param object $course the course + */ + public function __construct($course) { + parent::__construct($course, 'publication'); + parent::load_data(); + } + + /** + * Returns the duedates for a specific publication + * @param cm_info $cm + * @return array + * @throws coding_exception + */ + public function get_settings(cm_info $cm) { + $publication = $this->mods[$cm->instance]; + + return array( + 'allowsubmissionsfromdate' => new report_editdates_date_setting( + get_string('allowsubmissionsfromdate', 'publication'), + $publication->allowsubmissionsfromdate, + self::DATETIME, true, 5), + 'duedate' => new report_editdates_date_setting( + get_string('duedate', 'publication'), + $publication->duedate, + self::DATETIME, true, 5), + ); + } + + /** + * Validates dates + * @param cm_info $cm + * @param array $dates + * @return array + * @throws coding_exception + */ + public function validate_dates(cm_info $cm, array $dates) { + $errors = array(); + if ($dates['allowsubmissionsfromdate'] && $dates['duedate'] + && $dates['duedate'] < $dates['allowsubmissionsfromdate']) { + $errors['duedate'] = get_string('duedatevalidation', 'publication'); + } + + return $errors; + } + + /** + * Saves the dates + * @param cm_info $cm + * @param array $dates + * @throws dml_exception + */ + public function save_dates(cm_info $cm, array $dates) { + global $DB; + + $update = new stdClass(); + $update->id = $cm->instance; + $update->duedate = $dates['duedate']; + $update->allowsubmissionsfromdate = $dates['allowsubmissionsfromdate']; + + $DB->update_record('publication', $update); + } +} diff --git a/mod/publication/classes/search/activity.php b/mod/publication/classes/search/activity.php new file mode 100644 index 0000000..6d154a4 --- /dev/null +++ b/mod/publication/classes/search/activity.php @@ -0,0 +1,66 @@ +<?php +// This file is part of mod_publication for 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/>. + +/** + * Search area for mod_publication activities. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_publication\search; + +use core_search\base_activity as base_activity; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Search area for mod_publication activities. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity extends base_activity { + /** + * Returns true if this area uses file indexing. + * + * @return bool + */ + public function uses_file_indexing() { + return true; + } + + /** + * Add the attached description files. + * + * @param \core_search\document $document The current document + */ + public function attach_files($document) { + $fs = get_file_storage(); + + $cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid')); + $context = \context_module::instance($cm->id); + + $files = $fs->get_area_files($context->id, 'mod_publication', 'intro', false, 'sortorder DESC, id ASC', false); + + foreach ($files as $file) { + $document->add_stored_file($file); + } + } +} diff --git a/mod/publication/db/access.php b/mod/publication/db/access.php new file mode 100644 index 0000000..4c3c7fc --- /dev/null +++ b/mod/publication/db/access.php @@ -0,0 +1,91 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Access control for mod_publication (capabilities definitions) + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + 'mod/publication:view' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'guest' => CAP_ALLOW, + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + + 'mod/publication:addinstance' => [ + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ], + + 'mod/publication:upload' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + + 'mod/publication:approve' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + + 'mod/publication:grantextension' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + + 'mod/publication:receiveteachernotification' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW + ] + ], +]; diff --git a/mod/publication/db/events.php b/mod/publication/db/events.php new file mode 100644 index 0000000..77ade15 --- /dev/null +++ b/mod/publication/db/events.php @@ -0,0 +1,37 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * By mod_publication observed events + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$observers = [ + [ + 'eventname' => 'mod_assign\event\assessable_submitted', + 'callback' => 'mod_publication\observer::import_assessable', + 'includefile' => '/mod/publication/classes/observer.php', + 'priority' => 0, + 'internal' => true, + ], + +]; diff --git a/mod/publication/db/install.xml b/mod/publication/db/install.xml new file mode 100644 index 0000000..5f19631 --- /dev/null +++ b/mod/publication/db/install.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="mod/publication/db" VERSION="20161101" COMMENT="XMLDB file for Moodle mod/publication" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="publication" COMMENT="Defines publication"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="alwaysshowdescription" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="duedate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="allowsubmissionsfromdate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="cutoffdate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="mode" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="importfrom" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="obtainstudentapproval" TYPE="int" LENGTH="2" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="maxfiles" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="maxbytes" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="allowedfiletypes" TYPE="char" LENGTH="255" NOTNULL="false" DEFAULT="" SEQUENCE="false"/> + <FIELD NAME="obtainteacherapproval" TYPE="int" LENGTH="2" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="autoimport" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="groupapproval" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="notifyteacher" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="f"/> + <FIELD NAME="notifystudents" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + <INDEXES> + <INDEX NAME="course" UNIQUE="false" FIELDS="course"/> + </INDEXES> + </TABLE> + <TABLE NAME="publication_file" COMMENT="Defines publication files"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="publication" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="fileid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="filesourceid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="filename" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="contenthash" TYPE="char" LENGTH="40" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="type" TYPE="int" LENGTH="2" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="teacherapproval" TYPE="int" LENGTH="2" NOTNULL="false" DEFAULT="3" SEQUENCE="false"/> + <FIELD NAME="studentapproval" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" + COMMENT="Holds the student approval or a cached cumulated state for approval in case of group approval"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + <TABLE NAME="publication_extduedates" COMMENT="Defines extension duedates for publications"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="publication" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="extensionduedate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + </KEYS> + </TABLE> + <TABLE NAME="publication_groupapproval" + COMMENT="Table containing group approval for the files imported from teamsubmissions, will always be cumulated up to date in files table!"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="fileid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="approval" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false" COMMENT="Approval or rejection"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id"/> + <KEY NAME="fileid" TYPE="foreign" FIELDS="fileid" REFTABLE="publication_files" REFFIELDS="id"/> + <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/> + </KEYS> + </TABLE> + </TABLES> +</XMLDB> diff --git a/mod/publication/db/messages.php b/mod/publication/db/messages.php new file mode 100644 index 0000000..d9bc75a --- /dev/null +++ b/mod/publication/db/messages.php @@ -0,0 +1,36 @@ +<?php +// This file is part of mod_checkmark for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * db/messages.php + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/* + * Notifications for graded submissions (teacher -> user) and + * new submissions (user -> teacher)! + * + */ + +$messageproviders = array ( + 'publication_updates' => array () +); diff --git a/mod/publication/db/services.php b/mod/publication/db/services.php new file mode 100644 index 0000000..00e5d14 --- /dev/null +++ b/mod/publication/db/services.php @@ -0,0 +1,49 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Webservice definition + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$services = [ + 'mod_publication_onlinetextpreview' => [ // The name of the web service. + 'functions' => ['mod_publication_get_onlinetextpreview'], // Web service functions of this service. + 'requiredcapability' => '', /* If set, the web service user need this capability to access any function of this + * service. For example: 'some/capability:specified'.*/ + 'restrictedusers' => 0, /* If enabled, the Moodle administrator must link some user to this service into the + * administration.*/ + 'enabled' => 1, // If enabled, the service can be reachable on a default installation. + ] +]; + +$functions = [ + 'mod_publication_get_onlinetextpreview' => [ // Web service function name. + 'classname' => 'mod_publication_external', // Class containing the external function. + 'methodname' => 'get_onlinetextpreview', // External function name. + 'classpath' => 'mod/publication/externallib.php', // File containing the class/external function. + 'description' => 'Fetches HTML snippet to preview onlinetext.', /* Human readable description of the web service + * function.*/ + 'type' => 'read', // Database rights of the WS-function (read, write). + 'ajax' => true, + ], +]; diff --git a/mod/publication/db/upgrade.php b/mod/publication/db/upgrade.php new file mode 100644 index 0000000..d6913ab --- /dev/null +++ b/mod/publication/db/upgrade.php @@ -0,0 +1,206 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Keeps track of all DB(-structure) changes and other upgrade steps for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * Handles all the upgrade steps for mod_publication + * + * @param int $oldversion the currently installed publication version + * @return bool true if everythings allright + */ +function xmldb_publication_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2014032201) { + $table = new xmldb_table('publication_file'); + + // Add field alwaysshowdescription. + $field = new xmldb_field('filesourceid', XMLDB_TYPE_INTEGER, '10', false, false, false, '0', 'fileid'); + + // Conditionally launch add field alwaysshowdescription. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Organizer savepoint reached. + upgrade_mod_savepoint(true, 2014032201, 'publication'); + } + + if ($oldversion < 2015120201) { + // Remove unused settings (requiremodintro and duplicates of stdexamplecount and requiremodintro)! + $DB->delete_records('config_plugins', [ + 'plugin' => 'publication', + 'name' => 'requiremodintro' + ]); + + upgrade_mod_savepoint(true, 2015120201, 'publication'); + } + + // Moodle v3.1.0 release upgrade line. + // Put any upgrade step following this. + + if ($oldversion < 2016051200) { + + // Define field autoimport to be added to publication. + $table = new xmldb_table('publication'); + $field = new xmldb_field('autoimport', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'obtainteacherapproval'); + + // Conditionally launch add field autoimport. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Assign savepoint reached. + upgrade_mod_savepoint(true, 2016051200, 'publication'); + } + + if ($oldversion < 2016062201) { + + // Define field groupapproval to be added to publication. + $table = new xmldb_table('publication'); + $field = new xmldb_field('groupapproval', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'autoimport'); + + // Conditionally launch add field groupapproval. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define table publication_groupapproval to be created. + $table = new xmldb_table('publication_groupapproval'); + + // Adding fields to table publication_groupapproval. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('fileid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('approval', XMLDB_TYPE_INTEGER, '4', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table publication_groupapproval. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('fileid', XMLDB_KEY_FOREIGN, ['fileid'], 'publication_files', ['id']); + $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']); + + // Conditionally launch create table for publication_groupapproval. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define field groupapproval to be added to publication. + $table = new xmldb_table('publication_groupapproval'); + + $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'approval'); + // Conditionally launch add field groupapproval. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'timecreated'); + // Conditionally launch add field groupapproval. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Publication savepoint reached. + upgrade_mod_savepoint(true, 2016062201, 'publication'); + } + + // Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this! + + // Moodle v3.3.0 release upgrade line. + // Put any upgrade step following this! + + if ($oldversion < 2017071200) { + // Get all old filetype-restrictions and convert them! + $rs = $DB->get_recordset_sql("SELECT id id, allowedfiletypes allowedfiletypes + FROM {publication} + WHERE " . $DB->sql_isnotempty('publication', 'allowedfiletypes', false, true)); + echo "<pre>"; + foreach ($rs as $cur) { + // We only convert old style entries! + if (!preg_match('/^([\.A-Za-z0-9]+([ ]*[,][ ]*[\.A-Za-z0-9]+)*)$/', $cur->allowedfiletypes)) { + echo "Skipping record with ID " . $cur->id . " having filetypes '" . $cur->allowedfiletypes . "'' allowed!<br />\n"; + continue; + } + + $allowedfiletypes = preg_split('([ ]*[,][ ]*)', $cur->allowedfiletypes); + array_walk($allowedfiletypes, function (&$type) { + if ((strpos($type, '.') === false) || (strpos($type, '.') !== 0)) { + $type = '.' . $type; + } + }); + echo "Update allowedfiletypes for ID " . $cur->id . ": " . $cur->allowedfiletypes . " --> " . + implode('; ', $allowedfiletypes) . + "<br />\n"; + $cur->allowedfiletypes = implode('; ', $allowedfiletypes); + $DB->update_record('publication', $cur); + } + echo "</pre>"; + $rs->close(); + + // Publication savepoint reached. + upgrade_mod_savepoint(true, 2017071200, 'publication'); + } + + if ($oldversion < 2019052100) { + + // Define field notifyteacher to be added to publication. + $table = new xmldb_table('publication'); + $field = new xmldb_field('notifyteacher', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1', 'groupapproval'); + $field2 = new xmldb_field('notifystudents', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'notifyteacher'); + + // Conditionally launch add field notifyteacher. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + if (!$dbman->field_exists($table, $field2)) { + $dbman->add_field($table, $field2); + } + + // Publication savepoint reached. + upgrade_mod_savepoint(true, 2019052100, 'publication'); + } + + if ($oldversion < 2020010500) { + + // Changing the default of field teacherapproval on table publication_file to 3. + $table = new xmldb_table('publication_file'); + $field = new xmldb_field('teacherapproval', XMLDB_TYPE_INTEGER, '2', null, null, null, '3', 'type'); + + $DB->set_field('publication_file', 'teacherapproval', 3, ['teacherapproval' => null]); + + // Launch change of default for field teacherapproval. + $dbman->change_field_default($table, $field); + + // Publication savepoint reached. + upgrade_mod_savepoint(true, 2020010500, 'publication'); + } + + return true; +} diff --git a/mod/publication/externallib.php b/mod/publication/externallib.php new file mode 100644 index 0000000..abc465d --- /dev/null +++ b/mod/publication/externallib.php @@ -0,0 +1,97 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * mod_publication external file + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->libdir . "/externallib.php"); +require_once($CFG->dirroot . "/mod/publication/locallib.php"); + +/** + * Class mod_publication_external contains external functions used by mod_publication's AJAX + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_external extends external_api { + + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function get_onlinetextpreview_parameters() { + // Function get_onlinetextpreview_parameters() always return an external_function_parameters(). + // The external_function_parameters constructor expects an array of external_description. + return new external_function_parameters( + // An external_description can be: external_value, external_single_structure or an external_multiple structure! + [ + 'itemid' => new external_value(PARAM_INT, 'Group\'s or user\'s ID'), + 'cmid' => new external_value(PARAM_INT, 'Coursemodule ID') + ] + ); + } + + + /** + * The function itself + * + * @param int $itemid the itemid (user ID or group ID) whom the onlinetext belongs to! + * @param int $cmid the course-module ID + * @return string welcome message + */ + public static function get_onlinetextpreview($itemid, $cmid) { + global $DB; + + // Parameters validation! + $params = self::validate_parameters(self::get_onlinetextpreview_parameters(), + [ + 'itemid' => $itemid, + 'cmid' => $cmid + ]); + $cm = get_coursemodule_from_id('publication', $params['cmid'], 0, false, MUST_EXIST); + $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + $context = context_module::instance($cm->id); + self::validate_context($context); + require_capability('mod/publication:view', $context); + require_login($course, true, $cm); + + $text = publication::export_onlinetext_for_preview($params['itemid'], $cm->instance, $context->id); + + return format_text($text, FORMAT_HTML); + } + + /** + * Returns description of method result value + * + * @return external_description + */ + public static function get_onlinetextpreview_returns() { + return new external_value(PARAM_RAW, 'HTML snippet representing the online text to use in overlay', VALUE_OPTIONAL, ''); + } +} diff --git a/mod/publication/grantextension.php b/mod/publication/grantextension.php new file mode 100644 index 0000000..fea4e41 --- /dev/null +++ b/mod/publication/grantextension.php @@ -0,0 +1,99 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Displays the form for granting extensions for student's submissions! + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); + +global $CFG, $DB, $OUTPUT, $PAGE; + +require_once($CFG->dirroot . '/mod/publication/locallib.php'); +require_once($CFG->dirroot . '/mod/publication/mod_publication_grantextension_form.php'); + +$id = optional_param('id', 0, PARAM_INT); // Course Module ID. +$userids = required_param_array('userids', PARAM_INT); // User id. + +$url = new moodle_url('/mod/publication/grantextension.php', ['id' => $id]); +if (!$cm = get_coursemodule_from_id('publication', $id, 0, false, MUST_EXIST)) { + print_error('invalidcoursemodule'); +} + +if (!$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST)) { + print_error('coursemisconf'); +} + +require_login($course, false, $cm); + +$context = context_module::instance($cm->id); + +require_capability('mod/publication:grantextension', $context); + +$publication = new publication($cm, $course, $context); + +$url = new moodle_url('/mod/publication/grantextension.php', ['cmid' => $cm->id]); +if (!empty($id)) { + $url->param('id', $id); +} + +$PAGE->set_url($url); + +// Create a new form object. +$mform = new mod_publication_grantextension_form(null, + ['publication' => $publication, 'userids' => $userids]); + +if ($mform->is_cancelled()) { + redirect(new moodle_url('/mod/publication/view.php', ['id' => $cm->id])); + +} else if ($data = $mform->get_data()) { + // Store updated set of files. + $dataobject = []; + $dataobject['publication'] = $publication->get_instance()->id; + + foreach ($data->userids as $uid) { + $dataobject['userid'] = $uid; + + $DB->delete_records('publication_extduedates', $dataobject); + + if ($data->extensionduedate > 0) { + // Create new record. + $dataobject['extensionduedate'] = $data->extensionduedate; + \mod_publication\event\publication_duedate_extended::duedate_extended($cm, $dataobject)->trigger(); + $DB->insert_record('publication_extduedates', (object)$dataobject); + } + } + + redirect(new moodle_url('/mod/publication/view.php', ['id' => $cm->id])); +} + +// Load existing files into draft area. + +echo $OUTPUT->header(); + +echo $OUTPUT->heading(format_string($publication->get_instance()->name)); + +$publication->display_intro(); + +$mform->display(); + +echo $OUTPUT->footer(); diff --git a/mod/publication/index.php b/mod/publication/index.php new file mode 100644 index 0000000..fae3e0b --- /dev/null +++ b/mod/publication/index.php @@ -0,0 +1,119 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Displays a list of all mod_publication instances in course + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +$id = required_param('id', PARAM_INT); // We need a course! + +if (!$course = $DB->get_record('course', ['id' => $id])) { + print_error('invalidcourseid'); +} + +require_course_login($course); +$PAGE->set_pagelayout('incourse'); + +$event = \mod_publication\event\course_module_instance_list_viewed::create([ + 'context' => context_course::instance($course->id) +]); +$event->trigger(); + +$strmodulenameplural = get_string('modulenameplural', 'publication'); +$strmodulname = get_string('modulename', 'publication'); +$strsectionname = get_string('sectionname', 'format_' . $course->format); +$strname = get_string('name'); +$strdesc = get_string('description'); + +$PAGE->set_url('/mod/publication/index.php', ['id' => $course->id]); +$PAGE->navbar->add($strmodulenameplural); +$PAGE->set_title($strmodulenameplural); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); +echo $OUTPUT->heading($strmodulname); + +if (!$cms = get_coursemodules_in_course('publication', $course->id, 'cm.idnumber')) { + notice(get_string('nopublicationsincourse', 'publication'), '../../course/view.php?id=' . $course->id); + die; +} + +$usesections = course_format_uses_sections($course->format); +if ($usesections) { + $sections = get_fast_modinfo($course->id)->get_section_info_all(); +} else { + $sections = []; +} + +$timenow = time(); + +$table = new html_table(); + +if ($usesections) { + $table->head = [$strsectionname, $strname, $strdesc]; +} else { + $table->head = [$strname, $strdesc]; +} + +$currentsection = ''; + +$modinfo = get_fast_modinfo($course); +foreach ($modinfo->instances['publication'] as $cm) { + if (!$cm->uservisible) { + continue; + } + + // Show dimmed if the mod is hidden! + $class = $cm->visible ? '' : 'dimmed'; + + $link = html_writer::tag('a', format_string($cm->name), ['href' => 'view.php?id=' . $cm->id, 'class' => $class]); + + $printsection = ''; + if ($usesections) { + if ($cm->sectionnum !== $currentsection) { + if ($cm->sectionnum) { + $printsection = get_section_name($course, $sections[$cm->sectionnum]); + } + if ($currentsection !== '') { + $table->data[] = 'hr'; + } + $currentsection = $cm->sectionnum; + } + } + + $publication = new publication($cm, $course); + $desc = $publication->get_instance()->intro; + + if ($usesections) { + $table->data[] = [$printsection, $link, $desc]; + } else { + $table->data[] = [$link, $desc]; + } +} + +echo html_writer::empty_tag('br'); + +echo html_writer::table($table); + +echo $OUTPUT->footer(); diff --git a/mod/publication/lang/en/deprecated.txt b/mod/publication/lang/en/deprecated.txt new file mode 100644 index 0000000..3196f60 --- /dev/null +++ b/mod/publication/lang/en/deprecated.txt @@ -0,0 +1,3 @@ +publication:allowsubmissionsfromdate_help,mod_publication +requiremodintro,mod_publication +configrequiremodintro,mod_publication diff --git a/mod/publication/lang/en/publication.php b/mod/publication/lang/en/publication.php new file mode 100644 index 0000000..c8a76fe --- /dev/null +++ b/mod/publication/lang/en/publication.php @@ -0,0 +1,269 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Strings for component 'publication', language 'en' + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['modulename'] = 'Student folder'; +$string['pluginname'] = 'Student folder'; +$string['modulename_help'] = 'The student folder offers the following features: + +* Participants can upload documents that are available to other participants immediately or after you have checked the documents and given your consent. +* An assignment can be chosen as a basis for a student folder. The teacher can decide which documents of the assignment are visible for all participants. Teachers can also let the participants decide whether their documents should be visible to others.'; + +$string['eventpublicationfiledeleted'] = 'Publication file delete'; +$string['eventpublicationfileuploaded'] = 'Publication file upload'; +$string['eventpublicationfileimported'] = 'Publication file import'; +$string['eventpublicationduedateextended'] = 'Publication due-date extended'; +$string['eventpublicationapprovalchanged'] = 'Publication file approval changed'; + +$string['modulenameplural'] = 'Student folders'; +$string['pluginadministration'] = 'Student folder administration'; +$string['publication:addinstance'] = 'Add a new student folder'; +$string['publication:view'] = 'View student folder'; +$string['publication:upload'] = 'Upload files to a student folder'; +$string['publication:approve'] = 'Decide if files should be visible for every student'; +$string['publication:grantextension'] = 'Grant extension'; +$string['publication:receiveteachernotification'] = 'Receive notifications for teachers'; +$string['search:activity'] = 'Student folder - activity information'; + +$string['messageprovider:publication_updates'] = 'Publication notifications'; + +$string['notifications'] = 'Notifications'; +$string['notifyteacher'] = 'Notify graders about uploads'; +$string['notifystudents'] = 'Notify students about approval changes'; +$string['notifyteacher_help'] = 'If enabled, graders (usually teachers) receive a message whenever a student uploads a file. Message methods are configurable.'; +$string['notifystudents_help'] = 'If enabled, students recieve a message whenever the approval status of one of their uploads changes. Message methods are configurable.'; + +$string['uploaded'] = 'Uploaded'; +$string['approvalchange'] = 'Approval status changed'; + +$string['emailteachermail'] = '---------------------------------------------------------------------\n{$a->username} has uploaded \'{$a->filename}\' +for \'{$a->publication}\' on {$a->dayupdated} at {$a->timeupdated}. + +It is available here: + + {$a->url}---------------------------------------------------------------------\n'; + +$string['emailteachermailhtml'] = '{$a->username} has uploaded \'{$a->filename}\' +for <i>\'{$a->publication}\' on {$a->dayupdated} at {$a->timeupdated}</i><br /><br /> +It is <a href="{$a->url}">available on the web site</a>.'; + +$string['emailstudentsmail'] = '{$a->username} has changed the approval status of \'{$a->filename}\' +for \'{$a->publication}\' to {$a->apstatus} on {$a->dayupdated} at {$a->timeupdated}. + +It is available here: + + {$a->url}'; + +$string['emailstudentsmailhtml'] = '{$a->username} has changed the approval status of \'{$a->filename}\' +for <i>\'{$a->publication}\'</i> to <b>{$a->apstatus}</b> on {$a->dayupdated} at {$a->timeupdated}</i><br /><br /> +It is <a href="{$a->url}">available on the web site</a>.'; + +$string['name'] = 'Student folder name'; +$string['obtainstudentapproval'] = 'Obtain approval'; +$string['saveapproval'] = 'save approval'; +$string['configobtainstudentapproval'] = 'Documents are visible after the student´s consent.'; +$string['obtainteacherapproval'] = 'Approved by default'; +$string['configobtainteacherapproval'] = 'Documents of students are by default visible for all other participants.'; +$string['maxfiles'] = 'Maximum number of attachments'; +$string['configmaxfiles'] = 'Default maximum number of attachments allowed per user.'; +$string['maxbytes'] = 'Maximum attachment size'; +$string['configmaxbytes'] = 'Default maximum size for all files in the student folder.'; + +$string['reset_userdata'] = 'All data'; + +// Strings from the File mod_form. +$string['autoimport'] = 'Sync automatically with Assignment'; +$string['autoimport_help'] = 'If activated, new submissions will in corresponding Assigment will be imported automatically into student folder module. (Optional) Studentapproval has to be obtained again for the new files.'; +$string['configautoimport'] = 'If you prefer to have student submissions be automatically imported into student folder instances. This feature can be enabled/disabled for each student folder instance separately.'; +$string['availability'] = 'Timeslot for Upload/Approval'; + +$string['allowsubmissionsfromdate'] = 'from'; +$string['allowsubmissionsfromdateh'] = 'Timeslot for Upload/Approval'; +$string['allowsubmissionsfromdateh_help'] = 'You can determine the period of time during which students can upload files or give their approval for file publication. During this time period students can edit their files and can also withdraw their approval for publication.'; +$string['allowsubmissionsfromdatesummary'] = 'This assignment will accept submissions from <strong>{$a}</strong>'; +$string['allowsubmissionsanddescriptionfromdatesummary'] = 'The assignment details and submission form will be available from <strong>{$a}</strong>'; +$string['alwaysshowdescription'] = 'Always show description'; +$string['alwaysshowdescription_help'] = 'If disabled, the Assignment Description above will only become visible to students at the "Allow submissions from" date.'; + +$string['duedate'] = 'to'; +$string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date but any assignments submitted after this date are marked as late. To prevent submissions after a certain date - set the assignment cut off date.'; +$string['duedatevalidation'] = 'Due date must be after the allow submissions from date.'; + +$string['cutoffdate'] = 'Cut-off date'; +$string['cutoffdate_help'] = 'If set, the assignment will not accept submissions after this date without an extension.'; +$string['cutoffdatevalidation'] = 'The cut-off date cannot be earlier than the due date.'; +$string['cutoffdatefromdatevalidation'] = 'Cut-off date must be after the allow submissions from date.'; + +$string['mode'] = 'Mode'; +$string['mode_help'] = 'Choose whether students can upload documents in the folder or documents of an assignment are the source of it.'; +$string['modeupload'] = 'students can upload documents'; +$string['modeimport'] = 'take documents from an assignment'; + +$string['courseuploadlimit'] = 'Course upload limit'; +$string['allowedfiletypes'] = 'Accepted file types'; +$string['allowedfiletypes_help'] = 'Accepted file types can be restricted by entering a comma-separated list of mimetypes, e.g. \'video/mp4, audio/mp3, image/png, image/jpeg\', or file extensions including a dot, e.g. \'.png, .jpg\'. If the field is left empty, then all file types are allowed.'; +$string['allowedfiletypes_err'] = 'Check input! Invalid file extensions or seperators'; +$string['obtainteacherapproval_help'] = 'Decide if files will be made visible immediately upon upload or not: <br><ul><li> yes - all files will be visible to everyone right away</li><li> no - files will be published only after the teacher approved</li></ul>'; +$string['assignment'] = 'Assignment'; +$string['assignment_help'] = 'Choose the assignment to import files from. In the moment group-assignments are not supported and therefore not selectable.'; +$string['obtainstudentapproval_help'] = 'Decide if students approval will be obtained: <br><ul><li> yes - files will be visible to all only after the student approved. The teacher may select individual students/files to ask for approval.</li><li> no - the student’s approval will not be obtained via Moodle. The file’s visibility is solely the teacher’s desicion.</li></ul>'; +$string['choose'] = 'please choose ...'; +$string['importfrom_err'] = 'You have to choose an assignment you want to import from.'; +$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}'; + +$string['groupapprovalmode'] = 'Groupapproval mode'; +$string['groupapprovalmode_help'] = 'Here you decide if approval of all group members or just approval of at least one group member is required for files to be visible. The files will only be visible after approval by all group members or at least one group member.'; +$string['groupapprovalmode_all'] = 'ALL members of the group have to approve'; +$string['groupapprovalmode_single'] = 'at least ONE member has to approve'; + +$string['warning_changefromobtainteacherapproval'] = 'After activating this setting, all uploaded files will be visible to other participants. All uploaded will become visible. You can manually make files invisible to certain students.'; +$string['warning_changetoobtainteacherapproval'] = 'After deactivating this setting uploaded files will not be visible to other participants automatically. You will have to determine which files are visible. Already visible files will become invisible.'; + +$string['warning_changefromobtainstudentapproval'] = 'If you perform this change, only you can decide which files are visible to all students. The students are not asked for their approval. All files marked as approved will become visible to all students independent of the students\' decisions.'; +$string['warning_changetoobtainstudentapproval'] = 'If you perform this change, the students are asked for their approval for all files marked as visible. Files will only become visible after the students\' approval.'; + +// Strings from the File mod_publication_grantextension_form.php. +$string['extensionduedate'] = 'Extension due date'; +$string['extensionnotafterduedate'] = 'Extension date must be after the due date'; +$string['extensionnotafterfromdate'] = 'Extension date must be after the allow submissions from date'; + +// Strings from the File index.php. +$string['nopublicationsincourse'] = 'There are no student folder instances in this course.'; + +// Strings from the File view.php. +$string['allowsubmissionsfromdate_upload'] = 'Upload possibility from'; +$string['allowsubmissionsfromdate_import'] = 'Approval from'; +$string['duedate_upload'] = 'Upload possibility to'; +$string['duedate_import'] = 'Approval to'; +$string['cutoffdate_upload'] = 'Last upload posssibility to'; +$string['cutoffdate_import'] = 'Last approval to'; +$string['extensionto'] = 'Extension to'; +$string['filedetails'] = 'Details'; +$string['assignment_notfound'] = 'The assignment from which files were imported, could no longer be found.'; +$string['assignment_notset'] = 'No assignment has been chosen.'; +$string['updatefiles'] = 'Update files'; +$string['updatefileswarning'] = 'Files from an individual student in the student folder will be updated with his/her submission of the assignment. Already visible files from students will be replaced too, if they are deleted or refreshed - the settings of the student as to the visibility will not be changed.'; +$string['myfiles'] = 'Own files'; +$string['mygroupfiles'] = 'My group\'s files'; +$string['add_uploads'] = 'Add files'; +$string['edit_uploads'] = 'edit/upload files'; +$string['edit_timeover'] = 'Files can only be edited during the changeperiod.'; +$string['approval_timeover'] = 'You can only change your approval during the changeperiod.'; +$string['noentries'] = 'No entries'; +$string['nofiles'] = 'No files available'; +$string['nothing_to_show_users'] = 'nothing to display - no students available'; +$string['nothing_to_show_groups'] = 'nothing to display - no group available'; +$string['notice'] = 'Notice:'; +$string['notice_uploadrequireapproval'] = 'All uploaded files will be made visible only after the teacher’s review'; +$string['notice_uploadnoapproval'] = 'All files will be immediately visible to everyone upon upload. The teacher reserves the right to hide published files at any time.'; +$string['notice_groupimportrequireallapproval'] = 'Decide whether your files are available for everyone. All group members must give their approval before the file will be visible.'; +$string['notice_groupimportrequireoneapproval'] = 'Decide whether your files are available for everyone. A single group member\'s approval is enough for the file to be visible. Please discuss group internally if your file should be visible or not before approving it!'; +$string['notice_importrequireapproval'] = 'Decide whether your files are available for everyone.'; +$string['notice_importnoapproval'] = 'The following files are visible to all.'; +$string['teacher_pending'] = 'confirmation pending'; +$string['teacher_approved'] = 'visible (approved)'; +$string['teacher_rejected'] = 'declined'; +$string['approved'] = 'Approved'; +$string['show_details'] = 'Show details'; +$string['student_approve'] = 'approve'; +$string['student_approved'] = 'approved'; +$string['student_pending'] = 'not visible (not approved)'; +$string['pending'] = 'Pending'; +$string['student_reject'] = 'reject'; +$string['student_rejected'] = 'rejected'; +$string['rejected'] = 'Rejected'; +$string['visible'] = 'visible'; +$string['hidden'] = 'hidden'; +$string['status:approved'] = 'approved'; +$string['status:approvednot'] = 'rejected'; + +$string['allfiles'] = 'All files'; +$string['publicfiles'] = 'Public files'; +$string['downloadall'] = 'Download all files as ZIP'; +$string['optionalsettings'] = 'Options'; +$string['entiresperpage'] = 'Participants shown per page'; +$string['nothingtodisplay'] = 'No entries to display'; +$string['nofilestozip'] = 'No files to zip'; +$string['status'] = 'Status'; +$string['studentapproval'] = 'Status'; // Previous 'Student approval'. +$string['studentapproval_help'] = 'The colum status represents the students reply of the approval: + +* ? - approval pending +* ✓ - approval given +* ✖ - approval declined'; +$string['teacherapproval'] = 'Approval'; +$string['visibility'] = 'visible for all'; +$string['visibleforstudents'] = 'visible to all'; +$string['visibleforstudents_yes'] = 'Stundets can see this file'; +$string['visibleforstudents_no'] = 'This file is NOT visible to stundets'; +$string['resetstudentapproval'] = 'Reset status'; // Previous 'Reset student approval'. +$string['savestudentapprovalwarning'] = 'Are you sure you want to save these changes? You can not change the status once is it set.'; + +$string['go'] = 'Go'; +$string['withselected'] = 'With selected...'; +$string['zipusers'] = "Download as ZIP"; +$string['approveusers'] = "visible for all"; +$string['rejectusers'] = "invisible for all"; +$string['grantextension'] = 'grant extension'; +$string['saveteacherapproval'] = 'save approval'; +$string['reset'] = 'Revert'; + +// Strings from the File upload.php. +$string['filesofthesetypes'] = 'Files of these types may be added:'; +$string['guideline'] = 'visible for everybody:'; +$string['published_immediately'] = 'yes immediately, without approval by a teacher'; +$string['published_aftercheck'] = 'no, only after approval by a teacher'; +$string['save_changes'] = 'Save changes'; + +// Strings for JS... +$string['total'] = 'total'; +$string['details'] = 'Details'; + +// Privacy strings... +$string['privacy:metadata:publicationperpage'] = 'How many entries should be displayed on a single table page!'; +$string['privacy:path:files'] = 'files'; +$string['privacy:path:resources'] = 'resources'; +$string['privacy:type:upload'] = 'uploaded file'; +$string['privacy:type:import'] = 'imported file'; +$string['privacy:type:onlinetext'] = 'imported onlinetext'; +$string['privacy:metadata:groupapproval'] = 'Stores information about group members\' approval or rejection of files imported from group\'s submission.'; +$string['privacy:metadata:publicationfileexplanation'] = 'Files and converted onlinetext-submissions for this plugin get stored via Moodle\'s file API.'; +$string['privacy:metadata:extduedates'] = 'Stores information about overridden/extended due dates for mod_publication.'; +$string['privacy:metadata:files'] = 'Stores information (identifier, whom it belongs, where it came from, hash of content, file name and if approved by teacher and/or student) about the files uploaded/imported into mod_publication.'; +$string['privacy:metadata:fileid'] = 'Identifier of the file.'; +$string['privacy:metadata:userid'] = 'Identifier of the user.'; +$string['privacy:metadata:timecreated'] = 'The time and date the data record was created.'; +$string['privacy:metadata:timemodified'] = 'The most recent time and date the data record got updated/modified.'; +$string['privacy:metadata:approval'] = 'Whether or not the group member approved or rejected the file.'; +$string['privacy:metadata:studentapproval'] = 'Whether or not the student approved or rejected the file.'; +$string['privacy:metadata:teacherapproval'] = 'Whether or not the teacher approved or rejected the file.'; +$string['privacy:metadata:type'] = 'Marks the origin of the file (uploaded by student, imported from assignment submission or converted onlinetext from assignment submission).'; +$string['privacy:metadata:contenthash'] = 'SHA1 hash of the file\'s content, used to determine if the file changed.'; +$string['privacy:metadata:filename'] = 'The file\'s name.'; +$string['privacy:metadata:extensionduedate'] = 'The due date effective for the user due to it being overridden/extended.'; + +// Deprecated since Moodle 2.9! +$string['requiremodintro'] = 'Require activity description'; +$string['configrequiremodintro'] = 'Disable this option if you do not want to force users to enter description of each activity.'; diff --git a/mod/publication/lib.php b/mod/publication/lib.php new file mode 100644 index 0000000..017f9be --- /dev/null +++ b/mod/publication/lib.php @@ -0,0 +1,281 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains interface and callback methods for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Adds a new publication instance + * + * @param stdClass $publication data (from mod_publication_mod_form) + * @return int publication ID + */ +function publication_add_instance($publication) { + global $DB, $OUTPUT; + + $cmid = $publication->coursemodule; + $courseid = $publication->course; + + $id = 0; + try { + $id = $DB->insert_record('publication', $publication); + } catch (Exception $e) { + echo $OUTPUT->notification($e->getMessage(), 'error'); + } + + $DB->set_field('course_modules', 'instance', $id, ['id' => $cmid]); + + $record = $DB->get_record('publication', ['id' => $id]); + + $record->course = $courseid; + $record->cmid = $cmid; + + $course = $DB->get_record('course', ['id' => $record->course], '*', MUST_EXIST); + $cm = get_coursemodule_from_id('publication', $cmid, 0, false, MUST_EXIST); + $context = context_module::instance($cm->id); + $instance = new publication($cm, $course, $context); + + if ($instance->get_instance()->mode == PUBLICATION_MODE_IMPORT + && !empty($instance->get_instance()->autoimport)) { + // Fetch all files right now! + $instance->importfiles(); + } + + return $record->id; +} + +/** + * Return the list if Moodle features this module supports + * + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed True if module supports feature, null if doesn't know + */ +function publication_supports($feature) { + switch ($feature) { + case FEATURE_GROUPS: + return true; + case FEATURE_GROUPINGS: + return true; + case FEATURE_MOD_INTRO: + return true; + case FEATURE_GRADE_HAS_GRADE: + return false; + case FEATURE_GRADE_OUTCOMES: + return false; + case FEATURE_BACKUP_MOODLE2: + return true; + case FEATURE_SHOW_DESCRIPTION: + return true; + case FEATURE_IDNUMBER: + return false; + + default: + return null; + } +} + +/** + * updates an existing publication instance + * + * @param stdClass $publication from mod_publication_mod_form + * @return bool true + */ +function publication_update_instance($publication) { + global $DB; + + $publication->id = $publication->instance; + + $publication->timemodified = time(); + + $DB->update_record('publication', $publication); + + $course = $DB->get_record('course', ['id' => $publication->course], '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('publication', $publication->id, 0, false, MUST_EXIST); + $context = context_module::instance($cm->id); + $instance = new publication($cm, $course, $context); + + if ($instance->get_instance()->mode == PUBLICATION_MODE_IMPORT + && !empty($instance->get_instance()->autoimport)) { + // Fetch all files right now! + $instance->importfiles(); + } + + return true; +} + +/** + * complete deletes an publication instance + * + * @param int $id + * @return bool + */ +function publication_delete_instance($id) { + global $DB; + + if (!$publication = $DB->get_record('publication', ['id' => $id])) { + return false; + } + + $DB->delete_records('publication_extduedates', ['publication' => $publication->id]); + + $fs = get_file_storage(); + + $fs->delete_area_files($publication->id, 'mod_publication', 'attachment'); + + $DB->delete_records('publication_file', ['publication' => $publication->id]); + + $result = true; + if (!$DB->delete_records('publication', ['id' => $publication->id])) { + $result = false; + } + + return $result; +} + +/** + * Returns info object about the course module + * + * @param stdClass $coursemodule The coursemodule object (record). + * @return bool|cached_cm_info An object on information that the courses will know about (most noticeably, an icon) or false. + */ +function publication_get_coursemodule_info($coursemodule) { + global $DB; + + $dbparams = ['id' => $coursemodule->instance]; + $fields = 'id, name, alwaysshowdescription, allowsubmissionsfromdate, intro, introformat'; + if (!$publication = $DB->get_record('publication', $dbparams, $fields)) { + return false; + } + + $result = new cached_cm_info(); + $result->name = $publication->name; + if ($coursemodule->showdescription) { + if ($publication->alwaysshowdescription || time() > $publication->allowsubmissionsfromdate) { + // Convert intro to html. Do not filter cached version, filters run at display time. + $result->content = format_module_intro('publication', $publication, $coursemodule->id, false); + } + } + + return $result; +} + +/** + * Defines which elements mod_publication needs to add to reset form + * + * @param MoodleQuickForm $mform The reset course form to extend + */ +function publication_reset_course_form_definition(&$mform) { + $mform->addElement('header', 'publicationheader', get_string('modulenameplural', 'publication')); + $mform->addElement('checkbox', 'reset_publication_userdata', get_string('reset_userdata', 'publication')); +} + +/** + * Reset the userdata in publication module + * + * @param object $data settings object which userdata to reset + * @return array[] array of associative arrays giving feedback what has been successfully reset + */ +function publication_reset_userdata($data) { + global $DB; + + if (!$DB->count_records('publication', ['course' => $data->courseid])) { + return []; + } + + $componentstr = get_string('modulenameplural', 'publication'); + $status = []; + + if (isset($data->reset_publication_userdata)) { + + $publications = $DB->get_records('publication', ['course' => $data->courseid]); + + foreach ($publications as $publication) { + + $DB->delete_records('publication_extduedates', ['publication' => $publication->id]); + + $filerecords = $DB->get_records('publication_file', ['publication' => $publication->id]); + + $fs = get_file_storage(); + foreach ($filerecords as $filerecord) { + if ($file = $fs->get_file_by_id($filerecord->fileid)) { + $file->delete(); + } + } + + $DB->delete_records('publication_file', ['publication' => $publication->id]); + + $status[] = [ + 'component' => $componentstr, + 'item' => $publication->name, + 'error' => false + ]; + } + } + + return $status; + +} + +/** + * Serves resource files. + * + * @param mixed $course course or id of the course + * @param mixed $cm course module or id of the course module + * @param context $context + * @param string $filearea + * @param array $args + * @param bool $forcedownload + * @param array $options additional options affecting the file serving + * @return bool false if file not found, does not return if found - just send the file + */ +function mod_publication_pluginfile($course, $cm, context $context, $filearea, $args, $forcedownload, array $options = []) { + if ($context->contextlevel != CONTEXT_MODULE) { + return false; + } + + require_login($course, false, $cm); + if (!has_capability('mod/publication:view', $context)) { + return false; + } + + if ($filearea !== 'attachment') { + return false; + } + + $itemid = (int)array_shift($args); + + $relativepath = implode('/', $args); + + $fullpath = "/{$context->id}/mod_publication/$filearea/$itemid/$relativepath"; + $fs = get_file_storage(); + if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { + return false; + } + + send_stored_file($file, 0, 0, $forcedownload, $options); + + // Wont be reached! + return false; +} diff --git a/mod/publication/locallib.php b/mod/publication/locallib.php new file mode 100644 index 0000000..3e82d91 --- /dev/null +++ b/mod/publication/locallib.php @@ -0,0 +1,1779 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains much of the logic needed for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +define('PUBLICATION_MODE_UPLOAD', 0); +define('PUBLICATION_MODE_IMPORT', 1); +// Used in DB to mark online-text-files! +define('PUBLICATION_MODE_ONLINETEXT', 2); + +define('PUBLICATION_APPROVAL_ALL', 0); +define('PUBLICATION_APPROVAL_SINGLE', 1); +require_once($CFG->dirroot . '/mod/publication/mod_publication_allfiles_form.php'); + +/** + * publication class contains much logic used in mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class publication { + // TODO replace $instance with proper properties + PHPDoc comments?!? + /** @var object instance */ + protected $instance; + /** @var object context */ + protected $context; + /** @var object course */ + protected $course; + /** @var object coursemodule */ + protected $coursemodule; + /** @var bool requiregroup if mode = import and group membership is required for submission in assign to import from */ + protected $requiregroup = 0; + + /** + * Constructor + * + * @param object $cm course module object + * @param object $course (optional) course object + * @param context_module $context (optional) Course Module Context + */ + public function __construct($cm, $course = null, $context = null) { + global $DB; + + $this->coursemodule = $cm; + + if ($course != null) { + $this->course = $course; + } else { + $this->course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + } + + if ($context != null) { + $this->context = $context; + } else { + $this->context = context_module::instance($cm->id); + } + + $this->instance = $DB->get_record("publication", ["id" => $cm->instance]); + + $this->instance->obtainteacherapproval = !$this->instance->obtainteacherapproval; + + if ($this->instance->mode == PUBLICATION_MODE_IMPORT) { + $cond = ['id' => $this->instance->importfrom]; + $this->requiregroup = $DB->get_field('assign', 'preventsubmissionnotingroup', $cond); + } + } + + /** + * Whether or not to show intro text right now + * + * @return bool + */ + public function show_intro() { + if ($this->get_instance()->alwaysshowdescription || + time() > $this->get_instance()->allowsubmissionfromdate) { + return true; + } + + return false; + } + + /** + * Display the intro text if available + */ + public function display_intro() { + global $OUTPUT; + + if ($this->show_intro()) { + if ($this->instance->intro) { + echo $OUTPUT->box_start('generalbox boxaligncenter', 'intro'); + echo format_module_intro('publication', $this->instance, $this->coursemodule->id); + echo $OUTPUT->box_end(); + } + } else { + if ($this->alwaysshowdescription) { + $message = get_string('allowsubmissionsfromdatesummary', + 'publication', userdate($this->instance->allowsubmissionsfromdate)); + } else { + $message = get_string('allowsubmissionsanddescriptionfromdatesummary', + 'publication', userdate($this->instance->allowsubmissionsfromdate)); + } + echo html_writer::div($message, '', ['id' => 'intro']); + } + } + + /** + * Display dates which limit submission timespan + */ + public function display_availability() { + global $USER, $OUTPUT; + + // Display availability dates. + $textsuffix = ($this->instance->mode == PUBLICATION_MODE_IMPORT) ? "_import" : "_upload"; + + echo $OUTPUT->box_start('generalbox boxaligncenter', 'dates'); + echo '<table>'; + if ($this->instance->allowsubmissionsfromdate) { + echo '<tr><td class="c0">' . get_string('allowsubmissionsfromdate' . $textsuffix, 'publication') . ':</td>'; + echo ' <td class="c1">' . userdate($this->instance->allowsubmissionsfromdate) . '</td></tr>'; + } + if ($this->instance->duedate) { + echo '<tr><td class="c0">' . get_string('duedate' . $textsuffix, 'publication') . ':</td>'; + echo ' <td class="c1">' . userdate($this->instance->duedate) . '</td></tr>'; + } + + $extensionduedate = $this->user_extensionduedate($USER->id); + + if ($extensionduedate) { + echo '<tr><td class="c0">' . get_string('extensionto', 'publication') . ':</td>'; + echo ' <td class="c1">' . userdate($extensionduedate) . '</td></tr>'; + } + + echo '</table>'; + + echo $OUTPUT->box_end(); + } + + /** + * If the mode is set to import then the link to the corresponding + * assignment will be displayed + */ + public function display_importlink() { + global $DB, $OUTPUT; + + if ($this->instance->mode == PUBLICATION_MODE_IMPORT) { + echo html_writer::start_div('assignurl'); + + if ($this->get_instance()->importfrom == -1) { + echo get_string('assignment_notset', 'publication'); + } else { + $assign = $DB->get_record('assign', ['id' => $this->instance->importfrom]); + + $assignmoduleid = $DB->get_field('modules', 'id', ['name' => 'assign']); + + if ($assign) { + $assigncm = $DB->get_record('course_modules', [ + 'course' => $assign->course, + 'module' => $assignmoduleid, + 'instance' => $assign->id + ]); + } else { + $assigncm = false; + } + if ($assign && $assigncm) { + $assignurl = new moodle_url('/mod/assign/view.php', ['id' => $assigncm->id]); + echo get_string('assignment', 'publication') . ': ' . html_writer::link($assignurl, $assign->name); + + if (has_capability('mod/publication:addinstance', $this->context)) { + $url = new moodle_url('/mod/publication/view.php', + ['id' => $this->coursemodule->id, 'sesskey' => sesskey(), 'action' => 'import']); + $label = get_string('updatefiles', 'publication'); + + echo $OUTPUT->single_button($url, $label); + } + + } else { + echo $OUTPUT->notification(get_string('assignment_notfound', 'publication'), 'warning'); + } + } + echo html_writer::end_div(); + } + + } + + /** + * Display Link to upload form if submission date is open + * and the user has the capability to upload files + * + * @return string HTML snippet with upload link (single button or plain text if not allowed) + */ + public function display_uploadlink() { + global $OUTPUT; + + if ($this->instance->mode == PUBLICATION_MODE_UPLOAD) { + if (has_capability('mod/publication:upload', $this->context)) { + if ($this->is_open()) { + $url = new moodle_url('/mod/publication/upload.php', + ['id' => $this->instance->id, 'cmid' => $this->coursemodule->id]); + $label = get_string('edit_uploads', 'publication'); + $editbutton = $OUTPUT->single_button($url, $label); + + return $editbutton; + } else { + return get_string('edit_timeover', 'publication'); + } + } else { + return get_string('edit_notcapable', 'publication'); + } + } + } + + /** + * Get the extension due date (if set) + * + * @param int $uid User ID to fetch extension due date for + * @return int extension due date if set or 0 + */ + public function user_extensionduedate($uid) { + global $DB; + + $extensionduedate = $DB->get_field('publication_extduedates', 'extensionduedate', [ + 'publication' => $this->get_instance()->id, + 'userid' => $uid + ]); + + if (!$extensionduedate) { + return 0; + } + + return $extensionduedate; + } + + /** + * Check if submission is currently allowed due to allowsubmissionsfromdae and duedate + * + * @return bool + */ + public function is_open() { + global $USER; + + if (!has_capability('mod/publication:upload', $this->get_context())) { + return false; + } + + $now = time(); + + $from = $this->get_instance()->allowsubmissionsfromdate; + $due = $this->get_instance()->duedate; + + $extensionduedate = $this->user_extensionduedate($USER->id); + + if ($extensionduedate) { + $due = $extensionduedate; + } + + if (($from == 0 || $from < $now) && + ($due == 0 || $due > $now)) { + return true; + } + + return false; + } + + /** + * Instance getter + * + * @return object instance object + */ + public function get_instance() { + return $this->instance; + } + + /** + * Context getter + * + * @return \context_module context object + */ + public function get_context() { + return $this->context; + } + + /** + * Coursemodule getter + * + * @return object coursemodule object + */ + public function get_coursemodule() { + return $this->coursemodule; + } + + /** + * Whether or not the assign to import from requires group membership for submissions! + * + * @return bool true if group membership is required, false if not or type = upload + */ + public function requiregroup() { + return $this->requiregroup; + } + + /** + * Get's all groups (optionaly filtered by groupingid or group-IDs in selgroups-array) + * + * @param int $groupingid (optional) Grouping-ID to filter groups for or 0 + * @param int[] $selgroups (optional) selected group's IDs to filter for or empty array() + * @return int[] array of group's IDs + */ + public function get_groups($groupingid = 0, $selgroups = []) { + $groups = groups_get_all_groups($this->get_instance()->course, 0, $groupingid); + $groups = array_keys($groups); + + if (empty($groupingid) && !$this->requiregroup()) { + $groups[] = 0; + } + + if (is_array($selgroups) && count($selgroups) > 0) { + $groups = array_intersect($groups, $selgroups); + } + + return $groups; + } + + /** + * Get userids to fetch files for, when displaying all submitted files or downloading them as ZIP + * + * @param int[] $users (optional) user ids for which the returned user ids have to filter + * @return int[] array of userids + */ + public function get_users($users = []) { + global $DB; + + $customusers = ''; + + if (is_array($users) && count($users) > 0) { + $customusers = " and u.id IN (" . implode($users, ', ') . ") "; + } else if ($users === false) { + return []; + } + + // Find out current groups mode. + $currentgroup = groups_get_activity_group($this->get_coursemodule(), true); + + // Get all ppl that are allowed to submit assignments. + list($esql, $params) = get_enrolled_sql($this->context, 'mod/publication:view', $currentgroup); + + if (has_capability('mod/publication:approve', $this->context) + || has_capability('mod/publication:grantextension', $this->context)) { + // We can skip the approval-checks for teachers! + $sql = 'SELECT u.id FROM {user} u ' . + 'LEFT JOIN (' . $esql . ') eu ON eu.id=u.id ' . + 'WHERE u.deleted = 0 AND eu.id=u.id ' . $customusers; + } else { + $sql = 'SELECT u.id FROM {user} u ' . + 'LEFT JOIN (' . $esql . ') eu ON eu.id=u.id ' . + 'LEFT JOIN {publication_file} files ON (u.id = files.userid) ' . + 'WHERE u.deleted = 0 AND eu.id=u.id ' . $customusers . + 'AND files.publication = ' . $this->get_instance()->id . ' '; + + if ($this->get_instance()->mode == PUBLICATION_MODE_UPLOAD) { + // Mode upload. + if ($this->get_instance()->obtainteacherapproval) { + // Need teacher approval. + + $where = 'files.teacherapproval = 1'; + } else { + // No need for teacher approval. + // Teacher only hasnt rejected. + $where = '(files.teacherapproval = 1 OR files.teacherapproval IS NULL)'; + } + } else { + // Mode import. + if (!$this->get_instance()->obtainstudentapproval) { + // No need to ask student and teacher has approved. + $where = 'files.teacherapproval = 1'; + } else { + // Student and teacher have approved. + $where = 'files.teacherapproval = 1 AND files.studentapproval = 1'; + } + } + + $sql .= 'AND ' . $where . ' '; + $sql .= 'GROUP BY u.id'; + } + + $users = $DB->get_fieldset_sql($sql, $params); + + if (empty($users)) { + return [-1]; + } + + return $users; + } + + /** + * Display form with table containing all files + * + * TODO: for Moodle 3.6 we should replace old form classes with a nice bootstrap based form layout! + */ + public function display_allfilesform() { + global $CFG, $DB; + + $cm = $this->coursemodule; + $context = $this->context; + + $updatepref = optional_param('updatepref', 0, PARAM_BOOL); + if ($updatepref) { + $perpage = optional_param('perpage', 10, PARAM_INT); + $perpage = ($perpage < 0) ? 10 : $perpage; + $filter = optional_param('filter', 0, PARAM_INT); + set_user_preference('publication_perpage', $perpage); + } + + // Next we get perpage param from database! + $perpage = get_user_preferences('publication_perpage', 10); + $filter = get_user_preferences('publicationfilter', 0); + + $page = optional_param('page', 0, PARAM_INT); + + $formattrs = []; + $formattrs['action'] = new moodle_url('/mod/publication/view.php'); + $formattrs['id'] = 'fastg'; + $formattrs['method'] = 'post'; + $formattrs['class'] = 'mform'; + + echo html_writer::start_tag('form', $formattrs) . + html_writer::empty_tag('input', [ + 'type' => 'hidden', + 'name' => 'id', + 'value' => $this->get_coursemodule()->id + ]) . + html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'page', 'value' => $page]) . + html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]); + + echo html_writer::start_tag('div', ['id' => 'id_allfiles', 'class' => 'clearfix', 'aria-live' => 'polite']); + $allfiles = get_string('allfiles', 'publication'); + $publicfiles = get_string('publicfiles', 'publication'); + $title = (has_capability('mod/publication:approve', $context)) ? $allfiles : $publicfiles; + echo html_writer::tag('div', $title, ['class' => 'legend']); + echo html_writer::start_div('fcontainer clearfix'); + + $f = groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/publication/view.php?id=' . $cm->id, true); + $mf = new mod_publication_allfiles_form(null, array('form' => $f)); + $mf->display(); + + if ($this->get_instance()->mode == PUBLICATION_MODE_UPLOAD) { + $table = new \mod_publication\local\allfilestable\upload('mod-publication-allfiles', $this); + } else { + if ($DB->get_field('assign', 'teamsubmission', ['id' => $this->get_instance()->importfrom])) { + $table = new \mod_publication\local\allfilestable\group('mod-publication-allgroupfiles', $this); + } else { + $table = new \mod_publication\local\allfilestable\import('mod-publication-allfiles', $this); + } + } + + $link = html_writer::link(new moodle_url('/mod/publication/view.php', [ + 'id' => $this->coursemodule->id, + 'action' => 'zip' + ]), + get_string('downloadall', 'publication')); + echo html_writer::tag('div', $link, ['class' => 'mod-publication-download-link']); + + $table->out($perpage, true); // Print the whole table. + + $options = []; + $options['zipusers'] = get_string('zipusers', 'publication'); + + if (has_capability('mod/publication:approve', $context) && $table->totalfiles() > 0) { + $options['approveusers'] = get_string('approveusers', 'publication'); + $options['rejectusers'] = get_string('rejectusers', 'publication'); + + if ($this->get_instance()->mode == PUBLICATION_MODE_IMPORT && + $this->get_instance()->obtainstudentapproval) { + $options['resetstudentapproval'] = get_string('resetstudentapproval', 'publication'); + } + } + if (has_capability('mod/publication:grantextension', $this->get_context())) { + $options['grantextension'] = get_string('grantextension', 'publication'); + } + + if (count($options) > 0) { + echo html_writer::start_div('form-row'); + if (has_capability('mod/publication:approve', $context)) { + $buttons = html_writer::empty_tag('input', [ + 'type' => 'reset', + 'name' => 'resetvisibility', + 'value' => get_string('reset', 'publication'), + 'class' => 'visibilitysaver btn btn-secondary' + ]); + + if ($this->get_instance()->mode == PUBLICATION_MODE_IMPORT && + $this->get_instance()->obtainstudentapproval) { + $buttons .= html_writer::empty_tag('input', [ + 'type' => 'submit', + 'name' => 'savevisibility', + 'value' => get_string('saveapproval', 'publication'), + 'class' => 'visibilitysaver btn btn-primary m-x-1' + ]); + } else { + $buttons .= html_writer::empty_tag('input', [ + 'type' => 'submit', + 'name' => 'savevisibility', + 'value' => get_string('saveteacherapproval', 'publication'), + 'class' => 'visibilitysaver btn btn-primary m-x-1' + ]); + } + } else { + $buttons = ''; + } + + echo html_writer::start_div('withselection col-7'). + html_writer::span(get_string('withselected', 'publication')). + html_writer::select($options, 'action'). + html_writer::empty_tag('input', [ + 'type' => 'submit', + 'name' => 'submitgo', + 'value' => get_string('go', 'publication'), + 'class' => 'btn btn-primary' + ]).html_writer::end_div(). + html_writer::div($buttons, 'col'); + + } + + // Select all/none. + echo html_writer::start_tag('div', ['class' => 'checkboxcontroller']) . " + <script type=\"text/javascript\"> + function toggle_userselection() { + var checkboxes = document.getElementsByClassName('userselection'); + var sel = document.getElementById('selectallnone'); + + if (checkboxes.length > 0) { + checkboxes[0].checked = sel.checked; + + for(var i = 1; i < checkboxes.length;i++) { + checkboxes[i].checked = checkboxes[0].checked; + } + } + } + </script>" . + html_writer::end_div() . + html_writer::end_div() . + html_writer::end_div() . + html_writer::end_tag('form'); + + // Mini form for setting user preference. + $formaction = new moodle_url('/mod/publication/view.php', ['id' => $this->coursemodule->id]); + $mform = new MoodleQuickForm('optionspref', 'post', $formaction, '', ['class' => 'optionspref']); + + $mform->addElement('hidden', 'updatepref'); + $mform->setDefault('updatepref', 1); + + $mform->addElement('header', 'qgprefs', get_string('optionalsettings', 'publication')); + + $mform->addElement('select', 'perpage', get_string('entiresperpage', 'publication'), [ + 0 => get_string('all'), + 10 => 10, + 20 => 20, + 50 => 50, + 100 => 100 + ]); + $mform->setDefault('perpage', $perpage); + $mform->addElement('submit', 'savepreferences', get_string('savepreferences')); + + $mform->display(); + } + + /** + * Returns if a user has the permission to view a file + * + * @param unknown $fileid + * @param number $userid use for custom user, if 0 then if public visible + * @return boolean + */ + public function has_filepermission($fileid, $userid = 0) { + global $DB; + + $conditions = []; + $conditions['publication'] = $this->get_instance()->id; + $conditions['fileid'] = $fileid; + + $filepermissions = $DB->get_record('publication_file', $conditions); + + $haspermission = false; + + if ($filepermissions) { + if ($userid != 0) { + if ($this->get_instance()->mode == PUBLICATION_MODE_UPLOAD && $filepermissions->userid == $userid) { + // Everyone is allowed to view their own files. + $haspermission = true; + } else if ($this->get_instance()->mode == PUBLICATION_MODE_IMPORT) { + // If it's a team-submission, we have to check for the group membership! + $teamsubmission = $DB->get_field('assign', 'teamsubmission', ['id' => $this->get_instance()->importfrom]); + if (!empty($teamsubmission)) { + $groupmembers = $this->get_submissionmembers($filepermissions->userid); + if (array_key_exists($userid, $groupmembers)) { + $haspermission = true; + } + } else if ($filepermissions->userid == $userid) { + // Everyone is allowed to view their own files. + $haspermission = true; + } + } + } + + if ($this->get_instance()->mode == PUBLICATION_MODE_UPLOAD) { + // Mode upload. + if ($this->get_instance()->obtainteacherapproval) { + // Need teacher approval. + if ($filepermissions->teacherapproval == 1) { + // Teacher has approved. + $haspermission = true; + } + } else { + // No need for teacher approval. + if (is_null($filepermissions->teacherapproval) || $filepermissions->teacherapproval == 1) { + // Teacher only hasnt rejected. + $haspermission = true; + } + } + } else { + // Mode import. + if (!$this->get_instance()->obtainstudentapproval && $filepermissions->teacherapproval == 1) { + // No need to ask student and teacher has approved. + $haspermission = true; + } else if ($this->get_instance()->obtainstudentapproval && + $filepermissions->teacherapproval == 1 && $filepermissions->studentapproval == 1) { + // Student and teacher have approved. + $haspermission = true; + } + } + } + + return $haspermission; + } + + /** + * Sets group approval for the specified user and returns current cumulated group approval! + * + * @param null|int $approval 0 if rejected, 1 if approved and 'null' if not set! + * @param int $pubfileid ID of publication file entry in DB + * @param int $userid ID of user to set approval/rejection for + * @return array cumulated approval for specified file, approving and needed count + * @throws coding_exception + * @throws dml_exception + */ + public function set_group_approval($approval, $pubfileid, $userid) { + global $DB; + + // Normalize approval value! + if ($approval !== null) { + $approval = empty($approval) ? 0 : 1; + } + + $record = $DB->get_record('publication_groupapproval', ['fileid' => $pubfileid, 'userid' => $userid]); + $filerec = $DB->get_record('publication_file', ['id' => $pubfileid]); + if (!empty($record)) { + if ($record->approval === $approval) { + // Nothing changed, return! + return $filerec->studentapproval; + } + $record->approval = $approval; + $record->timemodified = time(); + $DB->update_record('publication_groupapproval', $record); + } else { + $record = new stdClass(); + $record->fileid = $pubfileid; + $record->userid = $userid; + $record->approval = $approval; + $record->timecreated = time(); + $record->timemodified = $record->timecreated; + $record->id = $DB->insert_record('publication_groupapproval', $record); + } + + // Calculate new cumulated studentapproval for caching in file table! + + // Get group members! + $groupmembers = $this->get_submissionmembers($filerec->userid); + $stats = array(); + if (!empty($groupmembers)) { + list($usersql, $userparams) = $DB->get_in_or_equal(array_keys($groupmembers), SQL_PARAMS_NAMED, 'user'); + $select = "fileid = :fileid AND approval = :approval AND userid " . $usersql; + $params = ['fileid' => $pubfileid, 'approval' => 0] + $userparams; + if ($DB->record_exists_select('publication_groupapproval', $select, $params)) { + // If anyone rejected it's rejected, no matter what! + $approval = 0; + } else { + if ($this->get_instance()->groupapproval == PUBLICATION_APPROVAL_SINGLE) { + // If only one has to approve, we check for that! + $params['approval'] = 1; + if ($DB->record_exists_select('publication_groupapproval', $select, $params)) { + $approval = 1; + } else { + $approval = 0; + } + } else { + // All group members have to approve! + $select = "fileid = :fileid AND approval IS NULL AND userid " . $usersql; + $params = ['fileid' => $pubfileid] + $userparams; + $approving = $DB->count_records_sql("SELECT count(DISTINCT userid) + FROM {publication_groupapproval} + WHERE fileid = :fileid AND approval = 1 AND userid " . $usersql, $params); + $stats['approving'] = $approving; + $stats['needed'] = count($userparams); + if ($approving < count($userparams)) { + // Rejected if not every group member has approved the file! + $approval = null; + } else { + $approval = 1; + } + } + } + } else { + // Group without members, so no one could approve! (Should never happen, never ever!) + $approval = 0; + } + + // Update approval value and return it! + $filerec->studentapproval = $approval; + $DB->update_record('publication_file', $filerec); + $stats['approval'] = $approval; + return $stats; + } + + /** + * Determine and return the teacher's approval status for the given file! + * + * @param stored_file $file file to determine approval status for + * @return int|null teacher's approval status (null pending, 1 approved, all other rejected) + */ + public function teacher_approval(\stored_file $file) { + global $DB; + + if (empty($conditions)) { + static $conditions = []; + $conditions['publication'] = $this->get_instance()->id; + } + $conditions['fileid'] = $file->get_id(); + + $teacherapproval = $DB->get_field('publication_file', 'teacherapproval', $conditions); + + return $teacherapproval; + } + + /** + * Determine and return the student's approval status for the given file! + * + * @param stored_file $file file to determine approval status for + * @return int|null student's approval status (null/0 = pending, 1 = rejected, 2 = approved) + */ + public function student_approval(\stored_file $file) { + global $DB; + + if (empty($conditions)) { + static $conditions = []; + $conditions['publication'] = $this->get_instance()->id; + } + $conditions['fileid'] = $file->get_id(); + + $studentapproval = $DB->get_field('publication_file', 'studentapproval', $conditions); + + $studentapproval = (!is_null($studentapproval)) ? $studentapproval + 1 : null; + + return $studentapproval; + } + + /** + * Gets the group members for the specified group. Or users without membership if groupid is 0! + * + * @param int $groupid + * @return stdClass[] Group member's user records. + */ + public function get_submissionmembers($groupid) { + global $DB; + + if (($this->get_instance()->mode != PUBLICATION_MODE_IMPORT) + || !$DB->get_field('assign', 'teamsubmission', ['id' => $this->get_instance()->importfrom])) { + throw new coding_exception('Cannot be called if files get uploaded or teamsubmission is deactivated!'); + } + + if (!empty($groupid)) { + $groupmembers = groups_get_members($groupid); + } else if (!$DB->get_field('assign', 'preventsubmissionnotingroup', ['id' => $this->get_instance()->importfrom])) { + // If groupid == 0, we get all users without group! + $groupmembers = []; + $assigncm = get_coursemodule_from_instance('assign', $this->instance->importfrom); + $context = context_module::instance($assigncm->id); + $users = get_enrolled_users($context, "mod/assign:submit", 0); + if (!empty($users)) { + foreach ($users as $user) { + $ugrps = groups_get_user_groups($this->instance->course, $user->id); + if (!count($ugrps[0])) { + $groupmembers[$user->id] = $user; + } + } + } + } else { + $groupmembers = []; + } + + return $groupmembers; + } + + /** + * Gets group approval for the specified file! + * + * @param int $pubfileid ID of publication file entry in DB + * @return array cumulated approval for specified file and array with approval details + */ + public function group_approval($pubfileid) { + global $DB; + + if (($this->get_instance()->mode != PUBLICATION_MODE_IMPORT) + || !$DB->get_field('assign', 'teamsubmission', ['id' => $this->get_instance()->importfrom])) { + throw new coding_exception('Cannot be called if files get uploaded or teamsubmission is deactivated!'); + } + + $filerec = $DB->get_record('publication_file', ['id' => $pubfileid]); + + // Get group members! + $groupmembers = $this->get_submissionmembers($filerec->userid); + + if (!empty($groupmembers)) { + list($usersql, $userparams) = $DB->get_in_or_equal(array_keys($groupmembers), SQL_PARAMS_NAMED, 'user'); + $sql = "SELECT u.*, ga.approval, ga.timemodified AS approvaltime + FROM {user} u + LEFT JOIN {publication_groupapproval} ga ON u.id = ga.userid AND ga.fileid = :fileid + WHERE u.id " . $usersql; + $params = ['fileid' => $filerec->id] + $userparams; + $groupdata = $DB->get_records_sql($sql, $params); + } else { + $groupdata = []; + } + + return [$filerec->studentapproval, $groupdata]; + } + + /** + * Download a single file, returns file content and terminated script. + * + * @param int $fileid ID of the submitted file in filespace + */ + public function download_file($fileid) { + global $DB, $USER; + + $conditions = []; + $conditions['publication'] = $this->get_instance()->id; + $conditions['fileid'] = $fileid; + $record = $DB->get_record('publication_file', $conditions); + + $allowed = false; + + if (has_capability('mod/publication:approve', $this->get_context())) { + // Teachers has to see the files to know if they can allow them. + $allowed = true; + } else if ($this->has_filepermission($fileid, $USER->id)) { + // File is publicly viewable or is owned by the user. + $allowed = true; + } + + if ($allowed) { + $fs = get_file_storage(); + $file = $fs->get_file_by_id($fileid); + $itemid = $file->get_itemid(); + if ($record->type == PUBLICATION_MODE_ONLINETEXT) { + global $CFG; + + if ($this->get_instance()->importfrom == -1) { + $teamsubmission = false; + } else { + $teamsubmission = $DB->get_field('assign', 'teamsubmission', ['id' => $this->get_instance()->importfrom]); + } + if (!$teamsubmission) { + // Get user firstname/lastname. + $auser = $DB->get_record('user', ['id' => $itemid], get_all_user_name_fields(true)); + $itemname = str_replace(' ', '_', fullname($auser)).'_'; + } else { + if (empty($itemid)) { + $itemname = get_string('defaultteam', 'assign').'_'; + } else { + $itemname = $DB->get_field('groups', 'name', ['id' => $itemid]).'_'; + } + } + + // Create path for new zip file. + $zipfile = tempnam($CFG->dataroot . '/temp/', 'publication_'); + // Zip files. + $filename = $itemname.$file->get_filename(); + $zipname = str_replace('.html', '.zip', $filename); + $zipper = new zip_packer(); + $filesforzipping = []; + $this->add_onlinetext_to_zipfiles($filesforzipping, $file, '', $filename, $fs); + if (count($filesforzipping) == 1) { + // We can send the file directly, if it has no resources! + send_file($file, $filename, 'default', 0, false, true, $file->get_mimetype(), false); + } else if ($zipper->archive_to_pathname($filesforzipping, $zipfile)) { + send_temp_file($zipfile, $zipname); // Send file and delete after sending. + } + } else { + send_file($file, $file->get_filename(), 'default', 0, false, true, $file->get_mimetype(), false); + } + die(); + } else { + print_error('You are not allowed to see this file'); // TODO ge_string(). + } + } + + /** + * Creates a zip of all uploaded files and sends a zip to the browser + * + * @param unknown $uploaders false => empty zip, true all users, array files from uploaders (users/groups) in array + */ + public function download_zip($uploaders = []) { + global $CFG, $DB, $USER; + require_once($CFG->libdir . '/filelib.php'); + + $cm = $this->get_coursemodule(); + + $canapprove = has_capability('mod/publication:approve', $this->get_context()); + if ($this->get_instance()->importfrom == -1) { + $teamsubmission = false; + } else { + $teamsubmission = $DB->get_field('assign', 'teamsubmission', ['id' => $this->get_instance()->importfrom]); + } + + $conditions = []; + $conditions['publication'] = $this->get_instance()->id; + + $filesforzipping = []; + $fs = get_file_storage(); + + // Get group name for filename. + $groupname = ''; + $currentgroup = groups_get_activity_group($cm, true); + if (!empty($currentgroup)) { + $groupname = $DB->get_field('groups', 'name', ['id' => $currentgroup]) . '-'; + } + + if (!$teamsubmission) { + $uploaders = $this->get_users($uploaders); + } else { + $uploaders = $this->get_groups(0, $uploaders); + } + + $filename = str_replace(' ', '_', clean_filename($this->course->shortname . '-' . + $this->get_instance()->name . '-' . $groupname . $this->get_instance()->id . '.zip')); // Name of new zip file. + + $userfields = get_all_user_name_fields(); + $userfields['id'] = 'id'; + $userfields['username'] = 'username'; + $userfields = implode(', ', $userfields); + + // Get all files from each user/group. + foreach ($uploaders as $uploader) { + $conditions['userid'] = $uploader; + $records = $DB->get_records('publication_file', $conditions); + + if (!$teamsubmission) { + // Get user firstname/lastname. + $auser = $DB->get_record('user', ['id' => $uploader], $userfields); + $itemname = fullname($auser); + $itemunique = $uploader; + } else { + if (empty($uploader)) { + $itemname = get_string('defaultteam', 'assign'); + } else { + $itemname = $DB->get_field('groups', 'name', ['id' => $uploader]); + } + $itemunique = ''; + } + + foreach ($records as $record) { + if ($canapprove || $this->has_filepermission($record->fileid, $USER->id)) { + // Is teacher or file is public. + + $file = $fs->get_file_by_id($record->fileid); + + // Get files new name. + $fileext = strstr($file->get_filename(), '.'); + $fileoriginal = str_replace($fileext, '', $file->get_filename()); + $fileforzipname = clean_filename($itemname . '_' . $fileoriginal . '_' . $itemunique . $fileext); + if (key_exists($fileforzipname, $filesforzipping)) { + throw new coding_exception('Can\'t overwrite ' . $fileforzipname . '!'); + } + if ($record->type == PUBLICATION_MODE_ONLINETEXT) { + $this->add_onlinetext_to_zipfiles($filesforzipping, $file, $itemname, $fileforzipname, $fs, $itemunique); + } else { + // Save file name to array for zipping. + $filesforzipping[$fileforzipname] = $file; + } + } + } + } // End of foreach. + + if ($zipfile = $this->pack_files($filesforzipping)) { + send_temp_file($zipfile, $filename); // Send file and delete after sending. + } + } + + /** + * Pack files in ZIP + * + * @param object[] $filesforzipping Files for zipping + * @return object zipped files + */ + private function pack_files($filesforzipping) { + global $CFG; + // Create path for new zip file. + $tempzip = tempnam($CFG->dataroot . '/temp/', 'publication_'); + // Zip files. + $zipper = new zip_packer(); + if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) { + return $tempzip; + } + + return false; + } + + /** + * Adds onlinetext-file to zipping-files including all ressources! + * + * @param stored_file[] $filesforzipping array of stored files indexed by filename + * @param stored_file $file onlinetext-file to add to ZIP + * @param string $itemname User or group's name to use for filename + * @param string $fileforzipname Filename to use for the file being added + * @param file_storage $fs used to get the ressource files for the online-text-file + * @param string $itemunique user-ID of the uploading user or empty for teamsubmissions + */ + protected function add_onlinetext_to_zipfiles(array &$filesforzipping, stored_file $file, $itemname, $fileforzipname, + $fs = null, $itemunique = '') { + + if (empty($fs)) { + $fs = get_file_storage(); + } + + // First we get all ressources! + $resources = $fs->get_directory_files($this->get_context()->id, + 'mod_publication', + 'attachment', + $file->get_itemid(), + '/resources/', + true, + false); + if (count($resources) > 0) { + // If it's an online-Text with resources, we have to add altered content and all the ressources for it! + $content = $file->get_content(); + // We grabbed the resources already above! + // Then we change every occurence of the ressource-name from ./resourcename to ./ITEMNAME/resourcename! + $folder = clean_filename((!empty($itemname) ? $itemname . '_' : '') . + (($itemunique != '') ? $itemunique . '_' : '') . + 'resources'); + foreach ($resources as $resource) { + $search = './resources/' . $resource->get_filename(); + $replace = $folder . '/' . $resource->get_filename(); + $content = str_replace($search, './' . $replace, $content); + $filesforzipping[$replace] = $resource; + } + /* We add the altered filecontent instead of the stored one * + * (needs an array to differentiate between content and filepath)! */ + $filesforzipping[$fileforzipname] = [$content]; + } else { + $filesforzipping[$fileforzipname] = $file; + } + } + + /** + * Updates files from connected assignment + */ + public function importfiles() { + global $DB; + + if ($this->instance->mode == PUBLICATION_MODE_IMPORT) { + $assign = $DB->get_record('assign', ['id' => $this->instance->importfrom]); + $assignmoduleid = $DB->get_field('modules', 'id', ['name' => 'assign']); + $assigncm = $DB->get_record('course_modules', [ + 'course' => $assign->course, + 'module' => $assignmoduleid, + 'instance' => $assign->id + ]); + + $assigncontext = context_module::instance($assigncm->id); + + if ($assigncm && has_capability('mod/publication:addinstance', $this->context)) { + $this->import_assign_files($assigncm, $assigncontext); + $this->import_assign_onlinetexts($assigncm, $assigncontext); + + return true; + } + } + + return false; + } + + /** + * Import assignment's submission files! + * + * @param object $assigncm Assignment coursemodule object + * @param object $assigncontext Assignment context object + */ + protected function import_assign_files($assigncm, $assigncontext) { + global $DB, $CFG, $OUTPUT; + + $records = $DB->get_records('assignsubmission_file', ['assignment' => $this->get_instance()->importfrom]); + + $fs = get_file_storage(); + + require_once($CFG->dirroot . '/mod/assign/locallib.php'); + $assigncourse = $DB->get_record('course', ['id' => $assigncm->course]); + $assignment = new assign($assigncontext, $assigncm, $assigncourse); + + foreach ($records as $record) { + + $files = $fs->get_area_files($assigncontext->id, + "assignsubmission_file", + "submission_files", + $record->submission, + "id", + false); + $submission = $DB->get_record('assign_submission', ['id' => $record->submission]); + + $assignfileids = []; + + $assignfiles = []; + + foreach ($files as $file) { + $assignfiles[$file->get_id()] = $file; + $assignfileids[$file->get_id()] = $file->get_id(); + } + + $conditions = []; + $conditions['publication'] = $this->get_instance()->id; + if (empty($assignment->get_instance()->teamsubmission)) { + $conditions['userid'] = $submission->userid; + } else { + $conditions['userid'] = $submission->groupid; + } + // We look for regular imported files here! + $conditions['type'] = PUBLICATION_MODE_IMPORT; + + $oldpubfiles = $DB->get_records('publication_file', $conditions); + + foreach ($oldpubfiles as $oldpubfile) { + + if (in_array($oldpubfile->filesourceid, $assignfileids)) { + // File was in assign and is still there. + unset($assignfileids[$oldpubfile->filesourceid]); + + } else { + // File has been removed from assign. + // Remove from publication (file and db entry). + if ($file = $fs->get_file_by_id($oldpubfile->fileid)) { + $file->delete(); + } + + $conditions['id'] = $oldpubfile->id; + $dataobject = $DB->get_record('publication_file', ['id' => $conditions['id']->id]); + $cm = $this->coursemodule; + \mod_publication\event\publication_file_deleted::create_from_object($cm, $dataobject)->trigger(); + $DB->delete_records('publication_file', $conditions); + } + } + + // Add new files to publication. + foreach ($assignfileids as $assignfileid) { + $newfilerecord = new stdClass(); + $newfilerecord->contextid = $this->get_context()->id; + $newfilerecord->component = 'mod_publication'; + $newfilerecord->filearea = 'attachment'; + if (empty($assignment->get_instance()->teamsubmission)) { + $newfilerecord->itemid = $submission->userid; + } else { + $newfilerecord->itemid = $submission->groupid; + } + + try { + $newfile = $fs->create_file_from_storedfile($newfilerecord, $assignfiles[$assignfileid]); + + $dataobject = new stdClass(); + $dataobject->publication = $this->get_instance()->id; + $importtype = 'user'; + if (empty($assignment->get_instance()->teamsubmission)) { + $dataobject->userid = $submission->userid; + } else { + $importtype = 'group'; + $dataobject->userid = $submission->groupid; + } + $dataobject->timecreated = time(); + $dataobject->fileid = $newfile->get_id(); + $dataobject->filesourceid = $assignfileid; + $dataobject->filename = $newfile->get_filename(); + $dataobject->contenthash = "666"; + $dataobject->type = PUBLICATION_MODE_IMPORT; + + $dataobject->id = $DB->insert_record('publication_file', $dataobject); + $dataobject->typ = $importtype; + \mod_publication\event\publication_file_imported::file_added($assigncm, $dataobject)->trigger(); + + if ($this->get_instance()->notifyteacher) { + $cm = get_coursemodule_from_instance('publication', $this->get_instance()->id, 0, false, MUST_EXIST); + $user = $DB->get_record('user', ['id' => $submission->userid], '*', MUST_EXIST); + self::send_teacher_notification_uploaded($cm, $newfile, $user); + } + + } catch (Exception $e) { + // File could not be copied, maybe it does already exist. + // Should not happen. + echo $OUTPUT->box($OUTPUT->notification($e->getMessage(), 'notifyproblem'), 'generalbox'); + } + } + } + + } + + /** + * Import assignment's onlinetext submissions! + * + * @param object $assigncm Assignment coursemodule object + * @param object $assigncontext Assignment context object + * @throws coding_exception + */ + protected function import_assign_onlinetexts($assigncm, $assigncontext) { + if ($this->get_instance()->mode != PUBLICATION_MODE_IMPORT) { + return; + } + + self::update_assign_onlinetext($assigncm, $assigncontext, $this->get_instance()->id, $this->get_context()->id); + } + + /** + * Updates the online-submission(s) of a single assignment used for manual import and autoimport via event observer + * + * @param stdClass $assigncm Assign's coursemodule object + * @param stdClass $assigncontext Assign's context object + * @param int $publicationid Publication's instance ID + * @param int $contextid Publication's context ID + * @param int $submissionid (optional) If set, only process this submission, else process all submissions + */ + public static function update_assign_onlinetext($assigncm, $assigncontext, $publicationid, $contextid, $submissionid = 0) { + global $USER, $DB, $CFG; + + $fs = get_file_storage(); + + require_once($CFG->dirroot . '/mod/assign/locallib.php'); + $assigncourse = $DB->get_record('course', ['id' => $assigncm->course]); + $assignment = new assign($assigncontext, $assigncm, $assigncourse); + $teamsubmission = $assignment->get_instance()->teamsubmission; + + if (!empty($submissionid)) { + $records = $DB->get_records('assignsubmission_onlinetext', [ + 'assignment' => $assigncm->instance, + 'submission' => $submissionid + ]); + } else { + $records = $DB->get_records('assignsubmission_onlinetext', ['assignment' => $assigncm->instance]); + } + + foreach ($records as $record) { + $submission = $DB->get_record('assign_submission', ['id' => $record->submission]); + $itemid = empty($teamsubmission) ? $submission->userid : $submission->groupid; + $importtype = empty($teamsubmission) ? 'user' : 'group'; + + // First we fetch the resource files (embedded files in text!) + $fsfiles = $fs->get_area_files($assigncontext->id, + 'assignsubmission_onlinetext', + ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, + $submission->id, + 'timemodified', + false); + foreach ($fsfiles as $file) { + $filerecord = new \stdClass(); + $filerecord->contextid = $contextid; + $filerecord->component = 'mod_publication'; + $filerecord->filearea = 'attachment'; + $filerecord->itemid = $itemid; + $filerecord->filepath = '/resources/'; + $filerecord->filename = $file->get_filename(); + $pathnamehash = $fs->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, + $filerecord->itemid, $filerecord->filepath, $filerecord->filename); + + if ($fs->file_exists_by_hash($pathnamehash)) { + $otherfile = $fs->get_file_by_hash($pathnamehash); + if ($file->get_contenthash() != $otherfile->get_contenthash()) { + // We have to update the file! + $otherfile->delete(); + $fs->create_file_from_storedfile($filerecord, $file); + } + } else { + // We have to add the file! + $fs->create_file_from_storedfile($filerecord, $file); + } + } + + // Now we delete old resource-files, which are no longer present! + $resources = $fs->get_directory_files($contextid, + 'mod_publication', + 'attachment', + $itemid, + '/resources/', + true, + false); + foreach ($resources as $resource) { + $pathnamehash = $fs->get_pathname_hash($assignment->get_context()->id, 'assignsubmission_onlinetext', + ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submission->id, '/', + $resource->get_filename()); + if (!$fs->file_exists_by_hash($pathnamehash)) { + $resource->delete(); + } + } + + /* Here we convert the pluginfile urls to relative urls for the exported html-file + * (the resources have to be included in the download!) */ + $formattedtext = str_replace('@@PLUGINFILE@@/', './resources/', $record->onlinetext); + $formattedtext = format_text($formattedtext, $record->onlineformat, ['context' => $assigncontext]); + + $head = '<head><meta charset="UTF-8"></head>'; + $submissioncontent = '<!DOCTYPE html><html>' . $head . '<body>' . $formattedtext . '</body></html>'; + + $filename = get_string('onlinetextfilename', 'assignsubmission_onlinetext'); + + // Does the file exist... let's check it! + $pathhash = $fs->get_pathname_hash($contextid, 'mod_publication', 'attachment', $itemid, '/', $filename); + + $conditions = [ + 'publication' => $publicationid, + 'userid' => $itemid, + 'type' => PUBLICATION_MODE_ONLINETEXT + ]; + $pubfile = $DB->get_record('publication_file', $conditions, '*', IGNORE_MISSING); + + $createnew = false; + if ($fs->file_exists_by_hash($pathhash)) { + $file = $fs->get_file_by_hash($pathhash); + if (empty($formattedtext)) { + // The onlinetext was empty, delete the file! + $dataobject = $DB->get_record('publication_file', ['id' => $conditions['id']->id]); + \mod_publication\event\publication_file_deleted::create_from_object($assigncm, $dataobject)->trigger(); + $DB->delete_records('publication_file', $conditions); + $file->delete(); + } else if (($file->get_timemodified() < $submission->timemodified) + && ($file->get_contenthash() != sha1($submissioncontent))) { + /* If the submission has been modified after the file, * + * we check for different content-hashes to see if it was changed! */ + $createnew = true; + if (empty($pubfile) || ($file->get_id() == $pubfile->fileid)) { + // Everything's alright, we can delete the old file! + $file->delete(); + } else { + // Something unexcpected happened! + throw new coding_exception('Mismatching fileids (pubfile with id ' . $pubfile->fileid . + ' and stored file ' . + $file->get_id() . '!'); + } + } + } else if (!empty($formattedtext)) { + // There exists no such file, so we create one! + $createnew = true; + } + + if ($createnew === true) { + // We gotta create a new one! + $newfilerecord = new stdClass(); + $newfilerecord->contextid = $contextid; + $newfilerecord->component = 'mod_publication'; + $newfilerecord->filearea = 'attachment'; + $newfilerecord->itemid = $itemid; + $newfilerecord->filename = $filename; + $newfilerecord->filepath = '/'; + $newfile = $fs->create_file_from_string($newfilerecord, $submissioncontent); + if (!$pubfile) { + $pubfile = new stdClass(); + $pubfile->userid = $itemid; + $pubfile->type = PUBLICATION_MODE_ONLINETEXT; + $pubfile->publication = $publicationid; + } + // The file has been updated, so we set the new time. + $pubfile->timecreated = time(); + $pubfile->fileid = $newfile->get_id(); + $pubfile->filename = $filename; + $pubfile->contenthash = $newfile->get_contenthash(); + if (!empty($pubfile->id)) { + $dataobject = $pubfile; + $dataobject->typ = $importtype; + $dataobject->itemid = $itemid; + $dataobject->update = true; + \mod_publication\event\publication_file_imported::file_added($assigncm, $dataobject)->trigger(); + $DB->update_record('publication_file', $pubfile); + } else { + $dataobject = $pubfile; + $dataobject->id = $DB->insert_record('publication_file', $pubfile); + $dataobject->typ = $importtype; + $dataobject->itemid = $itemid; + \mod_publication\event\publication_file_imported::file_added($assigncm, $dataobject)->trigger(); + } + + $cm = get_coursemodule_from_instance('publication', $publicationid, 0, false, MUST_EXIST); + $publication = new publication($cm); + if ($publication->get_instance()->notifyteacher) { + self::send_teacher_notification_uploaded($cm, $newfile); + } + + } + } + } + + /** + * Send a notification about the change of the approval status to a student + * @param stdClass $cm coursemodule + * @param object $user the where the notification should go + * @param object $userfrom who cahnged the approval status + * @param string $newstatus whats the new status + * @param object $pubfile the publication-file on which the status change took place + * @param string $pubid id of the publication + * @param null|stdClass $publication the publication instance + * @throws coding_exception + */ + public static function send_student_notification_approval_changed($cm, $user, $userfrom, $newstatus, $pubfile, + $pubid, $publication=null) { + global $CFG; + $strsubmitted = get_string('approvalchange', 'publication'); + + if (!$publication) { + $publication = new publication($cm); + } + + $info = new stdClass(); + $info->username = fullname($userfrom); + $info->publication = format_string($cm->name, true); + $info->url = $CFG->wwwroot . '/mod/publication/view.php?id=' . $pubid; + $info->id = $pubid; + $info->filename = $pubfile->filename; + $info->apstatus = get_string('status:approved' . $newstatus, 'mod_publication'); + $info->dayupdated = userdate(time(), get_string('strftimedate')); + $info->timeupdated = userdate(time(), get_string('strftimetime')); + + $postsubject = $strsubmitted . ': ' . $info->username . ' -> ' . $cm->name; + $posttext = $publication->email_students_text($info); + $posthtml = ($user->mailformat == 1) ? $publication->email_students_html($info) : ''; + + $message = new \core\message\message(); + $message->component = 'mod_publication'; + $message->name = 'publication_updates'; + $message->courseid = $cm->course; + $message->userfrom = $userfrom; + $message->userto = $user; + $message->subject = $postsubject; + $message->fullmessage = $posttext; + $message->fullmessageformat = FORMAT_HTML; + $message->fullmessagehtml = $posthtml; + $message->smallmessage = $postsubject; + $message->notification = 1; + $message->contexturl = $info->url; + $message->contexturlname = $info->publication; + + try { + message_send($message); + } catch (coding_exception $e) { + throw new Exception("Coding exception while sending notification: " . $e->getMessage()); + } + } + + /** + * Sends a notification to assigned grades + * @param object $cm course module + * @param stored_file $file the file + * @param stdClass|null $user the user + * @param stdClass|null $publication object the publication, if available + * @throws coding_exception + */ + public static function send_teacher_notification_uploaded($cm, $file, $user=null, $publication=null) { + global $CFG, $USER; + $strsubmitted = get_string('uploaded', 'publication'); + if (!$publication) { + $publication = new publication($cm); + } + if (!$user) { + $user = $USER; + } + $graders = $publication->get_graders($user); + + foreach ($graders as $teacher) { + $info = new stdClass(); + $info->username = fullname($user); + $info->publication = format_string($publication->get_instance()->name, true); + $info->url = $CFG->wwwroot . '/mod/publication/view.php?id=' . $cm->id; + $info->id = $cm->id; + $info->filename = $file->get_filename(); + $info->dayupdated = userdate(time(), get_string('strftimedate')); + $info->timeupdated = userdate(time(), get_string('strftimetime')); + + $postsubject = $strsubmitted . ': ' . $info->username . ' -> ' . $info->publication; + $posttext = $publication->email_teachers_text($info); + $posthtml = ($teacher->mailformat == 1) ? $publication->email_teachers_html($info) : ''; + + $message = new \core\message\message(); + $message->component = 'mod_publication'; + $message->name = 'publication_updates'; + $message->courseid = $cm->course; + $message->userfrom = $user; + $message->userto = $teacher; + $message->subject = $postsubject; + $message->fullmessage = $posttext; + $message->fullmessageformat = FORMAT_HTML; + $message->fullmessagehtml = $posthtml; + $message->smallmessage = $postsubject; + $message->notification = 1; + $message->contexturl = $info->url; + $message->contexturlname = $info->publication; + + message_send($message); + } + } + + /** + * Format file content of imported onlinetexts to be rendered as preview. + * + * @param int $itemid User's or group's ID + * @param int $publicationid Publication instance's database ID + * @param int $contextid Publication instance's context ID + * @return string formatted HTML snippet ready to be output + */ + public static function export_onlinetext_for_preview($itemid, $publicationid, $contextid) { + global $DB; + + // Get file data/record! + $conditions = [ + 'publication' => $publicationid, + 'userid' => $itemid, + 'type' => PUBLICATION_MODE_ONLINETEXT + ]; + if (!$pubfile = $DB->get_record('publication_file', $conditions, '*')) { + return ''; + } + + $fs = get_file_storage(); + $file = $fs->get_file_by_id($pubfile->fileid); + $content = $file->get_content(); + + // Correct ressources filepaths for onine-view! + $resources = $fs->get_directory_files($contextid, + 'mod_publication', + 'attachment', + $itemid, + '/resources/', + true, + false); + foreach ($resources as $resource) { + // TODO watch the encoding of the file's names, in the event of core changing it, we have to change too! + $filename = rawurlencode($resource->get_filename()); + $search = './resources/' . $filename; + $replace = '@@PLUGINFILE@@/resources/' . $filename; + $content = str_replace($search, $replace, $content); + } + $content = file_rewrite_pluginfile_urls($content, 'pluginfile.php', $contextid, 'mod_publication', 'attachment', + $itemid, ['forcehttps' => true]); + + // Get only the body part! + $start = strpos($content, '<body>'); + $length = strrpos($content, '</body>') - strpos($content, '<body>'); + if ($start !== false && $length > 0) { + $content = substr($content, $start, $length); + } else { + $content = ''; + } + + return $content; + + } + + // Allowed file-types have been changed in Moodle 3.3 (and form element will probably change in Moodle 3.4 again)! + + /** + * Get the type sets configured for this publication. + * Adapted from assignsubmission_file! + * + * @return array('groupname', 'mime/type', ...) + */ + public function get_configured_typesets() { + $typeslist = (string)$this->instance->allowedfiletypes; + + $sets = self::get_typesets($typeslist); + + return $sets; + } + + /** + * Get the type sets passed. + * Adapted from assignsubmission_file! + * + * @param string $types The space , ; separated list of types + * @return array('groupname', 'mime/type', ...) + */ + public static function get_typesets($types) { + $sets = []; + if (!empty($types)) { + $sets = preg_split('/[\s,;:"\']+/', $types, null, PREG_SPLIT_NO_EMPTY); + } + + return $sets; + } + + /** + * Return the accepted types list for the file manager component. + * Adapted from assignsubmission_file! + * + * @return array|string + */ + public function get_accepted_types() { + $acceptedtypes = $this->get_configured_typesets(); + + if (!empty($acceptedtypes)) { + return $acceptedtypes; + } + + return '*'; + } + + /** + * List the nonexistent file types that need to be removed. + * Adapted from assignsubmission_file! + * + * @param string $types space , or ; separated types + * @return array A list of the nonexistent file types. + */ + public static function get_nonexistent_file_types($types) { + $nonexistent = []; + foreach (self::get_typesets($types) as $type) { + // If there's no extensions under that group, it doesn't exist. + $extensions = file_get_typegroup('extension', [$type]); + if (empty($extensions)) { + $nonexistent[$type] = true; + } + } + + return array_keys($nonexistent); + } + + /** + * Returns a list of teachers that should be notified of the file-upload + * + * @param object $user + * @return array Array of users able to grade + */ + public function get_graders($user) { + // Get potential graders! + $potgraders = get_users_by_capability($this->context, 'mod/publication:receiveteachernotification', '', '', '', + '', '', '', false, false); + + $graders = array(); + if (groups_get_activity_groupmode($this->coursemodule) == SEPARATEGROUPS) { + // Separate groups are being used! + if ($groups = groups_get_all_groups($this->course->id, $user->id)) { + // Try to find all groups! + foreach ($groups as $group) { + foreach ($potgraders as $t) { + if ($t->id == $user->id) { + continue; // Do not send self! + } + if (groups_is_member($group->id, $t->id)) { + $graders[$t->id] = $t; + } + } + } + } else { + // User not in group, try to find graders without group! + foreach ($potgraders as $t) { + if ($t->id == $user->id) { + continue; // Do not send to one self! + } + if (!groups_get_all_groups($this->course->id, $t->id)) { // Ugly hack! + $graders[$t->id] = $t; + } + } + } + } else { + foreach ($potgraders as $t) { + if ($t->id == $user->id) { + continue; // Do not send to one self! + } + $graders[$t->id] = $t; + } + } + return $graders; + } + + /** + * Creates the text content for emails to teachers + * + * @param object $info The info used by the 'emailteachermail' language string + * @return string Plain-Text snippet to use in messages + */ + public function email_teachers_text($info) { + $posttext = format_string($this->course->shortname).' -> '. + get_string('modulenameplural', 'publication').' -> '. + format_string($info->publication)."\n"; + $posttext .= get_string('emailteachermail', 'publication', $info)."\n"; + return $posttext; + } + + /** + * Creates the html content for emails to teachers + * + * @param object $info The info used by the 'emailteachermailhtml' language string + * @return string HTML snippet to use in messages + */ + public function email_teachers_html($info) { + global $CFG; + $posthtml = '<p><span style="font-family: sans-serif; ">' . + '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$this->course->id.'">'. + format_string($this->course->shortname).'</a> ->'. + '<a href="'.$CFG->wwwroot.'/mod/publication/view.php?id='. + $info->id.'">'.get_string('modulenameplural', 'publication').'</a> ->'. + '<a href="'.$CFG->wwwroot.'/mod/publication/view.php?id='.$info->id.'">'. + format_string($info->publication). '</a></span></p>'; + $posthtml .= '<hr /><span style="font-family: sans-serif; ">'; + $posthtml .= '<p>'.get_string('emailteachermailhtml', 'publication', $info).'</p>'; + $posthtml .= '</font><hr />'; + return $posthtml; + } + + /** + * Creates the text content for emails to students + * + * @param object $info The info used by the 'emailteachermail' language string + * @return string Plain-Text snippet to use in messages + */ + public function email_students_text($info) { + $posttext = format_string($this->course->shortname).' -> '. + get_string('modulenameplural', 'publication').' -> '. + format_string($info->publication)."\n"; + $posttext .= "---------------------------------------------------------------------\n"; + $posttext .= get_string('emailstudentsmail', 'publication', $info)."\n"; + $posttext .= "---------------------------------------------------------------------\n"; + return $posttext; + } + + /** + * Creates the html content for emails to students + * + * @param object $info The info used by the 'emailstudentsmailhtml' language string + * @return string HTML snippet to use in messages + */ + public function email_students_html($info) { + global $CFG; + $posthtml = '<p><span style="font-family: sans-serif; ">' . + '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$this->course->id.'">'. + format_string($this->course->shortname).'</a> ->'. + '<a href="'.$CFG->wwwroot.'/mod/publication/view.php?id='. + $info->id.'">'.get_string('modulenameplural', 'publication').'</a> ->'. + '<a href="'.$CFG->wwwroot.'/mod/publication/view.php?id='.$info->id.'">'. + format_string($info->publication). '</a></span></p>'; + $posthtml .= '<hr /><span style="font-family: sans-serif; ">'; + $posthtml .= '<p>'.get_string('emailstudentsmailhtml', 'publication', $info).'</p>'; + $posthtml .= '</font><hr />'; + return $posthtml; + } +} diff --git a/mod/publication/mod_form.php b/mod/publication/mod_form.php new file mode 100644 index 0000000..194567a --- /dev/null +++ b/mod/publication/mod_form.php @@ -0,0 +1,261 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Instance settings form. + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +/** + * Form for creating and editing mod_publication instances + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_mod_form extends moodleform_mod { + + /** + * Define this form - called by the parent constructor + */ + public function definition() { + global $DB, $CFG, $COURSE; + + $mform = $this->_form; + $mform->addElement('header', 'general', get_string('general', 'form')); + + // Name. + $mform->addElement('text', 'name', get_string('name', 'publication')); + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + $mform->addRule('name', null, 'required', null, 'client'); + + // Adding the standard "intro" and "introformat" fields! + $this->standard_intro_elements(); + + // Publication specific elements. + $mform->addElement('header', 'publication', get_string('modulename', 'publication')); + $mform->setExpanded('publication'); + + if (isset($this->current->id) && $this->current->id != "") { + $filecount = $DB->count_records('publication_file', ['publication' => $this->current->id]); + } else { + $filecount = 0; + } + + $disabled = []; + if ($filecount > 0) { + $disabled['disabled'] = 'disabled'; + } + + $modearray = []; + $modearray[] =& $mform->createElement('radio', 'mode', '', get_string('modeupload', 'publication'), + PUBLICATION_MODE_UPLOAD, $disabled); + $modearray[] =& $mform->createElement('radio', 'mode', '', get_string('modeimport', 'publication'), + PUBLICATION_MODE_IMPORT, $disabled); + $mform->addGroup($modearray, 'modegrp', get_string('mode', 'publication'), [' '], false); + $mform->addHelpButton('modegrp', 'mode', 'publication'); + if ($filecount === 0) { + $mform->addRule('modegrp', null, 'required', null, 'client'); + } + + // Publication mode import specific elements. + $choices = []; + $choices[-1] = get_string('choose', 'publication'); + $assigninstances = $DB->get_records('assign', ['course' => $COURSE->id]); + $select = $mform->createElement('select', 'importfrom', get_string('assignment', 'publication'), $choices, $disabled); + $notteamassigns = [-1]; + foreach ($assigninstances as $assigninstance) { + if (!$assigninstance->teamsubmission) { + $notteamassigns[] = $assigninstance->id; + } + $attributes = ['data-teamsubmission' => $assigninstance->teamsubmission]; + $select->addOption($assigninstance->name, $assigninstance->id, $attributes); + } + $mform->addElement($select); + $mform->addHelpButton('importfrom', 'assignment', 'publication'); + $mform->hideIf('importfrom', 'mode', 'neq', PUBLICATION_MODE_IMPORT); + + $mform->addElement('advcheckbox', 'autoimport', get_string('autoimport', 'publication')); + $mform->setDefault('autoimport', get_config('publication', 'autoimport')); + $mform->addHelpButton('autoimport', 'autoimport', 'publication'); + $mform->hideIf('autoimport', 'mode', 'neq', PUBLICATION_MODE_IMPORT); + + $attributes = []; + if (isset($this->current->id) && isset($this->current->obtainstudentapproval)) { + if ($this->current->obtainstudentapproval) { + $message = get_string('warning_changefromobtainstudentapproval', 'publication'); + $showwhen = "0"; + } else { + $message = get_string('warning_changetoobtainstudentapproval', 'publication'); + $showwhen = "1"; + } + + $message = trim(preg_replace('/\s+/', ' ', $message)); + $message = str_replace('\'', '\\\'', $message); + $attributes['onChange'] = "if (this.value==" . $showwhen . ") {alert('" . $message . "')}"; + } + + $mform->addElement('selectyesno', 'obtainstudentapproval', get_string('obtainstudentapproval', 'publication'), $attributes); + $mform->setDefault('obtainstudentapproval', get_config('publication', 'obtainstudentapproval')); + $mform->addHelpButton('obtainstudentapproval', 'obtainstudentapproval', 'publication'); + $mform->hideIf('obtainstudentapproval', 'mode', 'neq', PUBLICATION_MODE_IMPORT); + + $radioarray = []; + $radioarray[] = $mform->createElement('radio', 'groupapproval', '', get_string('groupapprovalmode_all', 'publication'), + PUBLICATION_APPROVAL_ALL, $attributes); + $radioarray[] = $mform->createElement('radio', 'groupapproval', '', get_string('groupapprovalmode_single', 'publication'), + PUBLICATION_APPROVAL_SINGLE, $attributes); + $mform->addGroup($radioarray, 'groupapprovalarray', get_string('groupapprovalmode', 'publication'), + [html_writer::empty_tag('br')], false); + $mform->addHelpButton('groupapprovalarray', 'groupapprovalmode', 'publication'); + $mform->setDefault('groupapproval', PUBLICATION_APPROVAL_ALL); + $mform->hideIf('groupapprovalarray', 'mode', 'neq', PUBLICATION_MODE_IMPORT); + foreach ($notteamassigns as $cur) { + $mform->hideIf('groupapprovalarray', 'importfrom', 'eq', $cur); + } + + // Publication mode upload specific elements. + $maxfiles = []; + for ($i = 1; $i <= 100 || $i <= get_config('publication', 'maxfiles'); $i++) { + $maxfiles[$i] = $i; + } + + $mform->addElement('select', 'maxfiles', get_string('maxfiles', 'publication'), $maxfiles); + $mform->setDefault('maxfiles', get_config('publication', 'maxfiles')); + $mform->hideIf('maxfiles', 'mode', 'neq', PUBLICATION_MODE_UPLOAD); + + $choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes); + $choices[0] = get_string('courseuploadlimit', 'publication') . ' (' . display_size($COURSE->maxbytes) . ')'; + $mform->addElement('select', 'maxbytes', get_string('maxbytes', 'publication'), $choices); + $mform->setDefault('maxbytes', get_config('publication', 'maxbytes')); + $mform->hideIf('maxbytes', 'mode', 'neq', PUBLICATION_MODE_UPLOAD); + + $mform->addElement('filetypes', 'allowedfiletypes', get_string('allowedfiletypes', 'publication')); + $mform->addHelpButton('allowedfiletypes', 'allowedfiletypes', 'publication'); + $mform->hideIf('allowedfiletypes', 'mode', 'neq', PUBLICATION_MODE_UPLOAD); + + $attributes = []; + if (isset($this->current->id) && isset($this->current->obtainteacherapproval)) { + if (!$this->current->obtainteacherapproval) { + $message = get_string('warning_changefromobtainteacherapproval', 'publication'); + $showwhen = "1"; + } else { + $message = get_string('warning_changetoobtainteacherapproval', 'publication'); + $showwhen = "0"; + } + + $message = trim(preg_replace('/\s+/', ' ', $message)); + $attributes['onChange'] = "if (this.value==" . $showwhen . ") {alert('" . $message . "')}"; + } + + $mform->addElement('selectyesno', 'obtainteacherapproval', + get_string('obtainteacherapproval', 'publication'), $attributes); + $mform->setDefault('obtainteacherapproval', get_config('publication', 'obtainteacherapproval')); + $mform->addHelpButton('obtainteacherapproval', 'obtainteacherapproval', 'publication'); + $mform->hideIf('obtainteacherapproval', 'mode', 'neq', PUBLICATION_MODE_UPLOAD); + + // Availability. + $mform->addElement('header', 'availability', get_string('availability', 'publication')); + $mform->setExpanded('availability', true); + + $name = get_string('allowsubmissionsfromdate', 'publication'); + $options = ['optional' => true]; + $mform->addElement('date_time_selector', 'allowsubmissionsfromdate', $name, $options); + $mform->addHelpButton('allowsubmissionsfromdate', 'allowsubmissionsfromdateh', 'publication'); + $mform->setDefault('allowsubmissionsfromdate', time()); + + $name = get_string('duedate', 'publication'); + $mform->addElement('date_time_selector', 'duedate', $name, ['optional' => true]); + + $mform->setDefault('duedate', time() + 7 * 24 * 3600); + + $mform->addElement('hidden', 'cutoffdate', false); + $mform->setType('cutoffdate', PARAM_BOOL); + + $mform->addElement('hidden', 'alwaysshowdescription', true); + $mform->setType('alwaysshowdescription', PARAM_BOOL); + + $mform->addElement('header', 'notifications', get_string('notifications', 'publication')); + + $name = get_string('notifyteacher', 'publication'); + $mform->addElement('selectyesno', 'notifyteacher', $name); + $mform->addHelpButton('notifyteacher', 'notifyteacher', 'publication'); + $mform->setDefault('notifyteacher', 0); + + $name = get_string('notifystudents', 'publication'); + $mform->addElement('selectyesno', 'notifystudents', $name); + $mform->addHelpButton('notifystudents', 'notifystudents', 'publication'); + $mform->setDefault('notifystudents', 0); + // Standard coursemodule elements. + $this->standard_coursemodule_elements(); + + // Buttons. + $this->add_action_buttons(); + } + + /** + * Perform minimal validation on the settings form + * + * @param array $data + * @param array $files + * @return string[] errors + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if ($data['allowsubmissionsfromdate'] && $data['duedate']) { + if ($data['allowsubmissionsfromdate'] > $data['duedate']) { + $errors['duedate'] = get_string('duedatevalidation', 'publication'); + } + } + if ($data['duedate'] && $data['cutoffdate']) { + if ($data['duedate'] > $data['cutoffdate']) { + $errors['cutoffdate'] = get_string('cutoffdatevalidation', 'publication'); + } + } + if ($data['allowsubmissionsfromdate'] && $data['cutoffdate']) { + if ($data['allowsubmissionsfromdate'] > $data['cutoffdate']) { + $errors['cutoffdate'] = get_string('cutoffdatefromdatevalidation', 'publication'); + } + } + + if ($data['mode'] == PUBLICATION_MODE_IMPORT) { + if ($data['importfrom'] == "0") { + $errors['importfrom'] = get_string('importfrom_err', 'publication'); + } + } + + return $errors; + } +} diff --git a/mod/publication/mod_publication_allfiles_form.php b/mod/publication/mod_publication_allfiles_form.php new file mode 100644 index 0000000..b48f607 --- /dev/null +++ b/mod/publication/mod_publication_allfiles_form.php @@ -0,0 +1,51 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains form class for approving publication files + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +/** + * Form for displaying and changing approval for publication files + * + * @package mod_publication + * @author Hannes Laimer + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_allfiles_form extends moodleform { + + /** + * Form definition. Abstract method - always override! + */ + protected function definition() { + $mform = $this->_form; + $generatedform = str_replace('<select', '<select onchange=\'this.form.submit()\'', $this->_customdata['form']); + $mform->addElement('html', $generatedform); + } +} \ No newline at end of file diff --git a/mod/publication/mod_publication_files_form.php b/mod/publication/mod_publication_files_form.php new file mode 100644 index 0000000..40bd290 --- /dev/null +++ b/mod/publication/mod_publication_files_form.php @@ -0,0 +1,143 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Contains form class for approving publication files + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +/** + * Form for displaying and changing approval for publication files + * + * @package mod_publication + * @author Hannes Laiemr + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_files_form extends moodleform { + /** + * Form definition method_exists + */ + public function definition() { + global $DB, $PAGE; + + $publication = &$this->_customdata['publication']; + + $mform = $this->_form; + + if ($publication->get_instance()->mode == PUBLICATION_MODE_UPLOAD) { + $table = new \mod_publication\local\filestable\upload($publication); + $headertext = get_string('myfiles', 'publication'); + if ($publication->get_instance()->obtainteacherapproval) { + $notice = get_string('notice_uploadrequireapproval', 'publication'); + } else { + $notice = get_string('notice_uploadnoapproval', 'publication'); + } + } else if ($DB->get_field('assign', 'teamsubmission', ['id' => $publication->get_instance()->importfrom])) { + $table = new \mod_publication\local\filestable\group($publication); + $headertext = get_string('mygroupfiles', 'publication'); + if ($publication->get_instance()->obtainstudentapproval) { + if ($publication->get_instance()->groupapproval == PUBLICATION_APPROVAL_ALL) { + $notice = get_string('notice_groupimportrequireallapproval', 'publication'); + } else { + $notice = get_string('notice_groupimportrequireoneapproval', 'publication'); + } + } else { + $notice = get_string('notice_importnoapproval', 'publication'); + } + } else { + $table = new \mod_publication\local\filestable\import($publication); + $headertext = get_string('myfiles', 'publication'); + if ($publication->get_instance()->obtainstudentapproval) { + $notice = get_string('notice_importrequireapproval', 'publication'); + } else { + $notice = get_string('notice_importnoapproval', 'publication'); + } + } + + $mform->addElement('header', 'myfiles', $headertext); + $mform->setExpanded('myfiles'); + + $PAGE->requires->js_call_amd('mod_publication/filesform', 'initializer', []); + $PAGE->requires->js_call_amd('mod_publication/alignrows', 'initializer', []); + + $noticehtml = html_writer::start_tag('div', ['class' => 'notice']); + $noticehtml .= get_string('notice', 'publication') . ' ' . $notice; + $noticehtml .= html_writer::end_tag('div'); + + $mform->addElement('html', $noticehtml); + + // Now we do all the table work and return 0 if there's no files to show! + if ($table->init()) { + $mform->addElement('html', \html_writer::table($table)); + } else { + $mform->addElement('static', 'nofiles', '', get_string('nofiles', 'publication')); + } + + // Display submit buttons if necessary. + if (!empty($table) && $table->changepossible()) { + if ($publication->is_open()) { + $buttonarray = []; + + $onclick = 'return confirm("' . get_string('savestudentapprovalwarning', 'publication') . '")'; + + $buttonarray[] = &$mform->createElement('submit', 'submitbutton', + get_string('savechanges'), ['onClick' => $onclick]); + $buttonarray[] = &$mform->createElement('reset', 'resetbutton', get_string('revert'), + ['class' => 'btn btn-secondary']); + + $mform->addGroup($buttonarray, 'submitgrp', '', [' '], false); + } else { + $mform->addElement('static', 'approvaltimeover', '', get_string('approval_timeover', 'publication')); + } + } + + if ($publication->get_instance()->mode == PUBLICATION_MODE_UPLOAD + && has_capability('mod/publication:upload', $publication->get_context())) { + if ($publication->is_open()) { + $buttonarray = []; + + if (empty($table)) { // This means, there are no files shown! + $label = get_string('add_uploads', 'publication'); + } else { + $label = get_string('edit_uploads', 'publication'); + } + + $buttonarray[] = &$mform->createElement('submit', 'gotoupload', $label); + $mform->addGroup($buttonarray, 'uploadgrp', '', [' '], false); + } else if (has_capability('mod/publication:upload', $publication->get_context())) { + $mform->addElement('static', 'edittimeover', '', get_string('edit_timeover', 'publication')); + } + } + + $mform->addElement('hidden', 'id', $publication->get_coursemodule()->id); + $mform->setType('id', PARAM_INT); + } +} diff --git a/mod/publication/mod_publication_grantextension_form.php b/mod/publication/mod_publication_grantextension_form.php new file mode 100644 index 0000000..81b2eff --- /dev/null +++ b/mod/publication/mod_publication_grantextension_form.php @@ -0,0 +1,117 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Form class for granting extensions for student's submissions + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +/** + * Form for granting extensions + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_grantextension_form extends moodleform { + /** @var object publication instance */ + private $instance; + + /** + * Form definition method + */ + public function definition() { + $publication = &$this->_customdata['publication']; + $this->instance = $publication->get_instance(); + $userids = &$this->_customdata['userids']; + + $mform = $this->_form; + + if ($publication->get_instance()->allowsubmissionsfromdate) { + $mform->addElement('static', 'fromdate', + get_string('allowsubmissionsfromdate', 'publication'), + userdate($publication->get_instance()->allowsubmissionsfromdate)); + } + + if ($publication->get_instance()->duedate) { + $mform->addElement('static', 'duedate', + get_string('duedate', 'publication'), userdate($publication->get_instance()->duedate)); + $finaldate = $publication->get_instance()->duedate; + } else { + $finaldate = 0; + } + + $mform->addElement('date_time_selector', 'extensionduedate', + get_string('extensionduedate', 'publication'), ['optional' => true]); + if ($finaldate) { + $mform->setDefault('extensionduedate', $finaldate); + } + + if (count($userids) == 1) { + $extensionduedate = $publication->user_extensionduedate($userids[0]); + if ($extensionduedate) { + $mform->setDefault('extensionduedate', $extensionduedate); + } + } + + $mform->addElement('hidden', 'id', $publication->get_coursemodule()->id); + $mform->setType('id', PARAM_INT); + + foreach ($userids as $idx => $userid) { + $mform->addElement('hidden', 'userids[' . $idx . ']', $userid); + $mform->setType('userids[' . $idx . ']', PARAM_INT); + } + + $this->add_action_buttons(true, get_string('save_changes', 'publication')); + } + + /** + * Perform validation on the extension form + * + * @param array $data + * @param array $files + * @return string[] errors + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + if ($this->instance->duedate && $data['extensionduedate']) { + if ($this->instance->duedate > $data['extensionduedate']) { + $errors['extensionduedate'] = get_string('extensionnotafterduedate', 'publication'); + } + } + if ($this->instance->allowsubmissionsfromdate && $data['extensionduedate']) { + if ($this->instance->allowsubmissionsfromdate > $data['extensionduedate']) { + $errors['extensionduedate'] = get_string('extensionnotafterfromdate', 'publication'); + } + } + + return $errors; + } +} diff --git a/mod/publication/onlinepreview.php b/mod/publication/onlinepreview.php new file mode 100644 index 0000000..7e6fe2d --- /dev/null +++ b/mod/publication/onlinepreview.php @@ -0,0 +1,52 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Displays a single mod_publication instance + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +$id = required_param('id', PARAM_INT); // Course Module ID. +$itemid = required_param('itemid', PARAM_INT); // Item-ID (group- or user-ID). +$itemname = optional_param('itemname', false, PARAM_TEXT); // Item-Name to save DB access! + +$url = new moodle_url('/mod/publication/onlinetextpreview.php', ['id' => $id, 'itemid' => $itemid]); +$cm = get_coursemodule_from_id('publication', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + +require_login($course, true, $cm); +$PAGE->set_url($url); + +$context = context_module::instance($cm->id); + +require_capability('mod/publication:view', $context); + +echo $OUTPUT->header(); + +echo $OUTPUT->heading(get_string('preview') . ' ' . get_string('onlinetextfilename', 'assignsubmission_onlinetext') . + ($itemname ? ' ' . strtolower(get_string('from')) . ' ' . $itemname : '')); + +echo publication::export_onlinetext_for_preview($itemid, $cm->instance, $context->id); + +echo $OUTPUT->footer(); diff --git a/mod/publication/phpunit.xml b/mod/publication/phpunit.xml new file mode 100644 index 0000000..754eb90 --- /dev/null +++ b/mod/publication/phpunit.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../lib/phpunit/phpunit.xsd" + bootstrap="../../lib/phpunit/bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + backupGlobals="false" + backupStaticAttributes="false" + stopOnError="false" + stopOnFailure="false" + stopOnIncomplete="false" + stopOnSkipped="false" + printerClass="Hint_ResultPrinter" + testSuiteLoaderClass="phpunit_autoloader" +> + + <php> + <!--<const name="PHPUNIT_LONGTEST" value="1"/> uncomment to execute also slow or otherwise expensive tests--> + <const name="PHPUNIT_SEQUENCE_START" value="163000"/> + + <!--Following constants instruct tests to fetch external test files from alternative location or skip tests if empty, clone https://github.com/moodlehq/moodle-exttests to local web server--> + <!--<const name="TEST_EXTERNAL_FILES_HTTP_URL" value="http://download.moodle.org/unittest"/> uncomment and alter to fetch external test files from alternative location--> + <!--<const name="TEST_EXTERNAL_FILES_HTTPS_URL" value="https://download.moodle.org/unittest"/> uncomment and alter to fetch external test files from alternative location--> + </php> + + + <testsuites> + <testsuite name="mod_publication_testsuite"> + <directory suffix="_test.php">.</directory> + </testsuite> + </testsuites> + <filter> + <whitelist processUncoveredFilesFromWhitelist="false"> + <directory suffix=".php">.</directory> + <exclude> + <directory suffix="_test.php">.</directory> + </exclude> + </whitelist> + </filter> + +</phpunit> diff --git a/mod/publication/pix/icon.png b/mod/publication/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e6c00c2e6569e4c6b91a798bcb0aaca98a8fa4b GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4kiW$hJ4Xg>lqjr*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l<VJ?tf(zOL-|7{!IegeRX8s$*bauJ?3t46!)9cIy4?7*~<w z=V#8We*f;S@9isUVYgeZnM8<otLWAWD*n)E`mv<RBfj>7{)zby<S*=BaHHwP>L<R5 zkGqwYNKBrw<)+!qy*Kav&VJv&)A;$14KLG<?s2KIFfd5}aprr$xtWe>ZPOE(;wG)U zxaphw&g&}$1R16@Y=1uYw)q|7#tAd|*}whsKiSKAJc5Bi>7M<D%kzG`X?8m-ebE0j ztE1<i=}Kt<Tzat~=dF4;oDNubuacknJNx6CDN`Pq2r(PfmmJ@3p{gF7oS>)eCnR@Q zUj0JGitOu4CKc>bXqjufw?4(sVd_Jb^lZ!1k<wC)0t;jq_!)kbnEJ{yRzwtp72S1p zInchJXZx;WrT_hx3N-T8{o7Yx8MVq_YjOCJ=bDR*lO+DIy?-;M(vu<Rs;!1rU(lv+ zIWkH!iZ@((W68m=>YY=N;ADj)3+KtohQ?ZJ-x{7M59F6@=zX|Zl`TP6DmhN$(}F8c ztPbtdxVa=WmCeU#XHey2)s(cwn@zs1+O(?jQAFU%lYt4k2R$!u2*~?>e(SQ@tdf}% z@9+7S`1sH2Gd&#Fg7UN9%E;UNJiWwq@BQB;WzQykh`l0e$QY^St;}Ast^RMFU+(>= z8L@k+9*SPH{qxpw=F$|-12;2mBQj+6?EZ1<kI!croBA&wtk-VMxZ<*OZR&<2N0N^l zRh)526ye=`CSvvB<;%1>c3x5CEAQEwD3Ml~xAyLxJ7+$}{49+OohH#)VY9#^AWVkk zu7zihVWEsPladOP!^Bjz7iEVGu0**9KB=51;AA1xctfD)vy!))_~+B{Id8W$FSz~s zYuBOz{reO1ChIU>{1Pqy|L>DXffDxSM#Hq!)YPmjFHV+D7oCu`W;fN&&eZ<*`t-ri zrJ61?4xWwnUd-HKQT=ys#rxj4{K+T0w(s1tg@wUF=gjv0TkM<)^Y%>BXu0?I<GUu2 zOK-pb-n)45-oq)szFk+SRcroje`jyy!H9R?f8Wgu*W9{m*Qy%-y3Z#=V=o@jcy#P> z?|oDAS<ib7oj61m>3uSP_<a4oJ=^!)yOnZ&_0JfQ4vnQb+i$N8^Ja9|thZ24{dWL= zb*2#W<gH~EGGeZZEjRAo%ec0{qdHJT=w{)G!a2zo>UDiD`z@cW!fE`xGERh5sV$L% zVXmJ#M^m82=}nt-S_KMa=JO>z)9AWnB3m0aIeuDDmVtza=JDTWdOH3y+?eYw;3;#V Rhk=2C!PC{xWt~$(69C8_)Rq7M literal 0 HcmV?d00001 diff --git a/mod/publication/pix/icon.svg b/mod/publication/pix/icon.svg new file mode 100644 index 0000000..a6f0309 --- /dev/null +++ b/mod/publication/pix/icon.svg @@ -0,0 +1,675 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="24" + height="24" + id="svg2" + version="1.1" + inkscape:version="0.48.4 r9939" + sodipodi:docname="studentfolder_icon.svg" + viewBox="0 0 36 36"> + <defs + id="defs4"> + <linearGradient + id="linearGradient4210"> + <stop + id="stop4212" + offset="0" + style="stop-color:#b3b3b3;stop-opacity:1"/> + <stop + id="stop4214" + offset="1" + style="stop-color:#666666;stop-opacity:1"/> + </linearGradient> + <linearGradient + id="linearGradient4371-2"> + <stop + style="stop-color:#ffe2af;stop-opacity:1;" + offset="0" + id="stop4373-3"/> + <stop + style="stop-color:#ec9b60;stop-opacity:1;" + offset="1" + id="stop4375-2"/> + </linearGradient> + <linearGradient + id="linearGradient4381-1"> + <stop + style="stop-color:#008abf;stop-opacity:1" + offset="0" + id="stop4383-6"/> + <stop + style="stop-color:#005581;stop-opacity:1" + offset="1" + id="stop4385-85"/> + </linearGradient> + <linearGradient + id="linearGradient4371-5"> + <stop + style="stop-color:#ffe2af;stop-opacity:1;" + offset="0" + id="stop4373-1"/> + <stop + style="stop-color:#ec9b60;stop-opacity:1;" + offset="1" + id="stop4375-7"/> + </linearGradient> + <linearGradient + id="linearGradient3822-1"> + <stop + id="stop3824-5" + offset="0" + style="stop-color:#feba12;stop-opacity:1"/> + <stop + id="stop3826-2" + offset="1" + style="stop-color:#f57c20;stop-opacity:1;"/> + </linearGradient> + <linearGradient + id="linearGradient4371"> + <stop + style="stop-color:#ffe2af;stop-opacity:1;" + offset="0" + id="stop4373"/> + <stop + style="stop-color:#ec9b60;stop-opacity:1;" + offset="1" + id="stop4375"/> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3822" + id="radialGradient3840" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.79485123,0,0,0.7119197,758.07506,215.47187)" + cx="257.12946" + cy="519.69897" + fx="257.12946" + fy="519.69897" + r="123.1"/> + <linearGradient + id="linearGradient3822"> + <stop + id="stop3824" + offset="0" + style="stop-color:#feba12;stop-opacity:1"/> + <stop + id="stop3826" + offset="1" + style="stop-color:#f57c20;stop-opacity:1;"/> + </linearGradient> + <linearGradient + id="SVGID_6_" + gradientUnits="userSpaceOnUse" + x1="11.9663" + y1="6.9565001" + x2="11.9663" + y2="18.0005"> + <stop + offset="0" + style="stop-color:#95BFF8" + id="stop3221"/> + <stop + offset="0.5569" + style="stop-color:#84ADEF" + id="stop3223"/> + <stop + offset="1" + style="stop-color:#7CA4EB" + id="stop3225"/> + <a:midPointStop + offset="0" + style="stop-color:#95BFF8"/> + <a:midPointStop + offset="0.4" + style="stop-color:#95BFF8"/> + <a:midPointStop + offset="1" + style="stop-color:#7CA4EB"/> + </linearGradient> + <linearGradient + id="SVGID_5_" + gradientUnits="userSpaceOnUse" + x1="11.9673" + y1="5.9541001" + x2="11.9673" + y2="19.0005" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)"> + <stop + offset="0" + style="stop-color:#BBE0F7" + id="stop3214"/> + <stop + offset="1" + style="stop-color:#82B4FB" + id="stop3216"/> + <a:midPointStop + offset="0" + style="stop-color:#BBE0F7"/> + <a:midPointStop + offset="0.5" + style="stop-color:#BBE0F7"/> + <a:midPointStop + offset="1" + style="stop-color:#82B4FB"/> + </linearGradient> + <linearGradient + id="SVGID_4_" + gradientUnits="userSpaceOnUse" + x1="11.936" + y1="5" + x2="11.936" + y2="20.0005" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)"> + <stop + offset="0" + style="stop-color:#76A1F0" + id="stop3207"/> + <stop + offset="1" + style="stop-color:#6B90D5" + id="stop3209"/> + <a:midPointStop + offset="0" + style="stop-color:#76A1F0"/> + <a:midPointStop + offset="0.5" + style="stop-color:#76A1F0"/> + <a:midPointStop + offset="1" + style="stop-color:#6B90D5"/> + </linearGradient> + <linearGradient + id="SVGID_3_" + gradientUnits="userSpaceOnUse" + x1="11.9351" + y1="1.9917001" + x2="11.9351" + y2="18.0005" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)"> + <stop + offset="0" + style="stop-color:#95BFF8" + id="stop3198"/> + <stop + offset="0.5569" + style="stop-color:#84ADEF" + id="stop3200"/> + <stop + offset="1" + style="stop-color:#7CA4EB" + id="stop3202"/> + <a:midPointStop + offset="0" + style="stop-color:#95BFF8"/> + <a:midPointStop + offset="0.4" + style="stop-color:#95BFF8"/> + <a:midPointStop + offset="1" + style="stop-color:#7CA4EB"/> + </linearGradient> + <linearGradient + id="SVGID_2_" + gradientUnits="userSpaceOnUse" + x1="11.9351" + y1="0.98879999" + x2="11.9351" + y2="19.0005" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)"> + <stop + offset="0" + style="stop-color:#BBE0F7" + id="stop3191"/> + <stop + offset="1" + style="stop-color:#82B4FB" + id="stop3193"/> + <a:midPointStop + offset="0" + style="stop-color:#BBE0F7"/> + <a:midPointStop + offset="0.5" + style="stop-color:#BBE0F7"/> + <a:midPointStop + offset="1" + style="stop-color:#82B4FB"/> + </linearGradient> + <linearGradient + id="SVGID_1_" + gradientUnits="userSpaceOnUse" + x1="11.9351" + y1="0" + x2="11.9351" + y2="20.0005"> + <stop + offset="0" + style="stop-color:#76A1F0" + id="stop3184"/> + <stop + offset="1" + style="stop-color:#6B90D5" + id="stop3186"/> + <a:midPointStop + offset="0" + style="stop-color:#76A1F0"/> + <a:midPointStop + offset="0.5" + style="stop-color:#76A1F0"/> + <a:midPointStop + offset="1" + style="stop-color:#6B90D5"/> + </linearGradient> + <linearGradient + y2="20.0005" + x2="11.9351" + y1="0" + x1="11.9351" + gradientUnits="userSpaceOnUse" + id="linearGradient3269" + xlink:href="#SVGID_1_" + inkscape:collect="always" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)"/> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3822-1-8" + id="radialGradient3334-4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.04268396,0,0,0.03823048,-7.7254699,1019.7434)" + cx="257.12946" + cy="519.69897" + fx="257.12946" + fy="519.69897" + r="123.1"/> + <linearGradient + id="linearGradient3822-1-8"> + <stop + id="stop3824-5-8" + offset="0" + style="stop-color:#b3b3b3;stop-opacity:1"/> + <stop + id="stop3826-2-2" + offset="1" + style="stop-color:#666666;stop-opacity:1"/> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4371-5-5" + id="radialGradient3336-4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + cx="285.71429" + cy="322.15512" + fx="285.71429" + fy="322.15512" + r="97.385712"/> + <linearGradient + id="linearGradient4371-5-5"> + <stop + style="stop-color:#ffe2af;stop-opacity:1;" + offset="0" + id="stop4373-1-5"/> + <stop + style="stop-color:#ec9b60;stop-opacity:1;" + offset="1" + id="stop4375-7-1"/> + </linearGradient> + <radialGradient + r="97.385712" + fy="322.15512" + fx="285.71429" + cy="322.15512" + cx="285.71429" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + gradientUnits="userSpaceOnUse" + id="radialGradient3359" + xlink:href="#linearGradient4210" + inkscape:collect="always"/> + <linearGradient + id="linearGradient3822-1-8-2"> + <stop + id="stop3824-5-8-7" + offset="0" + style="stop-color:#feba12;stop-opacity:1"/> + <stop + id="stop3826-2-2-6" + offset="1" + style="stop-color:#f57c20;stop-opacity:1;"/> + </linearGradient> + <radialGradient + r="97.385712" + fy="322.15512" + fx="285.71429" + cy="322.15512" + cx="285.71429" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + gradientUnits="userSpaceOnUse" + id="radialGradient3359-1" + xlink:href="#linearGradient4371-5-5-4" + inkscape:collect="always"/> + <linearGradient + id="linearGradient4371-5-5-4"> + <stop + style="stop-color:#ffe2af;stop-opacity:1;" + offset="0" + id="stop4373-1-5-2"/> + <stop + style="stop-color:#ec9b60;stop-opacity:1;" + offset="1" + id="stop4375-7-1-3"/> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3822-1-8" + id="radialGradient4222" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.04268396,0,0,0.03823048,-7.7254699,1019.7434)" + cx="257.12946" + cy="519.69897" + fx="257.12946" + fy="519.69897" + r="123.1"/> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4210" + id="radialGradient4224" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + cx="285.71429" + cy="322.15512" + fx="285.71429" + fy="322.15512" + r="97.385712"/> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3822-1-8-8" + id="radialGradient4222-6" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.04268396,0,0,0.03823048,-7.7254699,1019.7434)" + cx="257.12946" + cy="519.69897" + fx="257.12946" + fy="519.69897" + r="123.1"/> + <linearGradient + id="linearGradient3822-1-8-8"> + <stop + id="stop3824-5-8-5" + offset="0" + style="stop-color:#b3b3b3;stop-opacity:1"/> + <stop + id="stop3826-2-2-7" + offset="1" + style="stop-color:#666666;stop-opacity:1"/> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4210-1" + id="radialGradient4224-6" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + cx="285.71429" + cy="322.15512" + fx="285.71429" + fy="322.15512" + r="97.385712"/> + <linearGradient + id="linearGradient4210-1"> + <stop + id="stop4212-8" + offset="0" + style="stop-color:#b3b3b3;stop-opacity:1"/> + <stop + id="stop4214-9" + offset="1" + style="stop-color:#666666;stop-opacity:1"/> + </linearGradient> + <radialGradient + r="97.385712" + fy="322.15512" + fx="285.71429" + cy="322.15512" + cx="285.71429" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + gradientUnits="userSpaceOnUse" + id="radialGradient4247" + xlink:href="#linearGradient4210-1" + inkscape:collect="always"/> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3822-1-8-8-4" + id="radialGradient4222-6-5" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.04268396,0,0,0.03823048,-7.7254699,1019.7434)" + cx="257.12946" + cy="519.69897" + fx="257.12946" + fy="519.69897" + r="123.1"/> + <linearGradient + id="linearGradient3822-1-8-8-4"> + <stop + id="stop3824-5-8-5-3" + offset="0" + style="stop-color:#b3b3b3;stop-opacity:1"/> + <stop + id="stop3826-2-2-7-1" + offset="1" + style="stop-color:#666666;stop-opacity:1"/> + </linearGradient> + <radialGradient + r="97.385712" + fy="322.15512" + fx="285.71429" + cy="322.15512" + cx="285.71429" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + gradientUnits="userSpaceOnUse" + id="radialGradient4247-2" + xlink:href="#linearGradient4210-1-3" + inkscape:collect="always"/> + <linearGradient + id="linearGradient4210-1-3"> + <stop + id="stop4212-8-3" + offset="0" + style="stop-color:#b3b3b3;stop-opacity:1"/> + <stop + id="stop4214-9-4" + offset="1" + style="stop-color:#666666;stop-opacity:1"/> + </linearGradient> + <radialGradient + r="97.385712" + fy="322.15512" + fx="285.71429" + cy="322.15512" + cx="285.71429" + gradientTransform="matrix(1,0,0,1.0293385,0,-10.673061)" + gradientUnits="userSpaceOnUse" + id="radialGradient4299" + xlink:href="#linearGradient4210-1-3" + inkscape:collect="always"/> + <linearGradient + inkscape:collect="always" + xlink:href="#SVGID_1_" + id="linearGradient4350" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)" + x1="11.9351" + y1="0" + x2="11.9351" + y2="20.0005"/> + <linearGradient + inkscape:collect="always" + xlink:href="#SVGID_2_" + id="linearGradient4352" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)" + x1="11.9351" + y1="0.98879999" + x2="11.9351" + y2="19.0005"/> + <linearGradient + inkscape:collect="always" + xlink:href="#SVGID_3_" + id="linearGradient4354" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)" + x1="11.9351" + y1="1.9917001" + x2="11.9351" + y2="18.0005"/> + <linearGradient + inkscape:collect="always" + xlink:href="#SVGID_4_" + id="linearGradient4356" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)" + x1="11.936" + y1="5" + x2="11.936" + y2="20.0005"/> + <linearGradient + inkscape:collect="always" + xlink:href="#SVGID_5_" + id="linearGradient4358" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)" + x1="11.9673" + y1="5.9541001" + x2="11.9673" + y2="19.0005"/> + <linearGradient + inkscape:collect="always" + xlink:href="#SVGID_6_" + id="linearGradient4360" + gradientUnits="userSpaceOnUse" + x1="11.9663" + y1="6.9565001" + x2="11.9663" + y2="18.0005"/> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="14.008079" + inkscape:cx="13.017718" + inkscape:cy="17.442811" + inkscape:document-units="px" + inkscape:current-layer="g3330-7" + showgrid="false" + inkscape:window-width="1680" + inkscape:window-height="998" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1"/> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1020.3622)"> + <g + id="g4330" + transform="matrix(0.95363144,0,0,0.95971302,-0.05743944,41.579795)"> + <path + id="path3188" + d="m 28.61865,1046.7974 c 0,0.6203 -0.646528,1.2405 -1.293054,1.2405 H 4.0506571 c -0.646526,0 -1.2930523,-0.6202 -1.2930523,-1.2405 v -22.2026 c 0,-0.6202 0.6465263,-1.3645 1.2930523,-1.3645 h 5.1722091 c 0.6465256,0 1.8102718,0.3722 2.3274928,0.7443 l 1.163747,0.8683 c 0.646525,0.4962 1.680968,0.8683 2.327494,0.8683 l 12.283996,0 c 0.646526,0 1.293054,0.6202 1.293054,1.2403 v 19.8459 z" + style="fill:url(#linearGradient4350);overflow:visible" + inkscape:connector-curvature="0"/> + <path + id="path3195" + d="m 4.0506571,1046.7974 v -22.2026 c 0,0 0.1293054,-0.124 0.1293054,-0.124 l 5.0429037,0 c 0.3879146,0 1.1637458,0.248 1.5516618,0.4961 l 1.163746,0.9924 c 0.905138,0.4961 2.19819,0.9922 3.103326,0.9922 l 12.283996,0 v 19.8459 H 4.0506571 z" + style="fill:url(#linearGradient4352);overflow:visible" + inkscape:connector-curvature="0"/> + <path + id="path3204" + d="m 5.3437093,1045.5571 v -19.8459 l 3.8791569,0 c 0.1293047,0 0.6465256,0.124 0.7758293,0.2481 l 1.1637475,0.9922 c 1.034442,0.7443 2.71541,1.2404 3.879157,1.2404 l 10.990944,0 v 17.3652 H 5.3437093 z" + style="fill:url(#linearGradient4354);overflow:visible" + inkscape:connector-curvature="0"/> + <path + id="path3211" + d="m 30.041007,1046.7974 c 0,0.6203 -0.646527,1.2405 -1.422357,1.2405 H 2.7576048 c -0.646526,0 -1.2930522,-0.6202 -1.4223574,-1.2405 l -1.03444185,-16.1248 c 0,-0.6201 0.51722092,-1.2403 1.16374705,-1.2403 H 29.911702 c 0.646526,0 1.293052,0.6202 1.163747,1.2403 l -1.034442,16.1248 z" + style="fill:url(#linearGradient4356);overflow:visible" + inkscape:connector-curvature="0"/> + <path + id="path3218" + d="m 2.7576048,1046.7974 c 0,0 -0.1293052,-0.124 -0.1293052,-0.124 l -1.0344417,-16.0008 28.3178441,0 -1.163748,16.0008 c 0,0 0,0.124 -0.129304,0.124 H 2.7576048 z" + style="fill:url(#linearGradient4358);overflow:visible" + inkscape:connector-curvature="0"/> + <polygon + transform="matrix(1.2930523,0,0,1.2403694,0.30080555,1023.2303)" + id="polygon3227" + points="21.9,7 21.1,18 2.8,18 2.1,7 " + style="fill:url(#linearGradient4360);overflow:visible"/> + </g> + <g + id="g3330-7" + transform="matrix(0.94051911,0,0,0.91922984,12.786824,90.591095)"> + <g + id="g4216-2" + transform="matrix(1.0000077,0,0,0.94629377,9.6947608,57.061851)"> + <path + style="fill:url(#radialGradient4222-6);fill-opacity:1;stroke:#000000;stroke-width:0.26464048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" + d="m 5.2010983,1036.0134 c -2.8288434,0 -5.12207417,3.3582 -5.12207417,7.5004 0,0.5659 0.04427,1.1173 0.125385,1.6474 l 9.99471487,0 c 0.0811,-0.5301 0.124049,-1.0815 0.124049,-1.6474 0,-4.1422 -2.2932307,-7.5004 -5.1220747,-7.5004 z" + id="path3856-2-7-1-7" + inkscape:connector-curvature="0"/> + <path + transform="matrix(0.04268396,0,0,0.04268396,-7.7254699,1017.3152)" + sodipodi:type="arc" + style="fill:url(#radialGradient4247);fill-opacity:1;stroke:#000000;stroke-width:6.19999981;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" + id="path3861-4-6-1-9" + sodipodi:cx="302.85715" + sodipodi:cy="363.79074" + sodipodi:rx="94.285713" + sodipodi:ry="97.14286" + d="m 397.14286,363.79074 c 0,53.65052 -42.21315,97.14286 -94.28571,97.14286 -52.07256,0 -94.28572,-43.49234 -94.28572,-97.14286 0,-53.65052 42.21316,-97.14286 94.28572,-97.14286 52.07256,0 94.28571,43.49234 94.28571,97.14286 z"/> + </g> + <g + id="g4216-2-1" + transform="matrix(1.0000077,0,0,0.94629377,-2.8949894,57.068115)"> + <path + style="fill:url(#radialGradient4222-6-5);fill-opacity:1;stroke:#000000;stroke-width:0.26464048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" + d="m 5.2010983,1036.0134 c -2.8288434,0 -5.12207417,3.3582 -5.12207417,7.5004 0,0.5659 0.04427,1.1173 0.125385,1.6474 l 9.99471487,0 c 0.0811,-0.5301 0.124049,-1.0815 0.124049,-1.6474 0,-4.1422 -2.2932307,-7.5004 -5.1220747,-7.5004 z" + id="path3856-2-7-1-7-1" + inkscape:connector-curvature="0"/> + <path + transform="matrix(0.04268396,0,0,0.04268396,-7.7254699,1017.3152)" + sodipodi:type="arc" + style="fill:url(#radialGradient4299);fill-opacity:1;stroke:#000000;stroke-width:6.19999981;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" + id="path3861-4-6-1-9-3" + sodipodi:cx="302.85715" + sodipodi:cy="363.79074" + sodipodi:rx="94.285713" + sodipodi:ry="97.14286" + d="m 397.14286,363.79074 c 0,53.65052 -42.21315,97.14286 -94.28571,97.14286 -52.07256,0 -94.28572,-43.49234 -94.28572,-97.14286 0,-53.65052 42.21316,-97.14286 94.28572,-97.14286 52.07256,0 94.28571,43.49234 94.28571,97.14286 z"/> + </g> + </g> + </g> +</svg> diff --git a/mod/publication/pix/questionmark.png b/mod/publication/pix/questionmark.png new file mode 100644 index 0000000000000000000000000000000000000000..4659041c3408b04415a2ab56fe752a0c45b23adc GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l<H3=9nHC7!;n?DrViMO9V*`Yt}pz`$_J)5S5w;`G%?8?%@k z1={w{3=P=gEqa3a2S;VY6{jd~?clpJw_FQz^nA|EF|DkUAzOOB;*w3mlVnc5TVnYz zqIkK2=AYGfZ{B?WKRwbSSV(-|@;g>b`wQ9cFw8%2Ea6$uGk3;sO;dNWK0dkBr_0oq zL1Hz--u?q$XD^pz;%D9d@FL5`vhy)pU*{B8EYs{g#j-J~Z_9?*<Ikru*r#fo(kTD` zY2NY4?muQ;2u;;&nlX{};l61fq`qH0#qdhPJ1^nTgY~DcDof1$`|0lH2m3>Qu^r`C k^{kvfZ??nEioZ;({FdKs7nTV#FfcH9y85}Sb4q9e05}nd*Z=?k literal 0 HcmV?d00001 diff --git a/mod/publication/pix/questionmark.svg b/mod/publication/pix/questionmark.svg new file mode 100644 index 0000000..72e177c --- /dev/null +++ b/mod/publication/pix/questionmark.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7.58 13.25" width="16" height="16"> + <defs> + <style> + .a { + fill: #ff9100; + stroke: #ff9100; + stroke-linejoin: round; + stroke-width: 0.25px; + } + </style> + </defs> + <title>questionmark</title> + <path class="a" + d="M12.53,4.71a3.26,3.26,0,0,1-.13,1,2.56,2.56,0,0,1-.35.72,2.68,2.68,0,0,1-.51.54,7.25,7.25,0,0,1-.6.43l-0.7.44a3.72,3.72,0,0,0-.63.47,2.16,2.16,0,0,0-.46.62A1.92,1.92,0,0,0,9,9.77V9.92H6.74q0-.09,0-0.21c0-.09,0-0.15,0-0.21a3.27,3.27,0,0,1,.2-1.21,2.93,2.93,0,0,1,.5-0.84,3.55,3.55,0,0,1,.68-0.61l0.73-.47L9.3,6.05a2.25,2.25,0,0,0,.4-0.35A1.55,1.55,0,0,0,10,5.29a1.29,1.29,0,0,0,.1-0.52,1.24,1.24,0,0,0-.49-1A2.48,2.48,0,0,0,8,3.35a6.44,6.44,0,0,0-1.52.18A8.15,8.15,0,0,0,5.2,4V2.12a5.9,5.9,0,0,1,.68-0.25q0.38-.11.78-0.2t0.83-.13a7.43,7.43,0,0,1,.83,0,6.46,6.46,0,0,1,1.78.22,3.83,3.83,0,0,1,1.32.64,2.83,2.83,0,0,1,.82,1A3,3,0,0,1,12.53,4.71ZM6.56,12h2.7V14.5H6.56V12Z" + transform="translate(-5.08 -1.38)"/> +</svg> diff --git a/mod/publication/settings.php b/mod/publication/settings.php new file mode 100644 index 0000000..121d796 --- /dev/null +++ b/mod/publication/settings.php @@ -0,0 +1,48 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Settings definitions for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +global $CFG; + +if ($ADMIN->fulltree) { + + $settings->add(new admin_setting_configcheckbox('publication/obtainstudentapproval', + get_string('obtainstudentapproval', 'publication'), get_string('configobtainstudentapproval', 'publication'), 1)); + + $settings->add(new admin_setting_configcheckbox('publication/obtainteacherapproval', + get_string('obtainteacherapproval', 'publication'), get_string('configobtainteacherapproval', 'publication'), 1)); + + $settings->add(new admin_setting_configtext('publication/maxfiles', get_string('maxfiles', 'publication'), + get_string('configmaxfiles', 'publication'), 5, PARAM_INT)); + + if (isset($CFG->maxbytes)) { + $settings->add(new admin_setting_configselect('publication/maxbytes', get_string('maxbytes', 'publication'), + get_string('configmaxbytes', 'publication'), 5242880, get_max_upload_sizes($CFG->maxbytes))); + } + $settings->add(new admin_setting_configcheckbox('publication/autoimport', + get_string('autoimport', 'publication'), get_string('configautoimport', 'publication'), 0)); +} diff --git a/mod/publication/styles.css b/mod/publication/styles.css new file mode 100644 index 0000000..0e79151 --- /dev/null +++ b/mod/publication/styles.css @@ -0,0 +1,243 @@ +/** +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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 +// If not, see <http://www.gnu.org/licenses/>. +*/ +/** + * Styles for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/* General Styles */ + +#page-mod-publication-view div.availability { + margin-bottom: 10px; + margin-left: 10px; +} + +#page-mod-publication-view div.availability div { + clear: both; +} + +#page-mod-publication-view div.availability span { + display: block; + float: left; + min-width: 170px; +} + +#page-mod-publication-view div.assignurl { + margin-bottom: 10px; + margin-left: 10px; +} + +#page-mod-publication-view div.assignurl div.singlebutton, +#page-mod-publication-view div.assignurl div.singlebutton form, +#page-mod-publication-view div.assignurl div.singlebutton form div { + display: inline; +} + +#page-mod-publication-view div.assignurl div.singlebutton form div { + padding-left: 10px; +} + +#page-mod-publication-view fieldset#id_myfiles table { + margin-left: 8px; +} + +#page-mod-publication-view fieldset#id_myfiles table td { + padding: 1px 3px 1px 1px; +} + +/* scroll table if page is too small */ +#page-mod-publication-view div#id_allfiles { + border: 1px solid #d0d3ca; + margin: 10px; + padding: 10px; +} + +#page-mod-publication-view div#id_allfiles div.legend { + background-color: #fff; + font-weight: bold; + margin-top: -20px; + padding: 2px 5px; + position: absolute; +} + +.path-mod-publication table.publications .onlinetextpreview { + padding-left: 0.5rem; +} + +/* .. ie */ +.ie#page-mod-publication-view fieldset#id_allfiles { + display: block; +} + +/* end scroll */ + +#page-mod-publication-view fieldset#id_allfiles div div.no-overflow { + margin-bottom: 1em; + padding-bottom: 0; +} + +#page-mod-publication-view fieldset#id_allfiles table#attempts { + margin-bottom: 0; +} + +#page-mod-publication-view fieldset#id_allfiles table#attempts span.timemodified { + font-size: 0.9em; + font-style: italic; +} + +#page-mod-publication-view fieldset#id_allfiles { + padding-top: 4px; +} + +#page-mod-publication-view div.mod-publication-download-link { + text-align: right; +} + +#page-mod-publication-view div#id_allfiles table#attempts th.c0, +#page-mod-publication-view div#id_allfiles table#attempts td.c0 { + text-align: center; +} + +#page-mod-publication-view table.filetable, +#page-mod-publication-view table.filetable td, +#page-mod-publication-view table.filetable tr, +#page-mod-publication-view table.statustable, +#page-mod-publication-view table.statustable td, +#page-mod-publication-view table.statustable tr, +#page-mod-publication-view table.permissionstable, +#page-mod-publication-view table.permissionstable td, +#page-mod-publication-view table.permissionstable tr { + border-width: 0; + padding: 1px 3px 1px 1px; + white-space: nowrap; +} + +#page-mod-publication-view table.statustable tbody td, +#page-mod-publication-view table.permissionstable tbody td, +#page-mod-publication-view table.filetable tbody td { + background-color: transparent; +} + +#page-mod-publication-view table.statustable td, +#page-mod-publication-view table.filetable td { + height: 30px; +} + +#page-mod-publication-view table.statustable { + text-align: center; + width: 100%; +} + +#page-mod-publication-view table.permissionstable { + text-align: left; + width: 100%; +} + +#page-mod-publication-view table.permissionstable select { + margin-bottom: 0; +} + +#page-mod-publication-view div.withselection { + text-align: center; +} + +#page-mod-publication-view div.withselection { + text-align: center; +} + +#page-mod-publication-view div.withselection select { + margin-left: 4px; + margin-right: 4px; +} + +#page-mod-publication-view .visibilitysaver { + float: right; +} + +#page-mod-publication-view #dates { + border-width: 0; +} + +#page-mod-publication-view #dates .c0 { + text-align: left; +} + +#page-mod-publication-view #dates td { + padding-bottom: 0; + padding-top: 0; +} + +#page-mod-publication-upload .fstatic, +#page-mod-publication-upload .fstaticlabel { + font-style: italic; +} + +#page-mod-publication-view fieldset#id_myfiles .notice { + font-style: italic; + margin-bottom: 6px; + padding: 8px; +} + +#page-mod-publication-mod select[name=importfrom] option[disabled] { + color: #999; +} + +.path-mod-publication .publications table tr:nth-of-type(2n+1), +.path-mod-publication .publications table tr:hover { + background-color: transparent; +} + +.path-mod-publication .statustable .approvaldetails { + display: none; +} + +.path-mod-publication .statustable .approvaldetails img { + cursor: pointer; +} + +.path-mod-publication .publicationoverlay { + max-height: 90%; + overflow-y: scroll; +} + +.path-mod-publication .publicationoverlay table { + margin-left: auto; + margin-right: auto; +} + +.path-mod-publication .publicationoverlay table td, +.path-mod-publication .publicationoverlay table th { + padding: 5px; +} + +.path-mod-publication .publicationoverlay table td.noentries { + text-align: center; +} + +.path-mod-publication .publicationoverlay table td.time { + text-align: right; +} + +.path-mod-publication .modal .modal-body { + max-width: 100%; + overflow: auto; +} diff --git a/mod/publication/templates/approvaltooltip.mustache b/mod/publication/templates/approvaltooltip.mustache new file mode 100644 index 0000000..6297137 --- /dev/null +++ b/mod/publication/templates/approvaltooltip.mustache @@ -0,0 +1,106 @@ +{{! + This file is part of mod_publication for Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template mod_publication/approvaltooltip + + Publication's approvaltooltip template. + + The purpose of this template is to the contents of the approval tool-tip for group approval. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * id ID of related element + * approval Array with name & time pairs. + * rejected Array with name & time paris. + * pending Array with name & time pairs. + + Example context (json): + { + "id": 7, + "status": { "approved": false, + "rejected": true, + "pending": false }, + "mode": "at least <strong>ONE</strong> member has to approve", + "approval": [ + { "name": "John Doe", "time": "04.11.2016 15:37" }, + { "name": "Jane Doe", "time": "01.11.2016 09:42" } + ], + "rejected": [ + { "name": "Jessica Jones", "time": "07.10.2016 10:46" }, + { "name": "Luke Cage", "time": "13.10.2016 09:42" } + ], + "pending": [ + { "name": "Barry Allen", "time": "-" }, + { "name": "Bruce Banner", "time": "-" } + ] + } + +}} +<div class="publicationoverlay" role="tooltip"> + {{#status}} + <div><strong>{{# str }} status, mod_publication {{/ str }} — {{! + }}{{# str }} total, mod_publication {{/ str }}:</strong> + {{#approved}}{{# pix }} i/valid, core, {{# str }} approved, mod_publication {{/ str }} {{/ pix }}{{! + }}({{# str }} approved, mod_publication {{/ str }}){{/approved}} + {{#rejected}}{{# pix }} i/invalid, core, {{# str }} rejected, mod_publication {{/ str }} {{/ pix }}{{! + }}({{# str }} rejected, mod_publication {{/ str }}){{/rejected}} + {{#pending}}{{# pix }} questionmark, mod_publication, {{# str }} pending, mod_publication {{/ str }} {{/ pix }}{{! + }}({{# str }} pending, mod_publication {{/ str }}){{/pending}} + </div> + {{/status}} + <div> + <div><strong>{{# str }} status, mod_publication {{/ str }} — {{! + }}{{# str }} details, mod_publication {{/ str }}:</strong></div> + <table> + {{#approved}} + <tr> + <td>{{name}}</td> + <td>{{# pix }} i/valid, core, {{# str }} approved, mod_publication {{/ str }} {{/ pix }}{{! + }}({{# str }} approved, mod_publication {{/ str }}) + </td> + <td class="time">{{time}}</td> + </tr> + {{/approved}} + {{#rejected}} + <tr> + <td>{{name}}</td> + <td>{{# pix }} i/invalid, core, {{# str }} rejected, mod_publication {{/ str }} {{/ pix }}{{! + }}({{# str }} rejected, mod_publication {{/ str }}) + </td> + <td class="time">{{time}}</td> + </tr> + {{/rejected}} + {{#pending}} + <tr> + <td>{{name}}</td> + <td>{{# pix }} questionmark, mod_publication, {{# str }} pending, mod_publication {{/ str }} {{/ pix }}{{! + }}({{# str }} pending, mod_publication {{/ str }}) + </td> + <td class="time">{{time}}</td> + </tr> + {{/pending}} + </table> + </div> + {{#mode}} + <div><strong>{{# str }} mode, mod_publication {{/ str }}:</strong> {{{mode}}}</div> + {{/mode}} +</div> diff --git a/mod/publication/tests/allfilestable_test.php b/mod/publication/tests/allfilestable_test.php new file mode 100644 index 0000000..f77f74f --- /dev/null +++ b/mod/publication/tests/allfilestable_test.php @@ -0,0 +1,140 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Unit tests for mod_publication's allfilestable classes. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2017 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_publication\local\tests; + +use Exception; +use mod_assign_generator; +use coding_exception; + +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); // It must be included from a Moodle page! +} + +// Make sure the code being tested is accessible. +global $CFG; +require_once($CFG->dirroot . '/mod/publication/locallib.php'); // Include the code to test! + +/** + * This class contains the test cases for the formular validation. + * + * @package mod_publication + * @author Philipp Hager + * @copyright 2017 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class allfilestable_testcase extends base { + /* + * The base test class already contains a setUp-method setting up a course including users and groups. + */ + + /** + * Tests the basic creation of a publication instance with standardized settings! + */ + public function test_create_instance() { + self::assertNotEmpty($this->create_instance()); + } + + /** + * Tests if we can create an allfilestable without uploaded files + * + * @throws Exception + */ + public function test_allfilestable_upload() { + // Setup fixture! + $publication = $this->create_instance([ + 'mode' => PUBLICATION_MODE_UPLOAD, + 'obtainteacherapproval' => 0, + 'obtainstudentapproval' => 0 + ]); + + // Exercise SUT! + ob_start(); + $publication->display_allfilesform(); + $output = ob_get_contents(); + ob_end_clean(); + self::assertFalse(strpos($output, "Nothing to display")); + + // Teardown fixture! + $publication = null; + } + + /** + * Tests if we can create an allfilestable without imported files + * + * @throws coding_exception + */ + public function test_allfilestable_import() { + // Setup fixture! + /** @var mod_assign_generator $generator */ + $generator = self::getDataGenerator()->get_plugin_generator('mod_assign'); + $params['course'] = $this->course->id; + $assign = $generator->create_instance($params); + $publication = $this->create_instance([ + 'mode' => PUBLICATION_MODE_IMPORT, + 'importfrom' => $assign->id, + 'obtainteacherapproval' => 0, + 'obtainstudentapproval' => 0 + ]); + + // Exercise SUT! + ob_start(); + $publication->display_allfilesform(); + $output = ob_get_contents(); + ob_end_clean(); + self::assertFalse(strpos($output, "Nothing to display")); + + // Teardown fixture! + $publication = null; + } + + /** + * Tests if we can create an allfilestable without imported group-files + * + * @throws coding_exception + */ + public function test_allfilestable_group() { + // Setup fixture! + /** @var \mod_assign_generator $generator */ + $generator = self::getDataGenerator()->get_plugin_generator('mod_assign'); + $params['course'] = $this->course->id; + $params['teamsubmission'] = 1; + $params['preventsubmissionnotingroup'] = 0; + $assign = $generator->create_instance($params); + $publication = $this->create_instance([ + 'mode' => PUBLICATION_MODE_IMPORT, + 'importfrom' => $assign->id + ]); + + // Exercise SUT! + ob_start(); + $publication->display_allfilesform(); + $output = ob_get_contents(); + ob_end_clean(); + self::assertFalse(strpos($output, "Nothing to display")); + + // Teardown fixture! + $publication = null; + } +} diff --git a/mod/publication/tests/generator/lib.php b/mod/publication/tests/generator/lib.php new file mode 100644 index 0000000..f184c68 --- /dev/null +++ b/mod/publication/tests/generator/lib.php @@ -0,0 +1,82 @@ +<?php +// This file is part of mod_publication for 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/>. + +/** + * Generator file for mod_publication's PHPUnit tests + * + * @package mod_publication + * @category test + * @author Philipp Hager + * @copyright 2017 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * publication module data generator class + * + * @package mod_publication + * @category test + * @author Philipp Hager + * @copyright 2017 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_generator extends testing_module_generator { + + /** + * Generator method creating a mod_publication instance. + * + * @param array|stdClass $record (optional) Named array containing instance settings + * @param array $options (optional) general options for course module. Can be merged into $record + * @return stdClass record from module-defined table with additional field cmid (corresponding id in course_modules table) + */ + public function create_instance($record = null, array $options = null) { + $record = (object)(array)$record; + + $timecreated = time(); + + $defaultsettings = [ + 'name' => 'publication', + 'intro' => 'Introtext', + 'introformat' => 1, + 'alwaysshowdescription' => 1, + 'timecreated' => $timecreated, + 'timemodified' => $timecreated, + 'duedate' => $timecreated + 604800, // 1 week later! + 'allowsubmissionsfromdate' => $timecreated, + 'cutoffdate' => 0, + 'mode' => 0, // Equals PUBLICATION_MODE_UPLOAD! + 'importfrom' => -1, + 'autoimport' => 1, + 'obtainstudentapproval' => 1, + 'groupapproval' => 0, // Equals PUBLICATION_APPROVAL_ALL! + 'maxfiles' => 5, + 'maxbytes' => 2, + 'allowedfiletypes' => '', + 'obtainteacherapproval' => 1, + 'groupmode' => SEPARATEGROUPS, + ]; + + foreach ($defaultsettings as $name => $value) { + if (!isset($record->{$name})) { + $record->{$name} = $value; + } + } + + return parent::create_instance($record, (array)$options); + } +} diff --git a/mod/publication/tests/privacy_test.php b/mod/publication/tests/privacy_test.php new file mode 100644 index 0000000..eba7270 --- /dev/null +++ b/mod/publication/tests/privacy_test.php @@ -0,0 +1,374 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Unit Tests for mod/publication's privacy providers! + * + * @package mod_publication + * @copyright 2019 Academic Moodle Cooperation https://www.academic-moodle-cooperation.org/ + * @author Philipp Hager <philipp.hager@tuwien.ac.at> strongly based on mod_assign's privacy unit tests! + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_publication\local\tests; + +use \mod_publication\privacy\provider; +use context_module; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/publication/locallib.php'); + +/** + * Unit Tests for mod/publication's privacy providers! TODO: finish these unit tests here! + * + * @copyright 2019 Academic Moodle Cooperation https://www.academic-moodle-cooperation.org/ + * @author Philipp Hager <philipp.hager@tuwien.ac.at> strongly based on mod_assign's privacy unit tests! + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class privacy_testcase extends base { + /** @var stdClass */ + private $course1; + /** @var stdClass */ + private $course2; + /** @var stdClass */ + private $group11; + /** @var stdClass */ + private $group12; + /** @var stdClass */ + private $group21; + /** @var stdClass */ + private $group22; + /** @var stdClass */ + private $user1; + /** @var stdClass */ + private $user2; + /** @var stdClass */ + private $user3; + /** @var stdClass */ + private $teacher1; + /** @var publication */ + private $pubupload; + /** @var publication */ + private $pubupload2; + /** @var \testable_assign */ + private $assign; + /** @var \testable_assign */ + private $assign2; + /** @var publication */ + private $pubimport; + /** @var \testable_assign */ + private $teamassign; + /** @var \testable_assign */ + private $teamassign2; + /** @var publication */ + private $pubteamimport; + /** @var publication */ + private $pubteamimport2; + + /** + * Set up the common parts of the tests! + * + * The base test class already contains a setUp-method setting up a course including users and groups. + * + * @throws \coding_exception + */ + protected function setUp():void { + parent::setUp(); + + $this->resetAfterTest(); + + $this->course1 = self::getDataGenerator()->create_course(); + $this->course2 = self::getDataGenerator()->create_course(); + $this->group11 = self::getDataGenerator()->create_group((object)['courseid' => $this->course1->id]); + $this->group12 = self::getDataGenerator()->create_group((object)['courseid' => $this->course1->id]); + $this->group21 = self::getDataGenerator()->create_group((object)['courseid' => $this->course2->id]); + $this->group22 = self::getDataGenerator()->create_group((object)['courseid' => $this->course2->id]); + + $this->user1 = $this->students[0]; + self::getDataGenerator()->enrol_user($this->user1->id, $this->course1->id, 'student'); + self::getDataGenerator()->enrol_user($this->user1->id, $this->course2->id, 'student'); + $this->user2 = $this->students[1]; + self::getDataGenerator()->enrol_user($this->user2->id, $this->course1->id, 'student'); + self::getDataGenerator()->enrol_user($this->user2->id, $this->course2->id, 'student'); + $this->user3 = $this->students[2]; + self::getDataGenerator()->enrol_user($this->user3->id, $this->course1->id, 'student'); + self::getDataGenerator()->enrol_user($this->user3->id, $this->course2->id, 'student'); + // Need a second user as teacher. + $this->teacher1 = $this->editingteachers[0]; + self::getDataGenerator()->enrol_user($this->teacher1->id, $this->course1->id, 'editingteacher'); + self::getDataGenerator()->enrol_user($this->teacher1->id, $this->course2->id, 'editingteacher'); + + // Prepare groups! + self::getDataGenerator()->create_group_member((object)['userid' => $this->user1->id, 'groupid' => $this->group11->id]); + self::getDataGenerator()->create_group_member((object)['userid' => $this->user3->id, 'groupid' => $this->group11->id]); + self::getDataGenerator()->create_group_member((object)['userid' => $this->user1->id, 'groupid' => $this->group21->id]); + self::getDataGenerator()->create_group_member((object)['userid' => $this->user3->id, 'groupid' => $this->group21->id]); + + self::getDataGenerator()->create_group_member((object)['userid' => $this->user2->id, 'groupid' => $this->group12->id]); + self::getDataGenerator()->create_group_member((object)['userid' => $this->user2->id, 'groupid' => $this->group22->id]); + + // Create multiple publication instances. + // Publication with uploads. + $this->pubupload = $this->create_instance([ + 'name' => 'Pub Upload 1', + 'course' => $this->course1 + ]); + $this->pubupload2 = $this->create_instance([ + 'name' => 'Pub Upload 2', + 'course' => $this->course1 + ]); + + // Assign to import from. + $this->assign = $this->create_assign($this->course1, ['submissiondrafts' => false, + 'assignsubmission_onlinetext_enabled' => true]); + $this->assign2 = $this->create_assign($this->course1, ['submissiondrafts' => false, + 'assignsubmission_onlinetext_enabled' => true]); + + // Publication with imports. + $this->pubimport = $this->create_instance([ + 'name' => 'Pub Import 1', + 'course' => $this->course1, + 'mode' => PUBLICATION_MODE_IMPORT, + 'importfrom' => $this->assign->get_instance()->id, + ]); + + // Publication with import from teamsubmission. + $this->teamassign = $this->create_assign($this->course1, [ + 'name' => 'Teamassign 1', + 'teamsubmission' => true, + 'submissiondrafts' => false, + 'assignsubmission_onlinetext_enabled' => true + ]); + $this->teamassign2 = $this->create_assign($this->course2, [ + 'name' => 'Teamassign 2', + 'teamsubmission' => true, + 'submissiondrafts' => false, + 'assignsubmission_onlinetext_enabled' => true + ]); + $this->pubteamimport = $this->create_instance([ + 'name' => 'Teamimport 1', + 'course' => $this->course1, + 'mode' => PUBLICATION_MODE_IMPORT, + 'importfrom' => $this->teamassign->get_instance()->id, + 'requireallteammemberssubmit' => false, + 'preventsubmissionnotingroup' => false, + ]); + $this->pubteamimport2 = $this->create_instance([ + 'name' => 'Teamimport 2', + 'course' => $this->course2, + 'mode' => PUBLICATION_MODE_IMPORT, + 'importfrom' => $this->teamassign2->get_instance()->id, + 'requireallteammemberssubmit' => false, + 'preventsubmissionnotingroup' => false, + ]); + } + + /** + * Test that getting the contexts for a user works. + * + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function test_get_contexts_for_userid() { + // The user will be in these contexts. + $usercontextids = [ + $this->pubimport->get_context()->id, + $this->pubupload->get_context()->id, + $this->pubteamimport->get_context()->id, + ]; + + // User 1 submits to assign1 and teamassign1 and uploads in pubupload1! + $this->add_submission($this->user1, $this->assign, 'Textsubmission in assign1 by user1!', true); + $this->add_submission($this->user1, $this->teamassign, 'Textsubmission in teamassign1 by user1!', true); + $this->create_upload($this->user1->id, $this->pubupload->get_instance()->id, 'upload-no-1.txt', + 'THis is the first upload here!'); + // User 3 also submits to general assign & uploads in general publication! + $this->add_submission($this->user3, $this->assign2, 'Textsubmission for assign2 by user3!', true); + $this->create_upload($this->user3->id, $this->pubupload2->get_instance()->id, 'upload-no-2.txt', + 'This is another upload in another publication'); + + // Then we check, if user 1 appears only in pubimport1, pubupload1 and pubteamimport1! + $contextlist = provider::get_contexts_for_userid($this->user1->id); + + $this->assertEquals(count($usercontextids), count($contextlist->get_contextids())); + // There should be no difference between the contexts. + $this->assertEmpty(array_diff($usercontextids, $contextlist->get_contextids())); + + // User 3 is in a group with user 1 and submits to teamassign2! + $this->add_submission($this->user3, $this->teamassign2, + 'Another text submission, but this time valid for the whole group!'); + + // Now user 1 is also in pubteamimport2! + $usercontextids[] = $this->pubteamimport2->get_context()->id; + + $contextlist = provider::get_contexts_for_userid($this->user1->id); + $this->assertEquals(count($usercontextids), count($contextlist->get_contextids())); + // There should be no difference between the contexts. + $this->assertEmpty(array_diff($usercontextids, $contextlist->get_contextids())); + + // TODO: test for group approvals and extended due dates! + } + + /** + * Test returning a list of user IDs related to a context. + * + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function test_get_users_in_context() { + // User 1 submits to assign1 and teamassign1 and uploads in pubupload1! + $this->add_submission($this->user1, $this->assign, 'Textsubmission in assign1 by user1!', true); + $this->add_submission($this->user1, $this->teamassign, 'Textsubmission in teamassign1 by user1!', true); + $this->create_upload($this->user1->id, $this->pubupload->get_instance()->id, 'upload-no-1.txt', + 'This is the first upload here!'); + + $uploadcm = get_coursemodule_from_instance('publication', $this->pubupload->get_instance()->id); + $uploadctx = context_module::instance($uploadcm->id); + $userlist = new \core_privacy\local\request\userlist($uploadctx, 'publication'); + provider::get_users_in_context($userlist); + $userids = $userlist->get_userids(); + self::assertTrue(in_array($this->user1->id, $userids)); + self::assertFalse(in_array($this->user2->id, $userids)); + self::assertFalse(in_array($this->user3->id, $userids)); + + $upload2cm = get_coursemodule_from_instance('publication', $this->pubupload2->get_instance()->id); + $upload2ctx = context_module::instance($upload2cm->id); + $userlist2 = new \core_privacy\local\request\userlist($upload2ctx, 'publication'); + provider::get_users_in_context($userlist2); + $userids2 = $userlist2->get_userids(); + self::assertFalse(in_array($this->user1->id, $userids2)); + self::assertFalse(in_array($this->user2->id, $userids2)); + self::assertFalse(in_array($this->user3->id, $userids2)); + + $importcm = get_coursemodule_from_instance('publication', $this->pubimport->get_instance()->id); + $importctx = context_module::instance($importcm->id); + $importuserlist = new \core_privacy\local\request\userlist($importctx, 'publication'); + provider::get_users_in_context($importuserlist); + $importuserids = $importuserlist->get_userids(); + self::assertTrue(in_array($this->user1->id, $importuserids)); + self::assertFalse(in_array($this->user2->id, $importuserids)); + self::assertFalse(in_array($this->user3->id, $importuserids)); + + $teamcm = get_coursemodule_from_instance('publication', $this->pubteamimport->get_instance()->id); + $teamctx = context_module::instance($teamcm->id); + $teamuserlist = new \core_privacy\local\request\userlist($teamctx, 'publication'); + provider::get_users_in_context($teamuserlist); + $teamuserids = $teamuserlist->get_userids(); + self::assertTrue(in_array($this->user1->id, $teamuserids)); + self::assertFalse(in_array($this->user2->id, $teamuserids)); + self::assertTrue(in_array($this->user3->id, $teamuserids)); + + // TODO: check for extended due dates and groupapprovals! + } + + /** + * Test that a student with multiple submissions and grades is returned with the correct data. + */ + public function test_export_user_data_student() { + // Stop here and mark this test as incomplete. + self::markTestIncomplete( + 'This test has not been implemented yet.' + ); + } + + /** + * Tests the data returned for a teacher. + */ + public function test_export_user_data_teacher() { + // Stop here and mark this test as incomplete. + self::markTestIncomplete( + 'This test has not been implemented yet.' + ); + } + + /** + * A test for deleting all user data for a given context. + */ + public function test_delete_data_for_all_users_in_context() { + // Stop here and mark this test as incomplete. + self::markTestIncomplete( + 'This test has not been implemented yet.' + ); + } + + /** + * A test for deleting all user data for one user. + */ + public function test_delete_data_for_user() { + // Stop here and mark this test as incomplete. + self::markTestIncomplete( + 'This test has not been implemented yet.' + ); + } + + /** + * A test for deleting all user data for a bunch of users. + * + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function test_delete_data_for_users() { + global $DB; + + // User 1 submits to assign1 and teamassign1 and uploads in pubupload1! + $this->add_submission($this->user1, $this->assign, 'Textsubmission in assign1 by user1!', true); + $this->add_submission($this->user2, $this->assign, 'Textsubmission in assign1 by user2!', true); + $this->add_submission($this->user1, $this->teamassign, 'Textsubmission in teamassign1 by user1!', true); + $this->add_submission($this->user2, $this->teamassign, 'Textsubmission in teamassign1 by user2!', true); + $this->create_upload($this->user1->id, $this->pubupload->get_instance()->id, 'upload-no-1.txt', + 'This is the first upload here!'); + $this->create_upload($this->user2->id, $this->pubupload->get_instance()->id, 'upload-no-2.txt', + 'This is the second upload here!'); + + // Test for the data to be in place! + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubimport->get_instance()->id])); + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubteamimport->get_instance()->id])); + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubupload->get_instance()->id])); + + $userlist = new \core_privacy\local\request\approved_userlist($this->pubimport->get_context(), 'publication', + [$this->user1->id]); + provider::delete_data_for_users($userlist); + self::assertEquals(1, $DB->count_records('publication_file', ['publication' => $this->pubimport->get_instance()->id])); + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubteamimport->get_instance()->id])); + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubupload->get_instance()->id])); + $userlist = new \core_privacy\local\request\approved_userlist($this->pubupload->get_context(), 'publication', + [$this->user1->id]); + provider::delete_data_for_users($userlist); + self::assertEquals(1, $DB->count_records('publication_file', ['publication' => $this->pubimport->get_instance()->id])); + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubteamimport->get_instance()->id])); + self::assertEquals(1, $DB->count_records('publication_file', ['publication' => $this->pubupload->get_instance()->id])); + + $userlist = new \core_privacy\local\request\approved_userlist($this->pubteamimport->get_context(), 'publication', + [$this->user1->id, $this->user2->id, $this->user3->id]); + provider::delete_data_for_users($userlist); + $userlist = new \core_privacy\local\request\approved_userlist($this->pubupload->get_context(), 'publication', + [$this->user1->id, $this->user2->id, $this->user3->id]); + provider::delete_data_for_users($userlist); + $userlist = new \core_privacy\local\request\approved_userlist($this->pubimport->get_context(), 'publication', + [$this->user1->id, $this->user2->id, $this->user3->id]); + provider::delete_data_for_users($userlist); + + self::assertEquals(0, $DB->count_records('publication_file', ['publication' => $this->pubimport->get_instance()->id])); + self::assertEquals(2, $DB->count_records('publication_file', ['publication' => $this->pubteamimport->get_instance()->id])); + self::assertEquals(0, $DB->count_records('publication_file', ['publication' => $this->pubupload->get_instance()->id])); + } +} diff --git a/mod/publication/upload.php b/mod/publication/upload.php new file mode 100644 index 0000000..719c8a6 --- /dev/null +++ b/mod/publication/upload.php @@ -0,0 +1,184 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Handles file uploads by students! + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); +require_once($CFG->dirroot . '/mod/publication/upload_form.php'); + +$cmid = required_param('cmid', PARAM_INT); // Course Module ID. +$id = optional_param('id', 0, PARAM_INT); // EntryID. + +if (!$cm = get_coursemodule_from_id('publication', $cmid)) { + print_error('invalidcoursemodule'); +} + +if (!$course = $DB->get_record('course', ['id' => $cm->course])) { + print_error('coursemisconf'); +} + +require_login($course, false, $cm); + +$context = context_module::instance($cm->id); + +require_capability('mod/publication:upload', $context); + +$publication = new publication($cm, $course, $context); + +$url = new moodle_url('/mod/publication/upload.php', ['cmid' => $cm->id]); +if (!empty($id)) { + $url->param('id', $id); +} + +$PAGE->set_url($url); + +$entry = new stdClass(); +$entry->id = $USER->id; + +$entry->definition = ''; // Updated later. +$entry->definitionformat = FORMAT_HTML; // Updated later. + +$maxfiles = $publication->get_instance()->maxfiles; +$maxbytes = $publication->get_instance()->maxbytes; + +$acceptedfiletypes = $publication->get_accepted_types(); + +$definitionoptions = [ + 'trusttext' => true, + 'subdirs' => false, + 'maxfiles' => $maxfiles, + 'maxbytes' => $maxbytes, + 'context' => $context, + 'accepted_types' => $acceptedfiletypes +]; +$attachmentoptions = [ + 'subdirs' => false, + 'maxfiles' => $maxfiles, + 'maxbytes' => $maxbytes, + 'accepted_types' => $acceptedfiletypes +]; + +$entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_publication', 'entry', $entry->id); +$entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_publication', + 'attachment', $entry->id); + +$entry->cmid = $cm->id; + +// Create a new form object (found in lib.php). +$mform = new mod_publication_upload_form(null, [ + 'current' => $entry, + 'cm' => $cm, + 'publication' => $publication, + 'definitionoptions' => $definitionoptions, + 'attachmentoptions' => $attachmentoptions +]); + +if ($mform->is_cancelled()) { + redirect(new moodle_url('/mod/publication/view.php', ['id' => $cm->id])); + +} else if ($data = $mform->get_data()) { + // Store updated set of files. + + // Save and relink embedded images and save attachments. + $entry = file_postupdate_standard_editor($entry, 'definition', $definitionoptions, + $context, 'mod_publication', 'entry', $entry->id); + $entry = file_postupdate_standard_filemanager($entry, 'attachment', $attachmentoptions, + $context, 'mod_publication', 'attachment', $entry->id); + + $filearea = 'attachment'; + $sid = $USER->id; + $fs = get_file_storage(); + + $files = $fs->get_area_files($context->id, 'mod_publication', $filearea, $sid, 'timemodified', false); + + $values = []; + foreach ($files as $file) { + $values[] = $file->get_id(); + } + + $rows = $DB->get_records('publication_file', ['publication' => $publication->get_instance()->id, 'userid' => $USER->id]); + + // Find new files and store in db. + foreach ($files as $file) { + $found = false; + + foreach ($rows as $row) { + if ($row->fileid == $file->get_id()) { + $found = true; + } + } + + if (!$found) { + $dataobject = new stdClass(); + $dataobject->publication = $publication->get_instance()->id; + $dataobject->userid = $USER->id; + $dataobject->timecreated = $file->get_timecreated(); + $dataobject->fileid = $file->get_id(); + $dataobject->studentapproval = 1; // Upload always means user approves. + $dataobject->teacherapproval = $publication->get_instance()->obtainteacherapproval == 1 ? 3 : 1; + $dataobject->filename = $file->get_filename(); + $dataobject->type = PUBLICATION_MODE_UPLOAD; + + $dataobject->id = $DB->insert_record('publication_file', $dataobject); + + if ($publication->get_instance()->notifyteacher) { + publication::send_teacher_notification_uploaded($cm, $file, null, $publication); + } + + \mod_publication\event\publication_file_uploaded::create_from_object($cm, $dataobject)->trigger(); + } + } + + // Find deleted files and update db. + foreach ($rows as $idx => $row) { + $found = false; + foreach ($files as $file) { + if ($file->get_id() == $row->fileid) { + $found = true; + continue; + } + } + + if (!$found) { + $dataobject = $DB->get_record('publication_file', ['id' => $row->id]); + \mod_publication\event\publication_file_deleted::create_from_object($cm, $dataobject)->trigger(); + $DB->delete_records('publication_file', ['id' => $row->id]); + } + } + + redirect(new moodle_url('/mod/publication/view.php', ['id' => $cm->id])); +} + +// Load existing files into draft area. + +echo $OUTPUT->header(); + +echo $OUTPUT->heading(format_string($publication->get_instance()->name)); + +$publication->display_intro(); + +$mform->display(); + +echo $OUTPUT->footer(); diff --git a/mod/publication/upload_form.php b/mod/publication/upload_form.php new file mode 100644 index 0000000..b0d8a72 --- /dev/null +++ b/mod/publication/upload_form.php @@ -0,0 +1,104 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * File containing upload form class. + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->libdir . '/formslib.php'); // Putting this is as a safety as i got a class not found error. + +/** + * Form to upload files for mod_publication + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_publication_upload_form extends moodleform { + + /** + * Definition of file upload format + */ + public function definition() { + $mform = $this->_form; + + $currententry = $this->_customdata['current']; + $publication = $this->_customdata['publication']; + $attachmentoptions = $this->_customdata['attachmentoptions']; + + if ($publication->get_instance()->obtainteacherapproval) { + $text = get_string('published_aftercheck', 'publication'); + } else { + $text = get_string('published_immediately', 'publication'); + } + + $mform->addElement('header', 'myfiles', get_string('myfiles', 'publication')); + + $mform->addElement('static', 'guideline', get_string('guideline', 'publication'), $text); + + $mform->addElement('filemanager', 'attachment_filemanager', get_string('myfiles', 'publication'), null, $attachmentoptions); + + // Add notice of allowed file types if they're restricted! + if (!empty($attachmentoptions['accepted_types']) && $attachmentoptions['accepted_types'] !== '*') { + $text = html_writer::tag('p', get_string('filesofthesetypes', 'publication')); + $text .= html_writer::start_tag('ul'); + + $typesets = $publication->get_configured_typesets(); + foreach ($typesets as $type) { + $a = new stdClass(); + $extensions = file_get_typegroup('extension', $type); + $typetext = html_writer::tag('li', $type); + // Only bother checking if it's a mimetype or group if it has extensions in the group. + if (!empty($extensions)) { + if (strpos($type, '/') !== false) { + $a->name = get_mimetype_description($type); + $a->extlist = implode(' ', $extensions); + $typetext = html_writer::tag('li', $a->name . ' — ' . $a->extlist); + } else if (get_string_manager()->string_exists("group:$type", 'mimetypes')) { + $a->name = get_string("group:$type", 'mimetypes'); + $a->extlist = implode(' ', $extensions); + $typetext = html_writer::tag('li', $a->name . ' — ' . $a->extlist); + } + } + $text .= $typetext; + } + + $text .= html_writer::end_tag('ul'); + $mform->addElement('static', '', '', $text); + } + + // Hidden params. + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'cmid'); + $mform->setType('cmid', PARAM_INT); + + // Buttons. + $this->add_action_buttons(true, get_string('save_changes', 'publication')); + $this->set_data($currententry); + } +} diff --git a/mod/publication/version.php b/mod/publication/version.php new file mode 100644 index 0000000..672b4f6 --- /dev/null +++ b/mod/publication/version.php @@ -0,0 +1,34 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Version information + * + * @package mod_publication + * @author Hannes Laimer + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020111100; +$plugin->requires = 2020110300; +$plugin->component = 'mod_publication'; +$plugin->maturity = MATURITY_STABLE; +$plugin->release = "v3.10.0"; diff --git a/mod/publication/view.php b/mod/publication/view.php new file mode 100644 index 0000000..9f07a91 --- /dev/null +++ b/mod/publication/view.php @@ -0,0 +1,289 @@ +<?php +// This file is part of mod_publication for Moodle - http://moodle.org/ +// +// It 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. +// +// It 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/>. + +/** + * Displays a single mod_publication instance + * + * @package mod_publication + * @author Philipp Hager + * @author Andreas Windbichler + * @copyright 2014 Academic Moodle Cooperation {@link http://www.academic-moodle-cooperation.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/publication/locallib.php'); +require_once($CFG->dirroot . '/mod/publication/mod_publication_files_form.php'); +require_once($CFG->dirroot . '/mod/publication/mod_publication_allfiles_form.php'); + +$id = required_param('id', PARAM_INT); // Course Module ID. + +$url = new moodle_url('/mod/publication/view.php', ['id' => $id]); +$cm = get_coursemodule_from_id('publication', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + +require_login($course, true, $cm); +$PAGE->set_url($url); + +$context = context_module::instance($cm->id); + +require_capability('mod/publication:view', $context); + +$publication = new publication($cm, $course, $context); + +$event = \mod_publication\event\course_module_viewed::create([ + 'objectid' => $PAGE->cm->instance, + 'context' => $PAGE->context, +]); +$event->add_record_snapshot('course', $PAGE->course); +$event->trigger(); + +$pagetitle = strip_tags($course->shortname . ': ' . format_string($publication->get_instance()->name)); +$action = optional_param('action', 'view', PARAM_ALPHA); +$savevisibility = optional_param('savevisibility', false, PARAM_RAW); + +$download = optional_param('download', 0, PARAM_INT); +if ($download > 0) { + $publication->download_file($download); +} + +if ($savevisibility) { + require_capability('mod/publication:approve', $context); + + $files = optional_param_array('files', [], PARAM_INT); + + $params = []; + + $params['pubid'] = $publication->get_instance()->id; + + foreach ($files as $fileid => $val) { + $x = $DB->get_record('publication_file', array('fileid' => $fileid), $fields = "fileid,userid,teacherapproval,filename"); + + $oldval = $x->teacherapproval; + + if ($val != $oldval) { + $newstatus = ($val == 2 || ($val == 3 && !$publication->get_instance()->obtainteacherapproval)) ? '' : 'not'; + $logstatus = $newstatus; + $user = $DB->get_record('user', array('id' => $x->userid)); + $group = false; + + if (($publication->get_instance()->mode == PUBLICATION_MODE_IMPORT) + && $DB->get_field('assign', 'teamsubmission', ['id' => $publication->get_instance()->importfrom])) { + $logstatus = $newstatus." (Teacher) "; + $group = $x->userid; + } + + $dataforlog = new stdClass(); + $dataforlog->publication = $params['pubid']; + $dataforlog->approval = $logstatus." approved"; + $dataforlog->userid = $USER->id; + if ($user && !empty($user->id)) { + $dataforlog->reluser = $user->id; + } else { + $dataforlog->reluser = 0; + } + $dataforlog->fileid = $fileid; + + try { + \mod_publication\event\publication_approval_changed::approval_changed($cm, $dataforlog)->trigger(); + } catch (coding_exception $e) { + throw new Exception("Coding exception while sending notification: " . $e->getMessage()); + } + + $DB->set_field('publication_file', 'teacherapproval', $val, ['fileid' => $fileid]); + + if ($publication->get_instance()->notifystudents) { + $strsubmitted = get_string('approvalchange', 'publication'); + + if ($group) { + $select = 'groupid = :id'; + $params = array('id' => $group); + $usersingroup = $DB->get_records_select('groups_members', $select, $params, '', 'userid'); + foreach ($usersingroup as $u) { + $user = $DB->get_record('user', array('id' => $u->userid)); + $publication::send_student_notification_approval_changed($cm, $u, $USER, $newstatus, $x, $id, $publication); + } + } else { + $publication::send_student_notification_approval_changed($cm, $user, $USER, $newstatus, $x, $id, $publication); + } + } + } + } + +} else if ($action == "zip") { + $publication->download_zip(true); +} else if ($action == "zipusers") { + $users = optional_param_array('selectedeuser', false, PARAM_INT); + if (!$users) { + // No users selected. + header("Location: view.php?id=" . $id); + die(); + } + $users = array_keys($users); + $publication->download_zip($users); + +} else if ($action == "import") { + require_capability('mod/publication:approve', $context); + require_sesskey(); + + if (!isset($_POST['confirm'])) { + $message = get_string('updatefileswarning', 'publication'); + + echo $OUTPUT->header(); + echo $OUTPUT->heading(format_string($publication->get_instance()->name), 1); + echo $OUTPUT->confirm($message, 'view.php?id=' . $id . '&action=import&confirm=1', 'view.php?id=' . $id); + echo $OUTPUT->footer(); + exit; + } + + $publication->importfiles(); +} else if ($action == "grantextension") { + require_capability('mod/publication:grantextension', $context); + + $users = optional_param_array('selectedeuser', [], PARAM_INT); + $users = array_keys($users); + + if (count($users) > 0) { + $url = new moodle_url('/mod/publication/grantextension.php', ['id' => $cm->id]); + foreach ($users as $idx => $u) { + $url->param('userids[' . $idx . ']', $u); + } + + redirect($url); + die(); + } +} else if ($action == "approveusers" || $action == "rejectusers") { + require_capability('mod/publication:approve', $context); + + $users = optional_param_array('selectedeuser', [], PARAM_INT); + $users = array_keys($users); + + if (count($users) > 0) { + + list($usersql, $params) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'user'); + $approval = ($action == "approveusers") ? 1 : 0; + $params['pubid'] = $publication->get_instance()->id; + $select = ' publication=:pubid AND userid ' . $usersql; + $DB->set_field_select('publication_file', 'teacherapproval', $approval, $select, $params); + } +} else if ($action == "resetstudentapproval") { + require_capability('mod/publication:approve', $context); + + $users = optional_param_array('selectedeuser', [], PARAM_INT); + $users = array_keys($users); + + if (count($users) > 0) { + + list($usersql, $params) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'user'); + $select = ' publication=:pubid AND userid ' . $usersql; + $params['pubid'] = $publication->get_instance()->id; + + $DB->set_field_select('publication_file', 'studentapproval', null, $select, $params); + + if (($publication->get_instance()->mode == PUBLICATION_MODE_IMPORT) + && $DB->get_field('assign', 'teamsubmission', ['id' => $publication->get_instance()->importfrom])) { + $fileids = $DB->get_fieldset_select('publication_file', 'id', $select, $params); + if (count($fileids) == 0) { + $fileids = [-1]; + } + + $groups = $users; + $users = []; + foreach ($groups as $cur) { + $members = $publication->get_submissionmembers($cur); + $users = array_merge($users, array_keys($members)); + } + if (count($users) > 0) { // Attention, now we have real users! Above they may be groups! + list($usersql, $userparams) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'user'); + list($filesql, $fileparams) = $DB->get_in_or_equal($fileids, SQL_PARAMS_NAMED, 'file'); + $select = ' fileid ' . $filesql . ' AND userid ' . $usersql; + $params = $fileparams + $userparams; + $DB->set_field_select('publication_groupapproval', 'approval', null, $select, $params); + } + } + } +} + +$submissionid = $USER->id; + +$filesform = new mod_publication_files_form(null, + ['publication' => $publication, 'sid' => $submissionid, 'filearea' => 'attachment']); + +if ($data = $filesform->get_data() && $publication->is_open()) { + $datasubmitted = $filesform->get_submitted_data(); + + if (isset($datasubmitted->gotoupload)) { + redirect(new moodle_url('/mod/publication/upload.php', + ['id' => $publication->get_instance()->id, 'cmid' => $cm->id])); + } + + $studentapproval = optional_param_array('studentapproval', [], PARAM_INT); + + $conditions = []; + $conditions['publication'] = $publication->get_instance()->id; + $conditions['userid'] = $USER->id; + + $pubfileids = $DB->get_records_menu('publication_file', ['publication' => $publication->get_instance()->id], + 'id ASC', 'fileid, id'); + + // Update records. + foreach ($studentapproval as $idx => $approval) { + $conditions['fileid'] = $idx; + + $approval = ($approval >= 1) ? $approval - 1 : null; + + $dataforlog->approval = $approval == 1 ? 'approved' : 'rejected'; + + if (($publication->get_instance()->mode == PUBLICATION_MODE_IMPORT) + && $DB->get_field('assign', 'teamsubmission', ['id' => $publication->get_instance()->importfrom])) { + /* We have to deal with group approval! The method sets group approval for the specified user + * and returns current cumulated group approval (and it also sets it in publication_file table)! */ + $stats = $publication->set_group_approval($approval, $pubfileids[$idx], $USER->id); + + $dataforlog->approval = '(Students '.$stats['approving'].' out of '.$stats['needed'].') '.$dataforlog->approval; + } else { + $DB->set_field('publication_file', 'studentapproval', $approval, $conditions); + } + + $dataforlog->publication = $conditions['publication']; + $dataforlog->userid = $USER->id; + $dataforlog->reluser = $USER->id; + $dataforlog->fileid = $idx; + + \mod_publication\event\publication_approval_changed::approval_changed($cm, $dataforlog)->trigger(); + } +} + +$filesform = new mod_publication_files_form(null, + ['publication' => $publication, 'sid' => $submissionid, 'filearea' => 'attachment']); + +// Print the page header. +$PAGE->set_title($pagetitle); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); + +// Print the main part of the page. +echo $OUTPUT->heading(format_string($publication->get_instance()->name), 1); + +$publication->display_intro(); +$publication->display_availability(); +$publication->display_importlink(); + +$filesform->display(); + +$publication->display_allfilesform(); + +echo $OUTPUT->footer(); diff --git a/mod/scheduler/.travis.yml b/mod/scheduler/.travis.yml new file mode 100644 index 0000000..86c8878 --- /dev/null +++ b/mod/scheduler/.travis.yml @@ -0,0 +1,48 @@ +language: php + +sudo: true + +addons: + firefox: "47.0.1" + postgresql: "9.4" + apt: + packages: + - openjdk-8-jre-headless + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +php: + - 7.1 + +env: + global: + - MOODLE_BRANCH=MOODLE_37_STABLE + matrix: + - DB=pgsql + +before_install: + - phpenv config-rm xdebug.ini + - nvm install 8.9 + - nvm use 8.9 + - cd ../.. + - composer create-project -n --no-dev --prefer-dist blackboard-open-source/moodle-plugin-ci ci ^2 + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install + +script: + - moodle-plugin-ci phplint + #- moodle-plugin-ci phpcpd + #- moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit + #- moodle-plugin-ci behat diff --git a/mod/scheduler/README.txt b/mod/scheduler/README.txt new file mode 100644 index 0000000..4fecc48 --- /dev/null +++ b/mod/scheduler/README.txt @@ -0,0 +1,244 @@ +Appointment Scheduler for Moodle + +This program 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. + +This program 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: + +http://www.gnu.org/copyleft/gpl.html + + +=== Description === + +The Scheduler module helps you to schedule appointments with your students. +Teachers specify time slots for meetings, students then choose one of them on Moodle. +Teacher in turn can record the outcome of the meeting - and optionally a grade - +within the scheduler. + +For further information, please see: + http://docs.moodle.org/37/en/Scheduler_module + +(Note that the information there may refer to a previous version of the module.) + + +=== Installation instructions === + +Place the code of the module into the mod/scheduler directory of your Moodle +directory root. That is, the present file should be located at: +mod/scheduler/README.txt + +For further installation instructions please see: + http://docs.moodle.org/en/Installing_contributed_modules_or_plugins + +This module is intended for Moodle 3.7 and above. + + +=== Authors === + +Current maintainer: + Henning Bostelmann, University of York <henning.bostelmann@york.ac.uk> + +Based on previous work by: + +* Gustav Delius <gustav.delius@york.ac.uk> (until Moodle 1.7) +* Valery Fremaux <valery.fremaux@club-internet.fr> (Moodle 1.8 - Moodle 1.9) + +With further contributions taken from: + +* Vivek Arora (independent migration of the module to 2.0) +* Andriy Semenets (Russian and Ukrainian localization) +* Gaël Mifsud (French localization) +* Various authors of the core Moodle code + + +=== Release notes === + +--- Version 3.7 --- + +New features / improvements: + +Basic support for the completion API (completion on grade) + +Feature changes: + +The permissions / capabilities on the teacher side have been altered. +In particular, teachers now strictly need the capability +'canseeotherteachersbooking' (or 'manageallappointments') in order to see +the "All appointments" tab. +There are new capabilities 'editallgrades', 'editallnotes', and 'editallattended' +which allow users without the 'manageallappointments' capability +to edit the grades, the teacher notes, or the attended flag in all appointments +of all teachers, in the appointment screen only. +The configuration setting 'allteachersgrading' is no longer in use. + +--- Version 3.5 --- + +Intended for Moodle 3.5 and later. + +New features / improvements: + +Support for Moodle's Privacy API. + + +--- Version 3.3 --- + +Intended for Moodle 3.3 and later. + +New features / improvements: + +Optionally, before making an appointment, students now see a booking screen +in which they need to enter text, upload a file, and/or solve a captcha. + +Filter strings (e.g., multilang syntax) are now processed in course shortname, +course fullname, and location fields. + +Export files can now include custom profile fields of students. + +Feature changes: + +For booking in groups, students now need to select explicitly which group +they are booking for, or whether they want to make an individual booking. +Individual bookings can be disabled via a global configuration setting. + +For viewing student's email addresses, the capability +moodle/site:viewuseridentity is now required. + +When allowing an "unlimited" number of appointments, students will no longer +be included in reminder e-mails if they have booked at least one slot. + +Refactoring / API changes: + +The function scheduler_get_user_fields() in customlib.php has changed +signature. If you have customized it in an earlier version, you will want +to edit your code. + +--- Version 3.1 --- + +Intended for Moodle 3.1 and later. + +New features / improvements: + +An additional "confidential note" field is supplied for appointments; +the contents can be read by teachers only. + +Slot notes and appointment notes can now contain attachments. + +Students can now be allowed to see existing bookings of other students. +See https://docs.moodle.org/31/en/Scheduler_Module_capabilities#Student_side + +Feature changes: + +Sending of invitations and reminders is no longer handled via a "mailto" link +but rather via a webform, using Moodle's messaging system. + +The conflict detection feature (when creating new slots) has been reworked slightly. +See https://docs.moodle.org/31/en/Scheduler:_Conflicts + +Refactoring / API changes: + +All email-related features now use the Messaging API. + +Appointment reminders and deletion of past unused slots are now handled via +the Scheduled Tasks API. + +The new Search API is supported for the activity description only. + +--- Version 2.9 --- + +Intended for Moodle 2.9 and later. + +New features / improvements: + +The export screen now allows users to choose the format of the output file, +as well as the data fields to include in the export. File format may +slightly differ from previous versions. + +Improved gradebook integration: Grades overridden in the gradebook will now +show up as such in the scheduler. + +Lists of students to be scheduled now take availability conditions +(groups and groupings) into account. + +Feature changes: + +The handling of "group mode" in Scheduler has changed. The feature of "booking +entire groups into a slot" is now controlled by a setting "Booking in groups" +at the level of each scheduler. The setting "Group mode" in "Common module +settings" is now used in line with usual Moodle conventions - setting it to, +e.g., "Separate groups" will mean that students can only book slots with +teachers in the same group. The old "Group mode" settings are automatically +migrated to "Booking in groups" and the "Group mode" set to "None". +If you have used group scheduling in previous versions, please check your data +after migration. + +The student view has been redesigned. Bookable appointments are now displayed +in pages of 25, and student select a slot by clicking a button "Book slot" +rather then selecting with a radio button and clicking "Save choice". + +For using the Overview screen outside the current scheduler, e.g., for displaying +all slots of a user across the site, users will now need extra permissions; +see CONTRIB-5750 for details. + +Refactoring / API changes: + +Config settings have been migrated to the config_plugins table. + +--- Version 2.7 --- + +Intended for Moodle 2.7 and later. + +New features: + +Students can now be allowed to book several slots at a time. +"Volatile slots" replaced with "guard time" - students cannot change their booking +for slots closer than this time to the current time. + +Feature changes: + +"Notes" field will now be shown to students at booking time. + +Refactoring / API changes: + +Major refactoring of teacher view (slot list), student view (booking screen), +teacher view of individual appointments, as well as of the backend. +Security enhancements (sessionid parameter now used throughout). +Adapted to changes in core API and to the new logging/event system (Event 2). + +--- Version 2.5 --- + +Intended for Moodle 2.5 and later. + +Module adapted to API changes Moodle core. +"Add slot" and "Edit slot" forms refactored, now based on Moodle Forms. +Language packs migrated to AMOS, removed from plugin codebase. + +--- Version 2.3 --- + +Intended for Moodle 2.3 and later; no major functional changes, but API adapted and minor enhancements. + +--- Version 2.0 --- + +No major functional changes over 1.9; bug fixes and API migration only. Requires 1.9 for database upgrades. + + +=== Technical notes === + +The code of this module is rather old, some of it still predates even Moodle 1.9. +It has now largely, but not completely, been adapted to the new APIs. +The following aspects have been migrated, that is, malfunction in this respect +should be considered a bug: + +* Gradebook integration +* Moodle 2 backup +* New rich text editor and file API +* Localization / language packs +* Logging / event system +* Scheduler tasks API +* Messaging API + +The module does not use any deprecated API as of Moodle 3.3. diff --git a/mod/scheduler/ajax.php b/mod/scheduler/ajax.php new file mode 100644 index 0000000..fdbb9ba --- /dev/null +++ b/mod/scheduler/ajax.php @@ -0,0 +1,63 @@ +<?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/>. + +/** + * Process ajax requests + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('AJAX_SCRIPT', true); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\permission\scheduler_permissions; + +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once('locallib.php'); + +$id = required_param('id', PARAM_INT); +$action = required_param('action', PARAM_ALPHA); + +$cm = get_coursemodule_from_id('scheduler', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$scheduler = scheduler::load_by_coursemodule_id($id); + +require_login($course, true, $cm); +require_sesskey(); + +$permissions = new scheduler_permissions($scheduler->context, $USER->id); + +$return = 'OK'; + +switch ($action) { + case 'saveseen': + + $appid = required_param('appointmentid', PARAM_INT); + list($slot, $app) = $scheduler->get_slot_appointment($appid); + $newseen = required_param('seen', PARAM_BOOL); + + $permissions->ensure($permissions->can_edit_attended($app)); + + $app->attended = $newseen; + $slot->save(); + + break; +} + +echo json_encode($return); +die; diff --git a/mod/scheduler/appointmentforms.php b/mod/scheduler/appointmentforms.php new file mode 100644 index 0000000..4bebe35 --- /dev/null +++ b/mod/scheduler/appointmentforms.php @@ -0,0 +1,205 @@ +<?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/>. + +/** + * Appointment-related forms of the scheduler module (using Moodle formslib) + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\appointment; +use \mod_scheduler\permission\scheduler_permissions; + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Form to edit one appointment + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_editappointment_form extends moodleform { + + /** + * @var appointment the appointment being edited + */ + protected $appointment; + + /** + * @var bool whether to distribute grade to all group members + */ + protected $distribute; + + /** + * @var array permissions of the teacher + */ + protected $permissions; + + /** + * @var array options for notes fields + */ + public $noteoptions; + + /** + * Create a new edit appointment form + * + * @param appointment $appointment the appointment to edit + * @param mixed $action the action attribute for the form + * @param scheduler_permissions $permissions + * @param bool $distribute whether to distribute grades to all group members + */ + public function __construct(appointment $appointment, $action, scheduler_permissions $permissions, $distribute) { + $this->appointment = $appointment; + $this->distribute = $distribute; + $this->permissions = $permissions; + $this->noteoptions = array('trusttext' => true, 'maxfiles' => -1, 'maxbytes' => 0, + 'context' => $permissions->get_context(), + 'subdirs' => false, 'collapsed' => true); + parent::__construct($action, null); + } + + /** + * Form definition + */ + protected function definition() { + + global $output; + + $mform = $this->_form; + $scheduler = $this->appointment->get_scheduler(); + + $candistribute = false; + + // Seen tickbox. + $mform->addElement('checkbox', 'attended', get_string('attended', 'scheduler')); + if (!$this->permissions->can_edit_attended($this->appointment)) { + $mform->freeze('attended'); + } + + // Grade. + if ($scheduler->uses_grades()) { + if ($this->permissions->can_edit_grade($this->appointment)) { + $gradechoices = $output->grading_choices($scheduler); + $mform->addElement('select', 'grade', get_string('grade', 'scheduler'), $gradechoices); + $candistribute = true; + } else { + $gradetext = $output->format_grade($scheduler, $this->appointment->grade); + $mform->addElement('static', 'gradedisplay', get_string('grade', 'scheduler'), $gradetext); + } + } + // Appointment notes (visible to teacher and/or student). + if ($scheduler->uses_appointmentnotes()) { + if ($this->permissions->can_edit_notes($this->appointment)) { + $mform->addElement('editor', 'appointmentnote_editor', get_string('appointmentnote', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->noteoptions); + $mform->setType('appointmentnote', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. + $candistribute = true; + } else { + $note = $output->format_notes($this->appointment->appointmentnote, $this->appointment->appointmentnoteformat, + $scheduler->get_context(), 'appointmentnote', $this->appointment->id); + $mform->addElement('static', 'appointmentnote_display', get_string('appointmentnote', 'scheduler'), $note); + } + } + if ($scheduler->uses_teachernotes()) { + if ($this->permissions->can_edit_notes($this->appointment)) { + $mform->addElement('editor', 'teachernote_editor', get_string('teachernote', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->noteoptions); + $mform->setType('teachernote', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. + $candistribute = true; + } else { + $note = $output->format_notes($this->appointment->teachernote, $this->appointment->teachernoteformat, + $scheduler->get_context(), 'teachernote', $this->appointment->id); + $mform->addElement('static', 'teachernote_display', get_string('teachernote', 'scheduler'), $note); + } + } + if ($this->distribute && $candistribute) { + $mform->addElement('checkbox', 'distribute', get_string('distributetoslot', 'scheduler')); + $mform->setDefault('distribute', false); + } + + $this->add_action_buttons(); + } + + /** + * 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); + + return $errors; + } + + /** + * Prepare form data from an appointment record + * + * @param appointment $appointment appointment to edit + * @return stdClass form data + */ + public function prepare_appointment_data(appointment $appointment) { + $newdata = clone($appointment->get_data()); + $context = $this->appointment->get_scheduler()->get_context(); + + $newdata = file_prepare_standard_editor($newdata, 'appointmentnote', $this->noteoptions, $context, + 'mod_scheduler', 'appointmentnote', $this->appointment->id); + + $newdata = file_prepare_standard_editor($newdata, 'teachernote', $this->noteoptions, $context, + 'mod_scheduler', 'teachernote', $this->appointment->id); + return $newdata; + } + + /** + * Save form data into appointment record + * + * @param stdClass $formdata data extracted from form + * @param appointment $appointment appointment to update + */ + public function save_appointment_data(stdClass $formdata, appointment $appointment) { + $scheduler = $appointment->get_scheduler(); + $cid = $scheduler->context->id; + $appointment->set_data($formdata); + $appointment->attended = isset($formdata->attended); + if ($scheduler->uses_appointmentnotes() && isset($formdata->appointmentnote_editor)) { + $editor = $formdata->appointmentnote_editor; + $appointment->appointmentnote = file_save_draft_area_files($editor['itemid'], $cid, + 'mod_scheduler', 'appointmentnote', $appointment->id, + $this->noteoptions, $editor['text']); + $appointment->appointmentnoteformat = $editor['format']; + } + if ($scheduler->uses_teachernotes() && isset($formdata->teachernote_editor)) { + $editor = $formdata->teachernote_editor; + $appointment->teachernote = file_save_draft_area_files($editor['itemid'], $cid, + 'mod_scheduler', 'teachernote', $appointment->id, + $this->noteoptions, $editor['text']); + $appointment->teachernoteformat = $editor['format']; + } + $appointment->save(); + if (isset($formdata->distribute)) { + $slot = $appointment->get_slot(); + $slot->distribute_appointment_data($appointment); + } + } +} + diff --git a/mod/scheduler/backup/moodle2/backup_scheduler_activity_task.class.php b/mod/scheduler/backup/moodle2/backup_scheduler_activity_task.class.php new file mode 100644 index 0000000..d834a57 --- /dev/null +++ b/mod/scheduler/backup/moodle2/backup_scheduler_activity_task.class.php @@ -0,0 +1,75 @@ +<?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/>. + +/** + * Backup activity task for the Scheduler module + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/scheduler/backup/moodle2/backup_scheduler_stepslib.php'); + +/** + * Scheduler backup task that provides all the settings and steps to perform one + * + * complete backup of the activity. + * + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_scheduler_activity_task extends backup_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + // Scheduler only has one structure step. + $this->add_step(new backup_scheduler_activity_structure_step('scheduler_structure', 'scheduler.xml')); + } + + /** + * Code the transformations to perform in the activity in + * order to get transportable (encoded) links + * + * @param string $content some HTML text that eventually contains URLs to the activity instance scripts + */ + static public function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot, "/"); + + // Link to the list of schedulers. + $search = "/(".$base."\/mod\/scheduler\/index.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@SCHEDULERINDEX*$2@$', $content); + + // Link to scheduler view by coursemoduleid. + $search = "/(".$base."\/mod\/scheduler\/view.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@SCHEDULERVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/mod/scheduler/backup/moodle2/backup_scheduler_stepslib.php b/mod/scheduler/backup/moodle2/backup_scheduler_stepslib.php new file mode 100644 index 0000000..a2e4540 --- /dev/null +++ b/mod/scheduler/backup/moodle2/backup_scheduler_stepslib.php @@ -0,0 +1,105 @@ +<?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/>. + +/** + * Define all the backup steps that will be used by the backup_scheduler_activity_task + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Define the complete scheduler structure for backup, with file and id annotations + * + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_scheduler_activity_structure_step extends backup_activity_structure_step { + + /** + * define_structure + * + * @return backup_nested_element + */ + protected function define_structure() { + + // To know if we are including userinfo. + $userinfo = $this->get_setting_value('userinfo'); + + // Define each element separated. + $scheduler = new backup_nested_element('scheduler', array('id'), array( + 'name', 'intro', 'introformat', 'schedulermode', 'maxbookings', + 'guardtime', 'defaultslotduration', 'allownotifications', 'staffrolename', + 'scale', 'gradingstrategy', 'bookingrouping', 'usenotes', + 'usebookingform', 'bookinginstructions', 'bookinginstructionsformat', + 'usestudentnotes', 'requireupload', 'uploadmaxfiles', 'uploadmaxsize', + 'usecaptcha', 'timemodified')); + + $slots = new backup_nested_element('slots'); + + $slot = new backup_nested_element('slot', array('id'), array( + 'starttime', 'duration', 'teacherid', 'appointmentlocation', + 'timemodified', 'notes', 'notesformat', 'exclusivity', + 'emaildate', 'hideuntil')); + + $appointments = new backup_nested_element('appointments'); + + $appointment = new backup_nested_element('appointment', array('id'), array( + 'studentid', 'attended', 'grade', + 'appointmentnote', 'appointmentnoteformat', 'teachernote', 'teachernoteformat', + 'studentnote', 'studentnoteformat', 'timecreated', 'timemodified')); + + // Build the tree. + + $scheduler->add_child($slots); + $slots->add_child($slot); + + $slot->add_child($appointments); + $appointments->add_child($appointment); + + // Define sources. + $scheduler->set_source_table('scheduler', array('id' => backup::VAR_ACTIVITYID)); + $scheduler->annotate_ids('grouping', 'bookingrouping'); + + // Include appointments only if we back up user information. + if ($userinfo) { + $slot->set_source_table('scheduler_slots', array('schedulerid' => backup::VAR_PARENTID)); + $appointment->set_source_table('scheduler_appointment', array('slotid' => backup::VAR_PARENTID)); + } + + // Define id annotations. + $scheduler->annotate_ids('scale', 'scale'); + + if ($userinfo) { + $slot->annotate_ids('user', 'teacherid'); + $appointment->annotate_ids('user', 'studentid'); + } + + // Define file annotations. + $scheduler->annotate_files('mod_scheduler', 'intro', null); // Files stored in intro field. + $scheduler->annotate_files('mod_scheduler', 'bookinginstructions', null); // Files stored in intro field. + $slot->annotate_files('mod_scheduler', 'slotnote', 'id'); // Files stored in slot notes. + $appointment->annotate_files('mod_scheduler', 'appointmentnote', 'id'); // Files stored in appointment notes. + $appointment->annotate_files('mod_scheduler', 'teachernote', 'id'); // Files stored in teacher-only notes. + $appointment->annotate_files('mod_scheduler', 'studentfiles', 'id'); // Files uploaded by students. + + // Return the root element (scheduler), wrapped into standard activity structure. + return $this->prepare_activity_structure($scheduler); + } +} diff --git a/mod/scheduler/backup/moodle2/restore_scheduler_activity_task.class.php b/mod/scheduler/backup/moodle2/restore_scheduler_activity_task.class.php new file mode 100644 index 0000000..47bc47a --- /dev/null +++ b/mod/scheduler/backup/moodle2/restore_scheduler_activity_task.class.php @@ -0,0 +1,113 @@ +<?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/>. + +/** + * Restore task for Scheduler. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/scheduler/backup/moodle2/restore_scheduler_stepslib.php'); + +/** + * scheduler restore task that provides all the settings and steps to perform one + * + * complete restore of the activity + * + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_scheduler_activity_task extends restore_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + // Scheduler has only one structure step. + $this->add_step(new restore_scheduler_activity_structure_step('scheduler_structure', 'scheduler.xml')); + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder + */ + static public function define_decode_contents() { + $contents = array(); + + $contents[] = new restore_decode_content('scheduler', array('intro'), 'scheduler'); + + return $contents; + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder + */ + static public function define_decode_rules() { + $rules = array(); + + $rules[] = new restore_decode_rule('SCHEDULERVIEWBYID', '/mod/scheduler/view.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('SCHEDULERINDEX', '/mod/scheduler/index.php?id=$1', 'course'); + + return $rules; + + } + + /** + * Define the restore log rules that will be applied + * by the {@link restore_logs_processor} when restoring + * scheduler logs. It must return one array + * of {@link restore_log_rule} objects + */ + static public function define_restore_log_rules() { + $rules = array(); + + $rules[] = new restore_log_rule('scheduler', 'add', 'view.php?id={course_module}', '{scheduler}'); + $rules[] = new restore_log_rule('scheduler', 'update', 'view.php?id={course_module}', '{scheduler}'); + $rules[] = new restore_log_rule('scheduler', 'view', 'view.php?id={course_module}', '{scheduler}'); + + return $rules; + } + + /** + * Define the restore log rules that will be applied + * by the {@link restore_logs_processor} when restoring + * course logs. It must return one array + * of {@link restore_log_rule} objects + * + * Note this rules are applied when restoring course logs + * by the restore final task, but are defined here at + * activity level. All them are rules not linked to any module instance (cmid = 0) + */ + static public function define_restore_log_rules_for_course() { + $rules = array(); + + $rules[] = new restore_log_rule('scheduler', 'view all', 'index.php?id={course}', null); + + return $rules; + } +} diff --git a/mod/scheduler/backup/moodle2/restore_scheduler_stepslib.php b/mod/scheduler/backup/moodle2/restore_scheduler_stepslib.php new file mode 100644 index 0000000..1cea1e4 --- /dev/null +++ b/mod/scheduler/backup/moodle2/restore_scheduler_stepslib.php @@ -0,0 +1,150 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Define all the restore steps that will be used by the restore_scheduler_activity_task + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Structure step to restore one scheduler activity + * + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_scheduler_activity_structure_step extends restore_activity_structure_step { + + /** + * define_structure + * + * @return array + */ + protected function define_structure() { + + $paths = array(); + $userinfo = $this->get_setting_value('userinfo'); + + $scheduler = new restore_path_element('scheduler', '/activity/scheduler'); + $paths[] = $scheduler; + + if ($userinfo) { + $slot = new restore_path_element('scheduler_slot', '/activity/scheduler/slots/slot'); + $paths[] = $slot; + + $appointment = new restore_path_element('scheduler_appointment', + '/activity/scheduler/slots/slot/appointments/appointment'); + $paths[] = $appointment; + } + + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * process_scheduler + * + * @param stdClass $data + */ + protected function process_scheduler($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + $data->course = $this->get_courseid(); + + $data->timemodified = $this->apply_date_offset($data->timemodified); + + if ($data->scale < 0) { // Scale found, get mapping. + $data->scale = -($this->get_mappingid('scale', abs($data->scale))); + } + + if (is_null($data->gradingstrategy)) { // Catch inconsistent data created by pre-1.9 DB schema. + $data->gradingstrategy = 0; + } + + if ($data->bookingrouping > 0) { + $data->bookingrouping = $this->get_mappingid('grouping', $data->bookingrouping); + } + + // Insert the scheduler record. + $newitemid = $DB->insert_record('scheduler', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * process_scheduler_slot + * + * @param stdClass $data + */ + protected function process_scheduler_slot($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->schedulerid = $this->get_new_parentid('scheduler'); + $data->starttime = $this->apply_date_offset($data->starttime); + $data->timemodified = $this->apply_date_offset($data->timemodified); + $data->emaildate = $this->apply_date_offset($data->emaildate); + $data->hideuntil = $this->apply_date_offset($data->hideuntil); + + $data->teacherid = $this->get_mappingid('user', $data->teacherid); + + $newitemid = $DB->insert_record('scheduler_slots', $data); + $this->set_mapping('scheduler_slot', $oldid, $newitemid, true); + } + + /** + * process_scheduler_appointment + * + * @param stdClass $data + */ + protected function process_scheduler_appointment($data) { + global $DB; + + $data = (object)$data; + $oldid = $data->id; + + $data->slotid = $this->get_new_parentid('scheduler_slot'); + + $data->timecreated = $this->apply_date_offset($data->timecreated); + $data->timemodified = $this->apply_date_offset($data->timemodified); + + $data->studentid = $this->get_mappingid('user', $data->studentid); + + $newitemid = $DB->insert_record('scheduler_appointment', $data); + $this->set_mapping('scheduler_appointment', $oldid, $newitemid, true); + } + + /** + * after_execute + */ + protected function after_execute() { + // Add scheduler related files. + $this->add_related_files('mod_scheduler', 'intro', null); + $this->add_related_files('mod_scheduler', 'bookinginstructions', null); + $this->add_related_files('mod_scheduler', 'slotnote', 'scheduler_slot'); + $this->add_related_files('mod_scheduler', 'appointmentnote', 'scheduler_appointment'); + $this->add_related_files('mod_scheduler', 'teachernote', 'scheduler_appointment'); + $this->add_related_files('mod_scheduler', 'studentfiles', 'scheduler_appointment'); + } +} diff --git a/mod/scheduler/bookingform.php b/mod/scheduler/bookingform.php new file mode 100644 index 0000000..4ae84b0 --- /dev/null +++ b/mod/scheduler/bookingform.php @@ -0,0 +1,180 @@ +<?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/>. + +/** + * Appointment booking form of the scheduler module (using Moodle formslib) + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\slot; +use \mod_scheduler\model\appointment; + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Student-side form to book or edit an appointment in a selected slot + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_booking_form extends moodleform { + + /** @var mixed */ + protected $slot; + /** @var mixed */ + protected $appointment = null; + /** @var mixed */ + protected $uploadoptions; + /** @var mixed */ + protected $existing; + + /** + * scheduler_booking_form constructor. + * + * @param slot $slot + * @param mixed $action + * @param bool $existing + */ + public function __construct(slot $slot, $action, $existing = false) { + $this->slot = $slot; + $this->existing = $existing; + parent::__construct($action, null); + } + + /** + * Form definition + */ + protected function definition() { + + global $CFG, $output; + + $mform = $this->_form; + $scheduler = $this->slot->get_scheduler(); + + $this->noteoptions = array('trusttext' => false, 'maxfiles' => 0, 'maxbytes' => 0, + 'context' => $scheduler->get_context(), + 'collapsed' => true); + + $this->uploadoptions = array('subdirs' => 0, + 'maxbytes' => $scheduler->uploadmaxsize, + 'maxfiles' => $scheduler->uploadmaxfiles); + + // Text field for student-supplied data. + if ($scheduler->uses_studentnotes()) { + + $mform->addElement('editor', 'studentnote_editor', get_string('yourstudentnote', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->noteoptions); + $mform->setType('studentnote', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. + if ($scheduler->usestudentnotes == 2) { + $mform->addRule('studentnote_editor', get_string('notesrequired', 'scheduler'), 'required'); + } + } + + // Student file upload. + if ($scheduler->uses_studentfiles()) { + $mform->addElement('filemanager', 'studentfiles', + get_string('uploadstudentfiles', 'scheduler'), + null, $this->uploadoptions ); + if ($scheduler->requireupload) { + $mform->addRule('studentfiles', get_string('uploadrequired', 'scheduler'), 'required'); + } + } + + // Captcha. + if ($scheduler->uses_bookingcaptcha() && !$this->existing) { + $mform->addElement('recaptcha', 'bookingcaptcha', get_string('security_question', 'auth'), array('https' => true)); + $mform->addHelpButton('bookingcaptcha', 'recaptcha', 'auth'); + $mform->closeHeaderBefore('bookingcaptcha'); + } + + $submitlabel = $this->existing ? null : get_string('confirmbooking', 'scheduler'); + $this->add_action_buttons(true, $submitlabel); + } + + /** + * 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 (!$this->existing && $this->slot->get_scheduler()->uses_bookingcaptcha()) { + $recaptcha = $this->_form->getElement('bookingcaptcha'); + if (!empty($this->_form->_submitValues['g-recaptcha-response'])) { + $response = $this->_form->_submitValues['g-recaptcha-response']; + if (true !== ($result = $recaptcha->verify($response))) { + $errors['bookingcaptcha'] = $result; + } + } else { + $errors['bookingcaptcha'] = get_string('missingrecaptchachallengefield'); + } + } + + return $errors; + } + + /** + * prepare_booking_data + * + * @param appointment $appointment + * @return stdClass + */ + public function prepare_booking_data(appointment $appointment) { + $this->appointment = $appointment; + + $newdata = clone($appointment->get_data()); + $context = $appointment->get_scheduler()->get_context(); + + $newdata = file_prepare_standard_editor($newdata, 'studentnote', $this->noteoptions, $context); + + $draftitemid = file_get_submitted_draft_itemid('studentfiles'); + file_prepare_draft_area($draftitemid, $context->id, 'mod_scheduler', 'studentfiles', $appointment->id); + $newdata->studentfiles = $draftitemid; + + return $newdata; + } + + /** + * save_booking_data + * + * @param stdClass $formdata + * @param appointment $appointment + */ + public function save_booking_data(stdClass $formdata, appointment $appointment) { + $scheduler = $appointment->get_scheduler(); + if ($scheduler->uses_studentnotes() && isset($formdata->studentnote_editor)) { + $editor = $formdata->studentnote_editor; + $appointment->studentnote = $editor['text']; + $appointment->studentnoteformat = $editor['format']; + } + if ($scheduler->uses_studentfiles()) { + file_save_draft_area_files($formdata->studentfiles, $scheduler->context->id, + 'mod_scheduler', 'studentfiles', $appointment->id, + $this->uploadoptions); + } + $appointment->save(); + } +} diff --git a/mod/scheduler/classes/event/appointment_base.php b/mod/scheduler/classes/event/appointment_base.php new file mode 100644 index 0000000..2c4fb88 --- /dev/null +++ b/mod/scheduler/classes/event/appointment_base.php @@ -0,0 +1,105 @@ +<?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/>. + +/** + * Base class for appointment-based events. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler abstract base event class for appointment-based events. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class appointment_base extends \core\event\base { + + + /** + * @var \mod_scheduler\model\appointment the appointment associated with this event + */ + protected $appointment; + + /** + * Return the base data fields for an appointment + * + * @param \mod_scheduler\model\appointment $appointment the appointment in question + * @return array + */ + protected static function base_data(\mod_scheduler\model\appointment $appointment) { + return array( + 'context' => $appointment->get_parent()->get_context(), + 'objectid' => $appointment->id + ); + } + + /** + * Set data of the event from an appointment record. + * + * @param \mod_scheduler\model\appointment $appointment + */ + protected function set_appointment(\mod_scheduler\model\appointment $appointment) { + $this->add_record_snapshot('scheduler_appointment', $appointment->data); + $this->add_record_snapshot('scheduler_slots', $appointment->get_parent()->data); + $this->add_record_snapshot('scheduler', $appointment->get_parent()->get_parent()->data); + $this->appointment = $appointment; + $this->data['objecttable'] = 'scheduler_appointments'; + } + + /** + * Get appointment object. + * + * NOTE: to be used from observers only. + * + * @throws \coding_exception + * @return \mod_scheduler\model\appointment + */ + public function get_appointment() { + if ($this->is_restored()) { + throw new \coding_exception('get_appointment() is intended for event observers only'); + } + return $this->appointment; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/scheduler/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} diff --git a/mod/scheduler/classes/event/appointment_list_viewed.php b/mod/scheduler/classes/event/appointment_list_viewed.php new file mode 100644 index 0000000..d7f8802 --- /dev/null +++ b/mod/scheduler/classes/event/appointment_list_viewed.php @@ -0,0 +1,77 @@ +<?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/>. + +/** + * Defines the mod_scheduler appointment list viewed event. + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler appointment list viewed event. + * + * Indicates that a teacher has viewed the list of appointments and slots. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class appointment_list_viewed extends scheduler_base { + + /** + * Create this event on a given scheduler. + * + * @param \mod_scheduler\model\scheduler $scheduler + * @return \core\event\base + */ + public static function create_from_scheduler(\mod_scheduler\model\scheduler $scheduler) { + $event = self::create(self::base_data($scheduler)); + $event->set_scheduler($scheduler); + return $event; + } + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_appointmentlistviewed', 'scheduler'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' has viewed the list of appointments in the scheduler " . + "with course module id '$this->contextinstanceid'."; + } +} diff --git a/mod/scheduler/classes/event/booking_added.php b/mod/scheduler/classes/event/booking_added.php new file mode 100644 index 0000000..e96ac68 --- /dev/null +++ b/mod/scheduler/classes/event/booking_added.php @@ -0,0 +1,77 @@ +<?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/>. + +/** + * Defines the mod_scheduler booking form added event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler booking form added event. + * + * Indicates that a student has booked into a slot. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class booking_added extends slot_base { + + /** + * Create this event on a given scheduler. + * + * @param \mod_scheduler\model\slot $slot + * @return \core\event\base + */ + public static function create_from_slot(\mod_scheduler\model\slot $slot) { + $event = self::create(self::base_data($slot)); + $event->set_slot($slot); + return $event; + } + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_bookingadded', 'scheduler'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' has booked into the slot with id '{$this->objectid}'" + ." in the scheduler with course module id '$this->contextinstanceid'."; + } +} diff --git a/mod/scheduler/classes/event/booking_form_viewed.php b/mod/scheduler/classes/event/booking_form_viewed.php new file mode 100644 index 0000000..5a770e6 --- /dev/null +++ b/mod/scheduler/classes/event/booking_form_viewed.php @@ -0,0 +1,78 @@ +<?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/>. + +/** + * Defines the mod_scheduler booking form viewed event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler booking form viewed event. + * + * Indicates that a student has viewed the booking form. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class booking_form_viewed extends scheduler_base { + + /** + * Create this event on a given scheduler. + * + * @param \mod_scheduler\model\scheduler $scheduler + * @return \core\event\base + */ + public static function create_from_scheduler(\mod_scheduler\model\scheduler $scheduler) { + $event = self::create(self::base_data($scheduler)); + $event->set_scheduler($scheduler); + return $event; + } + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_bookingformviewed', 'scheduler'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' has viewed the booking form in the scheduler " . + "with course module id '$this->contextinstanceid'."; + } +} diff --git a/mod/scheduler/classes/event/booking_removed.php b/mod/scheduler/classes/event/booking_removed.php new file mode 100644 index 0000000..ddb5141 --- /dev/null +++ b/mod/scheduler/classes/event/booking_removed.php @@ -0,0 +1,78 @@ +<?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/>. + +/** + * Defines the mod_scheduler booking form removed event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler booking form removed event. + * + * Indicates that a student has removed their booking from a slot. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class booking_removed extends slot_base { + + /** + * Create this event on a given slot. + * + * @param \mod_scheduler\model\slot $slot + * @return \core\event\base + */ + public static function create_from_slot(\mod_scheduler\model\slot $slot) { + $event = self::create(self::base_data($slot)); + $event->set_slot($slot); + return $event; + } + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_bookingremoved', 'scheduler'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' has removed their booking from the slot with id '{$this->objectid}'" + ." in the scheduler with course module id '$this->contextinstanceid'."; + } +} diff --git a/mod/scheduler/classes/event/course_module_instance_list_viewed.php b/mod/scheduler/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 0000000..275b39e --- /dev/null +++ b/mod/scheduler/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,38 @@ +<?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/>. + +/** + * Defines the mod_scheduler course module viewed event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler course module viewed event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { +} + diff --git a/mod/scheduler/classes/event/scheduler_base.php b/mod/scheduler/classes/event/scheduler_base.php new file mode 100644 index 0000000..47b82e7 --- /dev/null +++ b/mod/scheduler/classes/event/scheduler_base.php @@ -0,0 +1,124 @@ +<?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/>. + +/** + * Defines a base class for scheduler events. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler abstract base event class. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class scheduler_base extends \core\event\base { + + /** + * @var \mod_scheduler\model\scheduler the scheduler associated with this event + */ + protected $scheduler; + + /** + * Legacy log data. + * + * @var array + */ + protected $legacylogdata; + + /** + * Retrieve base data for this event from a scheduler. + * + * @param \mod_scheduler\model\scheduler $scheduler + * @return array + */ + protected static function base_data(\mod_scheduler\model\scheduler $scheduler) { + return array( + 'context' => $scheduler->get_context(), + 'objectid' => $scheduler->id + ); + } + + /** + * Set the scheduler associated with this event. + * + * @param \mod_scheduler\model\scheduler $scheduler + */ + protected function set_scheduler(\mod_scheduler\model\scheduler $scheduler) { + $this->add_record_snapshot('scheduler', $scheduler->data); + $this->scheduler = $scheduler; + $this->data['objecttable'] = 'scheduler'; + } + + /** + * Get scheduler instance. + * + * NOTE: to be used from observers only. + * + * @throws \coding_exception + * @return \mod_scheduler\model\scheduler + */ + public function get_scheduler() { + if ($this->is_restored()) { + throw new \coding_exception('get_scheduler() is intended for event observers only'); + } + if (!isset($this->scheduler)) { + debugging('scheduler property should be initialised in each event', DEBUG_DEVELOPER); + global $CFG; + require_once($CFG->dirroot . '/mod/scheduler/locallib.php'); + $this->scheduler = \mod_scheduler\model\scheduler::load_by_coursemodule_id($this->contextinstanceid); + } + return $this->scheduler; + } + + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/scheduler/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Init method. + */ + protected function init() { + $this->data['objecttable'] = 'scheduler'; + } + + /** + * Custom validation. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} diff --git a/mod/scheduler/classes/event/slot_added.php b/mod/scheduler/classes/event/slot_added.php new file mode 100644 index 0000000..550fb2d --- /dev/null +++ b/mod/scheduler/classes/event/slot_added.php @@ -0,0 +1,78 @@ +<?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/>. + +/** + * Defines the mod_scheduler slot added event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler slot added event. + * + * Indicates that a teacher has added a slot. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slot_added extends slot_base { + + /** + * Create this event on a given slot. + * + * @param \mod_scheduler\model\slot $slot + * @return \core\event\base + */ + public static function create_from_slot(\mod_scheduler\model\slot $slot) { + $event = self::create(self::base_data($slot)); + $event->set_slot($slot); + return $event; + } + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_slotadded', 'scheduler'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the slot with id '{$this->objectid}'" + ." in the scheduler with course module id '$this->contextinstanceid'."; + } +} diff --git a/mod/scheduler/classes/event/slot_base.php b/mod/scheduler/classes/event/slot_base.php new file mode 100644 index 0000000..b7f391d --- /dev/null +++ b/mod/scheduler/classes/event/slot_base.php @@ -0,0 +1,104 @@ +<?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/>. + +/** + * Defines a base class for slot-based events. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler abstract base event class for slot-based events. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class slot_base extends \core\event\base { + + /** + * @var \mod_scheduler\model\slot the slot associated with this event + */ + protected $slot; + + /** + * Return the base data fields for a slot + * + * @param \mod_scheduler\model\slot $slot the slot in question + * @return array + */ + protected static function base_data(\mod_scheduler\model\slot $slot) { + return array( + 'context' => $slot->get_scheduler()->get_context(), + 'objectid' => $slot->id, + 'relateduserid' => $slot->teacherid + ); + } + + /** + * Set the slot associated with this event + * + * @param \mod_scheduler\model\slot $slot + */ + protected function set_slot(\mod_scheduler\model\slot $slot) { + $this->add_record_snapshot('scheduler_slots', $slot->data); + $this->add_record_snapshot('scheduler', $slot->get_scheduler()->data); + $this->slot = $slot; + $this->data['objecttable'] = 'scheduler_slots'; + } + + /** + * Get slot object. + * + * NOTE: to be used from observers only. + * + * @throws \coding_exception + * @return \mod_scheduler\model\slot + */ + public function get_slot() { + if ($this->is_restored()) { + throw new \coding_exception('get_slot() is intended for event observers only'); + } + return $this->slot; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/scheduler/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Custom validation. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if ($this->contextlevel != CONTEXT_MODULE) { + throw new \coding_exception('Context level must be CONTEXT_MODULE.'); + } + } +} diff --git a/mod/scheduler/classes/event/slot_deleted.php b/mod/scheduler/classes/event/slot_deleted.php new file mode 100644 index 0000000..77f4dea --- /dev/null +++ b/mod/scheduler/classes/event/slot_deleted.php @@ -0,0 +1,86 @@ +<?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/>. + +/** + * Defines the mod_scheduler slot deleted event. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_scheduler slot deleted event. + * + * Indicates that a teacher has deleted a slot. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slot_deleted extends slot_base { + + /** + * Create this event on a given slot. + * + * @param \mod_scheduler\model\slot $slot + * @param string $action + * @return \core\event\base + */ + public static function create_from_slot(\mod_scheduler\model\slot $slot, $action) { + $data = self::base_data($slot); + $data['other'] = array('action' => $action); + $event = self::create($data); + $event->set_slot($slot); + return $event; + } + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('event_slotdeleted', 'scheduler'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + $desc = "The user with id '$this->userid' deleted the slot with id '{$this->objectid}'" + ." in the scheduler with course module id '$this->contextinstanceid'"; + if ($act = $this->other['action']) { + $desc .= " during action '$act'"; + } + $desc .= '.'; + return $desc; + } +} diff --git a/mod/scheduler/classes/model/appointment.php b/mod/scheduler/classes/model/appointment.php new file mode 100644 index 0000000..6542d0f --- /dev/null +++ b/mod/scheduler/classes/model/appointment.php @@ -0,0 +1,153 @@ +<?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/>. + +/** + * A class for representing a scheduler appointment. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + + + +/** + * A class for representing a scheduler appointment. + * + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class appointment extends mvc_child_record_model { + + /** + * get_table + * + * @return string + */ + protected function get_table() { + return 'scheduler_appointment'; + } + + /** + * appointment constructor. + * + * @param slot $slot + */ + public function __construct(slot $slot) { + parent::__construct(); + $this->data = new \stdClass(); + $this->set_parent($slot); + $this->data->slotid = $slot->get_id(); + $this->data->attended = 0; + $this->data->appointmentnoteformat = FORMAT_HTML; + $this->data->teachernoteformat = FORMAT_HTML; + } + + /** + * save + */ + public function save() { + $this->data->slotid = $this->get_parent()->get_id(); + parent::save(); + $scheddata = $this->get_scheduler()->get_data(); + scheduler_update_grades($scheddata, $this->studentid); + } + + /** + * delete + */ + public function delete() { + $studid = $this->studentid; + parent::delete(); + + $scheddata = $this->get_scheduler()->get_data(); + scheduler_update_grades($scheddata, $studid); + + $fs = get_file_storage(); + $cid = $this->get_scheduler()->get_context()->id; + $fs->delete_area_files($cid, 'mod_scheduler', 'appointmentnote', $this->get_id()); + $fs->delete_area_files($cid, 'mod_scheduler', 'teachernote', $this->get_id()); + $fs->delete_area_files($cid, 'mod_scheduler', 'studentnote', $this->get_id()); + + } + + /** + * Retrieve the slot associated with this appointment + * + * @return slot; + */ + public function get_slot() { + return $this->get_parent(); + } + + /** + * Retrieve the scheduler associated with this appointment + * + * @return scheduler + */ + public function get_scheduler() { + return $this->get_parent()->get_parent(); + } + + /** + * Return the student object. + * May be null if no student is assigned to this appointment (this _should_ never happen). + */ + public function get_student() { + global $DB; + if ($this->data->studentid) { + return $DB->get_record('user', array('id' => $this->data->studentid), '*', MUST_EXIST); + } else { + return null; + } + } + + /** + * Has this student attended? + */ + public function is_attended() { + return (boolean) $this->data->attended; + } + + /** + * Are there any student notes associated with this appointment? + * @return boolean + */ + public function has_studentnotes() { + return $this->get_scheduler()->uses_studentnotes() && + strlen(trim(strip_tags($this->studentnote))) > 0; + } + + /** + * How many files has the student uploaded for this appointment? + * + * @return int + */ + public function count_studentfiles() { + if (!$this->get_scheduler()->uses_studentnotes()) { + return 0; + } + $ctx = $this->get_scheduler()->context->id; + $fs = get_file_storage(); + $files = $fs->get_area_files($ctx, 'mod_scheduler', 'studentfiles', $this->id, "filename", false); + return count($files); + } + +} diff --git a/mod/scheduler/classes/model/appointment_factory.php b/mod/scheduler/classes/model/appointment_factory.php new file mode 100644 index 0000000..91f091d --- /dev/null +++ b/mod/scheduler/classes/model/appointment_factory.php @@ -0,0 +1,46 @@ +<?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/>. + + +/** + * A factory class for scheduler appointments. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A factory class for scheduler appointments. + * + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class appointment_factory extends mvc_child_model_factory { + /** + * Create child + * + * @param mvc_record_model $parent + * @return appointment + */ + public function create_child(mvc_record_model $parent) { + return new appointment($parent); + } +} diff --git a/mod/scheduler/classes/model/mvc_child_list.php b/mod/scheduler/classes/model/mvc_child_list.php new file mode 100644 index 0000000..e30404c --- /dev/null +++ b/mod/scheduler/classes/model/mvc_child_list.php @@ -0,0 +1,218 @@ +<?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/>. + +/** + * A list of child records. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A list of child records. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mvc_child_list { + + /** + * @var array list of child records + */ + private $children; + + /** + * @var int number of child records + */ + private $childcount; + + /** + * @var string name of the table for child records + */ + private $childtable; + + /** + * @var string name of parent id field in child table + */ + private $childfield; + + /** + * @var mvc_child_model_factory factory for new child records + */ + private $childfactory; + + /** + * @var array list of child records marked for deletion + */ + private $childrenfordeletion; + + /** + * @var mvc_record_model parent record + */ + private $parentmodel; + + /** + * Create a new child list. + * + * @param mvc_record_model $parent parent record + * @param string $childtable name of table for child records + * @param string $childfield name of parent id field in child table + * @param mvc_model_factory $factory factory for child records + */ + public function __construct(mvc_record_model $parent, $childtable, $childfield, + mvc_model_factory $factory) { + $this->children = null; + $this->childcount = -1; + $this->childfield = $childfield; + $this->childtable = $childtable; + $this->childfactory = $factory; + $this->parentmodel = $parent; + $this->childrenfordeletion = array(); + } + + /** + * Retrieve the id of the parent record + * @return int + */ + private function get_parent_id() { + return $this->parentmodel->get_id(); + } + + /** + * Load the list of all children from the database + */ + public function load() { + global $DB; + if (!is_null($this->children)) { + return; // Children already loaded. + } else if (!$this->get_parent_id()) { + // Parent ID is invalid - not yet stored. + $this->children = array(); + } else { + $this->children = array(); + $childrecs = $DB->get_records($this->childtable, array($this->childfield => $this->get_parent_id())); + $cnt = 0; + foreach ($childrecs as $rec) { + $app = $this->childfactory->create_child_from_record($rec, $this->parentmodel); + $this->children[$rec->id] = $app; + $cnt++; + } + $this->childcount = $cnt; + } + } + + /** + * Return a child record by its id + * + * @param int $id + * @return mvc_child_record_model child record, or null if none found + */ + public function get_child_by_id($id) { + $this->load(); + $found = null; + foreach ($this->children as $child) { + if ($child->id == $id) { + $found = $child; + break; + } + } + return $found; + } + + /** + * Return all children in this list + * + * @return array + */ + public function get_children() { + $this->load(); + return $this->children; + } + + /** + * Count the children in this list. + * + * @return int + */ + public function get_child_count() { + global $DB; + if ($this->childcount >= 0) { + return $this->childcount; + } else if (!$this->get_parent_id()) { + return 0; // No valid parent. + } else { + $cnt = $DB->count_records($this->childtable, array($this->childfield => $this->get_parent_id())); + $this->childcount = $cnt; + return $cnt; + } + } + + /** + * Save all child records to the database. + */ + public function save_children() { + if (!is_null($this->children)) { + foreach ($this->children as $child) { + $child->save(); + } + } + foreach ($this->childrenfordeletion as $delchild) { + $delchild->delete(); + } + $this->childrenfordeletion = array(); + } + + /** + * Create a new, empty child record. + * @return mvc_child_record_model the new record + */ + public function create_child() { + $this->load(); + $newchild = $this->childfactory->create(); + $this->children[] = $newchild; + return $newchild; + } + + /** + * Remove a child record from the list + * @param mvc_child_record_model $child the record to remove + * @throws \coding_exception if the record does nto belong to this list + */ + public function remove_child(mvc_child_record_model $child) { + if (is_null($this->children) || !in_array($child, $this->children)) { + throw new \coding_exception ('Child record to remove not found in list'); + } + $key = array_search($child, $this->children, true); + unset($this->children[$key]); + $this->childrenfordeletion[] = $child; + } + + /** + * Delete all child records + */ + public function delete_children() { + $this->load(); + foreach ($this->children as $child) { + $child->delete(); + } + } + +} \ No newline at end of file diff --git a/mod/scheduler/classes/model/mvc_child_model_factory.php b/mod/scheduler/classes/model/mvc_child_model_factory.php new file mode 100644 index 0000000..0a2eb8f --- /dev/null +++ b/mod/scheduler/classes/model/mvc_child_model_factory.php @@ -0,0 +1,79 @@ +<?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/>. + + +/** + * An abstract factory class for loading child records from the database. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An abstract factory class for loading child records from the database. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class mvc_child_model_factory extends mvc_model_factory { + + /** + * @var mvc_model the parent record + */ + protected $myparent; + + /** + * Create a new factory based on a parent record + * @param mvc_record_model $parent + */ + public function __construct(mvc_record_model $parent) { + $this->myparent = $parent; + } + + /** + * create + * + * @return mvc_model + */ + public function create() { + return $this->create_child($this->myparent); + } + + /** + * Create a new child record (with no data) + * + * @param mvc_record_model $parent + */ + public abstract function create_child(mvc_record_model $parent); + + /** + * Create a child record from a database entry, already loaded + * + * @param \stdClass $rec the record from the database + * @return mvc_child_record_model the new child record + */ + public function create_child_from_record(\stdClass $rec) { + $new = $this->create_child($this->myparent); + $new->load_record($rec); + return $new; + } +} + diff --git a/mod/scheduler/classes/model/mvc_child_record_model.php b/mod/scheduler/classes/model/mvc_child_record_model.php new file mode 100644 index 0000000..72f884e --- /dev/null +++ b/mod/scheduler/classes/model/mvc_child_record_model.php @@ -0,0 +1,79 @@ +<?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/>. + +/** + * A model mirroring one datebase record which as a "parent-child" relationship to a record in another table. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A model mirroring one datebase record which as a "parent-child" relationship to a record in another table. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class mvc_child_record_model extends mvc_record_model { + + /** + * @var mvc_record_model the parent record + */ + private $parentrec; + + /** + * Set the parent record. + * + * @param mvc_record_model $newparent + * @throws \coding_exception + */ + protected function set_parent(mvc_record_model $newparent) { + if (is_null($this->parentrec)) { + $this->parentrec = $newparent; + } else { + throw new \coding_exception('parent record can be set only once'); + } + } + + /** + * Retrieve the parent record. + * + * @throws \coding_exception + * @return mvc_record_model + */ + protected function get_parent() { + if (is_null($this->parentrec)) { + throw new \coding_exception('parent has not been set'); + } + return $this->parentrec; + } + + /** + * Retrieve the id of the parent record + * + * @return int + */ + protected function get_parent_id() { + return $this->get_parent()->get_id(); + } + +} + diff --git a/mod/scheduler/classes/model/mvc_model.php b/mod/scheduler/classes/model/mvc_model.php new file mode 100644 index 0000000..b649841 --- /dev/null +++ b/mod/scheduler/classes/model/mvc_model.php @@ -0,0 +1,37 @@ +<?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/>. + +/** + * A generic MVC model. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A generic MVC model (currently rather empty!). + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class mvc_model { + +} diff --git a/mod/scheduler/classes/model/mvc_model_factory.php b/mod/scheduler/classes/model/mvc_model_factory.php new file mode 100644 index 0000000..d008a21 --- /dev/null +++ b/mod/scheduler/classes/model/mvc_model_factory.php @@ -0,0 +1,56 @@ +<?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/>. + +/** + * An abstract factory class for loading records from the database. + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An abstract factory class for loading records from the database. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class mvc_model_factory { + + /** + * Create a new instance of a record, with no data. + * + * @return mvc_model + */ + public abstract function create(); + + /** + * Create a new record by loading it from the database. + * + * @param int $id the id of the record to load + * @return mvc_model + */ + public function create_from_id($id) { + $new = $this->create(); + $new->load($id); + return $new; + } + +} diff --git a/mod/scheduler/classes/model/mvc_record_model.php b/mod/scheduler/classes/model/mvc_record_model.php new file mode 100644 index 0000000..6db2990 --- /dev/null +++ b/mod/scheduler/classes/model/mvc_record_model.php @@ -0,0 +1,184 @@ +<?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/>. + +/** + * A model mirroring one datebase record in a specific table of the Moodle DB. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + + +/** + * A model mirroring one datebase record in a specific table of the Moodle DB. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class mvc_record_model extends mvc_model { + + /** + * @var \stdClass the underlying data record + */ + protected $data = null; + + /** + * Retrieve the name of the underlying database table + * + * @return string + */ + abstract protected function get_table(); + + /** + * Create a new model. (To be used in subclass constructors.) + */ + protected function __construct() { + $data = new \stdClass(); + } + + /** + * Load data from database. Should be used only in constructors / factory methods. + * + * @param int $id + */ + public function load($id) { + global $DB; + $rec = $DB->get_record($this->get_table(), array('id' => $id), '*', MUST_EXIST); + $this->data = $rec; + } + + /** + * Load data from a database record + * + * @param \stdClass $rec the database record + */ + public function load_record(\stdClass $rec) { + $this->data = $rec; + } + + /** + * Magic get method + * + * Attempts to call a get_$key method to return the property. + * If not possible, returns the property from the internal record. + * If even that is not possible, fails with an exception. + * + * @param string $key + * @return mixed + */ + public function __get($key) { + if (method_exists($this, 'get_'.$key)) { + return $this->{'get_'.$key}(); + } else if (property_exists($this->data, $key)) { + return $this->data->{$key}; + } else { + throw new \coding_exception('unknown property: '.$key); + } + } + + /** + * Magic set method + * + * Attempts to call a set_$key method to set the property. + * If not possible, sets the property directly in the internal record. + * + * @param string $key + * @param mixed $value + */ + public function __set($key, $value) { + if (method_exists($this, 'set_'.$key)) { + $this->{'set_'.$key}($value); + } else { + $this->data->{$key} = $value; + } + } + + /** + * Save any changes to the database + */ + public function save() { + global $DB; + if (is_null($this->data)) { + throw new \coding_exception('Missing data, cannot save'); + } else if (property_exists($this->data, 'id') && ($this->data->id)) { + $DB->update_record($this->get_table(), $this->data); + } else { + $newid = $DB->insert_record($this->get_table(), $this->data); + $this->data->id = $newid; + } + } + + /** + * Retrieve the id number of the record + * + * @return int + */ + public function get_id() { + if (is_null($this->data)) { + return 0; + } else { + return $this->data->id; + } + } + + /** + * Retrieve the associated data record + * + * Note that this is a copy (clone) of the data, + * changes to the returned record object will not lead to changes in the + * data of the present record. + * + * @return \stdClass + */ + public function get_data() { + return clone($this->data); + } + + /** + * Set a number of properties at once. + * + * @param mixed $data either an array or an object describing the properties to be set + * @param array $propnames list of properties to be set, + * or null if all properties in the input should be used + */ + public function set_data($data, $propnames = null) { + $data = (array) $data; + if (is_null($propnames)) { + $propnames = array_keys($data); + } + foreach ($propnames as $propname) { + $this->{$propname} = $data[$propname]; + } + } + + /** + * Delete this model (from the database). + */ + public function delete() { + global $DB; + + $id = $this->get_id(); + if ($id != 0) { + $DB->delete_records($this->get_table(), array('id' => $id)); + } + } + +} diff --git a/mod/scheduler/classes/model/scheduler.php b/mod/scheduler/classes/model/scheduler.php new file mode 100644 index 0000000..3a24cff --- /dev/null +++ b/mod/scheduler/classes/model/scheduler.php @@ -0,0 +1,1248 @@ +<?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/>. + +/** + * A class for representing a scheduler instance. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/grade/lib.php'); + +/** + * A class for representing a scheduler instance, as an MVC model. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler extends mvc_record_model { + + /** + * @var \stdClass course module record for this scheduler + */ + protected $cm = null; + + /** + * @var \stdClass course record for this scheduler + */ + protected $courserec = null; + + /** + * @var \context_module context record of this scheduler + */ + protected $context = null; + + /** + * @var int effective group mode of this scheduler + */ + protected $groupmode; + + /** + * @var mvc_child_list list of slots in this scheduler + */ + protected $slots; + + /** + * get_table + * + * @return string + */ + protected function get_table() { + return 'scheduler'; + } + + /** + * scheduler constructor. + */ + protected function __construct() { + parent::__construct(); + $this->slots = new mvc_child_list($this, 'scheduler_slots', 'schedulerid', new slot_factory($this)); + } + + /** + * Create a scheduler instance from the database. + * + * @param int $id module id of the scheduler + * @return scheduler + */ + public static function load_by_id($id) { + global $DB; + $cm = get_coursemodule_from_instance('scheduler', $id, 0, false, MUST_EXIST); + return self::load_from_record($id, $cm); + } + + /** + * Create a scheduler instance from the database. + * + * @param int $cmid course module id of the scheduler + * @return scheduler + */ + public static function load_by_coursemodule_id($cmid) { + global $DB; + $cm = get_coursemodule_from_id('scheduler', $cmid, 0, false, MUST_EXIST); + return self::load_from_record($cm->instance, $cm); + } + + /** + * Create a scheduler instance from an already loaded record. + * + * @param int $id the module id of the scheduler + * @param \stdClass $coursemodule course module record + * @return scheduler + */ + protected static function load_from_record($id, \stdClass $coursemodule) { + $scheduler = new scheduler(); + $scheduler->load($id); + $scheduler->cm = $coursemodule; + $scheduler->groupmode = groups_get_activity_groupmode($coursemodule); + return $scheduler; + } + + /** + * Save any changes to the database + */ + public function save() { + parent::save(); + $this->slots->save_children(); + } + + /** + * Delete the scheduler + */ + public function delete() { + $this->delete_all_slots(); + scheduler_grade_item_delete($this); + parent::delete(); + } + + /** + * Delete all slots from this scheduler. + */ + public function delete_all_slots() { + $this->slots->delete_children(); + scheduler_grade_item_update($this, 'reset'); + } + + /** + * Retrieve the course module id of this scheduler + * + * @return int + */ + public function get_cmid() { + return $this->cm->id; + } + + /** + * Retrieve the course module record of this scheduler + * + * @return \stdClass + */ + public function get_cm() { + return $this->cm; + } + + /** + * Retrieve the course id of this scheduler + * + * @return int + */ + public function get_courseid() { + return $this->data->course; + } + + /** + * Retrieve the course record of this scheduler + * + * @return \stdClass + */ + public function get_courserec() { + global $DB; + if (is_null($this->courserec)) { + $this->courserec = $DB->get_record('course', array('id' => $this->get_courseid()), '*', MUST_EXIST); + } + return $this->courserec; + } + + /** + * Retrieve the activity module context of this scheduler + * + * @return context_module + */ + public function get_context() { + if ($this->context == null) { + $this->context = \context_module::instance($this->get_cmid()); + } + return $this->context; + } + + /** + * Return the last modification date (as stored in database) for this scheduler instance. + * + * @return int + */ + public function get_timemodified() { + return $this->data->timemodified; + } + + /** + * Retrieve the name of this scheduler + * + * @param bool $applyfilters whether to apply filters so that the output is printable + * @return string + */ + public function get_name($applyfilters = false) { + $name = $this->data->name; + if ($applyfilters) { + $name = format_text($name); + } + return $name; + } + + /** + * Retrieve the intro of this scheduler + * + * @param bool $applyfilters whether to apply filters so that the output is printable + * @return string + */ + public function get_intro($applyfilters = false) { + $intro = $this->data->intro; + if ($applyfilters) { + $intro = format_text($intro); + } + return $intro; + } + + /** + * Retrieve the name for "teacher" in the context of this scheduler + * + * TODO: This involves part of the presentation, should it be here? + * + * @return string + */ + public function get_teacher_name() { + $name = format_string($this->data->staffrolename); + if (empty($name)) { + $name = get_string('teacher', 'scheduler'); + } + return $name; + } + + /** + * Retrieve the default duration of a slot, in minutes + * + * @return int + */ + public function get_default_slot_duration() { + return $this->data->defaultslotduration; + } + + /** + * Retrieve whether group scheduling is enabled in this instance + * + * @return boolean + */ + public function is_group_scheduling_enabled() { + global $CFG; + $globalenable = (bool) get_config('mod_scheduler', 'groupscheduling'); + $localenable = $this->bookingrouping >= 0; + return $globalenable && $localenable; + } + + /** + * Retrieve whether individual scheduling is enabled in this instance. + * This is usually the case, but is disabled if the instance uses group scheduling + * and the configuration setting 'mixindivgroup' is set to inactive. + * + * @return boolean + */ + public function is_individual_scheduling_enabled() { + if ($this->is_group_scheduling_enabled()) { + return (bool) get_config('mod_scheduler', 'mixindivgroup'); + } else { + return true; + } + } + + /** + * get the last location of a certain teacher in this scheduler + * + * @param \stdClass $user + * @uses $DB + * @return string the last known location for the current user (teacher) + */ + public function get_last_location($user) { + global $DB; + + $conds = array('schedulerid' => $this->data->id, 'teacherid' => $user->id); + $recs = $DB->get_records('scheduler_slots', $conds, 'timemodified DESC', 'id,appointmentlocation', 0, 1); + $lastlocation = ''; + if ($recs) { + foreach ($recs as $rec) { + $lastlocation = $rec->appointmentlocation; + } + } + return $lastlocation; + } + + /** + * Whether this scheduler uses "appointment notes" visible to teachers and students + * @return bool whether appointment notes are used + */ + public function uses_appointmentnotes() { + return ($this->data->usenotes % 2 == 1); + } + + /** + * Whether this scheduler uses "teacher notes" visible to teachers only + * @return bool whether appointment notes are used + */ + public function uses_teachernotes() { + return (floor($this->data->usenotes / 2) % 2 == 1); + } + + /** + * Whether this scheduler uses booking forms at all + * @return bool whether the booking form is used + */ + public function uses_bookingform() { + return $this->data->usebookingform; + } + + /** + * Whether this scheduler has booking instructions + * @return bool whether booking instructions present + */ + public function has_bookinginstructions() { + $instr = trim(strip_tags($this->data->bookinginstructions)); + return $this->uses_bookingform() && strlen($instr) > 0; + } + + /** + * Whether this scheduler uses "student notes" filled by students at booking time + * @return bool whether student notes are used + */ + public function uses_studentnotes() { + return $this->uses_bookingform() && $this->usestudentnotes > 0; + } + + /** + * Whether this scheduler uses student file uploads at booking time + * @return bool whether student file uploads are used + */ + public function uses_studentfiles() { + return $this->uses_bookingform() && $this->uploadmaxfiles > 0; + } + + /** + * Whether this scheduler uses any data entered by the student at booking time + * @return bool whether student data is used + */ + public function uses_studentdata() { + return $this->uses_studentnotes() || $this->uses_studentfiles(); + } + + /** + * Whether this scheduler uses captchas at booking time + * @return bool whether captchas are used + */ + public function uses_bookingcaptcha() { + global $CFG; + return $this->uses_bookingform() && $this->data->usecaptcha + && !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey); + } + + + /** + * Checks whether this scheduler allows a student (in principle) to book several slots at a time + * @return bool whether the student can book multiple appointments + */ + public function allows_multiple_bookings() { + return ($this->maxbookings != 1); + } + + /** + * Checks whether this scheduler allows unlimited bookings per student. + * @return bool + */ + public function allows_unlimited_bookings() { + return ($this->maxbookings == 0); + } + + /** + * Checks whether this scheduler uses grading at all. + * @return boolean + */ + public function uses_grades() { + return ($this->scale != 0); + } + + /** + * Return grade for given user. + * This does not take gradebook data into account. + * + * @param int $userid user id + * @return int grade of this user + */ + public function get_user_grade($userid) { + $grades = $this->get_user_grades($userid); + return $grades[$userid]->rawgrade; + } + + /** + * Return grade for given user or all users. + * + * @param int $userid optional user id, 0 means all users + * @return array array of grades, false if none + */ + public function get_user_grades($userid = 0) { + global $CFG, $DB; + + if ($this->scale == 0) { + return false; + } + + $usersql = ''; + $params = array(); + if ($userid) { + $usersql = ' AND a.studentid = :userid'; + $params['userid'] = $userid; + } + $params['sid'] = $this->id; + + $sql = 'SELECT a.id, a.studentid, a.grade '. + 'FROM {scheduler_slots} s JOIN {scheduler_appointment} a ON s.id = a.slotid '. + 'WHERE s.schedulerid = :sid AND a.grade IS NOT NULL'.$usersql; + + $grades = $DB->get_records_sql($sql, $params); + $finalgrades = array(); + $gradesums = array(); + + foreach ($grades as $grade) { + $gradesums[$grade->studentid] = new \stdClass(); + $finalgrades[$grade->studentid] = new \stdClass(); + $finalgrades[$grade->studentid]->userid = $grade->studentid; + } + if ($this->scale > 0) { // Grading numerically. + foreach ($grades as $grade) { + $gradesums[$grade->studentid]->sum = @$gradesums[$grade->studentid]->sum + $grade->grade; + $gradesums[$grade->studentid]->count = @$gradesums[$grade->studentid]->count + 1; + $gradesums[$grade->studentid]->max = (@$gradesums[$grade->studentid]->max < $grade->grade) ? + $grade->grade : @$gradesums[$grade->studentid]->max; + } + + // Retrieve the adequate strategy. + foreach ($gradesums as $student => $gradeset) { + switch ($this->gradingstrategy) { + case SCHEDULER_MAX_GRADE: + $finalgrades[$student]->rawgrade = $gradeset->max; + break; + case SCHEDULER_MEAN_GRADE: + $finalgrades[$student]->rawgrade = $gradeset->sum / $gradeset->count; + break; + } + } + + } else { // Grading on scales. + $scaleid = - ($this->scale); + $maxgrade = ''; + if ($scale = $DB->get_record('scale', array('id' => $scaleid))) { + $scalegrades = make_menu_from_list($scale->scale); + foreach ($grades as $grade) { + $gradesums[$grade->studentid]->sum = @$gradesums[$grade->studentid]->sum + $grade->grade; + $gradesums[$grade->studentid]->count = @$gradesums[$grade->studentid]->count + 1; + $gradesums[$grade->studentid]->max = (@$gradesums[$grade->studentid]->max < $grade) ? + $grade->grade : @$gradesums[$grade->studentid]->max; + } + $maxgrade = $scale->name; + } + + // Retrieve the adequate strategy. + foreach ($gradesums as $student => $gradeset) { + switch ($this->gradingstrategy) { + case SCHEDULER_MAX_GRADE: + $finalgrades[$student]->rawgrade = $gradeset->max; + break; + case SCHEDULER_MEAN_GRADE: + $finalgrades[$student]->rawgrade = $gradeset->sum / $gradeset->count; + break; + } + } + + } + // Include any empty grades. + if ($userid > 0) { + if (!array_key_exists($userid, $finalgrades)) { + $finalgrades[$userid] = new \stdClass(); + $finalgrades[$userid]->userid = $userid; + $finalgrades[$userid]->rawgrade = null; + } + } else { + $gui = new \graded_users_iterator($this->get_courserec()); + // We must gracefully live through the gradesneedregrading that can be thrown by init(). + try { + $gui->init(); + while ($userdata = $gui->next_user()) { + $uid = $userdata->user->id; + if (!array_key_exists($uid, $finalgrades)) { + $finalgrades[$uid] = new \stdClass(); + $finalgrades[$uid]->userid = $uid; + $finalgrades[$uid]->rawgrade = null; + } + } + } catch (\moodle_exception $e) { + if ($e->errorcode != 'gradesneedregrading') { + throw $e; + } + } + } + return $finalgrades; + + } + + /** + * Get gradebook information for a particular student. + * The return value is a grade_grade object. + * + * @param int $studentid id number of the student + * @return \stdClass the gradebook information. May be null if no info is found. + */ + public function get_gradebook_info($studentid) { + + $gradinginfo = grade_get_grades($this->courseid, 'mod', 'scheduler', $this->id, $studentid); + if (!empty($gradinginfo->items)) { + $item = $gradinginfo->items[0]; + if (isset($item->grades[$studentid])) { + return $item->grades[$studentid]; + } + } + return null; + } + + + /* *********************** Loading lists of slots *********************** */ + + /** + * Fetch a generic list of slots from the database + * + * @param string $wherecond WHERE condition + * @param string $havingcond HAVING condition + * @param array $params parameters for DB query + * @param mixed $limitfrom query limit from here + * @param mixed $limitnum max number od records to fetch + * @param string $orderby ORDER BY fields + * @return slot[] + */ + protected function fetch_slots($wherecond, $havingcond, array $params, $limitfrom='', $limitnum='', $orderby='') { + global $DB; + $select = 'SELECT s.* FROM {scheduler_slots} s'; + + $where = 'WHERE schedulerid = :schedulerid'; + if ($wherecond) { + $where .= ' AND ('.$wherecond.')'; + } + $params['schedulerid'] = $this->data->id; + + $having = ''; + if ($havingcond) { + $having = 'HAVING '.$havingcond; + } + + if ($orderby) { + $order = "ORDER BY $orderby, s.id"; + } else { + $order = "ORDER BY s.id"; + } + + $sql = "$select $where $having $order"; + + $slotdata = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); + $slots = array(); + foreach ($slotdata as $slotrecord) { + $slot = new slot($this); + $slot->load_record($slotrecord); + $slots[] = $slot; + } + return $slots; + } + + /** + * Count a list of slots (for this scheduler) in the database + * + * @param string $wherecond WHERE condition + * @param array $params parameters for DB query + * @return int + */ + protected function count_slots($wherecond, array $params) { + global $DB; + $select = 'SELECT COUNT(*) FROM {scheduler_slots} s'; + + $where = 'WHERE schedulerid = :schedulerid'; + if ($wherecond) { + $where .= ' AND ('.$wherecond.')'; + } + $params['schedulerid'] = $this->data->id; + + $sql = "$select $where"; + + return $DB->count_records_sql($sql, $params); + } + + + /** + * Subquery that counts appointments in the current slot. + * Only to be used in conjunction with fetch_slots() + * + * @return string + */ + protected function appointment_count_query() { + return "(SELECT COUNT(a.id) FROM {scheduler_appointment} a WHERE a.slotid = s.id)"; + } + + /** + * @var int number of student parameters used in queries + */ + protected $studparno = 0; + + /** + * Return a WHERE condition relating to sutdents in a slot + * + * @param array $params parameters for the query (by reference) + * @param int $studentid id of student to look for + * @param bool $mustbeattended include only attended appointments? + * @param bool $mustbeunattended include only unattended appointments? + * @return string + */ + protected function student_in_slot_condition(&$params, $studentid, $mustbeattended, $mustbeunattended) { + $cond = 'EXISTS (SELECT 1 FROM {scheduler_appointment} a WHERE a.studentid = :studentid'. + $this->studparno.' and a.slotid=s.id'; + if ($mustbeattended) { + $cond .= ' AND a.attended = 1'; + } + if ($mustbeunattended) { + $cond .= ' AND a.attended = 0'; + } + $cond .= ')'; + $params['studentid'.$this->studparno] = $studentid; + $this->studparno++; + return $cond; + } + + + /** + * Retrieve a slot by id. + * + * @param int $id + * @return slot + * @uses $DB + */ + public function get_slot($id) { + + global $DB; + + $slotdata = $DB->get_record('scheduler_slots', array('id' => $id, 'schedulerid' => $this->id), '*', MUST_EXIST); + $slot = new slot($this); + $slot->load_record($slotdata); + return $slot; + } + + /** + * Retrieve a list of all slots in this scheduler + * + * @return slot[] + */ + public function get_slots() { + return $this->slots->get_children(); + } + + /** + * Retrieve the number of slots in the scheduler + * + * @return int + */ + public function get_slot_count() { + return $this->slots->get_child_count(); + } + + /** + * Load a list of all slots, between certain limits + * + * @param string $limitfrom start from this entry + * @param string $limitnum max number of entries + * @return slot[] + */ + public function get_all_slots($limitfrom='', $limitnum='') { + return $this->fetch_slots('', '', array(), $limitfrom, $limitnum, 's.starttime ASC'); + } + + /** + * Retrieves attended slots of a student. These will be sorted by start time. + * + * @param int $studentid + * @return slot[] + */ + public function get_attended_slots_for_student($studentid) { + + $params = array(); + $wherecond = $this->student_in_slot_condition($params, $studentid, true, false); + + $slots = $this->fetch_slots($wherecond, '', $params, '', '', 's.starttime'); + + return $slots; + } + + /** + * Retrieves upcoming slots booked by a student. These will be sorted by start time. + * A slot is "upcoming" if it as been booked but is not attended. + * + * @param int $studentid + * @return slot[] + */ + public function get_upcoming_slots_for_student($studentid) { + + $params = array(); + $wherecond = $this->student_in_slot_condition($params, $studentid, false, true); + $slots = $this->fetch_slots($wherecond, '', $params, '', '', 's.starttime'); + + return $slots; + } + + /** + * Retrieves the slots available to a student. + * + * Note: this does not check for scheduling conflicts. + * It does however check for group restrictions if group mode is enabled. + * + * @param int $studentid + * @param bool $includefullybooked include slots that are already fully booked + * @return slot[] + */ + public function get_slots_available_to_student($studentid, $includefullybooked = false) { + + global $DB; + + $params = array(); + $wherecond = "(s.starttime > :cutofftime) AND (s.hideuntil < :nowhide)"; + $params['nowhide'] = time(); + $params['cutofftime'] = time() + $this->guardtime; + $subcond = 'NOT ('.$this->student_in_slot_condition($params, $studentid, false, false).')'; + if (!$includefullybooked) { + $subcond .= ' AND (s.exclusivity = 0 OR s.exclusivity > '.$this->appointment_count_query().')'; + } + if ($this->groupmode != NOGROUPS) { + $groups = groups_get_all_groups($this->cm->course, $studentid, $this->cm->groupingid); + if ($groups) { + $groupids = array(); + foreach ($groups as $group) { + $groupids[] = $group->id; + } + list($sqlin, $paramsin) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED); + $subquery = "SELECT 1 FROM {groups_members} gm WHERE gm.userid = s.teacherid AND gm.groupid $sqlin"; + $subcond .= " AND EXISTS ($subquery)"; + $params = array_merge($params, $paramsin); + } else { + $subcond .= " AND FALSE"; + } + } + $wherecond .= " AND ($subcond)"; + $order = 's.starttime ASC, s.duration ASC, s.teacherid'; + $slots = $this->fetch_slots($wherecond, '', $params, '', '', $order); + + return $slots; + } + + /** + * Does htis scheduler have a slot where a certain student is booked? + * + * @param int $studentid student to look for + * @param bool $mustbeattended include only attended slots + * @param bool $mustbeunattended include only unattended slots + * @return boolean + */ + public function has_slots_for_student($studentid, $mustbeattended, $mustbeunattended) { + $params = array(); + $where = $this->student_in_slot_condition($params, $studentid, $mustbeattended, $mustbeunattended); + $cnt = $this->count_slots($where, $params); + return $cnt > 0; + } + + + /** + * Does this scheduler contain any slots where a certain group has booked? + * + * @param int $groupid the group to look for + * @param bool $mustbeattended include only attended slots + * @param bool $mustbeunattended include only unattended slots + * @return boolean + */ + public function has_slots_booked_for_group($groupid, $mustbeattended = false, $mustbeunattended = false) { + global $DB; + $attendcond = ''; + if ($mustbeattended) { + $attendcond .= " AND a.attended = 1"; + } + if ($mustbeunattended) { + $attendcond .= " AND a.attended = 0"; + } + $sql = "SELECT COUNT(*) + FROM {scheduler_slots} s + JOIN {scheduler_appointment} a ON a.slotid = s.id + JOIN {groups_members} gm ON a.studentid = gm.userid + WHERE s.schedulerid = :schedulerid + AND gm.groupid = :groupid + $attendcond"; + $params = array('schedulerid' => $this->id, 'groupid' => $groupid); + return $DB->count_records_sql($sql, $params) > 0; + } + + + /** + * retrieves slots without any appointment made + * + * @param int $teacherid if given, will return only slots for this teacher + * @return slot[] list of unused slots + */ + public function get_slots_without_appointment($teacherid = 0) { + $wherecond = '('.$this->appointment_count_query().' = 0)'; + $params = array(); + if ($teacherid > 0) { + list($twhere, $params) = $this->slots_for_teacher_cond($teacherid, 0, false); + $wherecond .= " AND $twhere"; + } + $slots = $this->fetch_slots($wherecond, '', $params); + return $slots; + } + + /** + * Retrieve a list of slots for a certain teacher or group of teachers + * @param int $teacherid id of teacher to look for, can be 0 + * @param int $groupid find only slots with a teacher in this group, can be 0 + * @param bool|int $timerange include only slots in the future/past? + * Accepted values are: 0=all, 1=future, 2=past, false=all, true=past + * @return mixed SQL condition and parameters + */ + protected function slots_for_teacher_cond($teacherid, $groupid, $timerange) { + $wheres = array(); + $params = array(); + if ($teacherid > 0) { + $wheres[] = "teacherid = :tid"; + $params['tid'] = $teacherid; + } + if ($groupid > 0) { + $wheres[] = "EXISTS (SELECT 1 FROM {groups_members} gm WHERE gm.groupid = :gid AND gm.userid = s.teacherid)"; + $params['gid'] = $groupid; + } + if ($timerange === true || $timerange == 2) { + $wheres[] = "s.starttime < ".strtotime('now'); + } else if ($timerange == 1) { + $wheres[] = "s.starttime >= ".strtotime('now'); + } + $where = implode(" AND ", $wheres); + return array($where, $params); + } + + /** + * Count the number of slots available to a teacher or group of teachers + * + * @param int $teacherid id of teacher to look for, can be 0 + * @param int $groupid find only slots with a teacher in this group, can be 0 + * @param bool $inpast include only slots in the past? + * @return int + */ + public function count_slots_for_teacher($teacherid, $groupid = 0, $inpast = false) { + list($where, $params) = $this->slots_for_teacher_cond($teacherid, $groupid, $inpast); + return $this->count_slots($where, $params); + } + + /** + * Retrieve slots available to a teacher or group of teachers + * + * @param int $teacherid id of teacher to look for, can be 0 + * @param int $groupid find only slots with a teacher in this group, can be 0 + * @param mixed $limitfrom start from this entry + * @param mixed $limitnum max number of entries + * @param int $timerange whether to include past/future slots (0=all, 1=future, 0=past) + * @return slot[] + */ + public function get_slots_for_teacher($teacherid, $groupid = 0, $limitfrom = '', $limitnum = '', $timerange = 0) { + list($where, $params) = $this->slots_for_teacher_cond($teacherid, $groupid, $timerange); + return $this->fetch_slots($where, '', $params, $limitfrom, $limitnum, 's.starttime ASC, s.duration ASC, s.teacherid'); + } + + /** + * Retrieve slots available to a group of teachers + * + * @param int $groupid find only slots with a teacher in this group + * @param mixed $limitfrom start from this entry + * @param mixed $limitnum max number of entries + * @param int $timerange whether to include past/future slots (0=all, 1=future, 0=past) + * @return slot[] + */ + public function get_slots_for_group($groupid, $limitfrom = '', $limitnum = '', $timerange = 0) { + list($where, $params) = $this->slots_for_teacher_cond(0, $groupid, $timerange); + return $this->fetch_slots($where, '', $params, $limitfrom, $limitnum, 's.starttime ASC, s.duration ASC, s.teacherid'); + } + + + /* ************** End of slot retrieveal routines ******************** */ + + /** + * Returns an array of slots that would overlap with this one. + * + * @param int $starttime the start of time slot as a timestamp + * @param int $endtime end of time slot as a timestamp + * @param int $teacher the id of the teacher constraint, or 0 for "all teachers" + * @param int $student the id of the student constraint, or 0 for "all students" + * @param int $others selects where to search for conflicts, [SCHEDULER_SELF, SCHEDULER_OTHERS, SCHEDULER_ALL] + * @param int $excludeslot exclude slot with this id (useful to exclude present slot when saving) + * @uses $DB + * @return array conflicting slots + */ + public function get_conflicts($starttime, $endtime, $teacher = 0, $student = 0, + $others = SCHEDULER_SELF, $excludeslot = 0) { + global $DB; + + $params = array(); + + $slotscope = ($excludeslot == 0) ? "" : "sl.id != :excludeslot AND "; + $params['excludeslot'] = $excludeslot; + + switch ($others) { + case SCHEDULER_SELF: + $schedulerscope = "sl.schedulerid = :schedulerid AND "; + $params['schedulerid'] = $this->id; + break; + case SCHEDULER_OTHERS: + $schedulerscope = "sl.schedulerid != :schedulerid AND "; + $params['schedulerid'] = $this->id; + break; + default: + $schedulerscope = ''; + } + if ($teacher != 0) { + $teacherscope = "sl.teacherid = :teacherid AND "; + $params['teacherid'] = $teacher; + } else { + $teacherscope = ""; + } + + $studentjoin = ($student != 0) ? "JOIN {scheduler_appointment} a ON a.slotid = sl.id AND a.studentid = :studentid " : ''; + $params['studentid'] = $student; + + $timeclause = "( (sl.starttime <= :starttime1 AND sl.starttime + sl.duration * 60 > :starttime2) OR + (sl.starttime < :endtime1 AND sl.starttime + sl.duration * 60 >= :endtime2) OR + (sl.starttime >= :starttime3 AND sl.starttime + sl.duration * 60 <= :endtime3) ) + AND sl.starttime + sl.duration * 60 > :nowtime"; + $params['starttime1'] = $starttime; + $params['starttime2'] = $starttime; + $params['starttime3'] = $starttime; + $params['endtime1'] = $endtime; + $params['endtime2'] = $endtime; + $params['endtime3'] = $endtime; + $params['nowtime'] = time(); + + $sql = "SELECT sl.*, + s.name AS schedulername, + (CASE WHEN (s.id = :thisid) THEN 1 ELSE 0 END) AS isself, + c.id AS courseid, c.shortname AS courseshortname, c.fullname AS coursefullname, + (SELECT COUNT(*) FROM {scheduler_appointment} ac WHERE sl.id = ac.slotid) AS numstudents + FROM {scheduler_slots} sl + $studentjoin + JOIN {scheduler} s ON sl.schedulerid = s.id + JOIN {course} c ON c.id = s.course + WHERE $slotscope $schedulerscope $teacherscope $timeclause + ORDER BY sl.starttime ASC, sl.duration ASC"; + + $params['thisid'] = $this->id; + + $conflicting = $DB->get_records_sql($sql, $params); + + return $conflicting; + } + + /** + * retrieves an appointment and the corresponding slot + * + * @param mixed $appointmentid + * @return mixed List of (slot, scheduler_appointment) + */ + public function get_slot_appointment($appointmentid) { + global $DB; + + $slotid = $DB->get_field('scheduler_appointment', 'slotid', array('id' => $appointmentid)); + $slot = $this->get_slot($slotid); + $app = $slot->get_appointment($appointmentid); + + return array($slot, $app); + } + + /** + * Retrieves all appointments of a student. These will be sorted by start time. + * + * @param int $studentid + * @return array of appointment objects + */ + public function get_appointments_for_student($studentid) { + + global $DB; + + $sql = "SELECT s.*, a.id as appointmentid + FROM {scheduler_slots} s, {scheduler_appointment} a + WHERE s.schedulerid = :schedulerid + AND s.id = a.slotid + AND a.studentid = :studid + ORDER BY s.starttime"; + $params = array('schedulerid' => $this->id, 'studid' => $studentid); + + $slotrecs = $DB->get_records_sql($sql, $params); + + $appointments = array(); + foreach ($slotrecs as $rec) { + $slot = new slot($this); + $slot->load_record($rec); + $appointrec = $DB->get_record('scheduler_appointment', array('id' => $rec->appointmentid), '*', MUST_EXIST); + $appointment = new appointment($slot); + $appointment->load_record($appointrec); + $appointments[] = $appointment; + } + + return $appointments; + } + + /** + * Create a new slot relating to this scheduler. + * + * @return slot + */ + public function create_slot() { + return $this->slots->create_child(); + } + + /** + * Computes how many appointments a student can still book. + * + * @param int $studentid + * @param bool $includechangeable include appointments that are booked but can still be changed? + * @return int the number of bookable or changeable appointments, possibly 0; returns -1 if unlimited. + */ + public function count_bookable_appointments($studentid, $includechangeable = true) { + global $DB; + + // Find how many slots have already been booked. + $sql = 'SELECT COUNT(*) FROM {scheduler_slots} s' + .' JOIN {scheduler_appointment} a ON s.id = a.slotid' + .' WHERE s.schedulerid = :schedulerid AND a.studentid=:studentid'; + if ($this->schedulermode == 'onetime') { + if ($includechangeable) { + $sql .= ' AND s.starttime <= :cutofftime'; + } + $sql .= ' AND a.attended = 0'; + } else if ($includechangeable) { + $sql .= ' AND (s.starttime <= :cutofftime OR a.attended = 1)'; + } + $params = array('schedulerid' => $this->id, 'studentid' => $studentid, 'cutofftime' => time() + $this->guardtime); + + $booked = $DB->count_records_sql($sql, $params); + $allowed = $this->maxbookings; + + if ($allowed == 0) { + return -1; + } else if ($booked >= $allowed) { + return 0; + } else { + return $allowed - $booked; + } + + } + + /** + * Get list of teachers that have slots in this scheduler + * + * @return \stdClass[] + */ + public function get_teachers() { + global $DB; + $sql = "SELECT DISTINCT u.* + FROM {scheduler_slots} s, {user} u + WHERE s.teacherid = u.id + AND schedulerid = ?"; + $teachers = $DB->get_records_sql($sql, array($this->id)); + return $teachers; + } + + /** + * Get list of available users with a certain capability. + * + * @param string $capability the capabilty to look for + * @param int|array $groupids - group id or array of group ids; if set, will only return users who are in these groups. + * (for legacy processing, allow also group objects and arrays of these) + * @return \stdClass[] array of moodle user records + */ + protected function get_available_users($capability, $groupids = 0) { + + // If full group objects are given, reduce the array to only group ids. + if (is_array($groupids) && is_object(array_values($groupids)[0])) { + $groupids = array_keys($groupids); + } else if (is_object($groupids)) { + $groupids = $groupids->id; + } + + // Legacy: empty string amounts to no group filter. + if ($groupids === '') { + $groupids = 0; + } + + $users = array(); + if (is_integer($groupids)) { + $users = get_enrolled_users($this->get_context(), $capability, $groupids, 'u.*', null, 0, 0, true); + + } else if (is_array($groupids)) { + foreach ($groupids as $groupid) { + $groupusers = get_enrolled_users($this->get_context(), 'mod/scheduler:appoint', $groupid, + 'u.*', null, 0, 0, true); + foreach ($groupusers as $user) { + if (!array_key_exists($user->id, $users)) { + $users[$user->id] = $user; + } + } + } + } + + $modinfo = get_fast_modinfo($this->courseid); + $info = new \core_availability\info_module($modinfo->get_cm($this->cmid)); + $users = $info->filter_user_list($users); + + return $users; + } + + /** + * Get list of available students (i.e., users that can book slots) + * + * @param mixed $groupids - group id or array of group ids; if set, will only return users who are in these groups. + * @return \stdClass[] array of moodle user records + */ + public function get_available_students($groupids = 0) { + + return $this->get_available_users('mod/scheduler:appoint', $groupids); + } + + /** + * Get list of available teachers (i.e., users that can offer slots) + * + * @param mixed $groupids - only return users who are in this group. + * @return \stdClass array of moodle user records + */ + public function get_available_teachers($groupids = 0) { + + return $this->get_available_users('mod/scheduler:attend', $groupids); + } + + /** + * Checks whether there are any possible teachers for the scheduler + * + * @return bool whether teachers are present + */ + public function has_available_teachers() { + $teachers = $this->get_available_teachers(); + return count($teachers) > 0; + } + + /** + * Get a list of students that can still make an appointment. + * + * @param mixed $groups single group or array of groups - only return + * users who are in one of these group(s). + * @param int $cutoff if the number of students in the course is more than this limit, + * the routine will return the number of students rather than a list + * (this is for performance reasons). + * @param bool $onlymandatory include only students who _must_ (rather than _can_) make + * another appointment. This matters onyl in schedulers where students can make + * unlimited appointments. + * @return int|array of moodle user records; or int 0 if there are no students in the course; + * or the number of students if there are too many students. Array keys are student ids. + */ + public function get_students_for_scheduling($groups = '', $cutoff = 0, $onlymandatory = false) { + $studs = $this->get_available_students($groups); + if (($cutoff > 0 && count($studs) > $cutoff) || count($studs) == 0) { + return count($studs); + } + $schedstuds = array(); + foreach ($studs as $stud) { + $include = false; + if ($this->allows_unlimited_bookings()) { + $include = !$onlymandatory || !$this->has_slots_for_student($stud->id, false, false); + } else { + $include = ($this->count_bookable_appointments($stud->id, false) != 0); + } + if ($include) { + $schedstuds[$stud->id] = $stud; + } + } + return $schedstuds; + } + + + /** + * Delete an appointment, and do whatever is needed + * + * @param int $appointmentid + * @uses $DB + */ + public function delete_appointment($appointmentid) { + global $DB; + + if (!$oldrecord = $DB->get_record('scheduler_appointment', array('id' => $appointmentid))) { + return; + } + + $slot = $this->get_slot($oldrecord->slotid); + $appointment = $slot->get_appointment($appointmentid); + + // Delete the appointment. + $slot->remove_appointment($appointment); + $slot->save(); + } + + /** + * Frees all empty slots that are in the past, hence no longer bookable. + * This applies to all schedulers in the system. + * + * @uses $CFG + * @uses $DB + */ + public static function free_late_unused_slots() { + global $DB; + + $sql = "SELECT DISTINCT s.id + FROM {scheduler_slots} s + LEFT OUTER JOIN {scheduler_appointment} a ON s.id = a.slotid + WHERE a.studentid IS NULL + AND starttime < ?"; + $now = time(); + $todelete = $DB->get_records_sql($sql, array($now), 0, 1000); + if ($todelete) { + list($usql, $params) = $DB->get_in_or_equal(array_keys($todelete)); + $DB->delete_records_select('scheduler_slots', " id $usql ", $params); + } + } + +} diff --git a/mod/scheduler/classes/model/slot.php b/mod/scheduler/classes/model/slot.php new file mode 100644 index 0000000..11c0550 --- /dev/null +++ b/mod/scheduler/classes/model/slot.php @@ -0,0 +1,496 @@ +<?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/>. + +/** + * A class for representing a scheduler slot. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A class for representing a scheduler slot. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slot extends mvc_child_record_model { + + /** + * @var mvc_child_list list of appointments in this slot + */ + protected $appointments; + + /** + * get_table + * + * @return string + */ + protected function get_table() { + return 'scheduler_slots'; + } + + /** + * Create a new slot in a specific scheduler + * + * @param scheduler $scheduler + */ + public function __construct(scheduler $scheduler) { + parent::__construct(); + $this->data = new \stdClass(); + $this->data->id = 0; + $this->set_parent($scheduler); + $this->data->schedulerid = $scheduler->get_id(); + $this->appointments = new mvc_child_list($this, 'scheduler_appointment', 'slotid', + new appointment_factory($this)); + } + + /** + * Create a scheduler slot from the database. + * + * @param int $id + * @param scheduler $scheduler + */ + public static function load_by_id($id, scheduler $scheduler) { + $slot = new slot($scheduler); + $slot->load($id); + return $slot; + } + + /** + * Save any changes to the database + */ + public function save() { + $this->data->schedulerid = $this->get_parent()->get_id(); + parent::save(); + $this->appointments->save_children(); + $this->update_calendar(); + } + + /** + * Sets appointment-related data (grade, comments) for all student in this slot. + * + * @param appointment $template appointment from which the data will be read + */ + public function distribute_appointment_data(appointment $template) { + $scheduler = $this->get_scheduler(); + foreach ($this->appointments->get_children() as $appointment) { + if ($appointment->id != $template->id) { + if ($scheduler->uses_grades()) { + $appointment->grade = $template->grade; + } + if ($scheduler->uses_appointmentnotes()) { + $appointment->appointmentnote = $template->appointmentnote; + $appointment->appointmentnoteformat = $template->appointmentnoteformat; + $this->distribute_file_area('appointmentnote', $template->id, $appointment->id); + } + if ($scheduler->uses_teachernotes()) { + $appointment->teachernote = $template->teachernote; + $appointment->teachernoteformat = $template->teachernoteformat; + $this->distribute_file_area('teachernote', $template->id, $appointment->id); + } + $appointment->save(); + } + } + } + + /** + * Distribute plugin files from a source to a target id within a file area + * + * @param mixed $area + * @param mixed $sourceid + * @param mixed $targetid + */ + private function distribute_file_area($area, $sourceid, $targetid) { + + if ($sourceid == $targetid) { + return; + } + + $fs = get_file_storage(); + $component = 'mod_scheduler'; + $ctxid = $this->get_scheduler()->context->id; + + // Delete old files in the target area. + $files = $fs->get_area_files($ctxid, $component, $area, $targetid); + foreach ($files as $f) { + $f->delete(); + } + + // Copy files from the source to the target. + $files = $fs->get_area_files($ctxid, $component, $area, $sourceid); + foreach ($files as $f) { + $fs->create_file_from_storedfile(array('itemid' => $targetid), $f); + } + } + + /** + * Retrieve the scheduler associated with this appointment. + * + * @return scheduler the scheduler + */ + public function get_scheduler() { + return $this->get_parent(); + } + + /** + * Return the teacher object + * + * @return \stdClass + */ + public function get_teacher() { + global $DB; + if ($this->data->teacherid) { + return $DB->get_record('user', array('id' => $this->data->teacherid), '*', MUST_EXIST); + } else { + return new \stdClass(); + } + } + + /** + * Return the end time of the slot + * + * @return int + */ + public function get_endtime() { + return $this->data->starttime + $this->data->duration * MINSECS; + } + + /** + * Is this slot bookable in its bookable period for students. + * This checks for the availability time of the slot and for the "guard time" restriction, + * but not for the number of actualy booked appointments. + * + * @return boolean + */ + public function is_in_bookable_period() { + $available = $this->hideuntil <= time(); + $beforeguardtime = $this->starttime > time() + $this->parent->guardtime; + return $available && $beforeguardtime; + } + + /** + * Is this a group slot (i.e., more than one student is permitted) + * + * @return boolean + */ + public function is_groupslot() { + return (boolean) !($this->data->exclusivity == 1); + } + + + /** + * Count the number of appointments in this slot + * + * @return int + */ + public function get_appointment_count() { + return $this->appointments->get_child_count(); + } + + /** + * Get the appointment in this slot for a specific student, or null if the student doesn't have one. + * + * @param int $studentid the id number of the student in question + * @return appointment the appointment for the specified student + */ + public function get_student_appointment($studentid) { + $studapp = null; + foreach ($this->get_appointments() as $app) { + if ($app->studentid == $studentid) { + $studapp = $app; + break; + } + } + return $studapp; + } + + + /** + * Has the slot been attended? + * + * @return boolean + */ + public function is_attended() { + $isattended = false; + foreach ($this->appointments->get_children() as $app) { + $isattended = $isattended || $app->attended; + } + return $isattended; + } + + /** + * Has the slot been booked by a specific student? + * + * @param mixed $studentid + * @return boolean + */ + public function is_booked_by_student($studentid) { + $result = false; + foreach ($this->get_appointments() as $appointment) { + $result = $result || $appointment->studentid == $studentid; + } + return $result; + } + + /** + * Count the remaining free appointments in this slot + * + * @return int + */ + public function count_remaining_appointments() { + if ($this->exclusivity == 0) { + return -1; + } else { + $rem = $this->exclusivity - $this->get_appointment_count(); + if ($rem < 0) { + $rem = 0; + } + return $rem; + } + } + + /** + * Get an appointment by ID + * + * @param int $id + * @return appointment + */ + public function get_appointment($id) { + return $this->appointments->get_child_by_id($id); + } + + /** + * Get an array of all appointments + * + * @param mixed $userfilter + * @return array + */ + public function get_appointments($userfilter = null) { + $apps = $this->appointments->get_children(); + if ($userfilter) { + foreach ($apps as $key => $app) { + if (!in_array($app->studentid, $userfilter)) { + unset($apps[$key]); + } + } + } + return array_values($apps); + } + + /** + * Create a new appointment relating to this slot. + * + * @return appointment + */ + public function create_appointment() { + return $this->appointments->create_child(); + } + + /** + * Remove an appointment from this slot. + * + * @param appointment $app + */ + public function remove_appointment(appointment $app) { + $this->appointments->remove_child($app); + } + + /** + * delete + */ + public function delete() { + $this->appointments->delete_children(); + $this->clear_calendar(); + $fs = get_file_storage(); + $fs->delete_area_files($this->get_scheduler()->get_context()->id, 'mod_scheduler', 'slotnote', $this->get_id()); + parent::delete(); + } + + /** + * Delete all appointments in this slot. + */ + public function delete_all_appointments() { + $this->appointments->delete_children(); + $this->clear_calendar(); + } + + + /* The event code is SSstu (for a student event) or SSsup (for a teacher event). + * then, the id of the scheduler slot that it belongs to. + * finally, the courseID (legacy reasons -- not really used), + * all in a colon delimited string. This will run into problems when the IDs of slots and courses + * are bigger than 7 digits in length... + */ + + /** + * Get the id string for teacher events in this slot + * @return string + */ + private function get_teacher_eventtype() { + $slotid = $this->get_id(); + $courseid = $this->get_parent()->get_courseid(); + return "SSsup:{$slotid}:{$courseid}"; + } + + /** + * Get the id string for student events in this slot + * @return string + */ + private function get_student_eventtype() { + $slotid = $this->get_id(); + $courseid = $this->get_parent()->get_courseid(); + return "SSstu:{$slotid}:{$courseid}"; + } + + /** + * Remove all calendar events related to this slot from the DB + * + * @uses $DB + */ + private function clear_calendar() { + global $DB; + $DB->delete_records('event', array('eventtype' => $this->get_teacher_eventtype())); + $DB->delete_records('event', array('eventtype' => $this->get_student_eventtype())); + } + + /** + * Update calendar events related to this slot + * + * @uses $DB + */ + private function update_calendar() { + + global $DB; + + $scheduler = $this->get_parent(); + + $myappointments = $this->appointments->get_children(); + + $studentids = array(); + foreach ($myappointments as $appointment) { + if (!$appointment->is_attended()) { + $studentids[] = $appointment->studentid; + } + } + + $teacher = $DB->get_record('user', array('id' => $this->teacherid)); + $students = $DB->get_records_list('user', 'id', $studentids); + $studentnames = array(); + foreach ($students as $student) { + $studentnames[] = fullname($student); + } + + $schedulername = $scheduler->get_name(true); + $schedulerdescription = $scheduler->get_intro(); + + $slotid = $this->get_id(); + $courseid = $scheduler->get_courseid(); + + $baseevent = new \stdClass(); + $baseevent->description = "$schedulername<br/><br/>$schedulerdescription"; + $baseevent->format = 1; + $baseevent->modulename = 'scheduler'; + $baseevent->courseid = 0; + $baseevent->instance = $this->get_parent_id(); + $baseevent->timestart = $this->starttime; + $baseevent->timeduration = $this->duration * MINSECS; + $baseevent->visible = 1; + + // Update student events. + + $studentevent = clone($baseevent); + $studenteventname = get_string('meetingwith', 'scheduler').' '.$scheduler->get_teacher_name().', '.fullname($teacher); + $studentevent->name = shorten_text($studenteventname, 200); + + $this->update_calendar_events( $this->get_student_eventtype(), $studentids, $studentevent); + + // Update teacher events. + + $teacherids = array(); + $teacherevent = clone($baseevent); + if (count($studentids) > 0) { + $teacherids[] = $teacher->id; + if (count($studentids) > 1) { + $teachereventname = get_string('meetingwithplural', 'scheduler').' '. + get_string('students', 'scheduler').', '.implode(', ', $studentnames); + } else { + $teachereventname = get_string('meetingwith', 'scheduler').' '. + get_string('student', 'scheduler').', '.$studentnames[0]; + } + $teacherevent->name = shorten_text($teachereventname, 200); + } + + $this->update_calendar_events( $this->get_teacher_eventtype(), $teacherids, $teacherevent); + + } + + /** + * Update a certain type of calendat events related to this slot. + * + * @param string $eventtype + * @param array $userids users to assign to the event + * @param \stdClass $eventdata dertails of the event + */ + private function update_calendar_events($eventtype, array $userids, \stdClass $eventdata) { + + global $CFG, $DB; + require_once($CFG->dirroot.'/calendar/lib.php'); + + $eventdata->eventtype = $eventtype; + + $existingevents = $DB->get_records('event', array('modulename' => 'scheduler', 'eventtype' => $eventtype)); + $handledevents = array(); + $handledusers = array(); + + // Update existing calendar events. + foreach ($existingevents as $eventid => $existingdata) { + if (in_array($existingdata->userid, $userids)) { + $eventdata->userid = $existingdata->userid; + $calendarevent = \calendar_event::load($existingdata); + $calendarevent->update($eventdata, false); + $handledevents[] = $eventid; + $handledusers[] = $existingdata->userid; + } + } + + // Add new calendar events. + foreach ($userids as $userid) { + if (!in_array($userid, $handledusers)) { + $thisevent = clone($eventdata); + $thisevent->userid = $userid; + \calendar_event::create($thisevent, false); + } + } + + // Remove old, obsolete calendar events. + foreach ($existingevents as $eventid => $existingdata) { + if (!in_array($eventid, $handledevents)) { + $calendarevent = \calendar_event::load($existingdata); + $calendarevent->delete(); + } + } + + } + + +} + diff --git a/mod/scheduler/classes/model/slot_factory.php b/mod/scheduler/classes/model/slot_factory.php new file mode 100644 index 0000000..da9dcaf --- /dev/null +++ b/mod/scheduler/classes/model/slot_factory.php @@ -0,0 +1,46 @@ +<?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/>. + + +/** + * A factory class for scheduler slots. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\model; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A factory class for scheduler slots. + * + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slot_factory extends mvc_child_model_factory { + /** + * Create child + * + * @param mvc_record_model $parent + * @return slot + */ + public function create_child(mvc_record_model $parent) { + return new slot($parent); + } +} diff --git a/mod/scheduler/classes/permission/permissions_manager.php b/mod/scheduler/classes/permission/permissions_manager.php new file mode 100644 index 0000000..dff2eca --- /dev/null +++ b/mod/scheduler/classes/permission/permissions_manager.php @@ -0,0 +1,114 @@ +<?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/>. + +/** + * Base class for MVC controllers. + * + * @package mod_scheduler + * @copyright 2019 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\permission; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The base class for controllers. + * + * @copyright 2019 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class permissions_manager { + + /** @var int */ + protected $userid; + /** @var \context */ + protected $context; + /** @var string */ + protected $prefix; + /** @var array */ + protected $caps; + + /** + * permissions_manager constructor. + * + * @param string $pluginname + * @param \context $context + * @param int $userid + */ + protected function __construct($pluginname, \context $context, $userid) { + + $this->userid = $userid; + $this->context = $context; + $this->prefix = str_replace('_', '/', $pluginname) . ':'; + + $this->caps = array(); + } + + /** + * has_capability + * + * @param string $cap + * @return bool|mixed + */ + protected function has_capability($cap) { + if (key_exists($cap, $this->caps)) { + return $this->caps[$cap]; + } else { + $fullname = $this->prefix . $cap; + $hasit = has_capability($fullname, $this->context, $this->userid); + $this->caps[$cap] = $hasit; + return $hasit; + } + } + + /** + * has_any_capability + * + * @param array $caps + * @return bool + */ + protected function has_any_capability(array $caps) { + foreach ($caps as $cap) { + if ($this->has_capability($cap)) { + return true; + } + } + return false; + } + + /** + * get_context + * + * @return \context + */ + public function get_context() { + return $this->context; + } + + /** + * ensure + * + * @param mixed $condition + * @throws \moodle_exception + */ + public function ensure($condition) { + if (!$condition) { + throw new \moodle_exception('nopermissions'); + } + } +} diff --git a/mod/scheduler/classes/permission/scheduler_permissions.php b/mod/scheduler/classes/permission/scheduler_permissions.php new file mode 100644 index 0000000..fdbf72c --- /dev/null +++ b/mod/scheduler/classes/permission/scheduler_permissions.php @@ -0,0 +1,166 @@ +<?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/>. + +/** + * Controller for scheduler module. + * + * @package mod_scheduler + * @copyright 2019 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\permission; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The base class for controllers. + * + * @copyright 2019 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_permissions extends permissions_manager { + + /** + * scheduler_permissions constructor. + * + * @param \context $context + * @param int $userid + */ + public function __construct(\context $context, $userid) { + parent::__construct('mod_scheduler', $context, $userid); + } + + /** + * teacher_can_see_slot + * + * @param \mod_scheduler\model\slot $slot + * @return bool + */ + public function teacher_can_see_slot(\mod_scheduler\model\slot $slot) { + if ($this->has_any_capability(['manageallappointments', 'canseeotherteachersbooking'])) { + return true; + } else if ($this->has_any_capability(['manage', 'attend'])) { + return $this->userid == $slot->teacherid; + } else { + return false; + } + } + + /** + * can_edit_slot + * + * @param \mod_scheduler\model\slot $slot + * @return bool + */ + public function can_edit_slot(\mod_scheduler\model\slot $slot) { + if ($this->has_capability('manageallappointments')) { + return true; + } else if ($this->has_capability('manage')) { + return $this->userid == $slot->teacherid; + } else { + return false; + } + } + + /** + * can_edit_own_slots + * + * @return bool + */ + public function can_edit_own_slots() { + return $this->has_any_capability(['manage', 'manageallappointments']); + } + + /** + * can_edit_all_slots + * + * @return bool|mixed + */ + public function can_edit_all_slots() { + return $this->has_capability('manageallappointments'); + } + + /** + * can_see_all_slots + * + * @return bool + */ + public function can_see_all_slots() { + return $this->has_any_capability(['manageallappointments', 'canseeotherteachersbooking']); + } + + /** + * can_see_appointment + * + * @param \mod_scheduler\model\appointment $app + * @return bool + */ + public function can_see_appointment(\mod_scheduler\model\appointment $app) { + if ($this->has_any_capability(['manageallappointments', 'canseeotherteachersbooking'])) { + return true; + } else if ($this->has_capability('attend') && $this->userid == $app->get_slot()->teacherid) { + return true; + } else if ($this->has_capability('appoint') && $this->userid == $app->studentid) { + return true; + } else { + return false; + } + } + + /** + * can_edit_grade + * + * @param \mod_scheduler\model\appointment $app + * @return bool + */ + public function can_edit_grade(\mod_scheduler\model\appointment $app) { + if ($this->has_any_capability(['manageallappointments', 'editallgrades'])) { + return true; + } else { + return $this->userid == $app->get_slot()->teacherid; + } + } + + /** + * can_edit_attended + * + * @param \mod_scheduler\model\appointment $app + * @return bool + */ + public function can_edit_attended(\mod_scheduler\model\appointment $app) { + if ($this->has_any_capability(['manageallappointments', 'editallattended'])) { + return true; + } else { + return $this->userid == $app->get_slot()->teacherid; + } + } + + /** + * can_edit_notes + * + * @param \mod_scheduler\model\appointment $app + * @return bool + */ + public function can_edit_notes(\mod_scheduler\model\appointment $app) { + if ($this->has_any_capability(['manageallappointments', 'editallnotes'])) { + return true; + } else { + return $this->userid == $app->get_slot()->teacherid; + } + } + +} diff --git a/mod/scheduler/classes/privacy/provider.php b/mod/scheduler/classes/privacy/provider.php new file mode 100644 index 0000000..78ba7f2 --- /dev/null +++ b/mod/scheduler/classes/privacy/provider.php @@ -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/>. + +/** + * Privacy Subsystem implementation for mod_scheduler. + * + * @package mod_scheduler + * @copyright 2018 Henning Bostelmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\userlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\content_writer; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implementation of the privacy subsystem plugin provider for the scheduler activity module. + * + * @package mod_scheduler + * @copyright 2018 Henning Bostelmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This plugin stores personal data. + \core_privacy\local\metadata\provider, + + // This plugin is a core_user_data_provider. + \core_privacy\local\request\plugin\provider, + + \core_privacy\local\request\core_userlist_provider { + + /** @var mixed */ + private static $renderer; + + /** + * Return the fields which contain personal data. + * + * @param collection $collection a reference to the collection to use to store the metadata. + * @return collection the updated collection of metadata items. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_database_table( + 'scheduler_slots', + [ + 'teacherid' => 'privacy:metadata:scheduler_slots:teacherid', + 'starttime' => 'privacy:metadata:scheduler_slots:starttime', + 'duration' => 'privacy:metadata:scheduler_slots:duration', + 'appointmentlocation' => 'privacy:metadata:scheduler_slots:appointmentlocation', + 'notes' => 'privacy:metadata:scheduler_slots:notes', + 'notesformat' => 'privacy:metadata:scheduler_slots:notesformat', + 'exclusivity' => 'privacy:metadata:scheduler_slots:exclusivity' + // The fields "timemodified", "emaildate" and "hideuntil" do not contain personal data. + ], + 'privacy:metadata:scheduler_slots' + ); + $collection->add_database_table( + 'scheduler_appointment', + [ + 'studentid' => 'privacy:metadata:scheduler_appointment:studentid', + 'attended' => 'privacy:metadata:scheduler_appointment:attended', + 'grade' => 'privacy:metadata:scheduler_appointment:grade', + 'appointmentnote' => 'privacy:metadata:scheduler_appointment:appointmentnote', + 'appointmentnoteformat' => 'privacy:metadata:scheduler_appointment:appointmentnoteformat', + 'teachernote' => 'privacy:metadata:scheduler_appointment:teachernote', + 'teachernoteformat' => 'privacy:metadata:scheduler_appointment:teachernoteformat', + 'studentnote' => 'privacy:metadata:scheduler_appointment:studentnote', + 'studentnoteformat' => 'privacy:metadata:scheduler_appointment:studentnoteformat' + // The fields "timecreated" and "timemodifed" are technical only, they do not contain personal data. + ], + 'privacy:metadata:scheduler_appointment' + ); + + // Subsystems used. + $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid the userid. + * @return contextlist the list of contexts containing user info for the user. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new contextlist(); + + // Fetch all scheduler records for teachers. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {scheduler} s ON s.id = cm.instance + INNER JOIN {scheduler_slots} t ON t.schedulerid = s.id + WHERE t.teacherid = :userid"; + + $params = [ + 'modname' => 'scheduler', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + + // Fetch all scheduler records for students. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {scheduler} s ON s.id = cm.instance + INNER JOIN {scheduler_slots} t ON t.schedulerid = s.id + INNER JOIN {scheduler_appointment} a ON a.slotid = t.id + WHERE a.studentid = :userid"; + + $params = [ + 'modname' => 'scheduler', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + + $context = $userlist->get_context(); + + if (!is_a($context, \context_module::class)) { + return; + } + + // Fetch teachers. + $sql = "SELECT t.teacherid + FROM {course_modules} cm + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {scheduler} s ON s.id = cm.instance + INNER JOIN {scheduler_slots} t ON t.schedulerid = s.id + WHERE cm.id = :cmid"; + + $params = [ + 'modname' => 'scheduler', + 'cmid' => $context->instanceid + ]; + + $userlist->add_from_sql('teacherid', $sql, $params); + + // Fetch students. + $sql = "SELECT a.studentid + FROM {course_modules} cm + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {scheduler} s ON s.id = cm.instance + INNER JOIN {scheduler_slots} t ON t.schedulerid = s.id + INNER JOIN {scheduler_appointment} a ON a.slotid = t.id + WHERE cm.id = :cmid"; + + $params = [ + 'modname' => 'scheduler', + 'cmid' => $context->instanceid + ]; + + $userlist->add_from_sql('studentid', $sql, $params); + + return $userlist; + } + + /** + * Load a scheduler instance from a context. + * + * Will return null if the context was not found. + * + * @param \context $context the context of the scheduler. + * @return \mod_scheduler\model\scheduler scheduler object, or null if not found. + */ + private static function load_scheduler_for_context(\context $context) { + global $DB; + + if (!$context instanceof \context_module) { + return null; + } + + $sql = "SELECT s.id as schedulerid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {scheduler} s ON s.id = cm.instance + WHERE cm.id = :cmid"; + $params = ['cmid' => $context->instanceid, 'modname' => 'scheduler']; + $rec = $DB->get_record_sql($sql, $params); + if ($rec) { + return \mod_scheduler\model\scheduler::load_by_id($rec->schedulerid); + } else { + return null; + } + + } + + /** + * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + if (!$contextlist->count()) { + return; + } + + self::$renderer = new \mod_scheduler_renderer(); + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT cm.id AS cmid, s.name AS schedulername, s.id as schedulerid, cm.course AS courseid, + t.id as slotid, t.teacherid, t.starttime, t.duration, + t.appointmentlocation, t.notes, t.notesformat, t.exclusivity, + a.id as appointmentid, + a.studentid, a.attended, a.grade, + a.appointmentnote, a.appointmentnoteformat, + a.teachernote, a.teachernoteformat, + a.studentnote, a.studentnoteformat + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {scheduler} s ON s.id = cm.instance + JOIN {scheduler_slots} t ON t.schedulerid = s.id + JOIN {scheduler_appointment} a ON a.slotid = t.id + WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel + AND t.teacherid = :userid1 OR a.studentid = :userid2 + ORDER BY cm.id, t.id, a.id"; + $rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE, + 'modname' => 'scheduler', 'userid1' => $user->id, 'userid2' => $user->id]); + + $context = null; + $lastrow = null; + $scheduler = null; + foreach ($rs as $row) { + if (!$context || $context->instanceid != $row->cmid) { + // This row belongs to the different scheduler than the previous row. + // Export the data for the previous module. + self::export_scheduler($context, $user); + // Start new scheduler module. + $context = \context_module::instance($row->cmid); + $scheduler = \mod_scheduler\model\scheduler::load_by_id($row->schedulerid); + } + + if (!$lastrow || $row->slotid != $lastrow->slotid) { + // Export previous slot record. + self::export_slot($context, $user, $row); + } + self::export_appointment($context, $scheduler, $user, $row); + $lastrow = $row; + } + $rs->close(); + self::export_slot($context, $user, $lastrow); + self::export_scheduler($context, $user); + } + + /** + * format_note + * + * @param string $notetext + * @param int $noteformat + * @param string $filearea + * @param int $id + * @param \context $context + * @param content_writer $wrc + * @param string $exportarea + * @return string + */ + private static function format_note($notetext, $noteformat, $filearea, $id, + \context $context, content_writer $wrc, $exportarea) { + $message = $notetext; + if ($filearea) { + $message = $wrc->rewrite_pluginfile_urls($exportarea, 'mod_scheduler', $filearea, $id, $notetext); + } + $opts = (object) [ + 'para' => false, + 'context' => $context + ]; + $message = format_text($message, $noteformat, $opts); + return $message; + } + + /** + * Export one slot in a scheduler (one record in {scheduler_slots} table) + * + * @param \context $context + * @param \stdClass $user + * @param \stdClass $record + */ + protected static function export_slot($context, $user, $record) { + if (!$record) { + return; + } + $slotarea = ['slot '.$record->slotid]; + $wrc = writer::with_context($context); + + $data = [ + 'teacherid' => transform::user($record->teacherid), + 'starttime' => transform::datetime($record->starttime), + 'duration' => $record->duration, + 'appointmentlocation' => format_string($record->appointmentlocation), + 'notes' => self::format_note($record->notes, $record->notesformat, + 'slotnote', $record->slotid, $context, $wrc, $slotarea), + 'exclusivity' => $record->exclusivity, + ]; + + // Data about the slot. + $wrc->export_data($slotarea, (object)$data); + $wrc->export_area_files($slotarea, 'mod_scheduler', 'slotnote', $record->slotid); + } + + /** + * Export one appointment in a scheduler (one record in {scheduler_appointment} table) + * + * @param \context $context + * @param \mod_scheduler\model\scheduler $scheduler + * @param \stdClass $user + * @param \stdClass $record + */ + protected static function export_appointment($context, $scheduler, $user, $record) { + if (!$record) { + return; + } + $wrc = writer::with_context($context); + $apparea = ['slot '.$record->slotid, 'appointment '.$record->appointmentid]; + + $revealteachernote = ($user->id == $record->teacherid) || + get_config('mod_scheduler', 'revealteachernotes'); + + $data = [ + 'studentid' => transform::user($record->studentid), + 'attended' => transform::yesno($record->attended), + 'grade' => self::$renderer->format_grade($scheduler, $record->grade), + 'appointmentnote' => self::format_note($record->appointmentnote, $record->appointmentnoteformat, + 'appointmentnote', $record->appointmentid, $context, $wrc, $apparea), + 'studentnote' => self::format_note($record->studentnote, $record->studentnoteformat, + '', 0, $context, $wrc, $apparea), + ]; + if ($revealteachernote) { + $data['teachernote'] = self::format_note($record->teachernote, $record->teachernoteformat, + 'teachernote', $record->appointmentid, $context, $wrc, $apparea); + } + + // Data about the appointment. + + $wrc->export_data($apparea, (object)$data); + + $wrc->export_area_files($apparea, 'mod_scheduler', 'appointmentnote', $record->appointmentid); + if ($revealteachernote) { + $wrc->export_area_files($apparea, 'mod_scheduler', 'teachernote', $record->appointmentid); + } + $wrc->export_area_files($apparea, 'mod_scheduler', 'studentfiles', $record->appointmentid); + } + + /** + * Export basic info about a scheduler activity module + * + * @param \context $context + * @param \stdClass $user + */ + protected static function export_scheduler($context, $user) { + if (!$context) { + return; + } + $contextdata = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $contextdata); + } + + /** + * Delete all data for all users in the specified context. + * + * This will delete both slots and appointments for all users. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + if ($scheduler = self::load_scheduler_for_context($context)) { + $scheduler->delete_all_slots(); + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * This will delete only appointments where the specified user is a student. + * No data will be deleted if the user is (only) a teacher for the relevant slot/appointment, + * since deleting it may lose data for other users (namely, the students). + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + + if (empty($contextlist->count())) { + return; + } + + $user = $contextlist->get_user(); + + foreach ($contextlist->get_contexts() as $context) { + + if ($scheduler = self::load_scheduler_for_context($context)) { + $apps = $scheduler->get_appointments_for_student($user->id); + foreach ($apps as $app) { + $app->delete(); + } + } + } + } + + /** + * Delete all user data for the specified users (plural), in the specified context. + * + * This will delete only appointments where the specified user is a student. + * No data will be deleted if the user is (only) a teacher for the relevant slot/appointment, + * since deleting it may lose data for other users (namely, the students). + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + + $context = $userlist->get_context(); + $users = $userlist->get_userids(); + + if ($scheduler = self::load_scheduler_for_context($context)) { + foreach ($users as $userid) { + $apps = $scheduler->get_appointments_for_student($userid); + foreach ($apps as $app) { + $app->delete(); + } + } + } + } + +} diff --git a/mod/scheduler/classes/search/activity.php b/mod/scheduler/classes/search/activity.php new file mode 100644 index 0000000..1c10aff --- /dev/null +++ b/mod/scheduler/classes/search/activity.php @@ -0,0 +1,37 @@ +<?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/>. + +/** + * Search area for Scheduler activities. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\search; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Search area for mod_scheduler activities. + * + * This covers the activity description and intro section only. + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity extends \core_search\base_activity { +} diff --git a/mod/scheduler/classes/task/purge_unused_slots.php b/mod/scheduler/classes/task/purge_unused_slots.php new file mode 100644 index 0000000..f1fd35c --- /dev/null +++ b/mod/scheduler/classes/task/purge_unused_slots.php @@ -0,0 +1,52 @@ +<?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/>. + +/** + * Scheduled background task for sending automated appointment reminders + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\task; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Scheduled background task for sending automated appointment reminders + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class purge_unused_slots extends \core\task\scheduled_task { + /** + * get_name + * + * @return string + */ + public function get_name() { + return get_string('purgeunusedslots', 'mod_scheduler'); + } + + /** + * execute + */ + public function execute() { + \mod_scheduler\model\scheduler::free_late_unused_slots(); + } +} \ No newline at end of file diff --git a/mod/scheduler/classes/task/send_reminders.php b/mod/scheduler/classes/task/send_reminders.php new file mode 100644 index 0000000..6413eac --- /dev/null +++ b/mod/scheduler/classes/task/send_reminders.php @@ -0,0 +1,86 @@ +<?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/>. + +/** + * Scheduled background task for sending automated appointment reminders + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\task; + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/../../mailtemplatelib.php'); + +/** + * Scheduled background task for sending automated appointment reminders + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class send_reminders extends \core\task\scheduled_task { + + /** + * get_name + * + * @return string + */ + public function get_name() { + return get_string('sendreminders', 'mod_scheduler'); + } + + /** + * execute + */ + public function execute() { + + global $DB; + + $date = make_timestamp(date('Y'), date('m'), date('d'), date('H'), date('i')); + + // Find relevant slots in all schedulers. + $select = 'emaildate > 0 AND emaildate <= ? AND starttime > ?'; + $slots = $DB->get_records_select('scheduler_slots', $select, array($date, $date), 'starttime'); + + foreach ($slots as $slot) { + // Get teacher record. + $teacher = $DB->get_record('user', array('id' => $slot->teacherid)); + + // Get scheduler, slot and course. + $scheduler = \mod_scheduler\model\scheduler::load_by_id($slot->schedulerid); + $slotm = $scheduler->get_slot($slot->id); + $course = $scheduler->get_courserec(); + + // Mark as sent. (Do this first for safe fallback in case of an exception.) + $slot->emaildate = -1; + $DB->update_record('scheduler_slots', $slot); + + // Send reminder to all students in the slot. + foreach ($slotm->get_appointments() as $appointment) { + $student = $DB->get_record('user', array('id' => $appointment->studentid)); + cron_setup_user($student, $course); + \scheduler_messenger::send_slot_notification($slotm, + 'reminder', 'reminder', $teacher, $student, $teacher, $student, $course); + } + } + cron_setup_user(); + } + +} \ No newline at end of file diff --git a/mod/scheduler/customlib.php b/mod/scheduler/customlib.php new file mode 100644 index 0000000..200f256 --- /dev/null +++ b/mod/scheduler/customlib.php @@ -0,0 +1,79 @@ +<?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/>. + +/** + * Library with functions that are intended for local customizations. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Get a list of fields to be displayed in lists of users, etc. + * + * The input of the function is a user record; + * possibly null, in this case the function should return only the field titles. + * + * The function returns an array of objects that describe user data fields. + * Each of these objects has the following properties: + * $field->title : Displayable title of the field + * $field->value : Value of the field for this user (not set if $user is null) + * + * @param stdClass $user the user record; may be null + * @param context $context context for permission checks + * @return array an array of field objects + */ +function scheduler_get_user_fields($user, $context) { + + $fields = array(); + + if (has_capability('moodle/site:viewuseridentity', $context)) { + $emailfield = new stdClass(); + $fields[] = $emailfield; + $emailfield->title = get_string('email'); + if ($user) { + $emailfield->value = obfuscate_mailto($user->email); + } + } + + /* + * As an example: Uncomment the following lines in order to display the user's city and country. + */ + + /* + $cityfield = new stdClass(); + $cityfield->title = get_string('city'); + $fields[] = $cityfield; + + $countryfield = new stdClass(); + $countryfield->title = get_string('country'); + $fields[] = $countryfield; + + if ($user) { + $cityfield->value = $user->city; + if ($user->country) { + $countryfield->value = get_string($user->country, 'countries'); + } + else { + $countryfield->value = ''; + } + } + */ + return $fields; +} diff --git a/mod/scheduler/datelist.php b/mod/scheduler/datelist.php new file mode 100644 index 0000000..92098cc --- /dev/null +++ b/mod/scheduler/datelist.php @@ -0,0 +1,246 @@ +<?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/>. + +/** + * Shows a sortable list of appointments + * + * @package mod_scheduler + * @copyright 2015 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_docs_path('mod/scheduler/datelist'); + +$scope = optional_param('scope', 'activity', PARAM_TEXT); +if (!in_array($scope, array('activity', 'course', 'site'))) { + $scope = 'activity'; +} +$teacherid = optional_param('teacherid', 0, PARAM_INT); + +if ($scope == 'site') { + $scopecontext = context_system::instance(); +} else if ($scope == 'course') { + $scopecontext = context_course::instance($scheduler->courseid); +} else { + $scopecontext = $context; +} + +if (!has_capability('mod/scheduler:seeoverviewoutsideactivity', $context)) { + $scope = 'activity'; +} +if (!has_capability('mod/scheduler:canseeotherteachersbooking', $scopecontext)) { + $teacherid = 0; +} + +$taburl = new moodle_url('/mod/scheduler/view.php', + array('id' => $scheduler->cmid, 'what' => 'datelist', 'scope' => $scope, 'teacherid' => $teacherid)); +$returnurl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid)); + +$PAGE->set_url($taburl); + +echo $output->header(); + +// Print top tabs. + +echo $output->teacherview_tabs($scheduler, $permissions, $taburl, 'datelist'); + + +// Find active group in case that group mode is in use. +$currentgroupid = 0; +$groupmode = groups_get_activity_groupmode($scheduler->cm); +if ($groupmode) { + $currentgroupid = groups_get_activity_group($scheduler->cm, true); + + echo html_writer::start_div('dropdownmenu'); + groups_print_activity_menu($scheduler->cm, $taburl); + echo html_writer::end_div(); +} + +$scopemenukey = 'scopemenuself'; +if (has_capability('mod/scheduler:canseeotherteachersbooking', $scopecontext)) { + $teachers = $scheduler->get_available_teachers($currentgroupid); + $teachermenu = array(); + foreach ($teachers as $teacher) { + $teachermenu[$teacher->id] = fullname($teacher); + } + $select = $output->single_select($taburl, 'teacherid', $teachermenu, $teacherid, + array(0 => get_string('myself', 'scheduler')), 'teacheridform'); + echo html_writer::div(get_string('teachersmenu', 'scheduler', $select), 'dropdownmenu'); + $scopemenukey = 'scopemenu'; +} +if (has_capability('mod/scheduler:seeoverviewoutsideactivity', $context)) { + $scopemenu = array('activity' => get_string('thisscheduler', 'scheduler'), + 'course' => get_string('thiscourse', 'scheduler'), + 'site' => get_string('thissite', 'scheduler')); + $select = $output->single_select($taburl, 'scope', $scopemenu, $scope, null, 'scopeform'); + echo html_writer::div(get_string($scopemenukey, 'scheduler', $select), 'dropdownmenu'); +} + +// Getting date list. + +$params = array(); +$params['teacherid'] = $teacherid == 0 ? $USER->id : $teacherid; +$params['courseid'] = $scheduler->courseid; +$params['schedulerid'] = $scheduler->id; + +$scopecond = ''; +if ($scope == 'activity') { + $scopecond = ' AND sc.id = :schedulerid'; +} else if ($scope == 'course') { + $scopecond = ' AND c.id = :courseid'; +} + +$sql = "SELECT a.id AS id, ". + user_picture::fields('u1', array('email', 'department'), 'studentid', 'student').", ". + $DB->sql_fullname('u1.firstname', 'u1.lastname')." AS studentfullname, + a.appointmentnote, + a.appointmentnoteformat, + a.teachernote, + a.teachernoteformat, + a.grade, + sc.name, + sc.id AS schedulerid, + sc.scale, + c.shortname AS courseshort, + c.id AS courseid, ". + user_picture::fields('u2', null, 'teacherid').", + s.id AS sid, + s.starttime, + s.duration, + s.appointmentlocation, + s.notes, + s.notesformat + FROM {course} c, + {scheduler} sc, + {scheduler_appointment} a, + {scheduler_slots} s, + {user} u1, + {user} u2 + WHERE c.id = sc.course AND + sc.id = s.schedulerid AND + a.slotid = s.id AND + u1.id = a.studentid AND + u2.id = s.teacherid AND + s.teacherid = :teacherid ". + $scopecond; + +$sqlcount = + "SELECT COUNT(*) + FROM {course} c, + {scheduler} sc, + {scheduler_appointment} a, + {scheduler_slots} s + WHERE c.id = sc.course AND + sc.id = s.schedulerid AND + a.slotid = s.id AND + s.teacherid = :teacherid ". + $scopecond; + +$numrecords = $DB->count_records_sql($sqlcount, $params); + + +$limit = 30; + +if ($numrecords) { + + // Make the table of results. + + $coursestr = get_string('course', 'scheduler'); + $schedulerstr = get_string('scheduler', 'scheduler'); + $whenstr = get_string('when', 'scheduler'); + $wherestr = get_string('where', 'scheduler'); + $whostr = get_string('who', 'scheduler'); + $wherefromstr = get_string('department', 'scheduler'); + $whatstr = get_string('what', 'scheduler'); + $whatresultedstr = get_string('whatresulted', 'scheduler'); + $whathappenedstr = get_string('whathappened', 'scheduler'); + + $tablecolumns = array('courseshort', 'schedulerid', 'starttime', 'appointmentlocation', + 'studentfullname', 'studentdepartment', 'notes', 'grade', 'appointmentnote'); + $tableheaders = array($coursestr, $schedulerstr, $whenstr, $wherestr, + $whostr, $wherefromstr, $whatstr, $whatresultedstr, $whathappenedstr); + + $table = new flexible_table('mod-scheduler-datelist'); + $table->define_columns($tablecolumns); + $table->define_headers($tableheaders); + + $table->define_baseurl($taburl); + + $table->sortable(true, 'when'); // Sorted by date by default. + $table->collapsible(true); // Allow column hiding. + $table->initialbars(true); + + $table->column_suppress('courseshort'); + $table->column_suppress('schedulerid'); + $table->column_suppress('starttime'); + $table->column_suppress('studentfullname'); + $table->column_suppress('notes'); + + $table->set_attribute('id', 'dates'); + $table->set_attribute('class', 'datelist'); + + $table->column_class('course', 'datelist_course'); + $table->column_class('scheduler', 'datelist_scheduler'); + + $table->setup(); + + // Get extra query parameters from flexible_table behaviour. + $where = $table->get_sql_where(); + $sort = $table->get_sql_sort(); + $table->pagesize($limit, $numrecords); + + if (!empty($sort)) { + $sql .= " ORDER BY $sort"; + } + + $results = $DB->get_records_sql($sql, $params); + + foreach ($results as $id => $row) { + $courseurl = new moodle_url('/course/view.php', array('id' => $row->courseid)); + $coursedata = html_writer::link($courseurl, format_string($row->courseshort)); + $schedulerurl = new moodle_url('/mod/scheduler/view.php', array('a' => $row->schedulerid)); + $schedulerdata = html_writer::link($schedulerurl, format_string($row->name)); + $a = mod_scheduler_renderer::slotdatetime($row->starttime, $row->duration); + $whendata = get_string('slotdatetime', 'scheduler', $a); + $whourl = new moodle_url('/mod/scheduler/view.php', + array('what' => 'viewstudent', 'a' => $row->schedulerid, 'appointmentid' => $row->id)); + $whodata = html_writer::link($whourl, $row->studentfullname); + $whatdata = $output->format_notes($row->notes, $row->notesformat, $context, 'slotnote', $row->sid); + $gradedata = $row->scale == 0 ? '' : $output->format_grade($row->scale, $row->grade); + + $dataset = array( + $coursedata, + $schedulerdata, + $whendata, + format_string($row->appointmentlocation), + $whodata, + $row->studentdepartment, + $whatdata, + $gradedata, + $output->format_appointment_notes($scheduler, $row) ); + $table->add_data($dataset); + } + $table->print_html(); + echo $output->continue_button($returnurl); +} else { + notice(get_string('noresults', 'scheduler'), $returnurl); +} + +echo $output->footer(); \ No newline at end of file diff --git a/mod/scheduler/db/access.php b/mod/scheduler/db/access.php new file mode 100644 index 0000000..1547f4d --- /dev/null +++ b/mod/scheduler/db/access.php @@ -0,0 +1,193 @@ +<?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/>. + +/** + * Scheduler module capability definition + * + * @package mod_scheduler + * @copyright 2017 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'mod/scheduler:addinstance' => array( + 'riskbitmask' => RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ), + + 'mod/scheduler:appoint' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + ) + ), + + 'mod/scheduler:attend' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW + ) + ), + + 'mod/scheduler:manage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:manageallappointments' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:canscheduletootherteachers' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:canseeotherteachersbooking' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:seeoverviewoutsideactivity' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:editallattended' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:editallgrades' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:editallnotes' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:disengage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:viewslots' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + ), + 'clonepermissionsfrom' => 'mod/scheduler:appoint' + ), + + 'mod/scheduler:viewfullslots' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + ) + ), + + 'mod/scheduler:seeotherstudentsbooking' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/scheduler:seeotherstudentsresults' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'coursecreator' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ) + +); + + diff --git a/mod/scheduler/db/install.xml b/mod/scheduler/db/install.xml new file mode 100644 index 0000000..5df0139 --- /dev/null +++ b/mod/scheduler/db/install.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<XMLDB PATH="mod/scheduler/db" VERSION="20170205" COMMENT="XMLDB file for scheduler module" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" +> + <TABLES> + <TABLE NAME="scheduler" COMMENT="Scheduler instances"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="schedulermode" TYPE="char" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="maxbookings" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Maximum number of bookings for each student (depends on scheduler mode)"/> + <FIELD NAME="guardtime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="reuseguardtime" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="legacy"/> + <FIELD NAME="defaultslotduration" TYPE="int" LENGTH="4" NOTNULL="false" DEFAULT="15" SEQUENCE="false"/> + <FIELD NAME="allownotifications" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="staffrolename" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="scale" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="gradingstrategy" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="bookingrouping" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="-1" SEQUENCE="false" COMMENT="Groups can be scheduled from this grouping, -1 if none, 0 if all"/> + <FIELD NAME="usenotes" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Which types of notes to show for appointments"/> + <FIELD NAME="usebookingform" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="whether a booking form is used"/> + <FIELD NAME="bookinginstructions" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Text field with instructions for booking"/> + <FIELD NAME="bookinginstructionsformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="usestudentnotes" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="whether students can enter notes at time of booking"/> + <FIELD NAME="requireupload" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="whether students must upload files when booking"/> + <FIELD NAME="uploadmaxfiles" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Maximum number of files that a student can upload with a booking"/> + <FIELD NAME="uploadmaxsize" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Maximum file size for student uploads"/> + <FIELD NAME="usecaptcha" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether a CAPTCHA is used when students make a booking"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for scheduler"/> + </KEYS> + </TABLE> + <TABLE NAME="scheduler_slots" COMMENT="Scheduler slots"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="schedulerid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="starttime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="duration" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="teacherid" TYPE="int" LENGTH="11" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="appointmentlocation" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="reuse" TYPE="int" LENGTH="5" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="legacy"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="notes" TYPE="text" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="notesformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="exclusivity" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/> + <FIELD NAME="emaildate" TYPE="int" LENGTH="11" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="hideuntil" TYPE="int" LENGTH="11" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for scheduler_slots"/> + </KEYS> + <INDEXES> + <INDEX NAME="schedulerid-teacherid" UNIQUE="false" FIELDS="schedulerid, teacherid" COMMENT="By scheduler id, then teacher id"/> + </INDEXES> + </TABLE> + <TABLE NAME="scheduler_appointment" COMMENT="Scheduler appointments"> + <FIELDS> + <FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/> + <FIELD NAME="slotid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="studentid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="attended" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false"/> + <FIELD NAME="grade" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="appointmentnote" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Notes visible to teacher and student"/> + <FIELD NAME="appointmentnoteformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="teachernote" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Notes visible to teacher only"/> + <FIELD NAME="teachernoteformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="studentnote" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Note supplied by student at time of booking"/> + <FIELD NAME="studentnoteformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> + <FIELD NAME="timecreated" TYPE="int" LENGTH="11" NOTNULL="false" SEQUENCE="false"/> + <FIELD NAME="timemodified" TYPE="int" LENGTH="11" NOTNULL="false" SEQUENCE="false"/> + </FIELDS> + <KEYS> + <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for scheduler_appointment"/> + </KEYS> + <INDEXES> + <INDEX NAME="slotid" UNIQUE="false" FIELDS="slotid" COMMENT="By slot id"/> + <INDEX NAME="studentid" UNIQUE="false" FIELDS="studentid" COMMENT="By student id"/> + </INDEXES> + </TABLE> + </TABLES> +</XMLDB> \ No newline at end of file diff --git a/mod/scheduler/db/messages.php b/mod/scheduler/db/messages.php new file mode 100644 index 0000000..702fcf2 --- /dev/null +++ b/mod/scheduler/db/messages.php @@ -0,0 +1,41 @@ +<?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/>. + +/** + * Defines message providers (types of messages being sent) + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$messageproviders = array ( + + // Invitations to make a booking. + 'invitation' => array( + ), + + // Notifications about bookings (to teachers or students). + 'bookingnotification' => array( + ), + + // Automated reminders about upcoming appointments. + 'reminder' => array( + ), + +); diff --git a/mod/scheduler/db/tasks.php b/mod/scheduler/db/tasks.php new file mode 100644 index 0000000..891c8cd --- /dev/null +++ b/mod/scheduler/db/tasks.php @@ -0,0 +1,44 @@ +<?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/>. + +/** + * Scheduled background tasks in the scheduler module + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = array( + array( + 'classname' => 'mod_scheduler\task\send_reminders', + 'minute' => 'R', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'mod_scheduler\task\purge_unused_slots', + 'minute' => '*/5', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ) +); \ No newline at end of file diff --git a/mod/scheduler/db/upgrade.php b/mod/scheduler/db/upgrade.php new file mode 100644 index 0000000..bf9130e --- /dev/null +++ b/mod/scheduler/db/upgrade.php @@ -0,0 +1,341 @@ +<?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/>. + +/** + * Upgrade code for the scheduler module + * + * @package mod_scheduler + * @copyright 2017 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Migrate a configuration setting from global to plugin specific. + * + * @param string $name name of configuration setting + */ +function scheduler_migrate_config_setting($name) { + $oldval = get_config('core', 'scheduler_'.$name); + set_config($name, $oldval, 'mod_scheduler'); + unset_config('scheduler_'.$name); +} + +/** + * Migrate the group mode settings to new 2.9 conventions. + * + * @param int $sid id of the scheduler to migrate + */ +function scheduler_migrate_groupmode($sid) { + global $DB; + $globalenable = (bool) get_config('mod_scheduler', 'groupscheduling'); + $cm = get_coursemodule_from_instance('scheduler', $sid, 0, false, IGNORE_MISSING); + if ($cm) { + if ((groups_get_activity_groupmode($cm) > 0) && $globalenable) { + $g = $cm->groupingid; + } else { + $g = -1; + } + $DB->set_field('scheduler', 'bookingrouping', $g, array('id' => $sid)); + $DB->set_field('course_modules', 'groupmode', 0, array('id' => $cm->id)); + $DB->set_field('course_modules', 'groupingid', 0, array('id' => $cm->id)); + } +} + +/** + * This function does anything necessary to upgrade older versions to match current functionality. + * + * @param int $oldversion version number to be migrated from + * @return bool true if upgrade is successful + */ +function xmldb_scheduler_upgrade($oldversion=0) { + + global $CFG, $DB; + + $dbman = $DB->get_manager(); + + $result = true; + + /* ******************* 2.0 upgrade line ********************** */ + + if ($oldversion < 2011081302) { + + // Rename description field to intro, and define field introformat to be added to scheduler. + $table = new xmldb_table('scheduler'); + $introfield = new xmldb_field('description', XMLDB_TYPE_TEXT, 'small', null, XMLDB_NOTNULL, null, null, 'name'); + $dbman->rename_field($table, $introfield, 'intro', false); + + $formatfield = new xmldb_field('introformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'intro'); + + if (!$dbman->field_exists($table, $formatfield)) { + $dbman->add_field($table, $formatfield); + } + + // Conditionally migrate to html format in intro. + if ($CFG->texteditors !== 'textarea') { + $rs = $DB->get_recordset('scheduler', array('introformat' => FORMAT_MOODLE), + '', 'id, intro, introformat'); + foreach ($rs as $q) { + $q->intro = text_to_html($q->intro, false, false, true); + $q->introformat = FORMAT_HTML; + $DB->update_record('scheduler', $q); + upgrade_set_timeout(); + } + $rs->close(); + } + + // Savepoint reached. + upgrade_mod_savepoint(true, 2011081302, 'scheduler'); + } + + /* ******************* 2.5 upgrade line ********************** */ + + if ($oldversion < 2012102903) { + + // Define fields notesformat and appointmentnote in respective tables. + $table = new xmldb_table('scheduler_slots'); + $formatfield = new xmldb_field('notesformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'notes'); + if (!$dbman->field_exists($table, $formatfield)) { + $dbman->add_field($table, $formatfield); + } + + $table = new xmldb_table('scheduler_appointment'); + $formatfield = new xmldb_field('appointmentnoteformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, + XMLDB_NOTNULL, null, '0', 'appointmentnote'); + if (!$dbman->field_exists($table, $formatfield)) { + $dbman->add_field($table, $formatfield); + } + + // Migrate html format. + if ($CFG->texteditors !== 'textarea') { + upgrade_set_timeout(); + $DB->set_field('scheduler_slots', 'notesformat', FORMAT_HTML); + $DB->set_field('scheduler_appointment', 'appointmentnoteformat', FORMAT_HTML); + } + + // Savepoint reached. + upgrade_mod_savepoint(true, 2012102903, 'scheduler'); + } + + /* ******************* 2.7 upgrade line ********************** */ + + if ($oldversion < 2014071300) { + + // Define field teacher to be dropped from scheduler. + $table = new xmldb_table('scheduler'); + $field = new xmldb_field('teacher'); + + // Conditionally drop field teacher. + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Define field maxbookings to be added to scheduler. + $table = new xmldb_table('scheduler'); + $field = new xmldb_field('maxbookings', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '1', 'schedulermode'); + + // Conditionally launch add field maxbookings. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field guardtime to be added to scheduler. + $table = new xmldb_table('scheduler'); + $field = new xmldb_field('guardtime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'maxbookings'); + + // Conditionally launch add field guardtime. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Changing length of field staffrolename on table scheduler to (255). + $table = new xmldb_table('scheduler'); + $field = new xmldb_field('staffrolename', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'allownotifications'); + + // Launch change of precision for field staffrolename. + $dbman->change_field_precision($table, $field); + + // Changing length of field appointmentlocation on table scheduler_slots to (255). + $table = new xmldb_table('scheduler_slots'); + $field = new xmldb_field('appointmentlocation', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'teacherid'); + + // Launch change of precision for field appointmentlocation. + $dbman->change_field_precision($table, $field); + + // Define index schedulerid-teacherid (not unique) to be added to scheduler_slots. + $table = new xmldb_table('scheduler_slots'); + $index = new xmldb_index('schedulerid-teacherid', XMLDB_INDEX_NOTUNIQUE, array('schedulerid', 'teacherid')); + + // Conditionally launch add index schedulerid-teacherid. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Define index slotid (not unique) to be added to scheduler_appointment. + $table = new xmldb_table('scheduler_appointment'); + $index = new xmldb_index('slotid', XMLDB_INDEX_NOTUNIQUE, array('slotid')); + + // Conditionally add index slotid. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Define index studentid (not unique) to be added to scheduler_appointment. + $table = new xmldb_table('scheduler_appointment'); + $index = new xmldb_index('studentid', XMLDB_INDEX_NOTUNIQUE, array('studentid')); + + // Conditionally add index studentid. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Convert old calendar events. + $sql = 'UPDATE {event} SET modulename = ? WHERE eventtype LIKE ? OR eventtype LIKE ?'; + $DB->execute($sql, array('scheduler', 'SSsup:%', 'SSstu:%')); + + // Savepoint reached. + upgrade_mod_savepoint(true, 2014071300, 'scheduler'); + } + + /* ******************* 2.9 upgrade line ********************** */ + + if ($oldversion < 2015050400) { + + // Migrate config settings to config_plugins table. + scheduler_migrate_config_setting('allteachersgrading'); + scheduler_migrate_config_setting('showemailplain'); + scheduler_migrate_config_setting('groupscheduling'); + scheduler_migrate_config_setting('maxstudentlistsize'); + + // Savepoint reached. + upgrade_mod_savepoint(true, 2015050400, 'scheduler'); + } + + if ($oldversion < 2015062601) { + + // Define field bookingrouping to be added to scheduler. + $table = new xmldb_table('scheduler'); + $field = new xmldb_field('bookingrouping', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '-1', 'gradingstrategy'); + + // Conditionally launch add field bookingrouping. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Convert old group mode into instance setting for scheduler. + $sids = $DB->get_fieldset_select('scheduler', 'id', ''); + foreach ($sids as $sid) { + scheduler_migrate_groupmode($sid); + } + + // Scheduler savepoint reached. + upgrade_mod_savepoint(true, 2015062601, 'scheduler'); + } + + /* ******************* 3.1 upgrade line ********************** */ + + if ($oldversion < 2016051700) { + + // Add configuration field "usenotes" to scheduler table. + $table = new xmldb_table('scheduler'); + $field = new xmldb_field('usenotes', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1', 'bookingrouping'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add field "teachernote" (note visible to teachers only) and corresponding format field to scheduler_appointment. + $table = new xmldb_table('scheduler_appointment'); + $field1 = new xmldb_field('teachernote', XMLDB_TYPE_TEXT, null, null, null, null, null, 'appointmentnoteformat'); + if (!$dbman->field_exists($table, $field1)) { + $dbman->add_field($table, $field1); + } + $field2 = new xmldb_field('teachernoteformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'teachernote'); + if (!$dbman->field_exists($table, $field2)) { + $dbman->add_field($table, $field2); + } + + // Drop old unused field "appointmentnote" from scheduler_slots table. + $table = new xmldb_table('scheduler_slots'); + $field = new xmldb_field('appointmentnote'); + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Scheduler savepoint reached. + upgrade_mod_savepoint(true, 2016051700, 'scheduler'); + } + + /* ******************* 3.3 upgrade line ********************** */ + + if ($oldversion < 2017040100) { + + // Add new configuration fields (relating to booking form) to scheduler. + $table = new xmldb_table('scheduler'); + + $field = new xmldb_field('usebookingform', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'usenotes'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('bookinginstructions', XMLDB_TYPE_TEXT, null, null, null, null, null, 'usebookingform'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('bookinginstructionsformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, + null, '1', 'bookinginstructions'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('usestudentnotes', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, + null, '0', 'bookinginstructionsformat'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('requireupload', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'usestudentnotes'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('uploadmaxfiles', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'requireupload'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('uploadmaxsize', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'uploadmaxfiles'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + $field = new xmldb_field('usecaptcha', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'uploadmaxsize'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Add field "studentnote" and corresponding format field to scheduler_appointment. + $table = new xmldb_table('scheduler_appointment'); + $field1 = new xmldb_field('studentnote', XMLDB_TYPE_TEXT, null, null, null, null, null, 'teachernoteformat'); + if (!$dbman->field_exists($table, $field1)) { + $dbman->add_field($table, $field1); + } + $field2 = new xmldb_field('studentnoteformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'studentnote'); + if (!$dbman->field_exists($table, $field2)) { + $dbman->add_field($table, $field2); + } + + // Scheduler savepoint reached. + upgrade_mod_savepoint(true, 2017040100, 'scheduler'); + } + return true; +} \ No newline at end of file diff --git a/mod/scheduler/export.php b/mod/scheduler/export.php new file mode 100644 index 0000000..c3dea46 --- /dev/null +++ b/mod/scheduler/export.php @@ -0,0 +1,138 @@ +<?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/>. + +/** + * Export scheduler data to a file. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(__FILE__).'/exportform.php'); + +$PAGE->set_docs_path('mod/scheduler/export'); + +// Find active group in case that group mode is in use. +$currentgroupid = 0; +$groupmode = groups_get_activity_groupmode($scheduler->cm); +if ($groupmode) { + $currentgroupid = groups_get_activity_group($scheduler->cm, true); +} + +$actionurl = new moodle_url('/mod/scheduler/view.php', array('what' => 'export', 'id' => $scheduler->cmid)); +$returnurl = new moodle_url('/mod/scheduler/view.php', array('what' => 'view', 'id' => $scheduler->cmid)); +$PAGE->set_url($actionurl); +$mform = new scheduler_export_form($actionurl, $scheduler); + +if ($mform->is_cancelled()) { + redirect($returnurl); +} + +$data = $mform->get_data(); +if ($data) { + $availablefields = scheduler_get_export_fields($scheduler); + $selectedfields = array(); + foreach ($availablefields as $field) { + $inputid = 'field-'.$field->get_id(); + if (isset($data->{$inputid}) && $data->{$inputid} == 1) { + $selectedfields[] = $field; + $field->set_renderer($output); + } + } + $userid = $USER->id; + if (isset($data->includewhom) && $data->includewhom == 'all') { + $permissions->ensure($permissions->can_see_all_slots()); + $userid = 0; + } + $pageperteacher = isset($data->paging) && $data->paging == 'perteacher'; + $preview = isset($data->preview); +} else { + $preview = false; +} + +if (!$data || $preview) { + echo $OUTPUT->header(); + + // Print top tabs. + $taburl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid, 'what' => 'export')); + echo $output->teacherview_tabs($scheduler, $permissions, $taburl, 'export'); + + if ($groupmode) { + groups_print_activity_menu($scheduler->cm, $taburl); + } + + echo $output->heading(get_string('exporthdr', 'scheduler'), 2); + + $mform->display(); + + if ($preview) { + $canvas = new scheduler_html_canvas(); + $export = new scheduler_export($canvas); + + $export->build($scheduler, + $selectedfields, + $data->content, + $userid, + $currentgroupid, + $data->timerange, + $data->includeemptyslots, + $pageperteacher); + + $limit = 20; + echo $canvas->as_html($limit, false); + + echo html_writer::div(get_string('previewlimited', 'scheduler', $limit), 'previewlimited'); + } + + echo $output->footer(); + exit(); +} + +switch ($data->outputformat) { + case 'csv': + $canvas = new scheduler_csv_canvas($data->csvseparator); + break; + case 'xls': + $canvas = new scheduler_excel_canvas(); + break; + case 'ods': + $canvas = new scheduler_ods_canvas(); + break; + case 'html': + $canvas = new scheduler_html_canvas($returnurl); + break; + case 'pdf': + $canvas = new scheduler_pdf_canvas($data->pdforientation); + break; +} + +$export = new scheduler_export($canvas); + +$export->build($scheduler, + $selectedfields, + $data->content, + $userid, + $currentgroupid, + $data->timerange, + $data->includeemptyslots, + $pageperteacher); + +$filename = clean_filename(format_string($course->shortname).'_'.format_string($scheduler->name)); +$canvas->send($filename); + diff --git a/mod/scheduler/exportform.php b/mod/scheduler/exportform.php new file mode 100644 index 0000000..e092eec --- /dev/null +++ b/mod/scheduler/exportform.php @@ -0,0 +1,191 @@ +<?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/>. + +/** + * Export settings form + * + * @package mod_scheduler + * @copyright 2015 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; + +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/mod/scheduler/exportlib.php'); + +/** + * Export settings form (using Moodle formslib) + * + * @package mod_scheduler + * @copyright 2015 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_export_form extends moodleform { + + /** + * @var scheduler the scheduler to be exported + */ + protected $scheduler; + + /** + * Create a new export settings form. + * + * @param string $action + * @param scheduler $scheduler the scheduler to export + * @param object $customdata + */ + public function __construct($action, scheduler $scheduler, $customdata=null) { + $this->scheduler = $scheduler; + parent::__construct($action, $customdata); + } + + /** + * Form definition + */ + protected function definition() { + + $mform = $this->_form; + + // General introduction. + $mform->addElement('header', 'general', get_string('general', 'form')); + + $radios = array(); + $radios[] = $mform->createElement('radio', 'content', '', + get_string('onelineperslot', 'scheduler'), 'onelineperslot'); + $radios[] = $mform->createElement('radio', 'content', '', + get_string('onelineperappointment', 'scheduler'), 'onelineperappointment'); + $radios[] = $mform->createElement('radio', 'content', '', + get_string('appointmentsgrouped', 'scheduler'), 'appointmentsgrouped'); + $mform->addGroup($radios, 'contentgroup', + get_string('contentformat', 'scheduler'), null, false); + $mform->setDefault('content', 'onelineperappointment'); + $mform->addHelpButton('contentgroup', 'contentformat', 'scheduler'); + + if (has_capability('mod/scheduler:canseeotherteachersbooking', $this->scheduler->get_context())) { + $selopt = array('me' => get_string('myself', 'scheduler'), + 'all' => get_string ('everyone', 'scheduler')); + $mform->addElement('select', 'includewhom', get_string('includeslotsfor', 'scheduler'), $selopt); + $mform->setDefault('includewhom', 'all'); + + $selopt = array('all' => get_string('allononepage', 'scheduler'), + 'perteacher' => get_string('pageperteacher', 'scheduler', $this->scheduler->get_teacher_name()) ); + $mform->addElement('select', 'paging', get_string('pagination', 'scheduler'), $selopt); + $mform->addHelpButton('paging', 'pagination', 'scheduler'); + + } + + $timeoptions = [ + 0 => get_string('exporttimerangeall', 'scheduler'), + 1 => get_string('exporttimerangefuture', 'scheduler'), + 2 => get_string('exporttimerangepast', 'scheduler') + ]; + $mform->addElement('select', 'timerange', get_string('exporttimerange', 'scheduler'), $timeoptions); + $mform->setDefault('timerange', 0); + + $mform->addElement('selectyesno', 'includeemptyslots', get_string('includeemptyslots', 'scheduler')); + $mform->setDefault('includeemptyslots', 1); + + // Select data to export. + $mform->addElement('header', 'datafieldhdr', get_string('datatoinclude', 'scheduler')); + $mform->addHelpButton('datafieldhdr', 'datatoinclude', 'scheduler'); + + $this->add_exportfield_group('slot', 'slot'); + $this->add_exportfield_group('student', 'student'); + $this->add_exportfield_group('appointment', 'appointment'); + + $mform->setDefault('field-date', 1); + $mform->setDefault('field-starttime', 1); + $mform->setDefault('field-endtime', 1); + $mform->setDefault('field-teachername', 1); + $mform->setDefault('field-studentfullname', 1); + $mform->setDefault('field-attended', 1); + + // Output file format. + $mform->addElement('header', 'fileformathdr', get_string('fileformat', 'scheduler')); + $mform->addHelpButton('fileformathdr', 'fileformat', 'scheduler'); + + $radios = array(); + $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('csvformat', 'scheduler'), 'csv'); + $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('excelformat', 'scheduler'), 'xls'); + $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('odsformat', 'scheduler'), 'ods'); + $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('htmlformat', 'scheduler'), 'html'); + $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('pdfformat', 'scheduler'), 'pdf'); + $mform->addGroup($radios, 'outputformatgroup', get_string('fileformat', 'scheduler'), null, false); + $mform->setDefault('outputformat', 'csv'); + + $selopt = array('comma' => get_string('sepcomma', 'scheduler'), + 'colon' => get_string('sepcolon', 'scheduler'), + 'semicolon' => get_string('sepsemicolon', 'scheduler'), + 'tab' => get_string('septab', 'scheduler')); + $mform->addElement('select', 'csvseparator', get_string('csvfieldseparator', 'scheduler'), $selopt); + $mform->setDefault('csvseparator', 'comma'); + $mform->disabledIf('csvseparator', 'outputformat', 'neq', 'csv'); + + $selopt = array('P' => get_string('portrait', 'scheduler'), + 'L' => get_string('landscape', 'scheduler')); + $mform->addElement('select', 'pdforientation', get_string('pdforientation', 'scheduler'), $selopt); + $mform->disabledIf('pdforientation', 'outputformat', 'neq', 'pdf'); + + $buttonarray = array(); + $buttonarray[] = $mform->createElement('submit', 'preview', get_string('preview', 'scheduler')); + $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('createexport', 'scheduler')); + $buttonarray[] = $mform->createElement('cancel'); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('buttonar'); + + } + + /** + * Add a group of export fields to the form. + * + * @param string $groupid id of the group in the list of fields + * @param string $labelid language string id for the group label + */ + private function add_exportfield_group($groupid, $labelid) { + + $mform = $this->_form; + $fields = scheduler_get_export_fields($this->scheduler); + $checkboxes = array(); + + foreach ($fields as $field) { + if ($field->get_group() == $groupid && $field->is_available($this->scheduler)) { + $inputid = 'field-'.$field->get_id(); + $label = $field->get_formlabel($this->scheduler); + $checkboxes[] = $mform->createElement('checkbox', $inputid, '', $label); + } + } + $grouplabel = get_string($labelid, 'scheduler'); + $mform->addGroup($checkboxes, 'fields-'.$groupid, $grouplabel, null, false); + } + + /** + * 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); + + return $errors; + } + +} diff --git a/mod/scheduler/exportlib.php b/mod/scheduler/exportlib.php new file mode 100644 index 0000000..81a96b6 --- /dev/null +++ b/mod/scheduler/exportlib.php @@ -0,0 +1,2226 @@ +<?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/>. + +/** + * Library for export functions + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; +use \mod_scheduler\model\appointment; + +require_once($CFG->dirroot.'/lib/excellib.class.php'); +require_once($CFG->dirroot.'/lib/odslib.class.php'); +require_once($CFG->dirroot.'/lib/csvlib.class.php'); +require_once($CFG->dirroot.'/lib/pdflib.php'); +require_once($CFG->dirroot.'/user/profile/lib.php'); + + +/** + * A data field included in an export from Scheduler. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class scheduler_export_field { + + /** @var mixed */ + protected $renderer; + + /** + * set_renderer + * + * @param mod_scheduler_renderer $renderer + */ + public function set_renderer(mod_scheduler_renderer $renderer) { + $this->renderer = $renderer; + } + + /** + * Is the field available in this scheduler? + * + * @param scheduler $scheduler + * @return bool whether the field is available + */ + public function is_available(scheduler $scheduler) { + return true; + } + + /** + * Retrieve the unique id (a string) for this field + */ + public abstract function get_id(); + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public abstract function get_group(); + + /** + * Retrieve the header (in the sense of table header in the output) + * used for this field. + * + * @param scheduler $scheduler the scheduler instance in question + * @return string the header for this field + */ + public function get_header(scheduler $scheduler) { + return get_string('field-'.$this->get_id(), 'scheduler'); + } + + /** + * Retrieve the header (in the sense of table header in the output) as an array. + * Needs to be overridden for multi-column fields only. + * + * @param scheduler $scheduler the scheduler instance in question + * @return array the header for this field + */ + public function get_headers(scheduler $scheduler) { + return array($this->get_header($scheduler)); + } + + /** + * Retrieve the label used in the configuration form to label this field. + * By default, this equals the table header. + * + * @param scheduler $scheduler the scheduler instance in question + * @return string the form label for this field + */ + public function get_formlabel(scheduler $scheduler) { + return $this->get_header($scheduler); + } + + /** + * Retrieves the numer of table columns used by this field (typically 1). + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the number of columns used + */ + public function get_num_columns(scheduler $scheduler) { + return 1; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return strlen($this->get_formlabel($scheduler)); + } + + /** + * Does this field use wrapped text? + * + * @return bool whether wrapping is used for this field + */ + public function is_wrapping() { + return false; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public abstract function get_value(slot $slot, $appointment); + + /** + * Retrieve the value of this field as an array. + * Needs to be overriden for multi-column fields only. + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return array an array of strings containing the column values + */ + public function get_values(slot $slot, $appointment) { + return array($this->get_value($slot, $appointment)); + } + +} + +/** + * Get a list of all export fields available. + * + * @param scheduler $scheduler + * @return array the fields as an array of scheduler_export_field objects. + */ +function scheduler_get_export_fields(scheduler $scheduler) { + $result = array(); + $result[] = new slotdate_field(); + $result[] = new scheduler_starttime_field(); + $result[] = new scheduler_endtime_field(); + $result[] = new scheduler_location_field(); + $result[] = new scheduler_teachername_field(); + $result[] = new scheduler_maxstudents_field(); + $result[] = new slotnotes_field(); + + $result[] = new scheduler_student_field('studentfullname', 'fullname', 25); + $result[] = new scheduler_student_field('studentfirstname', 'firstname'); + $result[] = new scheduler_student_field('studentlastname', 'lastname'); + $result[] = new scheduler_student_field('studentemail', 'email', 0, true); + $result[] = new scheduler_student_field('studentusername', 'username'); + $result[] = new scheduler_student_field('studentidnumber', 'idnumber', 0, true); + + $pfields = profile_get_custom_fields(); + foreach ($pfields as $id => $field) { + $type = $field->datatype; + $result[] = new scheduler_profile_field('profile_'.$type, $id, $type); + } + + $result[] = new scheduler_groups_single_field(); + $result[] = new scheduler_groups_multi_field($scheduler); + + $result[] = new scheduler_attended_field(); + $result[] = new scheduler_grade_field(); + $result[] = new scheduler_appointmentnote_field(); + $result[] = new scheduler_teachernote_field(); + $result[] = new scheduler_studentnote_field(); + $result[] = new scheduler_filecount_field(); + + return $result; +} + + +/** + * Export field: Date of the slot + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slotdate_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'date'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return strlen(mod_scheduler_renderer::userdate(1)) + 3; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return mod_scheduler_renderer::userdate($slot->starttime); + } +} + +/** + * Export field: Start time of the slot + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_starttime_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'starttime'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return mod_scheduler_renderer::usertime($slot->starttime); + } + +} + + +/** + * Export field: End time of the slot + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_endtime_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'endtime'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return mod_scheduler_renderer::usertime($slot->endtime); + } + +} + +/** + * Export field: Full name of the teacher + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_teachername_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'teachername'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the header (in the sense of table header in the output) + * used for this field. + * + * @param scheduler $scheduler the scheduler instance in question + * @return string the header for this field + */ + public function get_header(scheduler $scheduler) { + return $scheduler->get_teacher_name(); + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return 20; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return fullname($slot->teacher); + } + +} + +/** + * Export field: Appointment location + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_location_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'location'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return format_string($slot->appointmentlocation); + } + +} + +/** + * Export field: Maximum number of students / appointments in the slot + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_maxstudents_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'maxstudents'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if ($slot->exclusivity <= 0) { + return get_string('unlimited', 'scheduler'); + } else { + return $slot->exclusivity; + } + } + +} + +/** + * Export field: A field in the student record (to be chosen via the constructor) + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_student_field extends scheduler_export_field { + + /** @var mixed */ + protected $id; + /** @var mixed */ + protected $studfield; + /** @var mixed */ + protected $typicalwidth; + /** @var mixed */ + protected $idfield; + + /** + * scheduler_student_field constructor. + * + * @param int $id + * @param mixed $studfield + * @param int $typicalwidth + * @param bool $idfield + */ + public function __construct($id, $studfield, $typicalwidth = 0, $idfield = false) { + $this->id = $id; + $this->studfield = $studfield; + $this->typicalwidth = $typicalwidth; + $this->idfield = $idfield; + } + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return $this->id; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'student'; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + if (!$this->idfield) { + return true; + } + $ctx = $scheduler->get_context(); + return has_capability('moodle/site:viewuseridentity', $ctx); + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + if ($this->typicalwidth > 0) { + return $this->typicalwidth; + } else { + return parent::get_typical_width($scheduler); + } + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + $student = $appointment->get_student(); + if (is_null($student)) { + return ''; + } + if ($this->studfield == 'fullname') { + return fullname($student); + } else { + return $student->{$this->studfield}; + } + } + +} + +/** + * Export field: A cutom profile field in the student record + * + * @package mod_scheduler + * @copyright 2017 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_profile_field extends scheduler_export_field { + + /** @var mixed */ + protected $id; + /** @var mixed */ + protected $field; + + /** + * Create a new export entry for a custom profile field. + * + * @param string $id the id of the field (for internal use) + * @param int $fieldid id of the field in the database table + * @param string $type data type of profile field to add + */ + public function __construct($id, $fieldid, $type) { + global $CFG; + + $this->id = $id; + require_once($CFG->dirroot.'/user/profile/field/'.$type.'/field.class.php'); + $fieldclass = 'profile_field_'.$type; + $fieldobj = new $fieldclass($fieldid, 0); + $this->field = $fieldobj; + } + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return $this->id; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'student'; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool|mixed + */ + public function is_available(scheduler $scheduler) { + return $this->field->is_visible(); + } + + /** + * Retrieve the header (in the sense of table header in the output) + * used for this field. + * + * @param scheduler $scheduler the scheduler instance in question + * @return string the header for this field + */ + public function get_header(scheduler $scheduler) { + return format_string($this->field->field->name); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (!$appointment instanceof appointment || $appointment->studentid == 0) { + return ''; + } + $this->field->set_userid($appointment->studentid); + $this->field->load_data(); + if ($this->field->is_visible()) { + $content = $this->field->display_data(); + return strip_tags($content); + } + return ''; + } + +} + + +/** + * Export field: Whether the appointment has been attended + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_attended_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'attended'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'appointment'; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + $str = $appointment->is_attended() ? get_string('yes') : get_string('no'); + return $str; + } + +} + +/** + * Export field: Slot notes + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slotnotes_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'slotnotes'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'slot'; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return 30; + } + + /** + * Does this field use wrapped text? + * + * @return bool whether wrapping is used for this field + */ + public function is_wrapping() { + return true; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return strip_tags($slot->notes); + } + +} + +/** + * Export field: Appointment notes + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_appointmentnote_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'appointmentnote'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'appointment'; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return 30; + } + + /** + * Does this field use wrapped text? + * + * @return bool whether wrapping is used for this field + */ + public function is_wrapping() { + return true; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + return $scheduler->uses_appointmentnotes(); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + return strip_tags($appointment->appointmentnote); + } + +} + +/** + * Export field: Teacher notes + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_teachernote_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'teachernote'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'appointment'; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return 30; + } + + /** + * Does this field use wrapped text? + * + * @return bool whether wrapping is used for this field + */ + public function is_wrapping() { + return true; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + return $scheduler->uses_teachernotes(); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + return strip_tags($appointment->teachernote); + } + +} + +/** + * Export field: Student-provided notes + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_studentnote_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'studentnote'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'appointment'; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return 30; + } + + /** + * Does this field use wrapped text? + * + * @return bool whether wrapping is used for this field + */ + public function is_wrapping() { + return true; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + return $scheduler->uses_studentnotes(); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + return strip_tags($appointment->studentnote); + } + +} + +/** + * Export field: Number of student-provided files + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_filecount_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'filecount'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'appointment'; + } + + /** + * Retrieve the typical width (in characters) of this field. + * This is used to set the width of columns in the output, where this is relevant. + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the width of this field (number of characters per column) + */ + public function get_typical_width(scheduler $scheduler) { + return 2; + } + + /** + * Does this field use wrapped text? + * + * @return bool whether wrapping is used for this field + */ + public function is_wrapping() { + return false; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + return $scheduler->uses_studentfiles(); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + return $appointment->count_studentfiles(); + } + +} + +/** + * Export field: Grade for the appointment + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_grade_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'grade'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'appointment'; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + return $scheduler->uses_grades(); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + return $this->renderer->format_grade($slot->get_scheduler(), $appointment->grade); + } + +} + +/** + * Export field: Student groups (in one column) + * + * @package mod_scheduler + * @copyright 2018 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_groups_single_field extends scheduler_export_field { + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'groupssingle'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'student'; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + $g = groups_get_all_groups($scheduler->courseid, 0, $scheduler->get_cm()->groupingid); + return count($g) > 0; + } + + /** + * Retrieve the label used in the configuration form to label this field. + * By default, this equals the table header. + * + * @param scheduler $scheduler the scheduler instance in question + * @return string the form label for this field + */ + public function get_formlabel(scheduler $scheduler) { + return get_string('field-groupssingle-label', 'scheduler'); + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + $scheduler = $slot->get_scheduler(); + $groups = groups_get_user_groups($scheduler->courseid, $appointment->studentid); + $groupingid = $scheduler->get_cm()->groupingid; + $gn = array(); + foreach ($groups[$groupingid] as $groupid) { + $gn[] = groups_get_group_name($groupid); + } + return implode(',', $gn); + } + +} + +/** + * Export field: Student groups (in several columns) + * + * @package mod_scheduler + * @copyright 2018 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_groups_multi_field extends scheduler_export_field { + + /** @var mixed */ + protected $coursegroups; + + /** + * scheduler_groups_multi_field constructor. + * + * @param scheduler $scheduler + */ + public function __construct(scheduler $scheduler) { + $this->coursegroups = groups_get_all_groups($scheduler->courseid, 0, $scheduler->get_cm()->groupingid); + } + + /** + * Retrieve the unique id (a string) for this field + */ + public function get_id() { + return 'groupsmulti'; + } + + /** + * Retrieve the group that this field belongs to - + * either 'slot' or 'student' or 'appointment', + * + * @return string the group id as above + */ + public function get_group() { + return 'student'; + } + + /** + * is_available + * + * @param scheduler $scheduler + * @return bool + */ + public function is_available(scheduler $scheduler) { + return count($this->coursegroups) > 0; + } + + /** + * Retrieves the numer of table columns used by this field (typically 1). + * + * @param scheduler $scheduler the scheduler instance in question + * @return int the number of columns used + */ + public function get_num_columns(scheduler $scheduler) { + return count($this->coursegroups); + } + + /** + * Retrieve the header (in the sense of table header in the output) as an array. + * Needs to be overridden for multi-column fields only. + * + * @param scheduler $scheduler the scheduler instance in question + * @return array the header for this field + */ + public function get_headers(scheduler $scheduler) { + $result = array(); + foreach ($this->coursegroups as $group) { + $result[] = $group->name; + } + return $result; + } + + /** + * Retrieve the value of this field in a particular data record + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return string the value of this field for the given data + */ + public function get_value(slot $slot, $appointment) { + return ''; + } + + /** + * Retrieve the value of this field as an array. + * Needs to be overriden for multi-column fields only. + * + * @param slot $slot the scheduler slot to get data from + * @param mixed $appointment the appointment to evaluate (may be null for an empty slot) + * @return array an array of strings containing the column values + */ + public function get_values(slot $slot, $appointment) { + if (! $appointment instanceof appointment) { + return ''; + } + $usergroups = groups_get_user_groups($slot->get_scheduler()->courseid, $appointment->studentid)[0]; + $result = array(); + foreach ($this->coursegroups as $group) { + $key = in_array($group->id, $usergroups) ? 'yes' : 'no'; + $result[] = get_string($key); + } + return $result; + } + +} + +/** + * An "output device" for scheduler exports + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class scheduler_canvas { + + /** + * @var object format instructions for header + */ + public $formatheader; + + /** + * @var object format instructions for boldface text + */ + public $formatbold; + + /** + * @var object format instructions for boldface italic text + */ + public $formatboldit; + + /** + * @var object format instructions for text with line wrapping + */ + public $formatwrap; + + /** + * Start a new page (tab, etc.) with an optional title. + * + * @param mixed $title the title of the page + */ + public abstract function start_page($title); + + /** + * Write a string into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $str the string to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public abstract function write_string($row, $col, $str, $format); + + /** + * Write a number into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $num the number to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public abstract function write_number($row, $col, $num, $format); + + /** + * Merge a range of cells in the same row. + * + * @param mixed $row the row in which to merge + * @param mixed $fromcol the first column to merge + * @param mixed $tocol the last column to merge + */ + public abstract function merge_cells($row, $fromcol, $tocol); + + /** + * Set the width of a particular column. (This will make sense only for certain outout formats, + * it can be ignored otherwise.) + * + * @param int $col the affected column + * @param int $width the width of that column + */ + public function set_column_width($col, $width) { + // Ignore widths by default. + } + + /** + * @var string title of the output file + */ + protected $title; + + /** + * Set the title of the entire output file. + * + * This is stored in the field $title, and can be used as appropriate for the particular implementation. + * + * @param string $title the title to set + */ + public function set_title($title) { + $this->title = $title; + } + + /** + * Send the output file via HTTP, as a downloadable file. + * + * @param string $filename the file name to send + */ + public abstract function send($filename); + +} + +/** + * Output device: Excel file + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_excel_canvas extends scheduler_canvas { + + /** @var mixed */ + protected $workbook; + /** @var mixed */ + protected $worksheet; + + /** + * scheduler_excel_canvas constructor. + */ + public function __construct() { + + // Create a workbook. + $this->workbook = new MoodleExcelWorkbook("-"); + + // Set up formats. + $this->formatheader = $this->workbook->add_format(); + $this->formatbold = $this->workbook->add_format(); + $this->formatbold = $this->workbook->add_format(); + $this->formatboldit = $this->workbook->add_format(); + $this->formatwrap = $this->workbook->add_format(); + $this->formatheader->set_bold(); + $this->formatbold->set_bold(); + $this->formatboldit->set_bold(); + $this->formatboldit->set_italic(); + $this->formatwrap->set_text_wrap(); + + } + + /** + * Start a new page (tab, etc.) with an optional title. + * + * @param mixed $title the title of the page + */ + public function start_page($title) { + $this->worksheet = $this->workbook->add_worksheet($title); + } + + /** + * ensure_open_page + */ + private function ensure_open_page() { + if (!$this->worksheet) { + $this->start_page(''); + } + } + + /** + * Write a string into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $str the string to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public function write_string($row, $col, $str, $format=null) { + $this->ensure_open_page(); + $this->worksheet->write_string($row, $col, $str, $format); + } + + /** + * Write a number into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $num the number to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public function write_number($row, $col, $num, $format=null) { + $this->ensure_open_page(); + $this->worksheet->write_number($row, $col, $num, $format); + } + + /** + * Merge a range of cells in the same row. + * + * @param mixed $row the row in which to merge + * @param mixed $fromcol the first column to merge + * @param mixed $tocol the last column to merge + */ + public function merge_cells($row, $fromcol, $tocol) { + $this->ensure_open_page(); + $this->worksheet->merge_cells($row, $fromcol, $row, $tocol); + } + + /** + * Set the width of a particular column. (This will make sense only for certain outout formats, + * it can be ignored otherwise.) + * + * @param int $col the affected column + * @param int $width the width of that column + */ + public function set_column_width($col, $width) { + $this->worksheet->set_column($col, $col, $width); + } + + /** + * Send the output file via HTTP, as a downloadable file. + * + * @param string $filename the file name to send + */ + public function send($filename) { + $this->workbook->send($filename); + $this->workbook->close(); + } + +} + +/** + * Output device: ODS file + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_ods_canvas extends scheduler_canvas { + + /** @var mixed */ + protected $workbook; + /** @var mixed */ + protected $worksheet; + + /** + * scheduler_ods_canvas constructor. + */ + public function __construct() { + + // Create a workbook. + $this->workbook = new MoodleODSWorkbook("-"); + + // Set up formats. + $this->formatheader = $this->workbook->add_format(); + $this->formatbold = $this->workbook->add_format(); + $this->formatboldit = $this->workbook->add_format(); + $this->formatwrap = $this->workbook->add_format(); + $this->formatheader->set_bold(); + $this->formatbold->set_bold(); + $this->formatboldit->set_bold(); + $this->formatboldit->set_italic(); + $this->formatwrap->set_text_wrap(); + + } + + /** + * Start a new page (tab, etc.) with an optional title. + * + * @param mixed $title the title of the page + */ + public function start_page($title) { + $this->worksheet = $this->workbook->add_worksheet($title); + } + + /** + * ensure_open_page + */ + private function ensure_open_page() { + if (!$this->worksheet) { + $this->start_page(''); + } + } + + /** + * Write a string into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $str the string to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public function write_string($row, $col, $str, $format=null) { + $this->ensure_open_page(); + $this->worksheet->write_string($row, $col, $str, $format); + } + + /** + * Write a number into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $num the number to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public function write_number($row, $col, $num, $format=null) { + $this->ensure_open_page(); + $this->worksheet->write_number($row, $col, $num, $format); + } + + /** + * Merge a range of cells in the same row. + * + * @param mixed $row the row in which to merge + * @param mixed $fromcol the first column to merge + * @param mixed $tocol the last column to merge + */ + public function merge_cells($row, $fromcol, $tocol) { + $this->ensure_open_page(); + $this->worksheet->merge_cells($row, $fromcol, $row, $tocol); + } + + /** + * Set the width of a particular column. (This will make sense only for certain outout formats, + * it can be ignored otherwise.) + * + * @param int $col the affected column + * @param int $width the width of that column + */ + public function set_column_width($col, $width) { + $this->worksheet->set_column($col, $col, $width); + } + + /** + * Send the output file via HTTP, as a downloadable file. + * + * @param string $filename the file name to send + */ + public function send($filename) { + $this->workbook->send($filename); + $this->workbook->close(); + } + +} + + +/** + * An output device that is based on first collecting all text in an array. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class scheduler_cached_text_canvas extends scheduler_canvas { + + /** @var mixed */ + protected $pages; + /** @var mixed */ + protected $curpage; + + /** + * scheduler_cached_text_canvas constructor. + */ + public function __construct() { + + $this->formatheader = 'header'; + $this->formatbold = 'bold'; + $this->formatboldit = 'boldit'; + $this->formatwrap = 'wrap'; + + $this->start_page(''); + + } + + /** + * get_col_count + * + * @param mixed $page + * @return int + */ + protected function get_col_count($page) { + $maxcol = 0; + foreach ($page->cells as $rownum => $row) { + foreach ($row as $colnum => $col) { + if ($colnum > $maxcol) { + $maxcol = $colnum; + } + } + } + return $maxcol + 1; + } + + /** + * get_row_count + * + * @param mixed $page + * @return int + */ + protected function get_row_count($page) { + $maxrow = 0; + foreach ($page->cells as $rownum => $row) { + if ($rownum > $maxrow) { + $maxrow = $rownum; + } + } + return $maxrow + 1; + } + + /** + * compute_relative_widths + * + * @param mixed $page + * @return array + */ + protected function compute_relative_widths($page) { + $cols = $this->get_col_count($page); + $sum = 0; + foreach ($page->columnwidths as $width) { + $sum += $width; + } + $relwidths = array(); + for ($col = 0; $col < $cols; $col++) { + if ($sum > 0 && isset($page->columnwidths[$col])) { + $relwidths[$col] = (int) ($page->columnwidths[$col] / $sum * 100); + } else { + $relwidths[$col] = 0; + } + } + return $relwidths; + } + + /** + * Start a new page (tab, etc.) with an optional title. + * + * @param mixed $title the title of the page + */ + public function start_page($title) { + $onemptypage = $this->curpage && !$this->curpage->cells && !$this->curpage->mergers && !$this->curpage->title; + if ($onemptypage) { + $this->curpage->title = $title; + } else { + $newpage = new stdClass; + $newpage->title = $title; + $newpage->cells = array(); + $newpage->formats = array(); + $newpage->mergers = array(); + $newpage->columnwidths = array(); + $this->pages[] = $newpage; + $this->curpage = $newpage; + } + } + + /** + * Write a string into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $str the string to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public function write_string($row, $col, $str, $format=null) { + $this->curpage->cells[$row][$col] = $str; + $this->curpage->formats[$row][$col] = $format; + } + + /** + * Write a number into a certain position of the canvas. + * + * @param mixed $row the row into which to write (starts with 0) + * @param mixed $col the column into which to write (starts with 0) + * @param mixed $num the number to write + * @param mixed $format the format to use (one of the $format... fields of this object), can be null + */ + public function write_number($row, $col, $num, $format=null) { + $this->write_string($row, $col, $num, $format); + } + + /** + * Merge a range of cells in the same row. + * + * @param mixed $row the row in which to merge + * @param mixed $fromcol the first column to merge + * @param mixed $tocol the last column to merge + */ + public function merge_cells($row, $fromcol, $tocol) { + $this->curpage->mergers[$row][$fromcol] = $tocol - $fromcol + 1; + } + + /** + * Set the width of a particular column. (This will make sense only for certain outout formats, + * it can be ignored otherwise.) + * + * @param int $col the affected column + * @param int $width the width of that column + */ + public function set_column_width($col, $width) { + $this->curpage->columnwidths[$col] = $width; + } + +} + +/** + * Output device: HTML file + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_html_canvas extends scheduler_cached_text_canvas { + + /** + * as_html + * + * @param mixed $rowcutoff + * @param bool $usetitle + * @return string + */ + public function as_html($rowcutoff, $usetitle = true) { + global $OUTPUT; + + $o = ''; + + if ($usetitle && $this->title) { + $o .= html_writer::tag('h1', $this->title); + } + + foreach ($this->pages as $page) { + if ($page->title) { + $o .= html_writer::tag('h2', $page->title); + } + + // Find extent of the table. + $rows = $this->get_row_count($page); + $cols = $this->get_col_count($page); + if ($rowcutoff && $rows > $rowcutoff) { + $rows = $rowcutoff; + } + $relwidths = $this->compute_relative_widths($page); + + $table = new html_table(); + $table->cellpadding = 3; + for ($row = 0; $row < $rows; $row++) { + $hrow = new html_table_row(); + $col = 0; + while ($col < $cols) { + $span = 1; + if (isset($page->mergers[$row][$col])) { + $mergewidth = (int) $page->mergers[$row][$col]; + if ($mergewidth >= 1) { + $span = $mergewidth; + } + } + $cell = new html_table_cell(''); + $text = ''; + if (isset($page->cells[$row][$col])) { + $text = $page->cells[$row][$col]; + } + if (isset($page->formats[$row][$col])) { + $cell->header = ($page->formats[$row][$col] == 'header'); + if ($page->formats[$row][$col] == 'boldit') { + $text = html_writer::tag('i', $text); + $text = html_writer::tag('b', $text); + } + if ($page->formats[$row][$col] == 'bold') { + $text = html_writer::tag('b', $text); + } + } + if ($span > 1) { + $cell->colspan = $span; + } + if ($row == 0 & $relwidths[$col] > 0) { + $cell->width = $relwidths[$col].'%'; + } + $cell->text = $text; + $hrow->cells[] = $cell; + $col = $col + $span; + } + $table->data[] = $hrow; + } + $o .= html_writer::table($table); + } + return $o; + } + + /** + * Send the output file via HTTP, as a downloadable file. + * + * @param string $filename the file name to send + */ + public function send($filename) { + global $OUTPUT, $PAGE; + $PAGE->set_pagelayout('print'); + echo $OUTPUT->header(); + echo $this->as_html(0, true); + echo $OUTPUT->footer(); + } + +} + +/** + * Output device: CSV (text) file + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_csv_canvas extends scheduler_cached_text_canvas { + + /** @var mixed */ + protected $delimiter; + + /** + * scheduler_csv_canvas constructor. + * + * @param mixed $delimiter + */ + public function __construct($delimiter) { + parent::__construct(); + $this->delimiter = $delimiter; + } + + /** + * Send the output file via HTTP, as a downloadable file. + * + * @param string $filename the file name to send + */ + public function send($filename) { + + $writer = new csv_export_writer($this->delimiter); + $writer->set_filename($filename); + + foreach ($this->pages as $page) { + if ($page->title) { + $writer->add_data(array('*** '.$page->title.' ***')); + } + + // Find extent of the table. + $rows = $this->get_row_count($page); + $cols = $this->get_col_count($page); + + for ($row = 0; $row < $rows; $row++) { + $data = array(); + $col = 0; + while ($col < $cols) { + if (isset($page->cells[$row][$col])) { + $data[] = $page->cells[$row][$col]; + } else { + $data[] = ''; + } + + $span = 1; + if (isset($page->mergers[$row][$col])) { + $mergewidth = (int) $page->mergers[$row][$col]; + if ($mergewidth >= 1) { + $span = $mergewidth; + } + } + $col += $span; + } + $writer->add_data($data); + } + } + + $writer->download_file(); + } + +} + +/** + * Output device: PDF file + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_pdf_canvas extends scheduler_cached_text_canvas { + + /** @var mixed */ + protected $orientation; + + /** + * scheduler_pdf_canvas constructor. + * + * @param mixed $orientation + */ + public function __construct($orientation) { + parent::__construct(); + $this->orientation = $orientation; + } + + /** + * Send the output file via HTTP, as a downloadable file. + * + * @param string $filename the file name to send + */ + public function send($filename) { + + $doc = new pdf($this->orientation); + if ($this->title) { + $doc->setHeaderData('', 0, $this->title); + $doc->setPrintHeader(true); + } else { + $doc->setPrintHeader(false); + } + $doc->setPrintFooter(false); + + foreach ($this->pages as $page) { + $doc->AddPage(); + if ($page->title) { + $doc->writeHtml('<h2>'.$page->title.'</h2>'); + } + + // Find extent of the table. + $rows = $this->get_row_count($page); + $cols = $this->get_col_count($page); + $relwidths = $this->compute_relative_widths($page); + + $o = html_writer::start_tag('table', array('border' => 1, 'cellpadding' => 1)); + for ($row = 0; $row < $rows; $row++) { + $o .= html_writer::start_tag('tr'); + $col = 0; + while ($col < $cols) { + $span = 1; + if (isset($page->mergers[$row][$col])) { + $mergewidth = (int) $page->mergers[$row][$col]; + if ($mergewidth >= 1) { + $span = $mergewidth; + } + } + $opts = array(); + if ($row == 0 && $relwidths[$col] > 0) { + $opts['width'] = $relwidths[$col].'%'; + } + if ($span > 1) { + $opts['colspan'] = $span; + } + $o .= html_writer::start_tag('td', $opts); + $cell = ''; + if (isset($page->cells[$row][$col])) { + $cell = s($page->cells[$row][$col]); + if (isset($page->formats[$row][$col])) { + $thisformat = $page->formats[$row][$col]; + if ($thisformat == 'header') { + $cell = html_writer::tag('b', $cell); + } else if ($thisformat == 'boldit') { + $cell = html_writer::tag('i', $cell); + } + } + } + $o .= $cell; + + $o .= html_writer::end_tag('td'); + + $col += $span; + } + $o .= html_writer::end_tag('tr'); + } + $o .= html_Writer::end_tag('table'); + $doc->writeHtml($o); + } + + $doc->Output($filename.'.pdf'); + } + +} + +/** + * A class that generates the export file with given settings. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_export { + + /** + * @var scheduler_canvas the canvas used for output + */ + protected $canvas; + + /** + * @var array a list of student ids to be filtered for + */ + protected $studfilter = null; + + /** + * Create a new export with a given canvas + * + * @param scheduler_canvas $canvas the canvas to use + */ + public function __construct(scheduler_canvas $canvas) { + $this->canvas = $canvas; + } + + /** + * Build the output on the canvas. + * + * @param scheduler $scheduler the scheduler to export + * @param array $fields the fields to include + * @param string $mode output mode + * @param int $userid id of the teacher to export for, 0 if slots for all teachers are exported + * @param int $groupid the id of the group (of students) to export appointments for, 0 if none + * @param mixed $timerange + * @param bool $includeempty whether to include slots without appointments + * @param bool $pageperteacher whether one page should be used for each teacher + */ + public function build(scheduler $scheduler, array $fields, $mode, $userid, $groupid, $timerange, $includeempty, + $pageperteacher) { + if ($groupid) { + $this->studfilter = array_keys(groups_get_members($groupid, 'u.id')); + } + $this->canvas->set_title(format_string($scheduler->name)); + if ($userid) { + $slots = $scheduler->get_slots_for_teacher($userid, $groupid, '', '', $timerange); + $this->build_page($scheduler, $fields, $slots, $mode, $includeempty); + } else if ($pageperteacher) { + $teachers = $scheduler->get_teachers(); + foreach ($teachers as $teacher) { + $slots = $scheduler->get_slots_for_teacher($teacher->id, $groupid, '', '', $timerange); + $title = fullname($teacher); + $this->canvas->start_page($title); + $this->build_page($scheduler, $fields, $slots, $mode, $includeempty); + } + } else { + $slots = $scheduler->get_slots_for_group($groupid, '', '', $timerange); + $this->build_page($scheduler, $fields, $slots, $mode, $includeempty); + } + } + + /** + * Write a page of output to the canvas. + * (Pages correspond to "tabs" in spreadsheet format, not to printed pages.) + * + * @param scheduler $scheduler the scheduler being exported + * @param array $fields the fields to include + * @param array $slots the slots to include + * @param string $mode output mode + * @param bool $includeempty whether to include slots without appointments + */ + protected function build_page(scheduler $scheduler, array $fields, array $slots, $mode, $includeempty) { + + // Output the header. + $row = 0; + $col = 0; + foreach ($fields as $field) { + if ($field->get_group() != 'slot' || $mode != 'appointmentsgrouped') { + $headers = $field->get_headers($scheduler); + $numcols = $field->get_num_columns($scheduler); + for ($i = 0; $i < $numcols; $i++) { + $this->canvas->write_string($row, $col + $i, $headers[$i], $this->canvas->formatheader); + $this->canvas->set_column_width($col + $i, $field->get_typical_width($scheduler)); + } + $col = $col + $numcols; + } + } + $row++; + + // Output the data rows. + foreach ($slots as $slot) { + $appts = $slot->get_appointments($this->studfilter); + if ($mode == 'appointmentsgrouped') { + if ($appts || $includeempty) { + $this->write_row_summary($row, $slot, $fields); + $row++; + } + foreach ($appts as $appt) { + $this->write_row($row, $slot, $appt, $fields, false); + $row++; + } + } else { + if ($appts) { + if ($mode == 'onelineperappointment') { + foreach ($appts as $appt) { + $this->write_row($row, $slot, $appt, $fields, true); + $row++; + } + } else { + $this->write_row($row, $slot, $appts[0], $fields, true, count($appts) > 1); + $row++; + } + } else if ($includeempty) { + $this->write_row($row, $slot, null, $fields, true); + $row++; + } + } + } + + } + + /** + * Write a row of the export to the canvas + * @param int $row row number on canvas + * @param slot $slot the slot of the appointment to write + * @param appointment $appointment the appointment to write + * @param array $fields list of fields to include + * @param bool $includeslotfields whether fields relating to slots, rather than appointments, should be included + * @param string $multiple whether the row represents multiple values (appointments) + */ + protected function write_row($row, slot $slot, $appointment, array $fields, $includeslotfields = true, + $multiple = false) { + + $col = 0; + foreach ($fields as $field) { + if ($includeslotfields || $field->get_group() != 'slot') { + if ($multiple && $field->get_group() != 'slot') { + $value = get_string('multiple', 'scheduler'); + $this->canvas->write_string($row, $col, $value); + $col++; + } else { + $numcols = $field->get_num_columns($slot->get_scheduler()); + $values = $field->get_values($slot, $appointment); + $format = $field->is_wrapping() ? $this->canvas->formatwrap : null; + for ($i = 0; $i < $numcols; $i++) { + $this->canvas->write_string($row, $col + $i, $values[$i], $format); + } + $col = $col + $numcols; + } + } + } + } + + /** + * Write a summary of slot-related data into a row + * + * @param int $row the row number on the canvas + * @param slot $slot the slot to be written + * @param array $fields the fields to include + */ + protected function write_row_summary($row, slot $slot, array $fields) { + + $strs = array(); + $cols = 0; + foreach ($fields as $field) { + if ($field->get_group() == 'slot') { + $strs[] = $field->get_value($slot, null); + } else { + $cols++; + } + } + $str = implode(' - ', $strs); + $this->canvas->write_string($row, 0, $str, $this->canvas->formatboldit); + $this->canvas->merge_cells($row, 0, $cols - 1); + } + +} diff --git a/mod/scheduler/index.php b/mod/scheduler/index.php new file mode 100644 index 0000000..85280f3 --- /dev/null +++ b/mod/scheduler/index.php @@ -0,0 +1,104 @@ +<?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/>. + +/** + * Main file of the scheduler package. + * + * It lists all the instances of scheduler in a particular course. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once(dirname(__FILE__).'/lib.php'); + +$id = required_param('id', PARAM_INT); // Course id. +$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); + +$PAGE->set_url('/mod/scheduler/index.php', array('id' => $id)); +$PAGE->set_pagelayout('incourse'); + +$coursecontext = context_course::instance($id); +require_login($course->id); + +$event = \mod_scheduler\event\course_module_instance_list_viewed::create(array( + 'context' => $coursecontext +)); +$event->add_record_snapshot('course', $course); +$event->trigger(); + +// Get all required strings. + +$strschedulers = get_string('modulenameplural', 'scheduler'); +$strscheduler = get_string('modulename', 'scheduler'); + +// Print the header. + +$title = $course->shortname . ': ' . $strschedulers; +$PAGE->set_title($title); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header($course); + + +// Get all the appropriate data. + +if (!$schedulers = get_all_instances_in_course('scheduler', $course)) { + notice(get_string('noschedulers', 'scheduler'), "../../course/view.php?id=$course->id"); + die; +} + +// Print the list of instances. + +$timenow = time(); +$strname = get_string('name'); +$strweek = get_string('week'); +$strtopic = get_string('topic'); + +$table = new html_table(); + +if ($course->format == 'weeks') { + $table->head = array ($strweek, $strname); + $table->align = array ('CENTER', 'LEFT'); +} else if ($course->format == 'topics') { + $table->head = array ($strtopic, $strname); + $table->align = array ('CENTER', 'LEFT', 'LEFT', 'LEFT'); +} else { + $table->head = array ($strname); + $table->align = array ('LEFT', 'LEFT', 'LEFT'); +} + +foreach ($schedulers as $scheduler) { + $url = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->coursemodule)); + // Show dimmed if the mod is hidden. + $attr = $scheduler->visible ? null : array('class' => 'dimmed'); + $link = html_writer::link($url, $scheduler->name, $attr); + if ($scheduler->visible or has_capability('moodle/course:viewhiddenactivities', $coursecontext)) { + if ($course->format == 'weeks' or $course->format == 'topics') { + $table->data[] = array ($scheduler->section, $link); + } else { + $table->data[] = array ($link); + } + } +} + +echo html_writer::table($table); + +// Finish the page. + +echo $OUTPUT->footer($course); + diff --git a/mod/scheduler/lang/en/scheduler.php b/mod/scheduler/lang/en/scheduler.php new file mode 100644 index 0000000..f5a67e5 --- /dev/null +++ b/mod/scheduler/lang/en/scheduler.php @@ -0,0 +1,637 @@ +<?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/>. + +/** + * Strings for component 'mod_scheduler', language 'en' + * + * @package mod_scheduler + * @copyright 2017 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Scheduler'; +$string['pluginadministration'] = 'Scheduler administration'; +$string['modulename'] = 'Scheduler'; +$string['modulename_help'] = 'The scheduler activity helps you in scheduling appointments with your students. + +Teachers specify time slots for meetings, students then choose one of them on Moodle. +Teachers in turn can record the outcome of the meeting - and optionally a grade - within the scheduler. + +Group scheduling is supported; that is, each time slot can accommodate several students, and optionally it is possible to schedule appointments for entire groups at the same time.'; +$string['modulename_link'] = 'mod/scheduler/view'; +$string['modulenameplural'] = 'Schedulers'; + +/* ***** Capabilities ****** */ +$string['scheduler:addinstance'] = 'Add a new scheduler'; +$string['scheduler:appoint'] = 'Book slots'; +$string['scheduler:attend'] = 'Attend students'; +$string['scheduler:canscheduletootherteachers'] = 'Schedule appointments for other staff members'; +$string['scheduler:canseeotherteachersbooking'] = 'See and browse other teachers booking'; +$string['scheduler:disengage'] = 'This capability is deprecated and does nothing'; +$string['scheduler:manage'] = 'Manage your slots and appointments'; +$string['scheduler:manageallappointments'] = 'Manage all scheduler data'; +$string['scheduler:viewslots'] = 'See slots that are open for booking (in student screen)'; +$string['scheduler:viewfullslots'] = 'See slots even if they are fully booked (in student screen)'; +$string['scheduler:seeotherstudentsbooking'] = 'See other students booked on the slot'; +$string['scheduler:seeotherstudentsresults'] = 'See other slot student\'s results'; +$string['scheduler:seeoverviewoutsideactivity'] = 'Use the overview screen to see slots outside the current scheduler activity.'; +$string['scheduler:editallattended'] = 'Mark students in all appointments as attended / not attended.'; +$string['scheduler:editallgrades'] = 'Edit grades in all appointments.'; +$string['scheduler:editallnotes'] = 'Edit appointment notes in all appointments.'; + +/* ***** Events ***** */ +$string['event_bookingformviewed'] = 'Scheduler booking form viewed'; +$string['event_bookingadded'] = 'Scheduler booking added'; +$string['event_bookingremoved'] = 'Scheduler booking removed'; +$string['event_appointmentlistviewed'] = 'Scheduler appointment list viewed'; +$string['event_slotadded'] = 'Scheduler slot added'; +$string['event_slotdeleted'] = 'Scheduler slot deleted'; + +/* ***** Message types ***** */ +$string['messageprovider:invitation'] = 'Invitation to book a slot'; +$string['messageprovider:bookingnotification'] = 'Notification when a booking is made or cancelled'; +$string['messageprovider:reminder'] = 'Reminder of an upcoming appointment'; + +/* ***** Search areas ***** */ +$string['search:activity'] = 'Scheduler - activity information'; + +/* ***** Privacy API strings **** */ + +$string['privacy:metadata:scheduler_slots'] = 'Represents one slot in a scheduler'; + +$string['privacy:metadata:scheduler_slots:teacherid'] = 'Teacher associated with the slot'; +$string['privacy:metadata:scheduler_slots:starttime'] = 'Start time of the slot'; +$string['privacy:metadata:scheduler_slots:duration'] = 'Duration of the slot in minutes'; +$string['privacy:metadata:scheduler_slots:appointmentlocation'] = 'Appointment location'; +$string['privacy:metadata:scheduler_slots:notes'] = 'Notes about the slot'; +$string['privacy:metadata:scheduler_slots:notesformat'] = "Format of the notes"; +$string['privacy:metadata:scheduler_slots:exclusivity'] = "Maximum number of students on the slot"; + +$string['privacy:metadata:scheduler_appointment'] = 'Represents a student appointment in a scheduler'; + +$string['privacy:metadata:scheduler_appointment:studentid'] = "Student who booked the appointment"; +$string['privacy:metadata:scheduler_appointment:attended'] = "Whether the appointment was attended"; +$string['privacy:metadata:scheduler_appointment:grade'] = "Grade for the appointment"; +$string['privacy:metadata:scheduler_appointment:appointmentnote'] = "Note by teacher (visible to student)"; +$string['privacy:metadata:scheduler_appointment:appointmentnoteformat'] = "Format of teacher note"; +$string['privacy:metadata:scheduler_appointment:teachernote'] = "Note by teacher (private)"; +$string['privacy:metadata:scheduler_appointment:teachernoteformat'] = "Format of private teacher note"; +$string['privacy:metadata:scheduler_appointment:studentnote'] = "Note by student"; +$string['privacy:metadata:scheduler_appointment:studentnoteformat'] = "Format of student note"; + +$string['privacy:metadata:filepurpose'] = 'File used in notes for the slot or appointment'; + + +/* ***** Interface strings ****** */ + +$string['onedaybefore'] = '1 day before slot'; +$string['oneweekbefore'] = '1 week before slot'; +$string['areaappointmentnote'] = 'Files in appointment notes'; +$string['areaslotnote'] = 'Files in slot notes'; +$string['areateachernote'] = 'Files in confidential notes'; +$string['action'] = 'Action'; +$string['actions'] = 'Actions'; +$string['addappointment'] = 'Add another student'; +$string['addcommands'] = 'Add slots'; +$string['addondays'] = 'Add appointments on'; +$string['addsession'] = 'Add repeated slots'; +$string['addsingleslot'] = 'Add single slot'; +$string['addslot'] = 'You can add additional appointment slots at any time.'; +$string['addstudenttogroup'] = 'Add this student to appointment group'; +$string['allappointments'] = 'All appointments'; +$string['allononepage'] = 'All slots on one page'; +$string['allowgroup'] = 'Exclusive slot - click to change'; +$string['alreadyappointed'] = 'Cannot make the appointment. The slot is already fully booked.'; +$string['appointfor'] = 'Make appointment for'; +$string['appointforgroup'] = 'Make appointments for: {$a}'; +$string['appointingstudent'] = 'Appointment for slot'; +$string['appointingstudentinnew'] = 'Appointment for new slot'; +$string['appointment'] = 'Appointment'; +$string['appointmentno'] = 'Appointment {$a}'; +$string['appointmentnote'] = 'Notes for appointment (visible to student)'; +$string['appointments'] = 'Appointments'; +$string['appointmentsgrouped'] = 'Appointments grouped by slot'; +$string['appointsolo'] = 'just me'; +$string['appointsomeone'] = 'Add new appointment'; +$string['appointmentsummary'] = 'Appointment on {$a->startdate} from {$a->starttime} to {$a->endtime} with {$a->teacher}'; +$string['attendable'] = 'Attendable'; +$string['attendablelbl'] = 'Total candidates for scheduling'; +$string['attended'] = 'Attended'; +$string['attendedlbl'] = 'Amount of attended students'; +$string['attendedslots'] = 'Attended slots'; +$string['availableslots'] = 'Available slots'; +$string['availableslotsall'] = 'All slots'; +$string['availableslotsnotowned'] = 'Not owned'; +$string['availableslotsowned'] = 'Owned'; +$string['bookingformoptions'] = 'Booking form and student-supplied data'; +$string['bookinginstructions'] = 'Booking instructions'; +$string['bookinginstructions_help'] = 'This text will be displayed to students before they make a booking. It can, for example, instruct students how to fill out the optional message field or which files to upload.'; +$string['bookslot'] = 'Book slot'; +$string['bookaslot'] = 'Book a slot'; +$string['bookingdetails'] = 'Booking details'; +$string['bookwithteacher'] = 'Teacher'; +$string['break'] = 'Break between slots'; +$string['breaknotnegative'] = 'Length of the break must not be negative'; +$string['cancelbooking'] = 'Cancel booking'; +$string['canbooksingleappointment'] = 'You can book one appointment in this scheduler.'; +$string['canbook1appointment'] = 'You can book one more appointment in this scheduler.'; +$string['canbooknappointments'] = 'You can book {$a} more appointments in this scheduler.'; +$string['canbooknofurtherappointments'] = 'You cannot book further appointments in this scheduler.'; +$string['canbookunlimitedappointments'] = 'You can book any number of appointments in this scheduler.'; +$string['chooseexisting'] = 'Choose existing'; +$string['choosingslotstart'] = 'Choosing the start time'; +$string['comments'] = 'Comments'; +$string['conflictlocal'] = '{$a->datetime} ({$a->duration} minutes) in this scheduler'; +$string['conflictremote'] = '{$a->datetime} ({$a->duration} minutes) in course {$a->courseshortname}, scheduler {$a->schedulername}'; +$string['contentformat'] = 'Format'; +$string['contentformat_help'] = '<p>There are three basic choices for the export format, + differing in how slots with several appointments are handled. + <dl> + <dt>One line per slot</dt>: + <dd> + The output file will contain one line for each slot. If a slot contains multiple + appointments, then instead of the student\'s name, etc., a marker "(multiple)" will be shown. + </dd> + <dt>One line per appointment</dt>: + <dd> + The output file will contain one line for each appointment. If a slot contains multiple + appointments, then it will appear several times in the list (with its data repeated). + </dd> + <dt>Appointments grouped by slot</dt>: + <dd> + All appointments of one slot are grouped together, preceded by a header line that + indicates the slot in question. This may not work well with the CSV output file format, + as the number of columns is not constant. + </dd> + </dl> + You can explore the effect of these options using the "Preview" button.</p>'; +$string['complete'] = 'Booked'; +$string['confirmbooking'] = "Confirm booking"; +$string['confirmdelete-all'] = 'This will delete <b>all</b> slots in this scheduler. Deletion cannot be undone. Continue anyway?'; +$string['confirmdelete-mine'] = 'This will delete all your slots in this scheduler. Deletion cannot be undone. Continue anyway?'; +$string['confirmdelete-myunused'] = 'This will delete all your unused slots in this scheduler. Deletion cannot be undone. Continue anyway?'; +$string['confirmdelete-selected'] = 'This will delete the selected slots. Deletion cannot be undone. Continue anyway?'; +$string['confirmdelete-one'] = 'Delete slot?'; +$string['confirmdelete-unused'] = 'This will delete all unused slots in this scheduler. Deletion cannot be undone. Continue anyway?'; +$string['confirmrevoke'] = 'Revoke all appointments in the current slot?'; +$string['conflictingslots'] = 'The slot on {$a} cannot be created due to conflicting slots:'; +$string['copytomyself'] = 'Send a copy to myself'; +$string['course'] = 'Course'; +$string['createexport'] = 'Create export file'; +$string['csvformat'] = 'CSV'; +$string['csvfieldseparator'] = 'Field separator for CSV'; +$string['cumulatedduration'] = 'Summed duration of appointments'; +$string['datatoinclude'] = 'Data to include'; +$string['datatoinclude_help'] = 'Select the fields that should be included in the export. Each of these will appear in one column of the output file.'; +$string['date'] = 'Date'; +$string['datelist'] = 'Overview'; +$string['defaultslotduration'] = 'Default slot duration'; +$string['defaultslotduration_help'] = 'The default length (in minutes) for appointment slots that you set up'; +$string['deleteallslots'] = 'Delete all slots'; +$string['deleteallunusedslots'] = 'Delete unused slots'; +$string['deletecommands'] = 'Delete slots'; +$string['deletemyslots'] = 'Delete all my slots'; +$string['deleteselection'] = 'Delete selected slots'; +$string['deletetheseslots'] = 'Delete these slots'; +$string['deleteunusedslots'] = 'Delete my unused slots'; +$string['deleteonsave'] = 'Delete this appointment (when saving the form)'; +$string['deletedconflictingslots'] = 'For the slot on {$a}, conflicting slots have been deleted:'; +$string['department'] = 'From where?'; +$string['disengage'] = 'Drop my appointments'; +$string['displayfrom'] = 'Display slot to students from'; +$string['distributetoslot'] = 'Distribute to the whole group'; +$string['divide'] = 'Divide into slots?'; +$string['duration'] = 'Duration'; +$string['durationrange'] = 'Slot duration must be between {$a->min} and {$a->max} minutes.'; +$string['editbooking'] = 'Edit booking'; +$string['emailreminder'] = 'Email a reminder'; +$string['emailreminderondate'] = 'Email a reminder on'; +$string['end'] = 'End'; +$string['enddate'] = 'Repeat time slots until'; +$string['excelformat'] = 'Excel'; +$string['exclusive'] = 'Exclusive'; +$string['exclusivity'] = 'Exclusivity'; +$string['exclusivitypositive'] = 'The number of students per slot needs to be 1 or more.'; +$string['exclusivityoverload'] = 'The slot has {$a} appointed students, more than allowed by this setting.'; +$string['explaingeneralconfig'] = 'These options can only be setup at site level and will apply to all schedulers of this Moodle installation.'; +$string['export'] = 'Export'; +$string['exporthdr'] = 'Export slots and appointments'; +$string['exporttimerange'] = 'Time range'; +$string['exporttimerangeall'] = 'Future and past slots'; +$string['exporttimerangefuture'] = 'Only future slots'; +$string['exporttimerangepast'] = 'Only past slots'; +$string['everyone'] = 'Everyone'; +$string['field-date'] = 'Date'; +$string['field-starttime'] = 'Start time'; +$string['field-endtime'] = 'End time'; +$string['field-location'] = 'Location'; +$string['field-maxstudents'] = 'Max. students'; +$string['field-studentfullname'] = 'Student full name'; +$string['field-studentfirstname'] = 'Student first name'; +$string['field-studentlastname'] = 'Student last name'; +$string['field-studentemail'] = 'Student e-mail'; +$string['field-studentusername'] = 'Student user name'; +$string['field-studentidnumber'] = 'Student id number'; +$string['field-attended'] = 'Attended'; +$string['field-slotnotes'] = 'Slot notes'; +$string['field-appointmentnote'] = 'Appointment note (to student)'; +$string['field-teachernote'] = 'Confidential note (teacher only)'; +$string['field-studentnote'] = 'Message by student'; +$string['field-filecount'] = 'Number of uploaded files'; +$string['field-grade'] = 'Grade'; +$string['field-groupssingle'] = 'Groups'; +$string['field-groupssingle-label'] = 'Groups (one column)'; +$string['field-groupsmulti'] = 'Groups (several columns)'; +$string['fileformat'] = 'File format'; +$string['fileformat_help'] = 'The following file formats are available: + <ul> + <li>Comma Separated Value (CSV) text files. The field separator, by default a comma, can be chosen below. + CSV files can be opened in most spreadshet applications;</li> + <li>Microsoft Excel files (Excel 2007 format);</li> + <li>Open Document spreadsheets (ODS);</li> + <li>HTML format - a web page displaying the output table, + which can be printed using the browser\'s print feature;</li> + <li>PDF documents. You can choose between landscape and portrait orientation.</li> + </ul>'; +$string['finalgrade'] = 'Final grade'; +$string['firstslotavailable'] = 'The first slot will be open on: {$a}'; +$string['forbidgroup'] = 'Group slot - click to change'; +$string['forcewhenoverlap'] = 'Force when overlap'; +$string['forcourses'] = 'Choose students in courses'; +$string['friday'] = 'Friday'; +$string['generalconfig'] = 'General configuration'; +$string['grade'] = 'Grade'; +$string['gradeingradebook'] = 'Grade in gradebook'; +$string['gradingstrategy'] = 'Grading strategy'; +$string['gradingstrategy_help'] = 'In a scheduler where students can have several appointments, select how grades are aggregated. + The gradebook can show either <ul><li>the mean grade or</li><li>the maximum grade</li></ul> that the student has achieved.'; +$string['group'] = 'group '; +$string['groupbreakdown'] = 'By group size'; +$string['groupbookings'] = 'Booking in groups'; +$string['groupbookings_help'] = 'Allow students to book a slot for all members of their group. +(Note that this is separate from the "group mode" setting, which controls the slots a student can see.)'; +$string['groupmodeyourgroups'] = 'Group mode: {$a->groupmode}. Only students in {$a->grouplist} can book appointments with you.'; +$string['groupmodeyourgroupsempty'] = 'Group mode: {$a->groupmode}. You are not member of any group, therefore students cannot book appointments with you.'; +$string['groupscheduling'] = 'Enable group scheduling'; +$string['groupscheduling_desc'] = 'Allow entire groups to be scheduled at once. +(Apart from the global option, the setting "Booking in groups" must be enabled in the respective scheduler instance.)'; +$string['groupsession'] = 'Group session'; +$string['groupsize'] = 'Group size'; +$string['guardtime'] = 'Guard time'; +$string['guestscantdoanything'] = 'Guests can\'t do anything here.'; +$string['htmlformat'] = 'HTML'; +$string['howtoaddstudents'] = 'For adding students to a global scoped scheduler, use the role setting for the module.<br/>You may also use module role definitions to define the attenders of your students.'; +$string['ignoreconflicts'] = 'Ignore scheduling conflicts'; +$string['ignoreconflicts_help'] = 'If this box is ticked, then the slot will be moved to the requested date and time, even if other slots exist at the same time. This may lead to overlapping appointments for some teachers or students, and should therefore be used with care.'; +$string['ignoreconflicts_link'] = 'mod/scheduler/conflict'; +$string['includeemptyslots'] = 'Include empty slots'; +$string['includeslotsfor'] = 'Include slots for'; +$string['incourse'] = ' in course '; +$string['mixindivgroup'] = 'Mix individual and group bookings'; +$string['mixindivgroup_desc'] = 'Where group scheduling is enabled, allow individual bookings as well.'; +$string['introduction'] = 'Introduction'; +$string['isnonexclusive'] = 'Non-exclusive'; +$string['landscape'] = 'Landscape'; +$string['lengthbreakdown'] = 'By slot duration'; +$string['limited'] = 'Limited ({$a} left)'; +$string['location'] = 'Location'; +$string['markseen'] = 'After you have had an appointment with a student please mark them as "Seen" by clicking the checkbox near to their user picture above.'; +$string['markasseennow'] = 'Mark as seen now'; +$string['maxgrade'] = 'Take the highest grade'; +$string['maxstudentsperslot'] = 'Maximum number of students per slot'; +$string['maxstudentsperslot_desc'] = 'Group slots / non-exclusive slots can have at most this number of students. Note that in addition, the setting "unlimited" can always be chosen for a slot.'; +$string['maxstudentlistsize'] = 'Maximum length of student list'; +$string['maxstudentlistsize_desc'] = 'The maximum length of the list of students who need to make an appointment, as shown in the teacher view of the scheduler. If there are more students than this, no list will be displayed.'; +$string['meangrade'] = 'Take the mean grade'; +$string['meetingwith'] = 'Meeting with your'; +$string['meetingwithplural'] = 'Meeting with your'; +$string['message'] = 'Message'; +$string['messagesent'] = 'Message sent to {$a} recipients'; +$string['messagesubject'] = 'Subject'; +$string['messagebody'] = 'Message body'; +$string['minutes'] = 'minutes'; +$string['minutesperslot'] = 'minutes per slot'; +$string['missingstudents'] = '{$a} students still need to make an appointment'; +$string['missingstudentsmany'] = '{$a} students still need to make an appointment. No list is being displayed due to size.'; +$string['mode'] = 'Mode'; +$string['modeintro'] = 'Students can register'; +$string['modeappointments'] = 'appointment(s)'; +$string['modeoneonly'] = 'in this scheduler'; +$string['modeoneatatime'] = 'at a time'; +$string['monday'] = 'Monday'; +$string['multiple'] = '(multiple)'; +$string['myappointments'] = 'My appointments'; +$string['myself'] = 'Myself'; +$string['name'] = 'Scheduler name'; +$string['needteachers'] = 'Slots cannot be added as this course has no teachers'; +$string['negativerange'] = 'Range is negative. This can\'t be.'; +$string['never'] = 'Never'; +$string['nfiles'] = '{$a} files'; +$string['noappointments'] = 'No appointments'; +$string['noexistingstudents'] = 'No students available for scheduling'; +$string['nogroups'] = 'No group available for scheduling.'; +$string['noresults'] = 'No results. '; +$string['noschedulers'] = 'There are no schedulers'; +$string['noslots'] = 'There are no appointment slots available.'; +$string['noslotsavailable'] = 'No slots are available for booking at this time.'; +$string['noslotsopennow'] = 'No slots are open for booking right now.'; +$string['nostudents'] = 'No students scheduled'; +$string['nostudenttobook'] = 'No student to book'; +$string['note'] = 'Grade'; +$string['noteacherforslot'] = 'No teacher for the slots'; +$string['noteachershere'] = 'No teacher available'; +$string['notenoughplaces'] = 'Sorry, there are not enough free appointments in this slot.'; +$string['notesrequired'] = 'You must enter text into this field before booking the slot.'; +$string['notifications'] = 'Notifications'; +$string['notseen'] = 'Not seen'; +$string['now'] = 'Now'; +$string['occurrences'] = 'Occurrences'; +$string['odsformat'] = 'ODS'; +$string['on'] = 'on'; +$string['onelineperappointment'] = 'One line per appointment'; +$string['onelineperslot'] = 'One line per slot'; +$string['oneslotadded'] = '1 slot added'; +$string['oneslotdeleted'] = '1 slot deleted'; +$string['onthemorningofappointment'] = 'On the morning of the appointment'; +$string['options'] = 'Options'; +$string['otherstudents'] = 'Other participants'; +$string['outlineappointments'] = '{$a->attended} appointments attended, {$a->upcoming} upcoming. '; +$string['outlinegrade'] = 'Grade: {$a}.'; +$string['overall'] = 'Overall'; +$string['overlappings'] = 'Some other slots are overlapping'; +$string['pageperteacher'] = 'One page for each {$a}'; +$string['pagination'] = 'Pagination'; +$string['pagination_help'] = 'Choose whether the export should contain a separate page for each teacher. + In Excel and in ODS file format, these pages correspond to tabs (worksheets) in the workbook.'; +$string['pdfformat'] = 'PDF'; +$string['pdforientation'] = 'PDF page orientation'; +$string['portrait'] = 'Portrait'; +$string['preview'] = 'Preview'; +$string['previewlimited'] = '(Preview is limited to {$a} rows.)'; +$string['purgeunusedslots'] = 'Purge unused slots in the past'; +$string['recipients'] = 'Recipients'; +$string['registeredlbl'] = 'Student appointed'; +$string['reminder'] = 'Reminder'; +$string['requireupload'] = 'File upload required'; +$string['resetslots'] = 'Delete scheduler slots'; +$string['resetappointments'] = 'Delete appointments and grades'; +$string['revealteachernotes'] = 'Reveal teacher notes in privacy exports'; +$string['revealteachernotes_desc'] = 'If this option is selected, then confidential teacher notes (which are normally not visible to students) +will be revealed to students in data export requests, i.e., via the privay API. You should decide based on individual usage of this field +whether it needs to be included in data exports for students under the GDPR.'; +$string['return'] = 'Back to course'; +$string['revoke'] = 'Revoke the appointment'; +$string['saturday'] = 'Saturday'; +$string['save'] = 'Save'; +$string['savechoice'] = 'Save my choice'; +$string['saveseen'] = 'Save seen'; +$string['schedule'] = 'Schedule'; +$string['scheduleappointment'] = 'Schedule appointment for {$a}'; +$string['schedulecancelled'] = '{$a} : Your appointment cancelled or moved'; +$string['schedulegroups'] = 'Schedule by group'; +$string['scheduleinnew'] = 'Schedule in a new slot'; +$string['scheduleinslot'] = 'Schedule in slot'; +$string['scheduler'] = 'Scheduler'; +$string['schedulestudents'] = 'Schedule by student'; +$string['scopemenu'] = 'Show slots in: {$a}'; +$string['scopemenuself'] = 'Show my slots in: {$a}'; +$string['seen'] = 'Seen'; +$string['selectedtoomany'] = 'You have selected too many slots. You can select no more than {$a}.'; +$string['sendmessage'] = 'Send message'; +$string['sendinvitation'] = 'Send invitation'; +$string['sendreminder'] = 'Send reminder'; +$string['sendreminders'] = 'Send e-mail reminders for upcoming appointments'; +$string['sepcolon'] = 'Colon'; +$string['sepcomma'] = 'Comma'; +$string['sepsemicolon'] = 'Semicolon'; +$string['septab'] = 'Tab'; +$string['showemailplain'] = 'Show e-mail addresses in plain text'; +$string['showemailplain_desc'] = 'In the teacher\'s view of the scheduler, show the e-mail addresses of students needing an appointment in plain text, in addition to mailto: links.'; +$string['showparticipants'] = 'Show participants'; +$string['slot_is_just_in_use'] = 'Sorry, the appointment has just been chosen by another student! Please try again.'; +$string['slotdatetime'] = '{$a->shortdatetime} for {$a->duration} minutes'; +$string['slotdatetimelong'] = '{$a->date}, {$a->starttime} – {$a->endtime}'; +$string['slotdatetimelabel'] = 'Date and time'; +$string['slotdescription'] = '{$a->status} on {$a->startdate} from {$a->starttime} to {$a->endtime} at {$a->location} with {$a->facilitator}.'; +$string['slot'] = 'Slot'; +$string['slots'] = 'Slots'; +$string['slotsadded'] = '{$a} slots have been added'; +$string['slotsdeleted'] = '{$a} slots have been deleted'; +$string['slottype'] = 'Slot type'; +$string['slotupdated'] = '1 slot updated'; +$string['slotwarning'] = '<strong>Warning:</strong> Moving this slot to the selected time conflicts with the slot(s) listed below. Tick "Ignore scheduling conflicts" if you want to move the slot nevertheless.'; +$string['staffbreakdown'] = 'By {$a}'; +$string['staffrolename'] = 'Role name of the teacher'; +$string['start'] = 'Start'; +$string['startpast'] = 'You can\'t start an empty appointment slot in the past'; +$string['statistics'] = 'Statistics'; +$string['student'] = 'Student'; +$string['studentbreakdown'] = 'By student'; +$string['studentcomments'] = 'Student\'s message'; +$string['studentdetails'] = 'Student details'; +$string['studentfiles'] = 'Uploaded files'; +$string['studentmultiselect'] = 'Each student can be selected only once in this slot'; +$string['studentnote'] = 'Message by student'; +$string['students'] = 'Students'; +$string['studentprovided'] = 'Student provided: {$a}'; +$string['sunday'] = 'Sunday'; +$string['tab-thisappointment'] = 'This appointment'; +$string['tab-otherappointments'] = 'All appointments of this student'; +$string['tab-otherstudents'] = 'Students in this slot'; +$string['teacher'] = 'Teacher'; +$string['teachernote'] = 'Confidential notes (visible to teacher only)'; +$string['teachersmenu'] = 'Show slots for: {$a}'; +$string['thisscheduler'] = 'this scheduler'; +$string['thiscourse'] = 'this course'; +$string['thissite'] = 'the entire site'; +$string['thursday'] = 'Thursday'; +$string['timefrom'] = 'From:'; +$string['timerange'] = 'Time range'; +$string['timeto'] = 'To:'; +$string['totalgrade'] = 'Total grade'; +$string['tuesday'] = 'Tuesday'; +$string['unattended'] = 'Unattended'; +$string['unlimited'] = 'Unlimited'; +$string['unregisteredlbl'] = 'Unappointed students'; +$string['upcomingslots'] = 'Upcoming slots'; +$string['updategrades'] = 'Update grades'; +$string['updatesingleslot'] = ''; +$string['uploadrequired'] = 'You must upload files here before booking the slot.'; +$string['uploadstudentfiles'] = 'Upload files'; +$string['uploadmaxfiles'] = 'Maximum number of uploaded files'; +$string['uploadmaxfiles_help'] = 'The maximum number of files that a student can upload in the booking form. File upload is optional unless the "File upload required" box is ticked. If set to 0, students will not see a file upload box.'; +$string['uploadmaxsize'] = 'Maximum file size'; +$string['uploadmaxsize_help'] = 'Maximum file size for student uploads. This limit applies per file.'; +$string['uploadmaxfilesglobal'] = 'Maximum number of uploaded files'; +$string['uploadmaxfilesglobal_desc'] = 'The maximum number of files that a student can upload in a booking form. This can be reduced further at the level of individual schedulers.'; +$string['usebookingform'] = 'Use booking form'; +$string['usebookingform_help'] = 'If enabled, student see a separate booking screen before they can book a slot. The booking screen may require them to enter data, upload files, or solve a captcha; see options below.'; +$string['usebookingform_link'] = 'mod/scheduler/bookingform'; +$string['usecaptcha'] = 'Use CAPTCHA for new bookings'; +$string['usecaptcha_help'] = 'If enabled, students will need to solve a CAPTCHA security question before making a new booking. +Use this setting if you suspect that students use automated programs to snap up available slots. +<p>No captcha will be displayed if the student edits an existing booking.</p>'; +$string['usenotes'] = 'Use notes for appointments'; +$string['usenotesnone'] = 'none'; +$string['usenotesstudent'] = 'Appointment note, visible to teacher and student'; +$string['usenotesteacher'] = 'Confidential note, visible to teachers only'; +$string['usenotesboth'] = 'Both types of notes'; +$string['usestudentnotes'] = 'Let students enter a message'; +$string['usestudentnotes_help'] = 'If enabled, the booking screen will contain a text box in which students can enter a message. Use the "booking instructions" above to instruct students what information they should supply.'; +$string['viewbooking'] = 'See details'; +$string['wednesday'] = 'Wednesday'; +$string['welcomebackstudent'] = 'You can book additional slots by clicking on the corresponding "Book slot" button below.'; +$string['welcomenewstudent'] = 'The table below shows all available slots for an appointment. Make your choice by clicking on the corresponding "Book slot" button. If you need to make a change later you can revisit this page.'; +$string['welcomenewteacher'] = 'Please click on the button below to add appointment slots.'; +$string['what'] = 'What?'; +$string['whathappened'] = 'What happened?'; +$string['whatresulted'] = 'What resulted?'; +$string['when'] = 'When?'; +$string['where'] = 'Where?'; +$string['who'] = 'With whom?'; +$string['whosthere'] = 'Who\'s there ?'; +$string['xdaysbefore'] = '{$a} days before slot'; +$string['xweeksbefore'] = '{$a} weeks before slot'; +$string['yesallgroups'] = 'Yes, for all groups'; +$string['yesingrouping'] = 'Yes, in grouping {$a}'; +$string['yesoptional'] = 'Yes, optional for student'; +$string['yesrequired'] = 'Yes, student must enter a message'; +$string['yourappointmentnote'] = 'Comments for your eyes'; +$string['yourslotnotes'] = 'Comments on the meeting'; +$string['yourstudentnote'] = 'Your message'; +$string['yourtotalgrade'] = 'Your total grade in this activity is <strong>{$a}</strong>.'; + + +/* *********** Help strings from here on ************ */ + +$string['forcewhenoverlap_help'] = ' +<h3>Forcing slot creation when slots overlap</h3> +<p>This setting determines how new slots will be handled if they overlap with other, already existing slots.</p> +<p>If enabled, the overlapping existing slot will be deleted and the new slot created.</p> +<p>If disabled, the overlapping existing slot will be kept and a new slot will <em>not</em> be created.</p> +'; +$string['forcewhenoverlap_link'] = 'mod/scheduler/conflict'; + +$string['appointmentmode'] = 'Setting the appointment mode'; +$string['appointmentmode_help'] = '<p>You may choose here some variants in the way appointments can be taken. </p> +<p><ul> +<li><strong>"<emph>n</emph> appointments in this scheduler":</strong> The student can only book a fixed number of appointments in this activity. Even if the teacher marks them as "seen", they will not be allowed to book further meetings. The only way to reset ability of a student to book is to delete the old "seen" records.</li> +<li><strong>"<emph>n</emph> appointments at a time":</strong> The student can book a fixed number of appointments. Once the meeting is over and the teacher has marked the student as "seen", the student can book further appointments. However the student is limited to <emph>n</emph> "open" (unseen) slots at any given time. +</li> +</ul> +</p>'; + +$string['appointagroup_help'] = 'Choose whether you want to make the appointment only for yourself, or for an entire group.'; + +$string['bookwithteacher_help'] = 'Choose a teacher for the appointment.'; + +$string['choosingslotstart_help'] = 'Change (or choose) the appointment start time. If this appointment collides with some other slots, you\'ll be asked +if this slot replaces all conflicting appointments. Note that the new slot parameters will override all previous +settings.'; + +$string['exclusivity_help'] = '<p>You can set a limit on the number of students that can apply for a given slot. </p> +<p>Setting a limit of 1 (default) will mean that the slot is exclusive to a single student.</p> +<p>Setting a limit of, e.g., 3 will mean that up to three students can book into the slot.</p> +<p>If disabled, any number of students can book the slot; it will never be considered "full".</p>'; + +$string['location_help'] = 'Specify the scheduled location of the meeting.'; + +$string['notifications_help'] = 'When this option is enabled, teachers and students will receive notifications when appointments are applied for or cancelled.'; + +$string['staffrolename_help'] = ' +The label for the role who attends students. This is not necessarily a "teacher".'; + +$string['guardtime_help'] = 'A guard time prevents students from changing their booking shortly before the appointment. +<p>If the guard time is enabled and set to, for example, 2 hours, then students will be unable to book a slot that starts in less than 2 hours time from now, +and they will be unable to drop an appointment if it start in less than 2 hours.</p>'; + + +/* *********** E-mail templates from here on ************ */ + +$string['email_applied_subject'] = '{$a->course_short}: New appointment'; +$string['email_applied_plain'] = 'An appointment has been applied for on {$a->date} at {$a->time}, +by the student {$a->attendee} for the course: + +{$a->course_short}: {$a->course} + +using the scheduler titled "{$a->module}" on the website: {$a->site}.'; + +$string['email_applied_html'] = '<p>An appointment has been applied for on {$a->date} at {$a->time},<br/> +by the student <a href="{$a->attendee_url}">{$a->attendee}</a> for the course: + +<p>{$a->course_short}: <a href="{$a->course_url}">{$a->course}</a></p> + +<p>using the scheduler titled "<em><a href="{$a->scheduler_url}">{$a->module}</a></em>" on the website: <a href="{$a->site_url}">{$a->site}</a>.</p>'; + +$string['email_cancelled_subject'] = '{$a->course_short}: Appointment cancelled or moved by a student'; + +$string['email_cancelled_plain'] = 'Your appointment on {$a->date} at {$a->time}, +with the student {$a->attendee} for course: + +{$a->course_short} : {$a->course} + +in the scheduler titled "{$a->module}" on the website : {$a->site} + +has been cancelled or moved.'; + +$string['email_cancelled_html'] = '<p>Your appointment on <strong>{$a->date}</strong> at <strong>{$a->time}</strong>,<br/> +with the student <strong><a href="{$a->attendee_url}">{$a->attendee}</a></strong> for course :</p> + +<p><strong>{$a->course_short} : <a href="{$a->course_url}">{$a->course}</a></strong></p> + +<p>in the scheduler titled "<em><a href="{$a->scheduler_url}">{$a->module}</a></em>" on the website : <strong><a href="{$a->site_url}">{$a->site}</a></strong></p> + +<p><strong><span class="error">has been cancelled or moved</span></strong>.</p>'; + +$string['email_reminder_subject'] = '{$a->course_short}: Appointment reminder'; + +$string['email_reminder_plain'] = 'You have an upcoming appointment +on {$a->date} from {$a->time} to {$a->endtime} +with {$a->attendant}. + +Location: {$a->location}'; + +$string['email_reminder_html'] = '<p>You have an upcoming appointment on <strong>{$a->date}</strong> +from <strong>{$a->time}</strong> to <strong>{$a->endtime}</strong><br/> +with <strong><a href="{$a->attendant_url}">{$a->attendant}</a></strong>.</p> + +<p>Location: <strong>{$a->location}</strong></p>'; + +$string['email_teachercancelled_subject'] = '{$a->course_short}: Appointment cancelled by the teacher'; + +$string['email_teachercancelled_plain'] = 'Your appointment on {$a->date} at {$a->time}, +with the {$a->staffrole} {$a->attendant} for course: + +{$a->course_short}: {$a->course} + +in the scheduler titled "{$a->module}" on the website: {$a->site} + +has been cancelled. Please apply for a new slot.'; + +$string['email_teachercancelled_html'] = '<p>Your appointment on <strong>{$a->date}</strong> at <strong>{$a->time} </strong>,<br/> +with the {$a->staffrole} <strong><a href="{$a->attendant_url}">{$a->attendant}</a></strong> for course:</p> + +<p><strong>{$a->course_short}: <a href="{$a->course_url}">{$a->course}</a></strong></p> + +<p>in the scheduler "<em><a href="{$a->scheduler_url}">{$a->module}</a></em>" on the website: <strong><a href="{$a->site_url}">{$a->site}</a></strong></p> + +<p><strong><span class="error">has been cancelled</span></strong>. Please apply for a new slot.</p>'; + +$string['email_invite_subject'] = 'Invitation: {$a->module}'; +$string['email_invite_html'] = '<p>Please choose a time slot for an appointment at:</p> <p>{$a->scheduler_url}</p>'; + +$string['email_invitereminder_subject'] = 'Reminder: {$a->module}'; +$string['email_invitereminder_html'] = '<p>This is just a reminder that you have not yet set up your appointment. Please choose a time slot as soon as possible at:</p><p>{$a->scheduler_url}</p>'; diff --git a/mod/scheduler/lib.php b/mod/scheduler/lib.php new file mode 100644 index 0000000..147bd44 --- /dev/null +++ b/mod/scheduler/lib.php @@ -0,0 +1,757 @@ +<?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/>. + +/** + * Library (public API) of the scheduler module + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; + +// Library of functions and constants for module Scheduler. + +require_once($CFG->dirroot.'/mod/scheduler/locallib.php'); +require_once($CFG->dirroot.'/mod/scheduler/mailtemplatelib.php'); +require_once($CFG->dirroot.'/mod/scheduler/renderer.php'); +require_once($CFG->dirroot.'/mod/scheduler/renderable.php'); + +define('SCHEDULER_TIMEUNKNOWN', 0); // This is used for appointments for which no time is entered. +define('SCHEDULER_SELF', 0); // Used for setting conflict search scope. +define('SCHEDULER_OTHERS', 1); // Used for setting conflict search scope. +define('SCHEDULER_ALL', 2); // Used for setting conflict search scope. + +define ('SCHEDULER_MEAN_GRADE', 0); // Used for grading strategy. +define ('SCHEDULER_MAX_GRADE', 1); // Used for grading strategy. + +/** + * Given an object containing all the necessary data, + * will create a new instance and return the id number + * of the new instance. + * + * @param stdClass $data the current instance + * @param mod_scheduler_mod_form $mform the form that the user filled + * @return int the new instance id + * @uses $DB + */ +function scheduler_add_instance($data, $mform = null) { + global $DB; + + $cmid = $data->coursemodule; + + $data->timemodified = time(); + $data->scale = isset($data->grade) ? $data->grade : 0; + + $data->id = $DB->insert_record('scheduler', $data); + + $DB->set_field('course_modules', 'instance', $data->id, array('id' => $cmid)); + $context = context_module::instance($cmid); + + if ($mform) { + $mform->save_mod_data($data, $context); + } + + scheduler_grade_item_update($data); + + if (class_exists('\core_completion\api')) { + $completiontimeexpected = !empty($data->completionexpected) ? $data->completionexpected : null; + \core_completion\api::update_completion_date_event($data->coursemodule, 'scheduler', $data->id, $completiontimeexpected); + } + + return $data->id; +} + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod.html) this function + * will update an existing instance with new data. + * + * @param stdClass $data + * @param mod_scheduler_mod_form $mform the form that the user filled + * @return bool the updated instance + * @uses $DB + */ +function scheduler_update_instance($data, $mform) { + global $DB; + + $data->timemodified = time(); + $data->id = $data->instance; + + $data->scale = $data->grade; + + $DB->update_record('scheduler', $data); + + $context = context_module::instance($data->coursemodule); + $mform->save_mod_data($data, $context); + + // Update grade item and grades. + scheduler_update_grades($data); + + if (class_exists('\core_completion\api')) { + $completiontimeexpected = !empty($data->completionexpected) ? $data->completionexpected : null; + \core_completion\api::update_completion_date_event($data->coursemodule, 'scheduler', $data->id, $completiontimeexpected); + } + + return true; +} + + +/** + * Given an ID of an instance of this module, + * this function will permanently delete the instance + * and any data that depends on it. + * + * @param int $id the instance to be deleted + * @return bool true if success, false otherwise + * @uses $DB + */ +function scheduler_delete_instance($id) { + global $DB; + + if (! $DB->record_exists('scheduler', array('id' => $id))) { + return false; + } + + $scheduler = scheduler::load_by_id($id); + $scheduler->delete(); + + // Clean up any possibly remaining event records. + $params = array('modulename' => 'scheduler', 'instance' => $id); + $DB->delete_records('event', $params); + + return true; +} + +/** + * Return a small object with summary information about what a + * user has done with a given particular instance of this module + * Used for user activity reports. + * + * $return->time = the time they did it + * $return->info = a short text description + * @param object $course the course instance + * @param object $user the concerned user instance + * @param object $mod the current course module instance + * @param object $scheduler the activity module behind the course module instance + * @return object an information object as defined above + */ +function scheduler_user_outline($course, $user, $mod, $scheduler) { + + $scheduler = scheduler::load_by_coursemodule_id($mod->id); + $upcoming = count($scheduler->get_upcoming_slots_for_student($user->id)); + $attended = count($scheduler->get_attended_slots_for_student($user->id)); + + $text = ''; + + if ($attended + $upcoming > 0) { + $a = array('attended' => $attended, 'upcoming' => $upcoming); + $text .= get_string('outlineappointments', 'scheduler', $a); + } + + if ($scheduler->uses_grades()) { + $grade = $scheduler->get_gradebook_info($user->id); + if ($grade) { + $text .= get_string('outlinegrade', 'scheduler', $grade->str_long_grade); + } + } + + $return = new stdClass(); + $return->info = $text; + return $return; +} + +/** + * Prints a detailed representation of what a user has done with + * a given particular instance of this module, for user activity reports. + * + * @param object $course the course instance + * @param object $user the concerned user instance + * @param object $mod the current course module instance + * @param object $scheduler the activity module behind the course module instance + */ +function scheduler_user_complete($course, $user, $mod, $scheduler) { + + global $PAGE; + + $scheduler = scheduler::load_by_coursemodule_id($mod->id); + $output = $PAGE->get_renderer('mod_scheduler', null, RENDERER_TARGET_GENERAL); + + $appointments = $scheduler->get_appointments_for_student($user->id); + + if (count($appointments) > 0) { + $table = new scheduler_slot_table($scheduler); + $table->showattended = true; + foreach ($appointments as $app) { + $table->add_slot($app->get_slot(), $app, null, false); + } + + echo $output->render($table); + } else { + echo get_string('noappointments', 'scheduler'); + } + + if ($scheduler->uses_grades()) { + $grade = $scheduler->get_gradebook_info($user->id); + if ($grade) { + $info = new scheduler_totalgrade_info($scheduler, $grade); + echo $output->render($info); + } + } + +} + +/** + * Given a course and a time, this module should find recent activity + * that has occurred in scheduler activities and print it out. + * Return true if there was output, or false is there was none. + * + * @param object $course the course instance + * @param bool $isteacher true tells a teacher uses the function + * @param int $timestart a time start timestamp + * @return bool true if anything was printed, otherwise false + */ +function scheduler_print_recent_activity($course, $isteacher, $timestart) { + + return false; +} + + +/** + * This function returns whether a scale is being used by a scheduler. + * + * @param int $cmid ID of an instance of this module + * @param int $scaleid the id of the scale in question + * @return mixed + * @uses $DB + **/ +function scheduler_scale_used($cmid, $scaleid) { + global $DB; + + $return = false; + + // Note: scales are assigned using negative index in the grade field of the appointment (see mod/assignement/lib.php). + $rec = $DB->get_record('scheduler', array('id' => $cmid, 'scale' => -$scaleid)); + + if (!empty($rec) && !empty($scaleid)) { + $return = true; + } + + return $return; +} + + +/** + * Checks if scale is being used by any instance of scheduler + * + * @param int $scaleid the id of the scale in question + * @return bool True if the scale is used by any scheduler + * @uses $DB + */ +function scheduler_scale_used_anywhere($scaleid) { + global $DB; + + if ($scaleid and $DB->record_exists('scheduler', array('scale' => -$scaleid))) { + return true; + } else { + return false; + } +} + + +/* + * Course resetting API + * + */ + +/** + * Called by course/reset.php + * + * @param MoodleQuickForm $mform form passed by reference + * @uses $COURSE + * @uses $DB + */ +function scheduler_reset_course_form_definition(&$mform) { + global $COURSE, $DB; + + $mform->addElement('header', 'schedulerheader', get_string('modulenameplural', 'scheduler')); + + if ($DB->record_exists('scheduler', array('course' => $COURSE->id))) { + + $mform->addElement('checkbox', 'reset_scheduler_slots', get_string('resetslots', 'scheduler')); + $mform->addElement('checkbox', 'reset_scheduler_appointments', get_string('resetappointments', 'scheduler')); + $mform->disabledIf('reset_scheduler_appointments', 'reset_scheduler_slots', 'checked'); + } +} + +/** + * Default values for the reset form + * + * @param stdClass $course the course in which the reset takes place + */ +function scheduler_reset_course_form_defaults($course) { + return array('reset_scheduler_slots' => 1, 'reset_scheduler_appointments' => 1); +} + + +/** + * This function is used by the remove_course_userdata function in moodlelib. + * If this function exists, remove_course_userdata will execute it. + * This function will remove all slots and appointments from the specified scheduler. + * + * @param object $data the reset options + * @return void + */ +function scheduler_reset_userdata($data) { + global $CFG, $DB; + + $status = array(); + $componentstr = get_string('modulenameplural', 'scheduler'); + + $success = true; + + if (!empty($data->reset_scheduler_appointments) || !empty($data->reset_scheduler_slots)) { + + $schedulers = $DB->get_records('scheduler', ['course' => $data->courseid]); + + foreach ($schedulers as $srec) { + $scheduler = scheduler::load_by_id($srec->id); + + if (!empty($data->reset_scheduler_slots) ) { + $scheduler->delete_all_slots(); + $status[] = array('component' => $componentstr, 'item' => get_string('resetslots', 'scheduler'), 'error' => false); + } else if (!empty($data->reset_scheduler_appointments) ) { + foreach ($scheduler->get_all_slots() as $slot) { + $slot->delete_all_appointments(); + } + $status[] = array( + 'component' => $componentstr, + 'item' => get_string('resetappointments', 'scheduler'), + 'error' => !$success + ); + } + } + } + return $status; +} + +/** + * Determine whether a certain feature is supported by Scheduler. + * + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed True if module supports feature, null if doesn't know + */ +function scheduler_supports($feature) { + switch($feature) { + case FEATURE_GROUPS: + return true; + case FEATURE_GROUPINGS: + return true; + case FEATURE_GROUPMEMBERSONLY: + return true; + case FEATURE_MOD_INTRO: + return true; + case FEATURE_COMPLETION_TRACKS_VIEWS: + return false; + case FEATURE_GRADE_HAS_GRADE: + return true; + case FEATURE_GRADE_OUTCOMES: + return false; + case FEATURE_BACKUP_MOODLE2: + return true; + + default: + return null; + } +} + +/* Gradebook API */ +/* + * add xxx_update_grades() function into mod/xxx/lib.php + * add xxx_grade_item_update() function into mod/xxx/lib.php + * patch xxx_update_instance(), xxx_add_instance() and xxx_delete_instance() to call xxx_grade_item_update() + * patch all places of code that change grade values to call xxx_update_grades() + * patch code that displays grades to students to use final grades from the gradebook + */ + +/** + * Update activity grades + * + * @param object $schedulerrecord + * @param int $userid specific user only, 0 means all + * @param bool $nullifnone not used + * @uses $CFG + * @uses $DB + */ +function scheduler_update_grades($schedulerrecord, $userid=0, $nullifnone=true) { + global $CFG, $DB; + require_once($CFG->libdir.'/gradelib.php'); + + $scheduler = scheduler::load_by_id($schedulerrecord->id); + + if ($scheduler->scale == 0) { + scheduler_grade_item_update($schedulerrecord); + + } else if ($grades = $scheduler->get_user_grades($userid)) { + foreach ($grades as $k => $v) { + if ($v->rawgrade == -1) { + $grades[$k]->rawgrade = null; + } + } + scheduler_grade_item_update($schedulerrecord, $grades); + + } else { + scheduler_grade_item_update($schedulerrecord); + } +} + + +/** + * Create grade item for given scheduler + * + * @param object $scheduler object + * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook + * @return int 0 if ok, error code otherwise + */ +function scheduler_grade_item_update($scheduler, $grades=null) { + global $CFG, $DB; + require_once($CFG->libdir.'/gradelib.php'); + + if (!isset($scheduler->courseid)) { + $scheduler->courseid = $scheduler->course; + } + $moduleid = $DB->get_field('modules', 'id', array('name' => 'scheduler')); + $cmid = $DB->get_field('course_modules', 'id', array('module' => $moduleid, 'instance' => $scheduler->id)); + + if ($scheduler->scale == 0) { + // Delete any grade item. + scheduler_grade_item_delete($scheduler); + return 0; + } else { + $params = array('itemname' => $scheduler->name, 'idnumber' => $cmid); + + if ($scheduler->scale > 0) { + $params['gradetype'] = GRADE_TYPE_VALUE; + $params['grademax'] = $scheduler->scale; + $params['grademin'] = 0; + + } else if ($scheduler->scale < 0) { + $params['gradetype'] = GRADE_TYPE_SCALE; + $params['scaleid'] = -$scheduler->scale; + + } else { + $params['gradetype'] = GRADE_TYPE_TEXT; // Allow text comments only. + } + + if ($grades === 'reset') { + $params['reset'] = true; + $grades = null; + } + + return grade_update('mod/scheduler', $scheduler->courseid, 'mod', 'scheduler', $scheduler->id, 0, $grades, $params); + } +} + + + +/** + * Update all grades in gradebook. + */ +function scheduler_upgrade_grades() { + global $DB; + + $sql = "SELECT COUNT('x') + FROM {scheduler} s, {course_modules} cm, {modules} m + WHERE m.name='scheduler' AND m.id=cm.module AND cm.instance=s.id"; + $count = $DB->count_records_sql($sql); + + $sql = "SELECT s.*, cm.idnumber AS cmidnumber, s.course AS courseid + FROM {scheduler} s, {course_modules} cm, {modules} m + WHERE m.name='scheduler' AND m.id=cm.module AND cm.instance=s.id"; + $rs = $DB->get_recordset_sql($sql); + if ($rs->valid()) { + $pbar = new progress_bar('schedulerupgradegrades', 500, true); + $i = 0; + foreach ($rs as $scheduler) { + $i++; + upgrade_set_timeout(60 * 5); // Set up timeout, may also abort execution. + scheduler_update_grades($scheduler); + $pbar->update($i, $count, "Updating scheduler grades ($i/$count)."); + } + upgrade_set_timeout(); // Reset to default timeout. + } + $rs->close(); +} + + +/** + * Delete grade item for given scheduler + * + * @param object $scheduler object + * @return object scheduler + */ +function scheduler_grade_item_delete($scheduler) { + global $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + if (!isset($scheduler->courseid)) { + $scheduler->courseid = $scheduler->course; + } + + return grade_update('mod/scheduler', $scheduler->courseid, 'mod', 'scheduler', $scheduler->id, 0, null, array('deleted' => 1)); +} + + +/* + * File API + */ + +/** + * Lists all browsable file areas + * + * @package mod_scheduler + * @category files + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @return array + */ +function scheduler_get_file_areas($course, $cm, $context) { + return array( + 'bookinginstructions' => get_string('bookinginstructions', 'scheduler'), + 'slotnote' => get_string('areaslotnote', 'scheduler'), + 'appointmentnote' => get_string('areaappointmentnote', 'scheduler'), + 'teachernote' => get_string('areateachernote', 'scheduler') + ); +} + +/** + * File browsing support for scheduler module. + * + * @param file_browser $browser + * @param array $areas + * @param stdClass $course + * @param cm_info $cm + * @param context $context + * @param string $filearea + * @param int $itemid + * @param string $filepath + * @param string $filename + * @return file_info_stored file_info_stored instance or null if not found + */ +function scheduler_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { + global $CFG, $DB, $USER; + + // Note: 'intro' area is handled in file_browser automatically. + + if (!has_any_capability(array('mod/scheduler:appoint', 'mod/scheduler:attend', + 'mod/scheduler:viewotherteachersbooking', 'mod/scheduler:manageallappointments'), $context)) { + return null; + } + + require_once(dirname(__FILE__).'/locallib.php'); + + $validareas = array_keys(scheduler_get_file_areas($course, $cm, $context)); + if (!in_array($filearea, $validareas)) { + return null; + } + + if (is_null($itemid)) { + return new scheduler_file_info($browser, $course, $cm, $context, $areas, $filearea); + } + + try { + $scheduler = scheduler::load_by_coursemodule_id($cm->id); + $permissions = new \mod_scheduler\permission\scheduler_permissions($context, $USER->id); + + if ($filearea === 'bookinginstructions') { + $cansee = true; + $canwrite = has_capability('moodle/course:manageactivities', $context); + $name = get_string('bookinginstructions', 'scheduler'); + + } else if ($filearea === 'slotnote') { + $slot = $scheduler->get_slot($itemid); + $cansee = true; + $canwrite = $permissions->can_edit_slot($slot); + $name = get_string('slot', 'scheduler'). ' '.$itemid; + + } else if ($filearea === 'appointmentnote') { + if (!$scheduler->uses_appointmentnotes()) { + return null; + } + list($slot, $app) = $scheduler->get_slot_appointment($itemid); + $cansee = $permissions->can_see_appointment($app); + $canwrite = $permissions->can_edit_notes($app); + $name = get_string('appointment', 'scheduler'). ' '.$itemid; + + } else if ($filearea === 'teachernote') { + if (!$scheduler->uses_teachernotes()) { + return null; + } + + list($slot, $app) = $scheduler->get_slot_appointment($itemid); + $cansee = $permissions->teacher_can_see_slot($slot); + $canwrite = $permissions->can_edit_notes($app); + $name = get_string('appointment', 'scheduler'). ' '.$itemid; + } + + $fs = get_file_storage(); + $filepath = is_null($filepath) ? '/' : $filepath; + $filename = is_null($filename) ? '.' : $filename; + if (!$storedfile = $fs->get_file($context->id, 'mod_scheduler', $filearea, $itemid, $filepath, $filename)) { + return null; + } + + $urlbase = $CFG->wwwroot.'/pluginfile.php'; + return new file_info_stored($browser, $context, $storedfile, $urlbase, $name, true, true, $canwrite, false); + } catch (Exception $e) { + return null; + } +} + +/** + * Serves the files embedded in various rich text fields, or uploaded by students + * + * @package mod_scheduler + * @category files + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClsss $context context object + * @param string $filearea file area + * @param array $args extra arguments + * @param bool $forcedownload whether or not force download + * @param array $options additional options affecting the file serving + * @return bool false if file not found, does not return if found - just send the file + */ +function scheduler_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { + global $CFG, $DB, $USER; + + if ($context->contextlevel != CONTEXT_MODULE) { + return false; + } + + require_course_login($course, true, $cm); + if (!has_any_capability(array('mod/scheduler:appoint', 'mod/scheduler:attend'), $context)) { + return false; + } + + try { + $scheduler = scheduler::load_by_coursemodule_id($cm->id); + $permissions = new \mod_scheduler\permission\scheduler_permissions($context, $USER->id); + + $entryid = (int)array_shift($args); + $relativepath = implode('/', $args); + + if ($filearea === 'slotnote') { + if (!$scheduler->get_slot($entryid)) { + return false; + } + // No further access control required - everyone can see slots notes. + + } else if ($filearea === 'appointmentnote') { + if (!$scheduler->uses_appointmentnotes()) { + return false; + } + + list($slot, $app) = $scheduler->get_slot_appointment($entryid); + if (!$app) { + return false; + } + + $permissions->ensure($permissions->can_see_appointment($app)); + + } else if ($filearea === 'teachernote') { + if (!$scheduler->uses_teachernotes()) { + return false; + } + + list($slot, $app) = $scheduler->get_slot_appointment($entryid); + if (!$app) { + return false; + } + + $permissions->ensure($permissions->teacher_can_see_slot($slot)); + + } else if ($filearea === 'bookinginstructions') { + $caps = array('moodle/course:manageactivities', 'mod/scheduler:appoint'); + if (!has_any_capability($caps, $context)) { + return false; + } + + } else if ($filearea === 'studentfiles') { + if (!$scheduler->uses_studentfiles()) { + return false; + } + + list($slot, $app) = $scheduler->get_slot_appointment($entryid); + if (!$app) { + return false; + } + + $permissions->ensure($permissions->can_see_appointment($app)); + + } else { + // Unknown file area. + return false; + } + } catch (Exception $e) { + // Typically, records that are not found in the database. + return false; + } + + $fullpath = "/$context->id/mod_scheduler/$filearea/$entryid/$relativepath"; + + $fs = get_file_storage(); + if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { + return false; + } + + send_stored_file($file, 0, 0, $forcedownload, $options); +} + +/** + * This function receives a calendar event and returns the action associated with it, or null if there is none. + * + * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event + * is not displayed on the block. + * + * @param calendar_event $event + * @param \core_calendar\action_factory $factory + * @return \core_calendar\local\event\entities\action_interface|null + */ +function mod_scheduler_core_calendar_provide_event_action(calendar_event $event, + \core_calendar\action_factory $factory) { + $cm = get_fast_modinfo($event->courseid)->instances['scheduler'][$event->instance]; + + $completion = new \completion_info($cm->get_course()); + + $completiondata = $completion->get_data($cm, false); + + if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { + return null; + } + + return $factory->create_instance( + get_string('view'), + new \moodle_url('/mod/scheduler/view.php', ['id' => $cm->id]), + 1, + true + ); +} + diff --git a/mod/scheduler/locallib.php b/mod/scheduler/locallib.php new file mode 100644 index 0000000..f45cf07 --- /dev/null +++ b/mod/scheduler/locallib.php @@ -0,0 +1,344 @@ +<?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/>. + +/** + * General library for the scheduler module. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/filelib.php'); +require_once(dirname(__FILE__).'/customlib.php'); + + +/* Events related functions */ + +/** + * Will delete calendar events for a given scheduler slot, and not complain if the record does not exist. + * The only argument this function requires is the complete database record of a scheduler slot. + * @param object $slot the slot instance + * @uses $DB + * @return bool true if success, false otherwise + */ +function scheduler_delete_calendar_events($slot) { + global $DB; + + $scheduler = $DB->get_record('scheduler', array('id' => $slot->schedulerid)); + + if (!$scheduler) { + return false; + } + + $teachereventtype = "SSsup:{$slot->id}:{$scheduler->course}"; + $studenteventtype = "SSstu:{$slot->id}:{$scheduler->course}"; + + $teacherdeletionsuccess = $DB->delete_records('event', array('eventtype' => $teachereventtype)); + $studentdeletionsuccess = $DB->delete_records('event', array('eventtype' => $studenteventtype)); + + return ($teacherdeletionsuccess && $studentdeletionsuccess); + // This return may not be meaningful if the delete records functions do not return anything meaningful. +} + + + + +/** + * Prints a summary of a user in a nice little box. + * + * @uses $CFG + * @uses $USER + * @param user $user A {@link $USER} object representing a user + * @param course $course A {@link $COURSE} object representing a course + * @param bool $messageselect whether to include a checkbox to select the user + * @param bool $return whether the HTML fragment is to be returned as a string (otherwise printed) + * @return string HTML fragment, if so selected + */ +function scheduler_print_user($user, $course, $messageselect=false, $return=false) { + + global $CFG, $USER, $OUTPUT; + + $output = ''; + + static $string; + static $datestring; + static $countries; + + $context = context_course::instance($course->id); + if (isset($user->context->id)) { + $usercontext = $user->context; + } else { + $usercontext = context_user::instance($user->id); + } + + if (empty($string)) { // Cache all the strings for the rest of the page. + + $string = new stdClass(); + $string->email = get_string('email'); + $string->lastaccess = get_string('lastaccess'); + $string->activity = get_string('activity'); + $string->loginas = get_string('loginas'); + $string->fullprofile = get_string('fullprofile'); + $string->role = get_string('role'); + $string->name = get_string('name'); + $string->never = get_string('never'); + + $datestring = new stdClass(); + $datestring->day = get_string('day'); + $datestring->days = get_string('days'); + $datestring->hour = get_string('hour'); + $datestring->hours = get_string('hours'); + $datestring->min = get_string('min'); + $datestring->mins = get_string('mins'); + $datestring->sec = get_string('sec'); + $datestring->secs = get_string('secs'); + $datestring->year = get_string('year'); + $datestring->years = get_string('years'); + + } + + // Get the hidden field list. + if (has_capability('moodle/course:viewhiddenuserfields', $context)) { + $hiddenfields = array(); + } else { + $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); + } + + $output .= '<table class="userinfobox">'; + $output .= '<tr>'; + $output .= '<td class="left side">'; + $output .= $OUTPUT->user_picture($user, array('size' => 100)); + $output .= '</td>'; + $output .= '<td class="content">'; + $output .= '<div class="username">'.fullname($user, has_capability('moodle/site:viewfullnames', $context)).'</div>'; + $output .= '<div class="info">'; + if (!empty($user->role) and ($user->role <> $course->teacher)) { + $output .= $string->role .': '. $user->role .'<br />'; + } + + $extrafields = scheduler_get_user_fields($user, $context); + foreach ($extrafields as $field) { + $output .= $field->title . ': ' . $field->value . '<br />'; + } + + if (!isset($hiddenfields['lastaccess'])) { + if ($user->lastaccess) { + $output .= $string->lastaccess .': '. userdate($user->lastaccess); + $output .= ' ('. format_time(time() - $user->lastaccess, $datestring) .')'; + } else { + $output .= $string->lastaccess .': '. $string->never; + } + } + $output .= '</div></td><td class="links">'; + // Link to blogs. + if ($CFG->bloglevel > 0) { + $output .= '<a href="'.$CFG->wwwroot.'/blog/index.php?userid='.$user->id.'">'.get_string('blogs', 'blog').'</a><br />'; + } + // Link to notes. + if (!empty($CFG->enablenotes) and (has_capability('moodle/notes:manage', $context) + || has_capability('moodle/notes:view', $context))) { + $output .= '<a href="'.$CFG->wwwroot.'/notes/index.php?course=' . $course->id. '&user='.$user->id.'">'. + get_string('notes', 'notes').'</a><br />'; + } + + if (has_capability('moodle/site:viewreports', $context) or + has_capability('moodle/user:viewuseractivitiesreport', $usercontext)) { + $output .= '<a href="'. $CFG->wwwroot .'/course/user.php?id='. $course->id .'&user='. $user->id .'">'. + $string->activity .'</a><br />'; + } + $output .= '<a href="'. $CFG->wwwroot .'/user/profile.php?id='. $user->id .'">'. $string->fullprofile .'...</a>'; + + if (!empty($messageselect)) { + $output .= '<br /><input type="checkbox" name="user'.$user->id.'" /> '; + } + + $output .= '</td></tr></table>'; + + if ($return) { + return $output; + } else { + echo $output; + } +} + + +/** + * File browsing support class + * + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_file_info extends file_info { + /** @var stdClass Course object */ + protected $course; + /** @var stdClass Course module object */ + protected $cm; + /** @var array Available file areas */ + protected $areas; + /** @var string File area to browse */ + protected $filearea; + /** @var scheduler The scheduler that this file area refers to */ + protected $scheduler; + + /** + * Constructor + * + * @param file_browser $browser file_browser instance + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context module context + * @param array $areas available file areas + * @param string $filearea file area to browse + */ + public function __construct($browser, $course, $cm, $context, $areas, $filearea) { + parent::__construct($browser, $context); + $this->course = $course; + $this->cm = $cm; + $this->areas = $areas; + $this->filearea = $filearea; + $this->scheduler = scheduler::load_by_coursemodule_id($cm->id); + } + + /** + * Returns list of standard virtual file/directory identification. + * The difference from stored_file parameters is that null values + * are allowed in all fields + * @return array with keys contextid, filearea, itemid, filepath and filename + */ + public function get_params() { + return array('contextid' => $this->context->id, + 'component' => 'mod_scheduler', + 'filearea' => $this->filearea, + 'itemid' => null, + 'filepath' => null, + 'filename' => null); + } + + /** + * Returns localised visible name. + * @return string + */ + public function get_visible_name() { + return $this->areas[$this->filearea]; + } + + /** + * Can I add new files or directories? + * @return bool + */ + public function is_writable() { + return false; + } + + /** + * Is directory? + * @return bool + */ + public function is_directory() { + return true; + } + + /** + * Returns list of children. + * @return array of file_info instances + */ + public function get_children() { + return $this->get_filtered_children('*', false, true); + } + + /** + * Helper function to return files matching extensions or their count + * + * @param string|array $extensions either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') + * @param bool|int $countonly if false returns the children, if an int returns just the + * count of children but stops counting when $countonly number of children is reached + * @param bool $returnemptyfolders if true returns items that don't have matching files inside + * @return array|int array of file_info instances or the count + * @uses $DB + */ + private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { + global $DB; + + $params = array('contextid' => $this->context->id, + 'component' => 'mod_scheduler', + 'filearea' => $this->filearea); + $sql = "SELECT DISTINCT f.itemid AS id + FROM {files} f + WHERE f.contextid = :contextid + AND f.component = :component + AND f.filearea = :filearea"; + if (!$returnemptyfolders) { + $sql .= ' AND filename <> :emptyfilename'; + $params['emptyfilename'] = '.'; + } + list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f'); + $sql .= ' '.$sql2; + $params = array_merge($params, $params2); + + $rs = $DB->get_recordset_sql($sql, $params); + $children = array(); + foreach ($rs as $record) { + if ($child = $this->browser->get_file_info($this->context, 'mod_scheduler', $this->filearea, $record->id)) { + if ($returnemptyfolders || $child->count_non_empty_children($extensions)) { + $children[] = $child; + } + } + if ($countonly !== false && count($children) >= $countonly) { + break; + } + } + $rs->close(); + if ($countonly !== false) { + return count($children); + } + return $children; + } + + /** + * Returns list of children which are either files matching the specified extensions + * or folders that contain at least one such file. + * + * @param string|array $extensions either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') + * @return array of file_info instances + */ + public function get_non_empty_children($extensions = '*') { + return $this->get_filtered_children($extensions, false); + } + + /** + * Returns the number of children which are either files matching the specified extensions + * or folders containing at least one such file. + * + * @param string|array $extensions for example '*' or array('.gif','.jpg') + * @param int $limit stop counting after at least $limit non-empty children are found + * @return int + */ + public function count_non_empty_children($extensions = '*', $limit = 1) { + return $this->get_filtered_children($extensions, $limit); + } + + /** + * Returns parent file_info instance + * + * @return file_info or null for root + */ + public function get_parent() { + return $this->browser->get_file_info($this->context); + } +} diff --git a/mod/scheduler/mailtemplatelib.php b/mod/scheduler/mailtemplatelib.php new file mode 100644 index 0000000..65ab932 --- /dev/null +++ b/mod/scheduler/mailtemplatelib.php @@ -0,0 +1,221 @@ +<?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/>. + +/** + * Message formatting from templates. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined ( 'MOODLE_INTERNAL' ) || die (); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; + +/** + * Message functionality for scheduler module + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_messenger { + /** + * Returns the language to be used in a message to a user + * + * @param stdClass $user + * the user to whom the message will be sent + * @param stdClass $course + * the course from which the message originates + * @return string + */ + protected static function get_message_language($user, $course) { + if ($course && ! empty ($course->id) and $course->id != SITEID and !empty($course->lang)) { + // Course language overrides user language. + $return = $course->lang; + } else if (!empty($user->lang)) { + $return = $user->lang; + } else if (isset ($CFG->lang)) { + $return = $CFG->lang; + } else { + $return = 'en'; + } + + return $return; + } + + /** + * Gets the content of an e-mail from language strings. + * + * Looks for the language string email_$template_$format and replaces the parameter values. + * + * @param string $template the template's identified + * @param string $format the mail format ('subject', 'html' or 'plain') + * @param array $parameters an array ontaining pairs of parm => data to replace in template + * @param string $module module to use language strings from + * @param string $lang language to use + * @return a fully resolved template where all data has been injected + * + */ + public static function compile_mail_template($template, $format, $parameters, $module = 'scheduler', $lang = null) { + $params = array (); + foreach ($parameters as $key => $value) { + $params [strtolower($key)] = $value; + } + $mailstr = get_string_manager()->get_string("email_{$template}_{$format}", $module, $params, $lang); + return $mailstr; + } + + /** + * Sends a message based on a template. + * Several template substitution values are automatically filled by this routine. + * + * @param string $modulename + * name of the module that sends the message + * @param string $messagename + * name of the message in messages.php + * @param int $isnotification + * 1 for notifications, 0 for personal messages + * @param stdClass $sender + * A {@link $USER} object describing the sender + * @param stdClass $recipient + * A {@link $USER} object describing the recipient + * @param object $course + * The course that the activity is in. Can be null. + * @param string $template + * the mail template name as in language config file (without "_html" part) + * @param array $parameters + * a hash containing pairs of parm => data to replace in template + * @return bool|int Returns message id if message was sent OK, "false" if there was another sort of error. + */ + public static function send_message_from_template($modulename, $messagename, $isnotification, + stdClass $sender, stdClass $recipient, $course, + $template, array $parameters) { + global $CFG; + global $SITE; + + $lang = self::get_message_language($recipient, $course); + + $defaultvars = array ( + 'SITE' => $SITE->fullname, + 'SITE_SHORT' => $SITE->shortname, + 'SITE_URL' => $CFG->wwwroot, + 'SENDER' => fullname ( $sender ), + 'RECIPIENT' => fullname ( $recipient ) + ); + + if ($course) { + $defaultvars['COURSE_SHORT'] = format_string($course->shortname); + $defaultvars['COURSE'] = format_string($course->fullname); + $defaultvars['COURSE_URL'] = $CFG->wwwroot . '/course/view.php?id=' . $course->id; + } + + $vars = array_merge($defaultvars, $parameters); + + $message = new \core\message\message(); + $message->component = $modulename; + $message->name = $messagename; + $message->userfrom = $sender; + $message->userto = $recipient; + $message->subject = self::compile_mail_template($template, 'subject', $vars, $modulename, $lang); + $message->fullmessage = self::compile_mail_template($template, 'plain', $vars, $modulename, $lang); + $message->fullmessageformat = FORMAT_PLAIN; + $message->fullmessagehtml = self::compile_mail_template ( $template, 'html', $vars, $modulename, $lang ); + $message->notification = '1'; + $message->courseid = $course->id; + $message->contexturl = $defaultvars['COURSE_URL']; + $message->contexturlname = $course->fullname; + + $msgid = message_send($message); + return $msgid; + } + + /** + * Construct an array with subtitution rules for mail templates, relating to + * a single appointment. Any of the parameters can be null. + * + * @param scheduler $scheduler The scheduler instance + * @param slot $slot The slot data as an MVC object, may be null + * @param user $teacher A {@link $USER} object describing the attendant (teacher) + * @param user $student A {@link $USER} object describing the attendee (student) + * @param object $course A course object relating to the ontext of the message + * @param object $recipient A {@link $USER} object describing the recipient of the message + * (used for determining the message language) + * @return array A hash with mail template substitutions + */ + public static function get_scheduler_variables(scheduler $scheduler, $slot, + $teacher, $student, $course, $recipient) { + + global $CFG; + + $lang = self::get_message_language($recipient, $course); + // Force any string formatting to happen in the target language. + $oldlang = force_current_language($lang); + + $tz = core_date::get_user_timezone($recipient); + + $vars = array(); + + if ($scheduler) { + $vars['MODULE'] = format_string($scheduler->name); + $vars['STAFFROLE'] = $scheduler->get_teacher_name(); + $vars['SCHEDULER_URL'] = $CFG->wwwroot.'/mod/scheduler/view.php?id='.$scheduler->cmid; + } + if ($slot) { + $vars ['DATE'] = userdate($slot->starttime, get_string('strftimedate'), $tz); + $vars ['TIME'] = userdate($slot->starttime, get_string('strftimetime'), $tz); + $vars ['ENDTIME'] = userdate($slot->endtime, get_string('strftimetime'), $tz); + $vars ['LOCATION'] = format_string($slot->appointmentlocation); + } + if ($teacher) { + $vars['ATTENDANT'] = fullname($teacher); + $vars['ATTENDANT_URL'] = $CFG->wwwroot.'/user/view.php?id='.$teacher->id.'&course='.$scheduler->course; + } + if ($student) { + $vars['ATTENDEE'] = fullname($student); + $vars['ATTENDEE_URL'] = $CFG->wwwroot.'/user/view.php?id='.$student->id.'&course='.$scheduler->course; + } + + // Reset language settings. + force_current_language($oldlang); + + return $vars; + + } + + + /** + * Send a notification message about a scheduler slot. + * + * @param slot $slot the slot that the notification relates to + * @param string $messagename name of message as in db/message.php + * @param string $template template name to use (language string up to prefix/postfix) + * @param stdClass $sender user record for sender + * @param stdClass $recipient user record for recipient + * @param stdClass $teacher user record for teacher + * @param stdClass $student user record for student + * @param stdClass $course course record + */ + public static function send_slot_notification(slot $slot, $messagename, $template, + stdClass $sender, stdClass $recipient, + stdClass $teacher, stdClass $student, stdClass $course) { + $vars = self::get_scheduler_variables($slot->get_scheduler(), $slot, $teacher, $student, $course, $recipient); + self::send_message_from_template('mod_scheduler', $messagename, 1, $sender, $recipient, $course, $template, $vars); + } + +} \ No newline at end of file diff --git a/mod/scheduler/message_form.php b/mod/scheduler/message_form.php new file mode 100644 index 0000000..db55ea8 --- /dev/null +++ b/mod/scheduler/message_form.php @@ -0,0 +1,122 @@ +<?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/>. + +/** + * Message form for invitations + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_scheduler\model\scheduler; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Message form for invitations (using Moodle formslib) + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_message_form extends moodleform { + + /** + * @var scheduler scheduler in whose context the messages are sent + */ + protected $scheduler; + + /** + * Create a new messge form + * + * @param string $action + * @param scheduler $scheduler scheduler in whose context the messages are sent + * @param object $customdata + */ + public function __construct($action, scheduler $scheduler, $customdata=null) { + $this->scheduler = $scheduler; + parent::__construct($action, $customdata); + } + + /** + * Form definition + */ + protected function definition() { + + $mform = $this->_form; + + // Select users to sent the message to. + $checkboxes = array(); + $recipients = $this->_customdata['recipients']; + foreach ($recipients as $recipient) { + $inputid = 'recipient['.$recipient->id.']'; + $label = fullname($recipient); + $checkboxes[] = $mform->createElement('checkbox', $inputid, '', $label); + $mform->setDefault($inputid, true); + } + $mform->addGroup($checkboxes, 'recipients', get_string('recipients', 'scheduler'), null, false); + + if (get_config('mod_scheduler', 'showemailplain')) { + $maillist = array(); + foreach ($recipients as $recipient) { + $maillist[] = trim($recipient->email); + } + $maildisplay = html_writer::div(implode(', ', $maillist)); + $mform->addElement('html', $maildisplay); + } + + $mform->addElement('selectyesno', 'copytomyself', get_string('copytomyself', 'scheduler')); + $mform->setDefault('copytomyself', true); + + $mform->addElement('text', 'subject', get_string('messagesubject', 'scheduler'), array('size' => '60')); + $mform->setType('subject', PARAM_TEXT); + $mform->addRule('subject', null, 'required'); + if (isset($this->_customdata['subject'])) { + $mform->setDefault('subject', $this->_customdata['subject']); + } + + $bodyedit = $mform->addElement('editor', 'body', get_string('messagebody', 'scheduler'), + array('rows' => 15, 'columns' => 60), array('collapsed' => true)); + $mform->setType('body', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. + if (isset($this->_customdata['body'])) { + $bodyedit->setValue(array('text' => $this->_customdata['body'])); + } + + $buttonarray = array(); + $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('sendmessage', 'scheduler')); + $buttonarray[] = $mform->createElement('cancel'); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + + } + + /** + * 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); + + return $errors; + } + +} diff --git a/mod/scheduler/mod_form.php b/mod/scheduler/mod_form.php new file mode 100644 index 0000000..90dcfbf --- /dev/null +++ b/mod/scheduler/mod_form.php @@ -0,0 +1,244 @@ +<?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/>. + +/** + * Defines the scheduler module settings form. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/course/moodleform_mod.php'); + +/** + * Scheduler modedit form - overrides moodleform + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_mod_form extends moodleform_mod { + + /** @var array */ + protected $editoroptions; + + /** + * Form definition + */ + public function definition() { + + global $CFG, $COURSE, $OUTPUT; + $mform =& $this->_form; + + // General introduction. + $mform->addElement('header', 'general', get_string('general', 'form')); + + $mform->addElement('text', 'name', get_string('name'), array('size' => '64')); + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + + $this->standard_intro_elements(get_string('introduction', 'scheduler')); + + // Scheduler options. + $mform->addElement('header', 'optionhdr', get_string('options', 'scheduler')); + $mform->setExpanded('optionhdr'); + + $mform->addElement('text', 'staffrolename', get_string('staffrolename', 'scheduler'), array('size' => '48')); + $mform->setType('staffrolename', PARAM_TEXT); + $mform->addRule('staffrolename', get_string('error'), 'maxlength', 255); + $mform->addHelpButton('staffrolename', 'staffrolename', 'scheduler'); + + $modegroup = array(); + $modegroup[] = $mform->createElement('static', 'modeintro', '', get_string('modeintro', 'scheduler')); + + $maxbookoptions = array(); + $maxbookoptions['0'] = get_string('unlimited', 'scheduler'); + for ($i = 1; $i <= 10; $i++) { + $maxbookoptions[(string)$i] = $i; + } + $modegroup[] = $mform->createElement('select', 'maxbookings', '', $maxbookoptions); + $mform->setDefault('maxbookings', 1); + + $modegroup[] = $mform->createElement('static', 'modeappointments', '', get_string('modeappointments', 'scheduler')); + + $modeoptions['oneonly'] = get_string('modeoneonly', 'scheduler'); + $modeoptions['onetime'] = get_string('modeoneatatime', 'scheduler'); + $modegroup[] = $mform->createElement('select', 'schedulermode', '', $modeoptions); + $mform->setDefault('schedulermode', 'oneonly'); + + $mform->addGroup($modegroup, 'modegrp', get_string('mode', 'scheduler'), ' ', false); + $mform->addHelpButton('modegrp', 'appointmentmode', 'scheduler'); + + if (get_config('mod_scheduler', 'groupscheduling')) { + $selopt = array( + -1 => get_string('no'), + 0 => get_string('yesallgroups', 'scheduler') + ); + $groupings = groups_get_all_groupings($COURSE->id); + foreach ($groupings as $grouping) { + $selopt[$grouping->id] = get_string('yesingrouping', 'scheduler', $grouping->name); + } + $mform->addElement('select', 'bookingrouping', get_string('groupbookings', 'scheduler'), $selopt); + $mform->addHelpButton('bookingrouping', 'groupbookings', 'scheduler'); + $mform->setDefault('bookingrouping', '-1'); + } + + $mform->addElement('duration', 'guardtime', get_string('guardtime', 'scheduler'), array('optional' => true)); + $mform->addHelpButton('guardtime', 'guardtime', 'scheduler'); + + $mform->addElement('text', 'defaultslotduration', get_string('defaultslotduration', 'scheduler'), array('size' => '2')); + $mform->setType('defaultslotduration', PARAM_INT); + $mform->addHelpButton('defaultslotduration', 'defaultslotduration', 'scheduler'); + $mform->setDefault('defaultslotduration', 15); + + $mform->addElement('selectyesno', 'allownotifications', get_string('notifications', 'scheduler')); + $mform->addHelpButton('allownotifications', 'notifications', 'scheduler'); + + $noteoptions['0'] = get_string('usenotesnone', 'scheduler'); + $noteoptions['1'] = get_string('usenotesstudent', 'scheduler'); + $noteoptions['2'] = get_string('usenotesteacher', 'scheduler'); + $noteoptions['3'] = get_string('usenotesboth', 'scheduler'); + $mform->addElement('select', 'usenotes', get_string('usenotes', 'scheduler'), $noteoptions); + $mform->setDefault('usenotes', '1'); + + // Grade settings. + $this->standard_grading_coursemodule_elements(); + + $mform->setDefault('grade', 0); + + $gradingstrategy[SCHEDULER_MEAN_GRADE] = get_string('meangrade', 'scheduler'); + $gradingstrategy[SCHEDULER_MAX_GRADE] = get_string('maxgrade', 'scheduler'); + $mform->addElement('select', 'gradingstrategy', get_string('gradingstrategy', 'scheduler'), $gradingstrategy); + $mform->addHelpButton('gradingstrategy', 'gradingstrategy', 'scheduler'); + $mform->disabledIf('gradingstrategy', 'grade[modgrade_type]', 'eq', 'none'); + + // Booking form and student-supplied data. + $mform->addElement('header', 'bookinghdr', get_string('bookingformoptions', 'scheduler')); + + $mform->addElement('selectyesno', 'usebookingform', get_string('usebookingform', 'scheduler')); + $mform->addHelpButton('usebookingform', 'usebookingform', 'scheduler'); + + $this->editoroptions = array('trusttext' => true, 'maxfiles' => -1, 'maxbytes' => 0, + 'context' => $this->context, 'collapsed' => true); + $mform->addElement('editor', 'bookinginstructions_editor', get_string('bookinginstructions', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->editoroptions); + $mform->setType('bookinginstructions', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. + $mform->disabledIf('bookinginstructions_editor', 'usebookingform', 'eq', '0'); + $mform->addHelpButton('bookinginstructions_editor', 'bookinginstructions', 'scheduler'); + + $studentnoteoptions['0'] = get_string('no'); + $studentnoteoptions['1'] = get_string('yesoptional', 'scheduler'); + $studentnoteoptions['2'] = get_string('yesrequired', 'scheduler'); + $mform->addElement('select', 'usestudentnotes', get_string('usestudentnotes', 'scheduler'), $studentnoteoptions); + $mform->setDefault('usestudentnotes', '0'); + $mform->disabledIf('usestudentnotes', 'usebookingform', 'eq', '0'); + $mform->addHelpButton('usestudentnotes', 'usestudentnotes', 'scheduler'); + + $uploadgroup = array(); + + $filechoices = array(); + for ($i = 0; $i <= get_config('mod_scheduler', 'uploadmaxfiles'); $i++) { + $filechoices[$i] = $i; + } + $uploadgroup[] = $mform->createElement('select', 'uploadmaxfiles', get_string('uploadmaxfiles', 'scheduler'), $filechoices); + $mform->setDefault('uploadmaxfiles', 0); + $mform->disabledIf('uploadmaxfiles', 'usebookingform', 'eq', '0'); + $uploadgroup[] = $mform->createElement('advcheckbox', 'requireupload', '', get_string('requireupload', 'scheduler')); + $mform->disabledIf('requireupload', 'usebookingform', 'eq', '0'); + + $mform->addGroup($uploadgroup, 'uploadgrp', get_string('uploadmaxfiles', 'scheduler'), ' ', false); + $mform->addHelpButton('uploadgrp', 'uploadmaxfiles', 'scheduler'); + + $sizechoices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes, 0); + $mform->addElement('select', 'uploadmaxsize', get_string('uploadmaxsize', 'scheduler'), $sizechoices); + $mform->setDefault('assignsubmission_file_maxsizebytes', $COURSE->maxbytes); + $mform->disabledIf('uploadmaxsize', 'usebookingform', 'eq', '0'); + $mform->disabledIf('uploadmaxsize', 'uploadmaxfiles', 'eq', '0'); + $mform->addHelpButton('uploadmaxsize', 'uploadmaxsize', 'scheduler'); + + if (!empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey)) { + $mform->addElement('selectyesno', 'usecaptcha', get_string('usecaptcha', 'scheduler'), $studentnoteoptions); + $mform->setDefault('usecaptcha', '0'); + $mform->disabledIf('usecaptcha', 'usebookingform', 'eq', '0'); + $mform->addHelpButton('usecaptcha', 'usecaptcha', 'scheduler'); + } + + // Common module settings. + $this->standard_coursemodule_elements(); + $mform->setDefault('groupmode', NOGROUPS); + + $this->add_action_buttons(); + } + + /** + * Allows module to modify data returned by get_moduleinfo_data() or prepare_new_moduleinfo_data() before calling set_data() + * This method is also called in the bulk activity completion form. + * + * Only available on moodleform_mod. + * + * @param array $defaultvalues passed by reference + */ + public function data_preprocessing(&$defaultvalues) { + parent::data_preprocessing($defaultvalues); + if ($this->current->instance) { + $newvalues = file_prepare_standard_editor((object)$defaultvalues, 'bookinginstructions', + $this->editoroptions, $this->context, + 'mod_scheduler', 'bookinginstructions', 0); + $defaultvalues['bookinginstructions_editor'] = $newvalues->bookinginstructions_editor; + } + if (array_key_exists('scale', $defaultvalues)) { + $dgrade = $defaultvalues['scale']; + $defaultvalues['grade'] = $dgrade; + $type = 'none'; + if ($dgrade > 0) { + $type = 'point'; + } else if ($dgrade < 0) { + $type = 'scale'; + } + $defaultvalues['grade[modgrade_type]'] = $type; + } + } + + /** + * save_mod_data + * + * @param stdClass $data + * @param context_module $context + */ + public function save_mod_data(stdClass $data, context_module $context) { + global $DB; + + $editor = $data->bookinginstructions_editor; + if ($editor) { + $data->bookinginstructions = file_save_draft_area_files($editor['itemid'], $context->id, + 'mod_scheduler', 'bookinginstructions', 0, + $this->editoroptions, $editor['text']); + $data->bookinginstructionsformat = $editor['format']; + $DB->update_record('scheduler', $data); + } + } + + + +} diff --git a/mod/scheduler/pix/attachment.png b/mod/scheduler/pix/attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..214462c8b2d00e02798b1816b0e05d924dca913d GIT binary patch literal 710 zcmeAS@N?(olHy`uVBq!ia0y~yVBlw9U=ZM7V_;x78K%m@z`(##?Bp53!NI{%!;#X# zz`(#+;1OBOz`#%l!i+NZ4do0B43Z_T5hcO-X(i=}MX3zs<>h*rdD+Fui3O>8`9<lo z-`PkpFfciLx;TbtOzgen?GaKa!uH|4^>@BmJ}0;OI<YzCa&c8P%wFK*s(mIdnY&r# z-5qP6h%0Iu85>%+T!=EMl;0b>B}>{xz(qHT#nI#9skv?~c}WGcs{b$V6%Otwop%5D zdmX;^AN`$=E4CY^F&*$@i1)mA=K1ITANmrm7aLYh3%#11on5_g$BrGM2TV?`3@sF3 zIqrEc?5f@U@AjroZhP2G4t*eS@}!AxwUi=5h}P6d2I;UPjsiTBWwz_AzP;`3*FX{5 zyf>R~zA<Pp|Hf6&@+36bXZor<rJo`P0`~GB{;*+HocP4r`UeI)pBeO+Uu?>)?lw$| za`D@-?ss10$3TWuLCYt*-swmu2p*_R_SxQ%!~Ve`l)d(T{eB+*NqbUzUW!lLyy}-1 zgF@FLZU!#TNirLBve_GctMp$<6mebd>DS@0zT*c&+14#v_M}O)?PW+xK65W>?Y1jd zu6&7f`4xNp?+%{xD_%v+T2?zPHCAb7WGK7s`PCh(;*={l258h|=jFX)_^_9+SwSN3 z>ZH_2Tcsja2Y0o}k{@i=GclN*wA~u@R+Zt**|W8o`T6Tx6D215gnUhywaj*vm0MBn z)+pO&m2q3^)EPIhC~z|@4bt?SlwyC9f0b2oWZ3eKp2;zfEzYgHc=6(W0S1@Q`RPV8 zYiu>dy6r#x{1bb;@R5q=yNOqR{`|QxM(_LQ_wy5W#=P5*`&+l6UCHR`ty{ZxY}jzY zvq<Pb&tr?{MLT1DnX@o3ImjHBzHGABag+Q3D+c3LRmZ>oRo7nl>krfS)}I+yHN2G> Q7#J8lUHx3vIVCg!07zglWdHyG literal 0 HcmV?d00001 diff --git a/mod/scheduler/pix/attachment.svg b/mod/scheduler/pix/attachment.svg new file mode 100644 index 0000000..36c5f5f --- /dev/null +++ b/mod/scheduler/pix/attachment.svg @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + id="Layer_1" + x="0px" + y="0px" + viewBox="0 0 16 16" + enable-background="new 0 0 500 500" + xml:space="preserve" + inkscape:version="0.91 r13725" + sodipodi:docname="attachment.svg" + width="16" + height="16"><metadata + id="metadata9"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs7" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1839" + inkscape:window-height="1050" + id="namedview5" + showgrid="false" + inkscape:zoom="56.061688" + inkscape:cx="8.0000087" + inkscape:cy="8.0001159" + inkscape:window-x="81" + inkscape:window-y="30" + inkscape:window-maximized="1" + inkscape:current-layer="Layer_1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /><path + d="m 9.8906244,0.96273053 -0.855802,0.78393997 -1.956118,1.79187 -5.616854,5.14521 C -0.42440662,10.41162 -0.49426762,13.15701 1.3046624,14.80489 c 1.79893,1.64787 4.795983,1.58388 6.682239,-0.14399 L 15.559873,7.7238205 14.826329,7.0518705 7.2533574,13.98575 c -1.481061,1.3567 -3.817923,1.42389 -5.21515,0.14399 C 0.64447238,12.85303 0.71433438,10.7124 2.1953954,9.3525005 l 5.595895,-5.12922 0.0035,0.003 c 0.007,-0.006 0.01397,-0.016 0.02096,-0.0224 l 1.956118,-1.79187 0.8558016,-0.78394 c 0.992031,-0.90872997 2.602336,-0.90872997 3.590874,0 0.992031,0.90873 0.992031,2.38382 0,3.28935 l -1.173671,1.07832 -1.638249,1.50069 c -0.007,0.006 -0.01747,0.0128 -0.02445,0.0192 l 0.0035,0 -4.0205216,3.6893195 c -0.52396,0.47996 -1.376269,0.47996 -1.900229,0 -0.52396,-0.47997 -0.52396,-1.2607095 0,-1.7406695 l 5.1906996,-4.75164 -0.7335446,-0.67195 -5.194192,4.75484 c -0.929156,0.85114 -0.929156,2.2366295 0,3.0877695 0.929156,0.85113 2.441655,0.85113 3.370811,0 l 4.0449726,-3.7053195 1.638249,-1.50069 1.170177,-1.07832 c 1.397228,-1.2799 1.397228,-3.35975 0,-4.63964997 -1.397227,-1.2831 -3.664228,-1.2767 -5.0614546,0.003 z" + id="path3" + inkscape:connector-curvature="0" + style="fill:#999999;fill-opacity:1" + inkscape:export-xdpi="2.8797832" + inkscape:export-ydpi="2.8797832" /></svg> \ No newline at end of file diff --git a/mod/scheduler/pix/icon.gif b/mod/scheduler/pix/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..b75dcd26a4103ed7b60eb37d6148fcef5fc3db66 GIT binary patch literal 217 zcmZ?wbhEHb6krfw*vtR||NsBL-PpV>Fy#926FatSEiNkl_4C);H*cp+o&M|pe>Zpc zZ(qJVxPSle-@pI=|5yCU!pOkD&!7X+2{MC$rR~E>OX0;|{erqQL?#QgBycrbngki` zDd=fBFLZx>f`{v>XH1h;O!cba;B8L4SWqI@#&lTFKxI-VtI`DqQK2perUZ@-;WYP0 zihLFVtw$F|8L&I_Y}&cdR)>R;Lq>;<C#|x9lT}7Go0T^)hS#Gf-deV)my^@W%Y-#- RZkVi;NLN3*goq%6H2~V>Q&0c^ literal 0 HcmV?d00001 diff --git a/mod/scheduler/pix/icon.png b/mod/scheduler/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a2b061d4087bc0b1ce7edeaf62dd37fd4779721a GIT binary patch literal 1271 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJoSc;uILpV4%IBGajIv5xj zI14-?iy0VruY)k7lg8`{1_lPn64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xh zq!<_&_&|z6GD~t&8JrVyQu9(0i%c{{rZF%uh=61qOG`5Hix|8TvrCf{f=f~}^HPhR z{r;}cz`)=Lk`K-=ElN&h$S5f(u+rC0&d<%w&nwm|&&<xuO-;#61X-jH;_16(=A>Hr zr=?{kr|Kp{Ez&D4OV5$Nr_R8@{MOUOF~p;D>ok84nQ)oo`}dpYm!)mmygP64=7Vci z?n=xR%HmVqvMeh@nz@auH7JXZ^NL8Rs8i+=b!FR(Th1RsbQqPFwmZC8dc@_!ge*Nx zMK|3g2_pQhqM~<qZM(3;x;~EowO?83>{kcn|5UvHoA&H?<$Lq}-_txe8vmMXTmOoo z;@yMU)27M)*;eZn7V4IhpL2ZM(N(FvW&FIQPmgKNUUcr<Ip?qXac64Rtg_p<gX7Vj z1sCdPdMqu_7Wna4{BjQuKmYC4umCY1HU5sPSI;n4PdYzi-D}r_2^SbPY+-)z`RB4_ zA)>_-J=|*g_2+kN$h-aN?7x3Y@9g0AP@9}@emrT-o=rl#SLrATFdUQU;Mk<{@Va~d zl)k<`W@cvQ|8E#7Z07m%9$s)ZM9cs9CY_v|oCjN**~<-dF1IYpoFLKs*rMX`4MWdq zPrDiN(%u((AASDaylU?~o5uCKc71sE@v%kaBbIxsS06t0$!Oa2X&=6B&9<n2C({=e za$u8=fS`n6(?Nx<t}d%$(-?=s!jFoX(NAOclyL61t!`7le0fIoKN(?R;SIUcZHW(Z zoS84J6bYL)-O)us$zxJWfrX4F$BH#8F8G{2#r(of{@wlE?fL8D-S5sc{;^>tXZzvA zlDM5sn|XN|8je=nVdXgPSX#POs{5$dXNI?L-{$X)i_^<xx2gOi;oRITQU8HqVE~5< zi$btrs(W7wLw9#VBtt`n$*Gdq|3>@@r$4n$nj};tqt5@qv)K4uU*DtSx3_=1JlA?g z)vqgOw<k&jgqY^Dy0GlO-=AS}Ytq#R3=h8lK3`obJNxAp?R)i)*}}rY0wg;h{5!#s z<v0J>?yXz(KcAI2btv)3b_N~xOE+#vym<HS--_+qzo+vtxM`F`Oj@$%=JjjWA3ywI zaqZf*4@a-Au2{Z|t!Jan!t;9ZZP#Cy9>1CWuwaI%sp;KUFJE#_5@0HKcRO?ISJgZ- zU-5v9)rQ9u44-Y=`S*?(gHUISt%b#p3EQ`B4LxpUwd2vp1InjaQ?}>a*4bQpyf9&= zn$PK`)L9+JAJ3RM^WdL9f8N*D*2)*0`}sRLIeEssd2MGbSr`<L@Es4kliS*mdV7QJ zX<NM(fe%%CBO)Rk6a)eo8??KMf}Y9Ls|f{!Ze8$Vb<*wQCsT}eOFFMzyEZiD@|MNB z)+N{0%6?iMw8dN4DWSMnc-K016W`qjOntSrbah)7X>7>-{a>^!F*G!kgRMD0<f(|i ziqL(3Kfk(jmeXaAPt3Xf%!%WfMW0GbLZth%*X|4sE=&yyO6sqB46U!Hr>CzivHB}3 zYW2I*gYm)bBHjijhhwKVpJq#7^kA%De4x)z&#idLdds)p+n>g7;95Lk_m`8e7#J8B NJYD@<);T3K0RS8@J>38R literal 0 HcmV?d00001 diff --git a/mod/scheduler/pix/icon.svg b/mod/scheduler/pix/icon.svg new file mode 100644 index 0000000..7aaee5a --- /dev/null +++ b/mod/scheduler/pix/icon.svg @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + version="1.1" + width="24" + height="24" + id="svg1288"> + <defs + id="defs3"> + <linearGradient + id="linearGradient2563"> + <stop + id="stop2565" + style="stop-color:#e2e2e2;stop-opacity:1" + offset="0" /> + <stop + id="stop2567" + style="stop-color:#9f9f9f;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient2551"> + <stop + id="stop2553" + style="stop-color:#000000;stop-opacity:1" + offset="0" /> + <stop + id="stop2555" + style="stop-color:#000000;stop-opacity:0" + offset="1" /> + </linearGradient> + <linearGradient + id="linearGradient2526"> + <stop + id="stop2528" + style="stop-color:#e7ebeb;stop-opacity:1" + offset="0" /> + <stop + id="stop2534" + style="stop-color:#e6ebeb;stop-opacity:1" + offset="0.5" /> + <stop + id="stop2530" + style="stop-color:#ffffff;stop-opacity:1" + offset="1" /> + </linearGradient> + <linearGradient + x1="13.357142" + y1="14.428571" + x2="42.214283" + y2="28.428572" + id="linearGradient2532" + xlink:href="#linearGradient2526" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,0.785718)" /> + <radialGradient + cx="26" + cy="29" + r="18.428572" + fx="26" + fy="29" + id="radialGradient2557" + xlink:href="#linearGradient2551" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.751938,0,7.193799)" /> + <linearGradient + x1="6.7857146" + y1="30.785713" + x2="42.214283" + y2="30.785713" + id="linearGradient2569" + xlink:href="#linearGradient2563" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.57963528,0,0,0.57963528,-2.409621,22.564906)" /> + <linearGradient + x1="13.357142" + y1="14.428571" + x2="42.214283" + y2="28.428572" + id="linearGradient2582" + xlink:href="#linearGradient2526" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,0.785718)" /> + </defs> + <g + transform="translate(0,-24)" + id="layer1"> + <path + d="M 44.428572,29 A 18.428572,13.857142 0 1 1 7.5714283,29 18.428572,13.857142 0 1 1 44.428572,29 z" + transform="matrix(0.63296633,0,0,0.31073261,-4.4669552,34.130721)" + id="path2549" + style="color:#000000;fill:url(#radialGradient2557);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999964;marker:none;visibility:visible;display:inline;overflow:visible" /> + <path + d="M 0.40574977,44.384029 C 0.28154198,44.963664 0.75767177,45.564 1.5443193,45.5433 l 20.9703767,0 c 0.745244,-0.0207 0.99366,-0.703843 0.82805,-1.262778 l -4.554279,-16.705916 -15.0705155,0 -3.31220173,16.809423 z" + id="path1316" + style="fill:#a7a7a7;fill-opacity:1;fill-rule:evenodd;stroke:#656565;stroke-width:0.57963532px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + d="m 2.9012894,39.965983 17.5238746,0 c 0.496864,0 0.896869,0.389681 0.896869,0.873723 l 0.769285,2.782792 c 0,0.484043 -0.400002,0.873723 -0.89687,0.873723 l -18.8060163,0 c -0.4968645,0 -0.8968673,-0.38968 -0.8968673,-0.873723 l 0.5128565,-2.782792 c 0,-0.484042 0.4000028,-0.873723 0.8968685,-0.873723 z" + id="path2575" + style="opacity:0.10857143;color:#000000;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.57963502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;font-family:Bitstream Vera Sans" /> + <path + d="m 1.0959216,44.022915 c -0.11584603,0.540618 -0.23106933,0.915626 0.2752804,0.915626 l 21.049267,0 c 0.530979,0 0.429942,-0.42875 0.275481,-0.950062 l -4.371919,-15.809087 -14.056059,0 -3.1720504,15.843523 z" + id="path2520" + style="fill:none;stroke:#ffffff;stroke-width:0.57963514px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.3446328" /> + <path + d="m 3.1791063,39.457131 16.9762557,0 c 0.481338,0 0.868841,0.377502 0.868841,0.846418 l 0.745245,2.69583 c 0,0.468915 -0.387503,0.846419 -0.868839,0.846419 l -18.2183324,0 c -0.4813385,0 -0.8688407,-0.377504 -0.8688407,-0.846419 l 0.4968295,-2.69583 c 0,-0.468916 0.3875037,-0.846418 0.8688409,-0.846418 z" + id="path2518" + style="color:#000000;fill:#c5c5c5;fill-opacity:1;fill-rule:evenodd;stroke:#696969;stroke-width:0.57963502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;font-family:Bitstream Vera Sans" /> + <path + d="m 3.1791063,38.215055 16.9762557,0 c 0.481338,0 0.868841,0.377505 0.868841,0.84642 l 0.745245,2.69583 c 0,0.468915 -0.387503,0.846418 -0.868839,0.846418 l -18.2183324,0 c -0.4813385,0 -0.8688407,-0.377503 -0.8688407,-0.846418 l 0.4968295,-2.69583 c 0,-0.468915 0.3875037,-0.84642 0.8688409,-0.84642 z" + id="rect2225" + style="color:#000000;fill:url(#linearGradient2569);fill-opacity:1;fill-rule:evenodd;stroke:#696969;stroke-width:0.57963502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible" /> + <g + transform="matrix(0.54633465,0,0,0.54633465,-2.5386593,23.185196)" + id="g2578"> + <path + d="M 10.891973,11.500004 6.5714281,33.21429 c 0,0 26.2857149,1e-6 26.2857149,1e-6 12.584832,0 15.228163,-4.000001 15.228163,-4.000001 0,0 -3.357142,-1.178571 -4.785713,-6.142857 0,0 -3.060953,-11.571429 -3.060953,-11.571429 l -29.346667,0 z" + id="path2524" + style="fill:url(#linearGradient2582);fill-opacity:1;fill-rule:evenodd;stroke:#696969;stroke-width:1.06095243px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + d="M 11.803734,12.474609 7.8122567,32.239668 c 0,0 17.1442613,10e-7 24.4261253,10e-7 11.226639,0 14.110429,-2.850865 14.110429,-2.850865 0,0 -2.99306,-1.862842 -4.293392,-6.381499 0,0 -2.738563,-10.461267 -2.738563,-10.461267 l -27.513122,-0.07143 z" + id="path2571" + style="fill:none;stroke:#ffffff;stroke-width:1.06095195px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <path + d="m 4.1493567,26.415335 14.5945293,0 c 0.506089,0 0.913518,0.387643 0.913518,0.869156 l 0.207015,1.242672 c 0,0.481512 -0.40743,0.869154 -0.913521,0.869154 l -15.0085535,0 c -0.5060899,0 -0.913519,-0.387642 -0.913519,-0.869154 l 0.2070121,-1.242672 c 0,-0.481513 0.4074302,-0.869156 0.9135191,-0.869156 z" + id="rect2522" + style="color:#000000;fill:#fa3c3c;fill-opacity:1;fill-rule:evenodd;stroke:#696969;stroke-width:0.5796349;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible" /> + <path + d="m 9.7448425,35.08926 c 0.5729195,0.05434 0.9983535,0.215856 1.2763005,0.484539 0.278294,0.26567 0.392988,0.6053 0.344081,1.018893 -0.07176,0.606809 -0.399236,1.095879 -0.982414,1.467207 -0.5831875,0.371331 -1.3375294,0.556995 -2.263033,0.556996 -0.7105273,-10e-7 -1.3094622,-0.111696 -1.7968055,-0.335104 l 0.1392353,-1.177387 c 0.5120729,0.353217 1.0942227,0.529823 1.7464503,0.529823 0.5210507,0 0.9318549,-0.08452 1.2324184,-0.253591 0.300912,-0.172078 0.469756,-0.413594 0.506532,-0.724545 0.03857,-0.326045 -0.08263,-0.564542 -0.363564,-0.715491 C 9.3031046,35.789656 8.8347041,35.714184 8.1788372,35.714179 l -0.7378537,0 0.1199567,-1.014362 0.7870439,0 c 0.5174047,3e-6 0.9220307,-0.0785 1.2138774,-0.235479 0.292198,-0.16 0.4556115,-0.386421 0.4902475,-0.679264 0.03248,-0.274719 -0.05258,-0.4951 -0.2552145,-0.661148 -0.202636,-0.166036 -0.5243958,-0.249057 -0.965281,-0.249061 -0.5574924,4e-6 -1.1032886,0.163027 -1.6373899,0.489068 l 0.133345,-1.127576 c 0.5984074,-0.253584 1.2692703,-0.38038 2.0125931,-0.380387 0.7178093,7e-6 1.2685553,0.149444 1.6522443,0.448312 0.387321,0.298883 0.553495,0.680778 0.498519,1.145691 -0.05106,0.431712 -0.222179,0.784929 -0.513368,1.059648 -0.290839,0.271708 -0.701745,0.464921 -1.2327145,0.579639" + id="text2541" + style="font-size:17.57787704px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:100%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Segoe" /> + <path + d="m 16.722619,38.603309 -1.453846,0 -0.644146,-5.425041 -1.792524,0.461897 -0.120978,-1.018892 3.211955,-0.751718 0.799539,6.733754" + id="text2545" + style="font-size:17.57787704px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:100%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Segoe" /> + <path + d="m 12.857143,9.9285717 a 1.3571428,1.3571428 0 1 1 -2.714286,0 1.3571428,1.3571428 0 1 1 2.714286,0 z" + transform="matrix(0.39659282,0,0,0.39659282,-0.34603013,23.429998)" + id="path2559" + style="opacity:0.52571429;color:#000000;fill:#fefefe;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999964;marker:none;visibility:visible;display:inline;overflow:visible" /> + <path + d="m 5.1500123,27.094925 13.5224597,0" + id="path2561" + style="opacity:0.3028571;fill:none;stroke:#ffffff;stroke-width:0.57963538px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + </g> +</svg> diff --git a/mod/scheduler/pix/ticked.gif b/mod/scheduler/pix/ticked.gif new file mode 100644 index 0000000000000000000000000000000000000000..e2137bbe7a58c44be70dacf4153374a26786a3e7 GIT binary patch literal 944 zcmZ?wbhEHb<Y(Y#_|5<U#taN;3=A_F7|t*-{AVy`U@$gjFivAIp2=W*hQathNHWcs zAuWv|Z6-t78HTj~Ahk1%8D^$2%$&(E^9;kx{~)brj2X_PF`SvnaOModng0y`85sT> zGyG3u_&<~3{~3n=|3L<Wj7~FVm}$&##+czh$R1;3W8*Yq<C(_BXN-;igRD(6Hcm@3 zPMc|*cE&jEKgbm`jg4of8PA+)JoAk4%>N)qoiR2(lV*Hortz6G#%KN;|7S4%Z*2TO z&G`RJ<Ns%j|Nl<|dC52p<mj1cAZPst`2b{7S{lf#Gik>EK^{soP6L6NX=!KD(*A?| zH`6$6W?I_JnQ1f6q|N*f^6nX9kmQ+}X=l!)o%x^kpCRqPaoYd1wEr{H{+~(v|9>VZ z?2KoELTBbokpKUK!Up7}w3#4BpP6a=9~4v|i_>O;j6O3n?LR2wK+<V5LEy~Hng2ln zdBzxI?U|V~&zzZg=Ksw93^V^5&-|Y@^Z(45|If_)|Njgql8n!QqG9G4P(c3&g*qtg z(m+~4!SNpy32DY>Kwg@82IT1fpg5Un4D!LunP)&o{|80U8Do%#K>Raj&ip^~pW)1Z z<1_!$&itQw=Kq;9|Nny`5FGs=SAb&_6rbQo0yzpC?x5%e2PMdj;LriZHrT@;CxiU} ziu^OiprAT46J#*h=>PvgvGo5x$WQ<O|Ia)15t0_^Oe+3lVPs&q!Jq@O7nCO$IC>e@ zbIN#Zc(7HHLqN;tL_wPa%e9Eu$r=}LIj}IL2W+|4_4r8kCYiWmf!;@^-J9}ax}P~D za7SK9D|}LMxXnR~$#dD1inGU9rfyD2nUeANn9h82ojC6g1&?|;T|@bDDlgnxGf%kc wgi^<6sbd*p>Jl{*3RgQUWihN0y>V)R!$Nk!6A>pKF><r(p4fF)T7bbC0G@h?`Tzg` literal 0 HcmV?d00001 diff --git a/mod/scheduler/pix/unticked.gif b/mod/scheduler/pix/unticked.gif new file mode 100644 index 0000000000000000000000000000000000000000..3a98e0ab708e5ae026a057c373d5ebb6c2961598 GIT binary patch literal 943 zcmZ?wbhEHb<Y(Y#_|5<U#taN;3=A_F7|t*-{AVy`U@$gjFivAIp2=W*hQathNHWcs zAuWv|Z6-t78HTj~Ahk1%8D^$2%$&(E^9;kx{~)brj2X_PF`SvnaOModng0y`85sT> zGyG3u_&<~3{~3n=|3L<Wj7~FVm}$&##+czh$R1;3W8*Yq<C(_BXN-;igRD(6Hcm@3 zPMc|*cE&jEKgbm`jg4of8PA+)JoAk4%>N)qoiR2(lV*Hortz6G#%KN;|7S4%Z*2TO z&G`RJ<Ns%j|Nl<|dC52p<mj1cAZPst`2b{7S{lf#Gik>EK^{soP6L6NX=!KD(*A?| zH`6$6W?I_JnQ1f6q|N*f^6nX9kmQ+}X=l!)o%x^kpCRqPaoYd1wEr{H{+~(v|9>VZ z?2KoELTBbokpKUK!Up7}w3#4BpP6a=9~4v|i_>O;j6O3n?LR2wK+<V5LEy~Hng2ln zdBzxI?U|V~&zzZg=Ksw93^V^5&-|Y@^Z(45|If_)|Njgql8n!QqG9G4P(c3&g*qtg z(m+~4!SNpy32DY>Kwg@82IT1fpg5Un4D!LunP)&o{|80U8Do%#K>Raj&ip^~pW)1Z z<1_!$&itQw=Kq;9|Nny`5FGs=SAb&_6rbQo0yzpC?x5%e2PMdj;LriZHrT@;CxiU} ziu^OiprAT46J#*h=>PvgvGo5x$WQ<O|Ia)15t0_^Oe+3lVPs&q!Jq@O7nCO$IC>b? zbIN#Zc(7HHLqN;tL_wPa%e9D;O=jInTe3H;Qxvdh4%N=s6cH<|seQ{-l3zJ2CpGuU zfnN4VS8dPW2S?fYwWn<{JN;y9<n@Sxl+@nT^V6<JsR?Kt&e$@|q1H-Z#(`UxS9q{; u+1`Bh{Ai=Y`iCOfqM0`uqg&^l)hbvOA;uS_P;o%1v8RpgGanBJgEauzV1=as literal 0 HcmV?d00001 diff --git a/mod/scheduler/renderable.php b/mod/scheduler/renderable.php new file mode 100644 index 0000000..9d9dd68 --- /dev/null +++ b/mod/scheduler/renderable.php @@ -0,0 +1,700 @@ +<?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/>. + +/** + * This file contains the definition for the renderable classes for the assignment + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; +use \mod_scheduler\model\appointment; + +/** + * This class represents a table of slots associated with one student + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_slot_table implements renderable { + + /** @var array list of slots in this table */ + public $slots = array(); + + /** @var scheduler the scheduler that the slots are in */ + public $scheduler; + + /** @var bool whether to show grades in the table */ + public $showgrades; + + /** @var bool whether any slot in the table has other students to show */ + public $hasotherstudents = false; + + /** @var bool whether to show start/end time of the slots */ + public $showslot = true; + + /** @var bool whether to show the attended/not attended icons */ + public $showattended = false; + + /** @var bool whether to show action buttons (for cancelling) */ + public $showactions = true; + + /** @var bool whether to show (confidential) teacher notes */ + public $showteachernotes = false; + + /** @var bool whether to show a link to edit appointments */ + public $showeditlink = false; + + /** @var bool whether to show the location of the appointment */ + public $showlocation = true; + + /** @var bool whether to show the students in the slot */ + public $showstudent = false; + + /** @var moodle_url|null action URL for buttons */ + public $actionurl; + + /** + * Add a slot to the table. + * + * @param slot $slotmodel the slot to be added + * @param appointment $appointmentmodel the corresponding appointment + * @param array $otherstudents any other students in the same slot + * @param bool $cancancel whether the user can canel the appointment + * @param bool $canedit whether the user can edit the slot/appointment + * @param bool $canview whether the user can view the appointment + */ + public function add_slot(slot $slotmodel, appointment $appointmentmodel, + $otherstudents, $cancancel = false, $canedit = false, $canview = false) { + $slot = new stdClass(); + $slot->slotid = $slotmodel->id; + if ($this->showstudent) { + $slot->student = $appointmentmodel->student; + } + $slot->starttime = $slotmodel->starttime; + $slot->endtime = $slotmodel->endtime; + $slot->attended = $appointmentmodel->attended; + $slot->location = $slotmodel->appointmentlocation; + $slot->slotnote = $slotmodel->notes; + $slot->slotnoteformat = $slotmodel->notesformat; + $slot->teacher = $slotmodel->get_teacher(); + $slot->appointmentid = $appointmentmodel->id; + if ($this->scheduler->uses_appointmentnotes()) { + $slot->appointmentnote = $appointmentmodel->appointmentnote; + $slot->appointmentnoteformat = $appointmentmodel->appointmentnoteformat; + } + if ($this->scheduler->uses_teachernotes() && $this->showteachernotes) { + $slot->teachernote = $appointmentmodel->teachernote; + $slot->teachernoteformat = $appointmentmodel->teachernoteformat; + } + $slot->otherstudents = $otherstudents; + $slot->cancancel = $cancancel; + $slot->canedit = $canedit; + $slot->canview = $canview; + if ($this->showgrades) { + $slot->grade = $appointmentmodel->grade; + } + $this->showactions = $this->showactions || $cancancel; + $this->hasotherstudents = $this->hasotherstudents || (bool) $otherstudents; + + $this->slots[] = $slot; + } + + /** + * Create a new slot table. + * + * @param scheduler $scheduler the scheduler in which the slots are + * @param bool $showgrades whether to show grades + * @param moodle_url|null $actionurl action URL for buttons + */ + public function __construct(scheduler $scheduler, $showgrades=true, $actionurl = null) { + $this->scheduler = $scheduler; + $this->showgrades = $showgrades && $scheduler->uses_grades(); + $this->actionurl = $actionurl; + } + +} + + +/** + * This class represents a list of students in a slot, to be displayed "inline" within a larger table + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_student_list implements renderable { + + /** @var array list of students to be displayed */ + public $students = array(); + + /** @var scheduler the scheduler in whose context the list is */ + public $scheduler; + + /** @var bool whether tho show the grades of the students */ + public $showgrades; + + /** @var bool whether to show students in an expandable list */ + public $expandable = true; + + /** @var bool whether the expandable list is already expanded */ + public $expanded = true; + + /** @var bool whether appointments can be edited */ + public $editable = false; + + /** @var string name of the checkbox group used for marking students as seen */ + public $checkboxname = ''; + + /** @var string text of the edit button */ + public $buttontext = ''; + + /** @var moodle_url|null action URL for buttons */ + public $actionurl = null; + + /** @var bool whether to include links to individual appointments */ + public $linkappointment = false; + + /** + * Add a student to the list. + * + * @param appointment $appointment the appointment to add (one student) + * @param bool $highlight whether this entry is highlighted + * @param bool $checked whether the "seen" tickbox is checked + * @param bool $showgrade whether to show a grade with this entry + * @param bool $showstudprovided whether to show an icon for student-provided files + * @param bool $editattended whether to make the attended tickbox editable + */ + public function add_student(appointment $appointment, $highlight, $checked = false, + $showgrade = true, $showstudprovided = false, $editattended = false) { + $student = new stdClass(); + $student->user = $appointment->get_student(); + if ($this->showgrades && $showgrade) { + $student->grade = $appointment->grade; + } else { + $student->grade = null; + } + $student->highlight = $highlight; + $student->checked = $checked; + $student->editattended = $editattended; + $student->entryid = $appointment->id; + $scheduler = $appointment->get_scheduler(); + $student->notesprovided = false; + $student->filesprovided = 0; + if ($showstudprovided) { + $student->notesprovided = $scheduler->uses_studentnotes() && $appointment->has_studentnotes(); + if ($scheduler->uses_studentfiles()) { + $student->filesprovided = $appointment->count_studentfiles(); + } + } + $this->students[] = $student; + } + + /** + * Create a new student list. + * + * @param scheduler $scheduler the scheduler in whose context the list is + * @param bool $showgrades whether tho show grades of students + */ + public function __construct(scheduler $scheduler, $showgrades = true) { + $this->scheduler = $scheduler; + $this->showgrades = $showgrades; + } + +} + + +/** + * This class represents a table of slots which a student can book. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_slot_booker implements renderable { + + /** + * @var array list of slots to be displayed + */ + public $slots = array(); + + /** + * @var scheduler scheduler in whose context the list is + */ + public $scheduler; + + /** + * @var int the id number of the student booking slots + */ + public $studentid; + + /** + * @var moodle_url action url for buttons + */ + public $actionurl; + + /** + * Add a slot to the list. + * + * @param slot $slotmodel the slot to be added + * @param bool $canbook whether the slot can be booked + * @param bool $bookedbyme whether the slot is already booked by the current student + * @param string $groupinfo information about group slots + * @param array $otherstudents other students in this slot + */ + public function add_slot(slot $slotmodel, $canbook, $bookedbyme, $groupinfo, $otherstudents) { + $slot = new stdClass(); + $slot->slotid = $slotmodel->id; + $slot->starttime = $slotmodel->starttime; + $slot->endtime = $slotmodel->endtime; + $slot->location = $slotmodel->appointmentlocation; + $slot->notes = $slotmodel->notes; + $slot->notesformat = $slotmodel->notesformat; + $slot->bookedbyme = $bookedbyme; + $slot->canbook = $canbook; + $slot->groupinfo = $groupinfo; + $slot->teacher = $slotmodel->get_teacher(); + $slot->otherstudents = $otherstudents; + + $this->slots[] = $slot; + } + + /** + * Contructs a slot booker. + * + * @param scheduler $scheduler the scheduler in which the booking takes place + * @param int $studentid the student who books + * @param moodle_url $actionurl + * @param int $maxselect no longer used + */ + public function __construct(scheduler $scheduler, $studentid, moodle_url $actionurl, $maxselect) { + $this->scheduler = $scheduler; + $this->studentid = $studentid; + $this->actionurl = $actionurl; + } + +} + +/** + * Command bar with action buttons, used by teachers. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_command_bar implements renderable { + + /** + * @var array list of drop-down menus in the command bar + */ + public $menus = array(); + + /** + * @var array list of action_link objects used in the menu + */ + public $linkactions = array(); + + /** + * @var string title of the menu + */ + public $title = ''; + + /** + * Adds a group of menu items in a menu. + * + * @param string $title the title of the group + * @param array $actions an array of action_menu_link instances, representing the commands + */ + public function add_group($title, array $actions) { + $menu = new action_menu($actions); + $menu->actiontext = $title; + $this->menus[] = $menu; + } + + /** + * Creates an action link with an optional confirmation dialogue attached. + * + * @param moodle_url $url URL of the action + * @param string $titlekey key of the link title + * @param string $iconkey key of the icon to display + * @param string|null $confirmkey key for the confirmation text + * @param string|null $id id attribute of the new link + * @return action_link the new action link + */ + public function action_link(moodle_url $url, $titlekey, $iconkey, $confirmkey = null, $id = null) { + $title = get_string($titlekey, 'scheduler'); + $pix = new pix_icon($iconkey, $title, 'moodle', array('class' => 'iconsmall', 'title' => '')); + $attributes = array(); + if ($id) { + $attributes['id'] = $id; + } + $confirmaction = null; + if ($confirmkey) { + $confirmaction = new confirm_action(get_string($confirmkey, 'scheduler')); + } + $act = new action_link($url, $title, $confirmaction, $attributes, $pix); + $act->primary = false; + return $act; + } + + /** + * Contructs a command bar + */ + public function __construct() { + // Nothing to add right now. + } + +} + +/** + * This class represents a table of slots displayed to a teacher, with options to modify the list. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_slot_manager implements renderable { + + /** + * @var array list of slots + */ + public $slots = array(); + + /** + * @var scheduler scheduler in whose context the list is + */ + public $scheduler; + + /** + * @var moodle_url action URL for buttons + */ + public $actionurl; + + /** + * @var bool should the teacher owning the slot be shown? + */ + public $showteacher = true; + + /** + * Add a slot to the list. + * + * @param slot $slotmodel the slot to be added + * @param scheduler_student_list $students the list of students in the slot + * @param bool $editable whether the slot is editable + */ + public function add_slot(slot $slotmodel, scheduler_student_list $students, $editable) { + $slot = new stdClass(); + $slot->slotid = $slotmodel->id; + $slot->starttime = $slotmodel->starttime; + $slot->endtime = $slotmodel->endtime; + $slot->location = $slotmodel->appointmentlocation; + $slot->teacher = $slotmodel->get_teacher(); + $slot->students = $students; + $slot->editable = $editable; + $slot->isattended = $slotmodel->is_attended(); + $slot->isappointed = $slotmodel->get_appointment_count(); + $slot->exclusivity = $slotmodel->exclusivity; + + $this->slots[] = $slot; + } + + /** + * Contructs a slot manager. + * + * @param scheduler $scheduler the scheduler in which the booking takes place + * @param moodle_url $actionurl action URL for buttons + */ + public function __construct(scheduler $scheduler, moodle_url $actionurl) { + $this->scheduler = $scheduler; + $this->actionurl = $actionurl; + } + +} + + +/** + * A list of students displayed to a teacher, with action menus to schedule the students. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_scheduling_list implements renderable { + + /** + * @var array lines in the list + */ + public $lines = array(); + + /** + * @var scheduler the scheduler in whose context the list is + */ + public $scheduler; + + /** + * @var array extra headers for custom fields in the list + */ + public $extraheaders; + + /** + * @var string HTML id of the list + */ + public $id = 'schedulinglist'; + + /** + * Add a line to the list. + * + * @param string $pix icon to display next to the student's name + * @param string $name name of the student + * @param array $extrafields content of extra data fields to be displayed + * @param array $actions actions to be displayed in an action menu + */ + public function add_line($pix, $name, array $extrafields, $actions) { + $line = new stdClass(); + $line->pix = $pix; + $line->name = $name; + $line->extrafields = $extrafields; + $line->actions = $actions; + + $this->lines[] = $line; + } + + /** + * Contructs a scheduling list. + * + * @param scheduler $scheduler the scheduler in which the booking takes place + * @param array $extraheaders headers for extra data fields + */ + public function __construct(scheduler $scheduler, array $extraheaders) { + $this->scheduler = $scheduler; + $this->extraheaders = $extraheaders; + } + +} + +/** + * Represents information about a student's total grade in the scheduler, plus gradebook information. + * + * To be used in teacher screens. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_totalgrade_info implements renderable { + + /** + * @var stdClass|null gradebook grade for the student + */ + public $gbgrade; + + /** + * @var scheduler scheduler in whose context the information is + */ + public $scheduler; + + /** + * @var bool whether to show a total grade + */ + public $showtotalgrade; + + /** + * @var int the total grade to display + */ + public $totalgrade; + + /** + * Constructs a grade info object + * + * @param scheduler $scheduler the scheduler in question + * @param stdClass $gbgrade information about the grade in the gradebook (may be null) + * @param bool $showtotalgrade whether the total grade in the scheduler should be shown + * @param int $totalgrade the total grade of the student in this scheduler + */ + public function __construct(scheduler $scheduler, $gbgrade, $showtotalgrade = false, $totalgrade = 0) { + $this->scheduler = $scheduler; + $this->gbgrade = $gbgrade; + $this->showtotalgrade = $showtotalgrade; + $this->totalgrade = $totalgrade; + } + +} + +/** + * This class represents a list of scheduling conflicts. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_conflict_list implements renderable { + + /** + * @var array list of conflicts + */ + public $conflicts = array(); + + /** + * Add a conflict to the list. + * + * @param stdClass $conflict information about the conflict + * @param stdClass $user the user who is affected + */ + public function add_conflict(stdClass $conflict, $user = null) { + $c = clone($conflict); + if ($user) { + $c->userfullname = fullname($user); + } else { + $c->userfullname = ''; + } + $this->conflicts[] = $c; + } + + /** + * Add several conflicts to the list. + * + * @param array $conflicts information about the conflicts + */ + public function add_conflicts(array $conflicts) { + foreach ($conflicts as $c) { + $this->add_conflict($c); + } + } + +} + +/** + * Information about an appointment in the scheduler. + * + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_appointment_info implements renderable { + + /** + * @var scheduler scheduler in whose context the appointment is + */ + public $scheduler; + + /** + * @var slot slot in which the appointment is + */ + public $slot; + + /** + * @var appointment the appointment itself + */ + public $appointment; + + /** + * @var bool whether to show information about the slot (times, etc.) + */ + public $showslotinfo; + + /** + * @var bool whether to show booking instructions + */ + public $showbookinginfo; + + /** + * @var bool whether to show information about the student + */ + public $showstudentdata; + + /** + * @var string information about the group the booking is for + */ + public $groupinfo; + + /** + * @var bool whether the information is shown to a student (rather than a teacher) + */ + public $onstudentside; + + /** + * @var bool whether to show grades and appointment notes + */ + public $showresult; + + /** + * Create appointment information for a new appointment in a slot. + * + * @param slot $slot the slot in question + * @param bool $showbookinginstr whether to show booking instructions + * @param bool $onstudentside whether the screen is shown to a student + * @param string $groupinfo information about the group that the booking is for + * @return scheduler_appointment_info + */ + public static function make_from_slot(slot $slot, $showbookinginstr = true, $onstudentside = true, + $groupinfo = null) { + $info = new scheduler_appointment_info(); + $info->slot = $slot; + $info->scheduler = $slot->get_scheduler(); + $info->showslotinfo = true; + $info->showbookinginfo = $showbookinginstr; + $info->showstudentdata = false; + $info->showresult = false; + $info->onstudentside = $onstudentside; + $info->groupinfo = $groupinfo; + + return $info; + } + + /** + * Create appointment information for an existing appointment. + * + * @param slot $slot the slot in question + * @param appointment $appointment the appointment in question + * @param string $onstudentside whether the screen is shown to a student + * @return scheduler_appointment_info + */ + public static function make_from_appointment(slot $slot, appointment $appointment, $onstudentside = true) { + $info = new scheduler_appointment_info(); + $info->slot = $slot; + $info->appointment = $appointment; + $info->scheduler = $slot->get_scheduler(); + $info->showslotinfo = true; + $info->showboookinginfo = true; + $info->showstudentdata = $info->scheduler->uses_studentdata(); + $info->showresult = true; + $info->onstudentside = $onstudentside; + $info->groupinfo = null; + + return $info; + } + + /** + * Create appointment information for an existing appointment, shown to a teacher. + * This excludes booking instructions and results. + * + * @param slot $slot the slot in question + * @param appointment $appointment the appointment in question + * @return scheduler_appointment_info + */ + public static function make_for_teacher(slot $slot, appointment $appointment) { + $info = new scheduler_appointment_info(); + $info->slot = $slot; + $info->appointment = $appointment; + $info->scheduler = $slot->get_scheduler(); + $info->showslotinfo = true; + $info->showboookinginfo = false; + $info->showstudentdata = $info->scheduler->uses_studentdata(); + $info->showresult = false; + $info->onstudentside = false; + $info->groupinfo = null; + + return $info; + } + +} \ No newline at end of file diff --git a/mod/scheduler/renderer.php b/mod/scheduler/renderer.php new file mode 100644 index 0000000..dc9314b --- /dev/null +++ b/mod/scheduler/renderer.php @@ -0,0 +1,1087 @@ +<?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/>. + +/** + * This file contains a renderer for the scheduler module + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\permission\scheduler_permissions; + +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + +/** + * A custom renderer class that extends the plugin_renderer_base and is used by the scheduler module. + * + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_renderer extends plugin_renderer_base { + + /** + * Constructor method, calls the parent constructor + * + * @param moodle_page $page + * @param string $target one of rendering target constants + */ + public function __construct($page = null, $target = null) { + if ($page) { + parent::__construct($page, $target); + } + } + + /** + * Format a date in the current user's timezone. + * @param int $date a timestamp + * @return string printable date + */ + public static function userdate($date) { + if ($date == 0) { + return ''; + } else { + return userdate($date, get_string('strftimedaydate')); + } + } + + /** + * Format a time in the current user's timezone. + * @param int $date a timestamp + * @return string printable time + */ + public static function usertime($date) { + if ($date == 0) { + return ''; + } else { + $timeformat = get_user_preferences('calendar_timeformat'); // Get user config. + if (empty($timeformat)) { + $timeformat = get_config(null, 'calendar_site_timeformat'); // Get calendar config if above not exist. + } + if (empty($timeformat)) { + $timeformat = get_string('strftimetime'); // Get locale default format if both of the above do not exist. + } + return userdate($date, $timeformat); + } + } + + /** + * Format a slot date and time, for use as a parameter in a language string. + * + * @param int $slotdate + * a timestamp, start time of the slot + * @param int $duration + * length of the slot in minutes + * @return stdClass date and time formatted for usage in language strings + */ + public static function slotdatetime($slotdate, $duration) { + $shortformat = get_string('strftimedatetimeshort'); + + $a = new stdClass(); + $a->date = self::userdate($slotdate); + $a->starttime = self::usertime($slotdate); + $a->shortdatetime = userdate($slotdate, $shortformat); + $a->endtime = self::usertime($slotdate + $duration * MINSECS); + $a->duration = $duration; + + return $a; + } + + /** + * @var array a cached version of scale levels + */ + protected $scalecache = array(); + + /** + * Get a list of levels in a grading scale. + * + * @param int $scaleid id number of the scale + * @return array levels on the scale + */ + public function get_scale_levels($scaleid) { + global $DB; + + if (!array_key_exists($scaleid, $this->scalecache)) { + $this->scalecache[$scaleid] = array(); + if ($scale = $DB->get_record('scale', array('id' => $scaleid))) { + $levels = explode(',', $scale->scale); + foreach ($levels as $levelid => $value) { + $this->scalecache[$scaleid][$levelid + 1] = $value; + } + } + } + return $this->scalecache[$scaleid]; + } + + /** + * Formats a grade in a specific scheduler for display. + * + * @param mixed $subject either a scheduler instance or a scale id + * @param string $grade the grade to be displayed + * @param bool $short formats the grade in short form (result empty if grading is + * not used, or no grade is available; parantheses are put around the grade if it is present) + * @return string the formatted grade + */ + public function format_grade($subject, $grade, $short = false) { + if ($subject instanceof scheduler) { + $scaleid = $subject->scale; + } else { + $scaleid = (int) $subject; + } + + $result = ''; + if ($scaleid == 0 || is_null($grade) ) { + // Scheduler doesn't allow grading, or no grade entered. + if (!$short) { + $result = get_string('nograde'); + } + } else { + $grade = (int) $grade; + if ($scaleid > 0) { + // Numeric grade. + $result .= $grade; + if (strlen($grade) > 0) { + $result .= '/' . $scaleid; + } + } else { + // Grade on scale. + if ($grade > 0) { + $levels = $this->get_scale_levels(-$scaleid); + if (array_key_exists($grade, $levels)) { + $result .= $levels[$grade]; + } + } + } + if ($short && (strlen($result) > 0)) { + $result = '('.$result.')'; + } + } + return $result; + } + + /** + * A utility function for producing grading lists (for use in formslib) + * + * Note that the selection list will contain a "nothing selected" option + * with key -1 which will be displayed as "No grade". + * + * @param reference $scheduler + * @return array the choices to be displayed in a grade chooser + */ + public function grading_choices($scheduler) { + if ($scheduler->scale > 0) { + $scalegrades = array(); + for ($i = 0; $i <= $scheduler->scale; $i++) { + $scalegrades[$i] = $i; + } + } else { + $scaleid = - ($scheduler->scale); + $scalegrades = $this->get_scale_levels($scaleid); + } + $scalegrades = array(-1 => get_string('nograde')) + $scalegrades; + return $scalegrades; + } + + /** + * Return a string describing the grading strategy of a scheduler. + * + * @param int $strategy id number for the strategy + * @return string description of the strategy + */ + public function format_grading_strategy($strategy) { + if ($strategy == SCHEDULER_MAX_GRADE) { + return get_string('maxgrade', 'scheduler'); + } else { + return get_string('meangrade', 'scheduler'); + } + } + + /** + * Format a user-entered "note" on a slot or appointment, adjusting any links to embedded files. + * The "note" may also be the booking instructions. + * + * @param string $content content of the note + * @param int $format format of the note + * @param context $context context of the note + * @param string $area file ara for embedded files + * @param int $itemid item id for embedded files + * @return string the formatted note + */ + public function format_notes($content, $format, $context, $area, $itemid) { + $text = file_rewrite_pluginfile_urls($content, 'pluginfile.php', $context->id, 'mod_scheduler', $area, $itemid); + return format_text($text, $format); + } + + /** + * Format the notes relating to an appointment (appointment notes and confidential notes). + * + * @param scheduler $scheduler the scheduler in whose context the appointment is + * @param stdClass $data database record describing the appointment + * @param string $idfield the field in the record containing the item id + * @return string formatted notes + */ + public function format_appointment_notes(scheduler $scheduler, $data, $idfield = 'id') { + $note = ''; + $id = $data->{$idfield}; + if (isset($data->appointmentnote) && $scheduler->uses_appointmentnotes()) { + $note .= $this->format_notes($data->appointmentnote, $data->appointmentnoteformat, $scheduler->get_context(), + 'appointmentnote', $id); + } + if (isset($data->teachernote) && $scheduler->uses_teachernotes()) { + $note .= $this->format_notes($data->teachernote, $data->teachernoteformat, $scheduler->get_context(), + 'teachernote', $id); + } + return $note; + } + + /** + * Produce HTML code for a link to a user's profile. + * That is, the full name of the user is displayed with a link to the user's course profile on it. + * + * @param scheduler $scheduler the scheduler in whose context the link is + * @param stdClass $user the user to link to + * @return string HTML code of the link + */ + public function user_profile_link(scheduler $scheduler, stdClass $user) { + $profileurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $scheduler->course)); + return html_writer::link($profileurl, fullname($user)); + } + + /** + * Produce HTML code for a link to a user's appointment. + * That is, the full name of the user is displayed with a link to a given appointment. + * + * @param unknown $scheduler the scheduler in whose context the link is + * @param unknown $user the use in question + * @param unknown $appointmentid id number of the appointment to link to + * @return string HTML code of the link + */ + public function appointment_link($scheduler, $user, $appointmentid) { + $paras = array( + 'what' => 'viewstudent', + 'id' => $scheduler->cmid, + 'appointmentid' => $appointmentid + ); + $url = new moodle_url('/mod/scheduler/view.php', $paras); + return html_writer::link($url, fullname($user)); + } + + /** + * Render a list of files in a filearea. + * + * @param int $contextid id number of the context of the files + * @param string $filearea name of the file area + * @param int $itemid item id in the file area + * @return string rendered list of files + */ + public function render_attachments($contextid, $filearea, $itemid) { + + $fs = get_file_storage(); + $o = ''; + + // We retrieve all files according to the time that they were created. In the case that several files were uploaded + // at the sametime (e.g. in the case of drag/drop upload) we revert to using the filename. + $files = $fs->get_area_files($contextid, 'mod_scheduler', $filearea, $itemid, "filename", false); + if ($files) { + $o .= html_writer::start_tag('ul', array('class' => 'scheduler_filelist')); + foreach ($files as $file) { + $filename = $file->get_filename(); + $pathname = $file->get_filepath(); + $mimetype = $file->get_mimetype(); + $iconimage = $this->pix_icon(file_file_icon($file), get_mimetype_description($file), + 'moodle', array('class' => 'icon')); + $path = moodle_url::make_pluginfile_url($contextid, 'mod_scheduler', $filearea, $itemid, $pathname, $filename); + + $ulitem = html_writer::link($path, $iconimage) . html_writer::link($path, s($filename)); + $o .= html_writer::tag('ul', $ulitem); + } + $o .= html_writer::end_tag('ul'); + } + + return $o; + } + + + /** + * Render the module introduction of a scheduler. + * + * @param scheduler $scheduler the scheduler in question + * @return string rendered module info + */ + public function mod_intro($scheduler) { + $o = $this->heading(format_string($scheduler->name), 2); + + if (trim(strip_tags($scheduler->intro))) { + $o .= $this->box_start('mod_introbox'); + $o .= format_module_intro('scheduler', $scheduler->get_data(), $scheduler->cmid); + $o .= $this->box_end(); + } + return $o; + } + + /** + * Construct a tab header in the teacher view. + * + * @param moodle_url $baseurl + * @param string $namekey + * @param string $what + * @param string $subpage + * @param string $nameargs + * @return tabobject + */ + private function teacherview_tab(moodle_url $baseurl, $namekey, $what, $subpage = '', $nameargs = null) { + $taburl = new moodle_url($baseurl, array('what' => $what, 'subpage' => $subpage)); + $tabname = get_string($namekey, 'scheduler', $nameargs); + $id = ($subpage != '') ? $subpage : $what; + $tab = new tabobject($id, $taburl, $tabname); + return $tab; + } + + /** + * Render the tab header hierarchy in the teacher view. + * + * @param scheduler $scheduler the scheduler in question + * @param scheduler_permissions $permissions the permissions manager (for hiding tabs) + * @param moodle_url $baseurl base URL for the tab addresses + * @param string $selected the selected tab + * @param array $inactive any inactive tabs + * @return string rendered tab tree + */ + public function teacherview_tabs(scheduler $scheduler, scheduler_permissions $permissions, + moodle_url $baseurl, $selected, $inactive = null) { + + $statstab = $this->teacherview_tab($baseurl, 'statistics', 'viewstatistics', 'overall'); + $statstab->subtree = array( + $this->teacherview_tab($baseurl, 'overall', 'viewstatistics', 'overall'), + $this->teacherview_tab($baseurl, 'studentbreakdown', 'viewstatistics', 'studentbreakdown'), + $this->teacherview_tab($baseurl, 'staffbreakdown', 'viewstatistics', 'staffbreakdown', + $scheduler->get_teacher_name()), + $this->teacherview_tab($baseurl, 'lengthbreakdown', 'viewstatistics', 'lengthbreakdown'), + $this->teacherview_tab($baseurl, 'groupbreakdown', 'viewstatistics', 'groupbreakdown') + ); + + $level1 = array(); + $level1[] = $this->teacherview_tab($baseurl, 'myappointments', 'view', 'myappointments'); + if ($permissions->can_see_all_slots()) { + $level1[] = $this->teacherview_tab($baseurl, 'allappointments', 'view', 'allappointments'); + } + $level1[] = $this->teacherview_tab($baseurl, 'datelist', 'datelist'); + $level1[] = $statstab; + $level1[] = $this->teacherview_tab($baseurl, 'export', 'export'); + + return $this->tabtree($level1, $selected, $inactive); + } + + /** + * Render a table of slots + * + * @param scheduler_slot_table $slottable the table to rended + * @return string the HTML output + */ + public function render_scheduler_slot_table(scheduler_slot_table $slottable) { + $table = new html_table(); + + if ($slottable->showslot) { + $table->head = array(get_string('date', 'scheduler')); + $table->align = array('left'); + } + if ($slottable->showstudent) { + $table->head[] = get_string('name'); + $table->align[] = 'left'; + } + if ($slottable->showattended) { + $table->head[] = get_string('seen', 'scheduler'); + $table->align[] = 'center'; + } + if ($slottable->showslot) { + $table->head[] = $slottable->scheduler->get_teacher_name(); + $table->align[] = 'left'; + } + if ($slottable->showslot && $slottable->showlocation) { + $table->head[] = get_string('location', 'scheduler'); + $table->align[] = 'left'; + } + + $table->head[] = get_string('comments', 'scheduler'); + $table->align[] = 'left'; + + if ($slottable->showgrades) { + $table->head[] = get_string('grade', 'scheduler'); + $table->align[] = 'left'; + } else if ($slottable->hasotherstudents) { + $table->head[] = get_string('otherstudents', 'scheduler'); + $table->align[] = 'left'; + } + if ($slottable->showactions) { + $table->head[] = ''; + $table->align[] = 'right'; + } + + $table->data = array(); + + foreach ($slottable->slots as $slot) { + $rowdata = array(); + + $studenturl = new moodle_url($slottable->actionurl, array('appointmentid' => $slot->appointmentid)); + + $timedata = $this->userdate($slot->starttime); + if ($slottable->showeditlink) { + $timedata = $this->action_link($studenturl, $timedata); + } + $timedata = html_writer::div($timedata, 'datelabel'); + + $starttime = $this->usertime($slot->starttime); + $endtime = $this->usertime($slot->endtime); + $timedata .= html_writer::div("{$starttime} – {$endtime}", 'timelabel'); + + if ($slottable->showslot) { + $rowdata[] = $timedata; + } + + if ($slottable->showstudent) { + $name = fullname($slot->student); + if ($slottable->showeditlink) { + $name = $this->action_link($studenturl, $name); + } + $rowdata[] = $name; + } + + if ($slottable->showattended) { + $iconid = $slot->attended ? 'ticked' : 'unticked'; + $iconhelp = $slot->attended ? 'seen' : 'notseen'; + $attendedpix = $this->pix_icon($iconid, get_string($iconhelp, 'scheduler'), 'mod_scheduler'); + $rowdata[] = $attendedpix; + } + + if ($slottable->showslot) { + $rowdata[] = $this->user_profile_link($slottable->scheduler, $slot->teacher); + } + + if ($slottable->showslot && $slottable->showlocation) { + $rowdata[] = format_string($slot->location); + } + + $notes = ''; + if ($slottable->showslot && isset($slot->slotnote)) { + $notes .= $this->format_notes($slot->slotnote, $slot->slotnoteformat, + $slottable->scheduler->get_context(), 'slotnote', $slot->slotid); + } + $notes .= $this->format_appointment_notes($slottable->scheduler, $slot, 'appointmentid'); + $rowdata[] = $notes; + + if ($slottable->showgrades || $slottable->hasotherstudents) { + $gradedata = ''; + if ($slot->otherstudents) { + $gradedata = $this->render($slot->otherstudents); + } else if ($slottable->showgrades) { + $gradedata = $this->format_grade($slottable->scheduler, $slot->grade); + } + $rowdata[] = $gradedata; + } + if ($slottable->showactions) { + $actions = ''; + if ($slot->canedit) { + $buttonurl = new moodle_url($slottable->actionurl, + array('what' => 'editbooking', 'appointmentid' => $slot->appointmentid)); + $button = new single_button($buttonurl, get_string('editbooking', 'scheduler')); + $actions .= $this->render($button); + } + if ($slot->canview) { + $buttonurl = new moodle_url($slottable->actionurl, + array('what' => 'viewbooking', 'appointmentid' => $slot->appointmentid)); + $button = new single_button($buttonurl, get_string('viewbooking', 'scheduler')); + $actions .= $this->render($button); + } + if ($slot->cancancel) { + $buttonurl = new moodle_url($slottable->actionurl, + array('what' => 'cancelbooking', 'slotid' => $slot->slotid)); + $button = new single_button($buttonurl, get_string('cancelbooking', 'scheduler')); + $actions .= $this->render($button); + } + $rowdata[] = $actions; + } + $table->data[] = $rowdata; + } + + return html_writer::table($table); + } + + /** + * Rendering a list of student, to be displayed within a larger table + * + * @param scheduler_student_list $studentlist + * @return string + */ + public function render_scheduler_student_list(scheduler_student_list $studentlist) { + + $o = ''; + + $toggleid = html_writer::random_id('toggle'); + + if ($studentlist->expandable && count($studentlist->students) > 0) { + $this->page->requires->yui_module('moodle-mod_scheduler-studentlist', + 'M.mod_scheduler.studentlist.init', + array($toggleid, (boolean) $studentlist->expanded) ); + $imgclass = 'studentlist-togglebutton'; + $alttext = get_string('showparticipants', 'scheduler'); + $o .= $this->output->pix_icon('t/switch', $alttext, 'moodle', + array('id' => $toggleid, 'class' => $imgclass)); + } + + $divprops = array('id' => 'list'.$toggleid); + $o .= html_writer::start_div('studentlist', $divprops); + if (count($studentlist->students) > 0) { + $editable = $studentlist->actionurl && $studentlist->editable; + if ($editable) { + $o .= html_writer::start_tag('form', array('action' => $studentlist->actionurl, + 'method' => 'post', 'class' => 'studentselectform')); + } + + foreach ($studentlist->students as $student) { + $class = 'otherstudent'; + $checkbox = ''; + if ($studentlist->checkboxname) { + if ($student->editattended) { + $checkbox = html_writer::checkbox($studentlist->checkboxname, $student->entryid, $student->checked, '', + array('class' => 'studentselect')); + } else { + $img = $student->checked ? 'ticked' : 'unticked'; + $checkbox = $this->render(new pix_icon($img, '', 'scheduler', array('class' => 'statictickbox'))); + } + } + if ($studentlist->linkappointment) { + $name = $this->appointment_link($studentlist->scheduler, $student->user, $student->entryid); + } else { + $name = fullname($student->user); + } + $studicons = ''; + $studprovided = array(); + if ($student->notesprovided) { + $studprovided[] = get_string('message', 'scheduler'); + } + if ($student->filesprovided) { + $studprovided[] = get_string('nfiles', 'scheduler', $student->filesprovided); + } + if ($studprovided) { + $providedstr = implode(', ', $studprovided); + $alttext = get_string('studentprovided', 'scheduler', $providedstr); + $attachicon = new pix_icon('attachment', $alttext, 'scheduler', array('class' => 'studdataicon')); + $studicons .= $this->render($attachicon); + } + + if ($student->highlight) { + $class .= ' highlight'; + } + $picture = $this->user_picture($student->user, array('courseid' => $studentlist->scheduler->courseid)); + $grade = ''; + if ($studentlist->showgrades && $student->grade) { + $grade = $this->format_grade($studentlist->scheduler, $student->grade, true); + } + $o .= html_writer::div($checkbox . $picture . ' ' . $name . $studicons . ' ' . $grade, $class); + } + + if ($editable) { + $o .= html_writer::empty_tag('input', array( + 'type' => 'submit', + 'class' => 'studentselectsubmit', + 'value' => $studentlist->buttontext + )); + $o .= html_writer::end_tag('form'); + } + } + $o .= html_writer::end_div(); + + return $o; + } + + /** + * Render a slot booker. + * + * @param scheduler_slot_booker $booker + * @return string + */ + public function render_scheduler_slot_booker(scheduler_slot_booker $booker) { + + $table = new html_table(); + $table->head = array( get_string('date', 'scheduler'), get_string('start', 'scheduler'), + get_string('end', 'scheduler'), get_string('location', 'scheduler'), + get_string('comments', 'scheduler'), s($booker->scheduler->get_teacher_name()), + get_string('groupsession', 'scheduler'), ''); + $table->align = array ('left', 'left', 'left', 'left', 'left', 'left', 'left', 'left'); + $table->id = 'slotbookertable'; + $table->data = array(); + + $previousdate = ''; + $previoustime = ''; + $previousendtime = ''; + $canappoint = false; + + foreach ($booker->slots as $slot) { + + $rowdata = array(); + + $startdate = $this->userdate($slot->starttime); + $starttime = $this->usertime($slot->starttime); + $endtime = $this->usertime($slot->endtime); + // Simplify display of dates, start and end times. + if ($startdate == $previousdate && $starttime == $previoustime && $endtime == $previousendtime) { + // If this row exactly matches previous, there's nothing to display. + $startdatestr = ''; + $starttimestr = ''; + $endtimestr = ''; + } else if ($startdate == $previousdate) { + // If this date matches previous date, just display times. + $startdatestr = ''; + $starttimestr = $starttime; + $endtimestr = $endtime; + } else { + // Otherwise, display all elements. + $startdatestr = $startdate; + $starttimestr = $starttime; + $endtimestr = $endtime; + } + + $rowdata[] = $startdatestr; + $rowdata[] = $starttimestr; + $rowdata[] = $endtimestr; + + $rowdata[] = format_string($slot->location); + + $rowdata[] = $this->format_notes($slot->notes, $slot->notesformat, $booker->scheduler->get_context(), + 'slotnote', $slot->slotid); + + $rowdata[] = $this->user_profile_link($booker->scheduler, $slot->teacher); + + $groupinfo = $slot->bookedbyme ? get_string('complete', 'scheduler') : $slot->groupinfo; + if ($slot->otherstudents) { + $groupinfo .= $this->render($slot->otherstudents); + } + + $rowdata[] = $groupinfo; + + if ($slot->canbook) { + $bookaction = $booker->scheduler->uses_bookingform() ? 'bookingform' : 'bookslot'; + $bookurl = new moodle_url($booker->actionurl, array('what' => $bookaction, 'slotid' => $slot->slotid)); + $button = new single_button($bookurl, get_string('bookslot', 'scheduler')); + $rowdata[] = $this->render($button); + } else { + $rowdata[] = ''; + } + + $table->data[] = $rowdata; + + $previoustime = $starttime; + $previousendtime = $endtime; + $previousdate = $startdate; + } + + return html_writer::table($table); + } + + /** + * Render a command bar. + * + * @param scheduler_command_bar $commandbar + * @return string + */ + public function render_scheduler_command_bar(scheduler_command_bar $commandbar) { + $o = ''; + foreach ($commandbar->linkactions as $id => $action) { + $this->add_action_handler($action, $id); + } + $o .= html_writer::start_div('commandbar'); + if ($commandbar->title) { + $o .= html_writer::span($commandbar->title, 'title'); + } + foreach ($commandbar->menus as $m) { + $o .= $this->render($m); + } + $o .= html_writer::end_div(); + return $o; + } + + /** + * Render a slot manager. + * + * @param scheduler_slot_manager $slotman + * @return string + */ + public function render_scheduler_slot_manager(scheduler_slot_manager $slotman) { + + $this->page->requires->yui_module('moodle-mod_scheduler-saveseen', + 'M.mod_scheduler.saveseen.init', array($slotman->scheduler->cmid) ); + + $o = ''; + + $table = new html_table(); + $table->head = array('', get_string('date', 'scheduler'), get_string('start', 'scheduler'), + get_string('end', 'scheduler'), get_string('location', 'scheduler'), get_string('students', 'scheduler') ); + $table->align = array ('center', 'left', 'left', 'left', 'left', 'left'); + if ($slotman->showteacher) { + $table->head[] = s($slotman->scheduler->get_teacher_name()); + $table->align[] = 'left'; + } + $table->head[] = get_string('action', 'scheduler'); + $table->align[] = 'center'; + + $table->id = 'slotmanager'; + $table->data = array(); + + $previousdate = ''; + $previoustime = ''; + $previousendtime = ''; + + foreach ($slotman->slots as $slot) { + + $rowdata = array(); + + $selectbox = html_writer::checkbox('selectedslot[]', $slot->slotid, false, '', array('class' => 'slotselect')); + $rowdata[] = $slot->editable ? $selectbox : ''; + + $startdate = $this->userdate($slot->starttime); + $starttime = $this->usertime($slot->starttime); + $endtime = $this->usertime($slot->endtime); + // Simplify display of dates, start and end times. + if ($startdate == $previousdate && $starttime == $previoustime && $endtime == $previousendtime) { + // If this row exactly matches previous, there's nothing to display. + $startdatestr = ''; + $starttimestr = ''; + $endtimestr = ''; + } else if ($startdate == $previousdate) { + // If this date matches previous date, just display times. + $startdatestr = ''; + $starttimestr = $starttime; + $endtimestr = $endtime; + } else { + // Otherwise, display all elements. + $startdatestr = $startdate; + $starttimestr = $starttime; + $endtimestr = $endtime; + } + + $rowdata[] = $startdatestr; + $rowdata[] = $starttimestr; + $rowdata[] = $endtimestr; + + $rowdata[] = format_string($slot->location); + + $rowdata[] = $this->render($slot->students); + + if ($slotman->showteacher) { + $rowdata[] = $this->user_profile_link($slotman->scheduler, $slot->teacher); + } + + $actions = ''; + if ($slot->editable) { + $url = new moodle_url($slotman->actionurl, array('what' => 'deleteslot', 'slotid' => $slot->slotid)); + $confirmdelete = new confirm_action(get_string('confirmdelete-one', 'scheduler')); + $actions .= $this->action_icon($url, new pix_icon('t/delete', get_string('delete')), $confirmdelete); + + $url = new moodle_url($slotman->actionurl, array('what' => 'updateslot', 'slotid' => $slot->slotid)); + $actions .= $this->action_icon($url, new pix_icon('t/edit', get_string('edit'))); + } + + if ($slot->isattended || $slot->isappointed > 1) { + $groupicon = 'i/groupevent'; + } else if ($slot->exclusivity == 1) { + $groupicon = 't/groupn'; + } else { + $groupicon = 't/groupv'; + } + $groupalt = ''; $groupact = null; + if ($slot->isattended) { + $groupalt = 'attended'; + } else if ($slot->isappointed > 1) { + $groupalt = 'isnonexclusive'; + } else if ($slot->editable) { + if ($slot->exclusivity == 1) { + $groupact = array('what' => 'allowgroup', 'slotid' => $slot->slotid); + $groupalt = 'allowgroup'; + } else { + $groupact = array('what' => 'forbidgroup', 'slotid' => $slot->slotid); + $groupalt = 'forbidgroup'; + } + } else { + if ($slot->exclusivity == 1) { + $groupalt = 'allowgroup'; + } else { + $groupalt = 'forbidgroup'; + } + } + if ($groupact) { + $url = new moodle_url($slotman->actionurl, $groupact); + $actions .= $this->action_icon($url, new pix_icon($groupicon, get_string($groupalt, 'scheduler'))); + } else { + $actions .= $this->pix_icon($groupicon, get_string($groupalt, 'scheduler')); + } + + if ($slot->editable && $slot->isappointed) { + $url = new moodle_url($slotman->actionurl, array('what' => 'revokeall', 'slotid' => $slot->slotid)); + $confirmrevoke = new confirm_action(get_string('confirmrevoke', 'scheduler')); + $actions .= $this->action_icon($url, new pix_icon('s/no', get_string('revoke', 'scheduler')), $confirmrevoke); + } + + if ($slot->exclusivity > 1) { + $actions .= ' ('.$slot->exclusivity.')'; + } + $rowdata[] = $actions; + + $table->data[] = $rowdata; + + $previoustime = $starttime; + $previousendtime = $endtime; + $previousdate = $startdate; + } + $o .= html_writer::table($table); + + return $o; + } + + /** + * Render a scheduling list. + * + * @param scheduler_scheduling_list $list + * @return string + */ + public function render_scheduler_scheduling_list(scheduler_scheduling_list $list) { + + $mtable = new html_table(); + + $mtable->id = $list->id; + $mtable->head = array ('', get_string('name')); + $mtable->align = array ('center', 'left'); + foreach ($list->extraheaders as $field) { + $mtable->head[] = $field; + $mtable->align[] = 'left'; + } + $mtable->head[] = get_string('action', 'scheduler'); + $mtable->align[] = 'center'; + + $mtable->data = array(); + foreach ($list->lines as $line) { + $data = array($line->pix, $line->name); + foreach ($line->extrafields as $field) { + $data[] = $field; + } + $actions = ''; + if ($line->actions) { + $menu = new action_menu($line->actions); + $menu->actiontext = get_string('schedule', 'scheduler'); + $actions = $this->render($menu); + } + $data[] = $actions; + $mtable->data[] = $data; + } + return html_writer::table($mtable); + } + + /** + * Render total grade information. + * + * @param scheduler_totalgrade_info $gradeinfo + * @return string + */ + public function render_scheduler_totalgrade_info(scheduler_totalgrade_info $gradeinfo) { + $items = array(); + + if ($gradeinfo->showtotalgrade) { + $items[] = array('gradingstrategy', $this->format_grading_strategy($gradeinfo->scheduler->gradingstrategy)); + $items[] = array('totalgrade', $this->format_grade($gradeinfo->scheduler, $gradeinfo->totalgrade)); + } + + if (!is_null($gradeinfo->gbgrade)) { + $gbgradeinfo = $this->format_grade($gradeinfo->scheduler, $gradeinfo->gbgrade->grade); + $attributes = array(); + if ($gradeinfo->gbgrade->hidden) { + $attributes[] = get_string('hidden', 'grades'); + } + if ($gradeinfo->gbgrade->locked) { + $attributes[] = get_string('locked', 'grades'); + } + if ($gradeinfo->gbgrade->overridden) { + $attributes[] = get_string('overridden', 'grades'); + } + if (count($attributes) > 0) { + $gbgradeinfo .= ' ('.implode(', ', $attributes) .')'; + } + $items[] = array('gradeingradebook', $gbgradeinfo); + } + + $o = html_writer::start_div('totalgrade'); + $o .= html_writer::start_tag('dl', array('class' => 'totalgrade')); + foreach ($items as $item) { + $o .= html_writer::tag('dt', get_string($item[0], 'scheduler')); + $o .= html_writer::tag('dd', $item[1]); + } + $o .= html_writer::end_tag('dl'); + $o .= html_writer::end_div('totalgrade'); + return $o; + } + + /** + * Render a conflict list. + * + * @param scheduler_conflict_list $cl + * @return string + */ + public function render_scheduler_conflict_list(scheduler_conflict_list $cl) { + + $o = html_writer::start_tag('ul'); + + foreach ($cl->conflicts as $conflict) { + $a = new stdClass(); + $a->datetime = userdate($conflict->starttime); + $a->duration = $conflict->duration; + if ($conflict->isself) { + $entry = get_string('conflictlocal', 'scheduler', $a); + } else { + $a->courseshortname = $conflict->courseshortname; + $a->coursefullname = $conflict->coursefullname; + $a->schedulername = format_string($conflict->schedulername); + $entry = get_string('conflictremote', 'scheduler', $a); + } + $o .= html_writer::tag('li', $entry); + } + + $o .= html_writer::end_tag('ul'); + + return $o; + } + + + /** + * Render a table containing information about a booked appointment + * + * @param scheduler_appointment_info $ai + * @return string + */ + public function render_scheduler_appointment_info(scheduler_appointment_info $ai) { + $o = ''; + $o .= $this->output->container_start('appointmentinfotable'); + + $o .= $this->output->box_start('boxaligncenter appointmentinfotable'); + + $t = new html_table(); + + if ($ai->showslotinfo) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('slotdatetimelabel', 'scheduler')); + $data = self::slotdatetime($ai->slot->starttime, $ai->slot->duration); + $cell2 = new html_table_cell(get_string('slotdatetimelong', 'scheduler', $data)); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + + $row = new html_table_row(); + $cell1 = new html_table_cell($ai->scheduler->get_teacher_name()); + $cell2 = new html_table_cell(fullname($ai->slot->get_teacher())); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + + if ($ai->slot->appointmentlocation) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('location', 'scheduler')); + $cell2 = new html_table_cell(format_string($ai->slot->appointmentlocation)); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + + if ($ai->slot->notes) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('comments', 'scheduler')); + $notes = $this->format_notes($ai->slot->notes, $ai->slot->notesformat, $ai->scheduler->get_context(), + 'slotnote', $ai->slot->id); + $cell2 = new html_table_cell($notes); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + } + + if ($ai->groupinfo) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('appointfor', 'scheduler')); + $cell2 = new html_table_cell(format_string($ai->groupinfo)); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + + if ($ai->showbookinginfo) { + if ($ai->scheduler->has_bookinginstructions()) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('bookinginstructions', 'scheduler')); + $note = $this->format_notes($ai->scheduler->bookinginstructions, $ai->scheduler->bookinginstructionsformat, + $ai->scheduler->get_context(), 'bookinginstructions', 0); + $cell2 = new html_table_cell($note); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + } + + if ($ai->showstudentdata) { + if ($ai->scheduler->uses_studentnotes()) { + $row = new html_table_row(); + if ($ai->onstudentside) { + $key = 'yourstudentnote'; + } else { + $key = 'studentnote'; + } + $cell1 = new html_table_cell(get_string($key, 'scheduler')); + $note = format_text($ai->appointment->studentnote, $ai->appointment->studentnoteformat); + $cell2 = new html_table_cell($note); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + if ($ai->scheduler->uses_studentfiles()) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('studentfiles', 'scheduler')); + $att = $this->render_attachments($ai->scheduler->context->id, 'studentfiles', $ai->appointment->id); + $cell2 = new html_table_cell($att); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + } + + if ($ai->showresult) { + if ($ai->scheduler->uses_appointmentnotes() && $ai->appointment->appointmentnote) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('appointmentnotes', 'scheduler')); + $note = $this->format_notes($ai->appointment->appointmentnote, $ai->appointment->appointmentnoteformat, + $ai->scheduler->get_context(), 'appointmentnote', $ai->appointment->id); + $cell2 = new html_table_cell($note); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + if ($ai->scheduler->uses_grades()) { + $row = new html_table_row(); + $cell1 = new html_table_cell(get_string('grade', 'scheduler')); + $gradetext = $this->format_grade($ai->scheduler, $ai->appointment->grade, false); + $cell2 = new html_table_cell($gradetext); + $row->cells = array($cell1, $cell2); + $t->data[] = $row; + } + } + + $o .= html_writer::table($t); + $o .= $this->output->box_end(); + + $o .= $this->output->container_end(); + return $o; + } + +} diff --git a/mod/scheduler/settings.php b/mod/scheduler/settings.php new file mode 100644 index 0000000..b2fee38 --- /dev/null +++ b/mod/scheduler/settings.php @@ -0,0 +1,61 @@ +<?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/>. + +/** + * Global configuration settings for the scheduler module. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + + require_once($CFG->dirroot.'/mod/scheduler/lib.php'); + + $settings->add(new admin_setting_configcheckbox('mod_scheduler/showemailplain', + get_string('showemailplain', 'scheduler'), + get_string('showemailplain_desc', 'scheduler'), + 0)); + + $settings->add(new admin_setting_configcheckbox('mod_scheduler/groupscheduling', + get_string('groupscheduling', 'scheduler'), + get_string('groupscheduling_desc', 'scheduler'), + 1)); + + $settings->add(new admin_setting_configcheckbox('mod_scheduler/mixindivgroup', + get_string('mixindivgroup', 'scheduler'), + get_string('mixindivgroup_desc', 'scheduler'), + 1)); + + $settings->add(new admin_setting_configtext('mod_scheduler/maxstudentlistsize', + get_string('maxstudentlistsize', 'scheduler'), + get_string('maxstudentlistsize_desc', 'scheduler'), + 200, PARAM_INT)); + + $settings->add(new admin_setting_configtext('mod_scheduler/uploadmaxfiles', + get_string('uploadmaxfilesglobal', 'scheduler'), + get_string('uploadmaxfilesglobal_desc', 'scheduler'), + 5, PARAM_INT)); + + $settings->add(new admin_setting_configcheckbox('mod_scheduler/revealteachernotes', + get_string('revealteachernotes', 'scheduler'), + get_string('revealteachernotes_desc', 'scheduler'), + 0)); + +} diff --git a/mod/scheduler/slotforms.php b/mod/scheduler/slotforms.php new file mode 100644 index 0000000..a2be009 --- /dev/null +++ b/mod/scheduler/slotforms.php @@ -0,0 +1,662 @@ +<?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/>. + +/** + * Slot-related forms of the scheduler module (using Moodle formslib) + * + * @package mod_scheduler + * @copyright 2013 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Base class for slot-related forms + * + * @package mod_scheduler + * @copyright 2013 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class scheduler_slotform_base extends moodleform { + + /** + * @var scheduler the scheduler that this form refers to + */ + protected $scheduler; + + /** + * @var array user groups to filter for + */ + protected $usergroups; + + /** + * @var bool does this form have a duration field? + */ + protected $hasduration = false; + + /** + * @var array options for note fields + */ + protected $noteoptions; + + /** + * Create a new form + * + * @param mixed $action the action attribute for the form + * @param scheduler $scheduler + * @param object $cm unused + * @param array $usergroups groups to filter for + * @param array $customdata + */ + public function __construct($action, scheduler $scheduler, $cm, $usergroups, $customdata=null) { + $this->scheduler = $scheduler; + $this->usergroups = $usergroups; + $this->noteoptions = array('trusttext' => true, 'maxfiles' => -1, 'maxbytes' => 0, + 'context' => $scheduler->get_context(), 'subdirs' => false); + + parent::__construct($action, $customdata); + } + + /** + * Add basic fields to this form. To be used in definition() methods of subclasses. + */ + protected function add_base_fields() { + + global $CFG, $USER; + + $mform = $this->_form; + + // Exclusivity. + $exclgroup = array(); + + $exclgroup[] = $mform->createElement('text', 'exclusivity', '', array('size' => '10')); + $mform->setType('exclusivity', PARAM_INTEGER); + $mform->setDefault('exclusivity', 1); + + $exclgroup[] = $mform->createElement('advcheckbox', 'exclusivityenable', '', get_string('enable')); + $mform->setDefault('exclusivityenable', 1); + $mform->disabledIf('exclusivity', 'exclusivityenable', 'eq', 0); + + $mform->addGroup($exclgroup, 'exclusivitygroup', get_string('maxstudentsperslot', 'scheduler'), ' ', false); + $mform->addHelpButton('exclusivitygroup', 'exclusivity', 'scheduler'); + + // Location of the appointment. + $mform->addElement('text', 'appointmentlocation', get_string('location', 'scheduler'), array('size' => '30')); + $mform->setType('appointmentlocation', PARAM_TEXT); + $mform->addRule('appointmentlocation', get_string('error'), 'maxlength', 255); + $mform->setDefault('appointmentlocation', $this->scheduler->get_last_location($USER)); + $mform->addHelpButton('appointmentlocation', 'location', 'scheduler'); + + // Choose the teacher (if allowed). + if (has_capability('mod/scheduler:canscheduletootherteachers', $this->scheduler->get_context())) { + $teachername = s($this->scheduler->get_teacher_name()); + $teachers = $this->scheduler->get_available_teachers(); + $teachersmenu = array(); + if ($teachers) { + foreach ($teachers as $teacher) { + $teachersmenu[$teacher->id] = fullname($teacher); + } + $mform->addElement('select', 'teacherid', $teachername, $teachersmenu); + $mform->addRule('teacherid', get_string('noteacherforslot', 'scheduler'), 'required'); + $mform->setDefault('teacherid', $USER->id); + } else { + $mform->addElement('static', 'teacherid', $teachername, get_string('noteachershere', 'scheduler', $teachername)); + } + $mform->addHelpButton('teacherid', 'bookwithteacher', 'scheduler'); + } else { + $mform->addElement('hidden', 'teacherid'); + $mform->setDefault('teacherid', $USER->id); + $mform->setType('teacherid', PARAM_INT); + } + + } + + /** + * Add an input field for a number of minutes + * + * @param string $name field name + * @param string $label language key for field label + * @param int $defaultval default value + * @param string $minuteslabel language key for suffix "minutes" + */ + protected function add_minutes_field($name, $label, $defaultval, $minuteslabel = 'minutes') { + $mform = $this->_form; + $group = array(); + $group[] =& $mform->createElement('text', $name, '', array('size' => 5)); + $group[] =& $mform->createElement('static', $name.'mintext', '', get_string($minuteslabel, 'scheduler')); + $mform->addGroup($group, $name.'group', get_string($label, 'scheduler'), array(' '), false); + $mform->setType($name, PARAM_INT); + $mform->setDefault($name, $defaultval); + } + + /** + * Add theduration field to the form. + * @param string $minuteslabel language key for the "minutes" label + */ + protected function add_duration_field($minuteslabel = 'minutes') { + $this->add_minutes_field('duration', 'duration', $this->scheduler->defaultslotduration, $minuteslabel); + $this->hasduration = true; + } + + /** + * 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); + + // Check duration for valid range. + if ($this->hasduration) { + $limits = array('min' => 1, 'max' => 24 * 60); + if ($data['duration'] < $limits['min'] || $data['duration'] > $limits['max']) { + $errors['durationgroup'] = get_string('durationrange', 'scheduler', $limits); + } + } + + return $errors; + } + +} + +/** + * Slot edit form + * + * @package mod_scheduler + * @copyright 2013 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_editslot_form extends scheduler_slotform_base { + + /** + * @var int id of the slot being edited + */ + protected $slotid; + + /** + * Form definition + */ + protected function definition() { + + global $DB, $output; + + $mform = $this->_form; + $this->slotid = 0; + if (isset($this->_customdata['slotid'])) { + $this->slotid = $this->_customdata['slotid']; + } + $timeoptions = null; + if (isset($this->_customdata['timeoptions'])) { + $timeoptions = $this->_customdata['timeoptions']; + } + + // Start date/time of the slot. + $mform->addElement('date_time_selector', 'starttime', get_string('date', 'scheduler'), $timeoptions); + $mform->setDefault('starttime', time()); + $mform->addHelpButton('starttime', 'choosingslotstart', 'scheduler'); + + // Duration of the slot. + $this->add_duration_field(); + + // Ignore conflict checkbox. + $mform->addElement('checkbox', 'ignoreconflicts', get_string('ignoreconflicts', 'scheduler')); + $mform->setDefault('ignoreconflicts', false); + $mform->addHelpButton('ignoreconflicts', 'ignoreconflicts', 'scheduler'); + + // Common fields. + $this->add_base_fields(); + + // Display slot from this date. + $mform->addElement('date_selector', 'hideuntil', get_string('displayfrom', 'scheduler')); + $mform->setDefault('hideuntil', time()); + + // Send e-mail reminder? + $mform->addElement('date_selector', 'emaildate', get_string('emailreminderondate', 'scheduler'), + array('optional' => true)); + $mform->setDefault('remindersel', -1); + + // Slot comments. + $mform->addElement('editor', 'notes_editor', get_string('comments', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->noteoptions); + $mform->setType('notes', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. + + // Appointments. + + $repeatarray = array(); + $grouparray = array(); + $repeatarray[] = $mform->createElement('header', 'appointhead', get_string('appointmentno', 'scheduler', '{no}')); + + // Choose student. + $students = $this->scheduler->get_available_students($this->usergroups); + $studentchoices = array(); + if ($students) { + foreach ($students as $astudent) { + $studentchoices[$astudent->id] = fullname($astudent); + } + } + $grouparray[] = $mform->createElement('searchableselector', 'studentid', '', $studentchoices); + $grouparray[] = $mform->createElement('hidden', 'appointid', 0); + + // Seen tickbox. + $grouparray[] = $mform->createElement('static', 'attendedlabel', '', get_string('seen', 'scheduler')); + $grouparray[] = $mform->createElement('checkbox', 'attended'); + + // Grade. + if ($this->scheduler->scale != 0) { + $gradechoices = $output->grading_choices($this->scheduler); + $grouparray[] = $mform->createElement('static', 'attendedlabel', '', get_string('grade', 'scheduler')); + $grouparray[] = $mform->createElement('select', 'grade', '', $gradechoices); + } + + $repeatarray[] = $mform->createElement('group', 'studgroup', get_string('student', 'scheduler'), $grouparray, null, false); + + // Appointment notes, visible to teacher and/or student. + + if ($this->scheduler->uses_appointmentnotes()) { + $repeatarray[] = $mform->createElement('editor', 'appointmentnote_editor', get_string('appointmentnote', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->noteoptions); + } + if ($this->scheduler->uses_teachernotes()) { + $repeatarray[] = $mform->createElement('editor', 'teachernote_editor', get_string('teachernote', 'scheduler'), + array('rows' => 3, 'columns' => 60), $this->noteoptions); + } + + // Tickbox to remove the student. + $repeatarray[] = $mform->createElement('advcheckbox', 'deletestudent', '', get_string('deleteonsave', 'scheduler')); + + if (isset($this->_customdata['repeats'])) { + $repeatno = $this->_customdata['repeats']; + } else if ($this->slotid) { + $repeatno = $DB->count_records('scheduler_appointment', array('slotid' => $this->slotid)); + $repeatno += 1; + } else { + $repeatno = 1; + } + + $repeateloptions = array(); + $repeateloptions['appointid']['type'] = PARAM_INT; + $repeateloptions['studentid']['disabledif'] = array('appointid', 'neq', 0); + $nostudcheck = array('studentid', 'eq', 0); + $repeateloptions['attended']['disabledif'] = $nostudcheck; + $repeateloptions['appointmentnote_editor']['disabledif'] = $nostudcheck; + $repeateloptions['teachernote_editor']['disabledif'] = $nostudcheck; + $repeateloptions['grade']['disabledif'] = $nostudcheck; + $repeateloptions['deletestudent']['disabledif'] = $nostudcheck; + $repeateloptions['appointhead']['expanded'] = true; + + $this->repeat_elements($repeatarray, $repeatno, $repeateloptions, + 'appointment_repeats', 'appointment_add', 1, get_string('addappointment', 'scheduler')); + + $this->add_action_buttons(); + + } + + /** + * 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) { + global $output; + + $errors = parent::validation($data, $files); + + // Check number of appointments vs exclusivity. + $numappointments = 0; + for ($i = 0; $i < $data['appointment_repeats']; $i++) { + if ($data['studentid'][$i] > 0 && $data['deletestudent'][$i] == 0) { + $numappointments++; + } + } + if ($data['exclusivityenable'] && $data['exclusivity'] <= 0) { + $errors['exclusivitygroup'] = get_string('exclusivitypositive', 'scheduler'); + } else if ($data['exclusivityenable'] && $numappointments > $data['exclusivity']) { + $errors['exclusivitygroup'] = get_string('exclusivityoverload', 'scheduler', $numappointments); + } + + // Avoid empty slots starting in the past. + if ($numappointments == 0 && $data['starttime'] < time()) { + $errors['starttime'] = get_string('startpast', 'scheduler'); + } + + // Check whether students have been selected several times. + for ($i = 0; $i < $data['appointment_repeats']; $i++) { + for ($j = 0; $j < $i; $j++) { + if ($data['deletestudent'][$j] == 0 && $data['studentid'][$i] > 0 + && $data['studentid'][$i] == $data['studentid'][$j]) { + $errors['studgroup['.$i.']'] = get_string('studentmultiselect', 'scheduler'); + $errors['studgroup['.$j.']'] = get_string('studentmultiselect', 'scheduler'); + } + } + } + + if (!isset($data['ignoreconflicts'])) { + /* Avoid overlapping slots by warning the user */ + $conflicts = $this->scheduler->get_conflicts( + $data['starttime'], $data['starttime'] + $data['duration'] * 60, + $data['teacherid'], 0, SCHEDULER_ALL, $this->slotid); + + if (count($conflicts) > 0) { + + $cl = new scheduler_conflict_list(); + $cl->add_conflicts($conflicts); + + $msg = get_string('slotwarning', 'scheduler'); + $msg .= $output->render($cl); + $msg .= $output->doc_link('mod/scheduler/conflict', '', true); + + $errors['starttime'] = $msg; + } + } + return $errors; + } + + /** + * Fill the form data from an existing slot + * + * @param slot $slot + * @return stdClass form data + */ + public function prepare_formdata(slot $slot) { + + $context = $slot->get_scheduler()->get_context(); + + $data = $slot->get_data(); + $data->exclusivityenable = ($data->exclusivity > 0); + + $data = file_prepare_standard_editor($data, "notes", $this->noteoptions, $context, + 'mod_scheduler', 'slotnote', $slot->id); + $data->notes = array(); + $data->notes['text'] = $slot->notes; + $data->notes['format'] = $slot->notesformat; + + if ($slot->emaildate < 0) { + $data->emaildate = 0; + } + + $i = 0; + foreach ($slot->get_appointments() as $appointment) { + $data->appointid[$i] = $appointment->id; + $data->studentid[$i] = $appointment->studentid; + $data->attended[$i] = $appointment->attended; + + $draftid = file_get_submitted_draft_itemid('appointmentnote'); + $currenttext = file_prepare_draft_area($draftid, $context->id, + 'mod_scheduler', 'appointmentnote', $appointment->id, + $this->noteoptions, $appointment->appointmentnote); + $data->appointmentnote_editor[$i] = array('text' => $currenttext, + 'format' => $appointment->appointmentnoteformat, + 'itemid' => $draftid); + + $draftid = file_get_submitted_draft_itemid('teachernote'); + $currenttext = file_prepare_draft_area($draftid, $context->id, + 'mod_scheduler', 'teachernote', $appointment->id, + $this->noteoptions, $appointment->teachernote); + $data->teachernote_editor[$i] = array('text' => $currenttext, + 'format' => $appointment->teachernoteformat, + 'itemid' => $draftid); + + $data->grade[$i] = $appointment->grade; + $i++; + } + + return $data; + } + + /** + * Save a slot object, updating it with data from the form + * @param int $slotid + * @param mixed $data form data + * @return slot the updated slot + */ + public function save_slot($slotid, $data) { + + $context = $this->scheduler->get_context(); + + if ($slotid) { + $slot = slot::load_by_id($slotid, $this->scheduler); + } else { + $slot = new slot($this->scheduler); + } + + // Set data fields from input form. + $slot->starttime = $data->starttime; + $slot->duration = $data->duration; + $slot->exclusivity = $data->exclusivityenable ? $data->exclusivity : 0; + $slot->teacherid = $data->teacherid; + $slot->appointmentlocation = $data->appointmentlocation; + $slot->hideuntil = $data->hideuntil; + $slot->emaildate = $data->emaildate; + $slot->timemodified = time(); + + if (!$slotid) { + $slot->save(); // Make sure that a new slot has a slot id before proceeding. + } + + $editor = $data->notes_editor; + $slot->notes = file_save_draft_area_files($editor['itemid'], $context->id, 'mod_scheduler', 'slotnote', $slotid, + $this->noteoptions, $editor['text']); + $slot->notesformat = $editor['format']; + + $currentapps = $slot->get_appointments(); + for ($i = 0; $i < $data->appointment_repeats; $i++) { + if ($data->deletestudent[$i] != 0) { + if ($data->appointid[$i]) { + $app = $slot->get_appointment($data->appointid[$i]); + $slot->remove_appointment($app); + } + } else if ($data->studentid[$i] > 0) { + $app = null; + if ($data->appointid[$i]) { + $app = $slot->get_appointment($data->appointid[$i]); + } else { + $app = $slot->create_appointment(); + $app->studentid = $data->studentid[$i]; + $app->save(); + } + $app->attended = isset($data->attended[$i]); + + if (isset($data->grade)) { + $selgrade = $data->grade[$i]; + $app->grade = ($selgrade >= 0) ? $selgrade : null; + } + + if ($this->scheduler->uses_appointmentnotes()) { + $editor = $data->appointmentnote_editor[$i]; + $app->appointmentnote = file_save_draft_area_files($editor['itemid'], $context->id, + 'mod_scheduler', 'appointmentnote', $app->id, + $this->noteoptions, $editor['text']); + $app->appointmentnoteformat = $editor['format']; + } + if ($this->scheduler->uses_teachernotes()) { + $editor = $data->teachernote_editor[$i]; + $app->teachernote = file_save_draft_area_files($editor['itemid'], $context->id, + 'mod_scheduler', 'teachernote', $app->id, + $this->noteoptions, $editor['text']); + $app->teachernoteformat = $editor['format']; + } + } + } + + $slot->save(); + + $slot = $this->scheduler->get_slot($slot->id); + + return $slot; + } +} + +/** + * "Add session" form + * + * @package mod_scheduler + * @copyright 2013 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduler_addsession_form extends scheduler_slotform_base { + + /** + * Form definition + */ + protected function definition() { + + global $DB; + + $mform = $this->_form; + + // Start and end of range. + $mform->addElement('date_selector', 'rangestart', get_string('date', 'scheduler')); + $mform->setDefault('rangestart', time()); + + $mform->addElement('date_selector', 'rangeend', get_string('enddate', 'scheduler'), + array('optional' => true) ); + + // Weekdays selection. + $checkboxes = array(); + $weekdays = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday'); + foreach ($weekdays as $day) { + $checkboxes[] = $mform->createElement('advcheckbox', $day, '', get_string($day, 'scheduler')); + $mform->setDefault($day, true); + } + $checkboxes[] = $mform->createElement('advcheckbox', 'saturday', '', get_string('saturday', 'scheduler')); + $checkboxes[] = $mform->createElement('advcheckbox', 'sunday', '', get_string('sunday', 'scheduler')); + $mform->addGroup($checkboxes, 'weekdays', get_string('addondays', 'scheduler'), null, false); + + // Start and end time. + $hours = array(); + $minutes = array(); + for ($i = 0; $i <= 23; $i++) { + $hours[$i] = sprintf("%02d", $i); + } + for ($i = 0; $i < 60; $i += 5) { + $minutes[$i] = sprintf("%02d", $i); + } + $timegroup = array(); + $timegroup[] = $mform->createElement('static', 'timefrom', '', get_string('timefrom', 'scheduler')); + $timegroup[] = $mform->createElement('select', 'starthour', get_string('hour', 'form'), $hours); + $timegroup[] = $mform->createElement('select', 'startminute', get_string('minute', 'form'), $minutes); + $timegroup[] = $mform->createElement('static', 'timeto', '', get_string('timeto', 'scheduler')); + $timegroup[] = $mform->createElement('select', 'endhour', get_string('hour', 'form'), $hours); + $timegroup[] = $mform->createElement('select', 'endminute', get_string('minute', 'form'), $minutes); + $mform->addGroup($timegroup, 'timerange', get_string('timerange', 'scheduler'), null, false); + + // Divide into slots? + $mform->addElement('selectyesno', 'divide', get_string('divide', 'scheduler')); + $mform->setDefault('divide', 1); + + // Duration of the slot. + $this->add_duration_field('minutesperslot'); + $mform->disabledIf('duration', 'divide', 'eq', '0'); + + // Break between slots. + $this->add_minutes_field('break', 'break', 0, 'minutes'); + $mform->disabledIf('break', 'divide', 'eq', '0'); + + // Force when overlap? + $mform->addElement('selectyesno', 'forcewhenoverlap', get_string('forcewhenoverlap', 'scheduler')); + $mform->addHelpButton('forcewhenoverlap', 'forcewhenoverlap', 'scheduler'); + + // Common fields. + $this->add_base_fields(); + + // Display slot from date - relative. + $hideuntilsel = array(); + $hideuntilsel[0] = get_string('now', 'scheduler'); + $hideuntilsel[DAYSECS] = get_string('onedaybefore', 'scheduler'); + for ($i = 2; $i < 7; $i++) { + $hideuntilsel[DAYSECS * $i] = get_string('xdaysbefore', 'scheduler', $i); + } + $hideuntilsel[WEEKSECS] = get_string('oneweekbefore', 'scheduler'); + for ($i = 2; $i < 7; $i++) { + $hideuntilsel[WEEKSECS * $i] = get_string('xweeksbefore', 'scheduler', $i); + } + $mform->addElement('select', 'hideuntilrel', get_string('displayfrom', 'scheduler'), $hideuntilsel); + $mform->setDefault('hideuntilsel', 0); + + // E-mail reminder from. + $remindersel = array(); + $remindersel[-1] = get_string('never', 'scheduler'); + $remindersel[0] = get_string('onthemorningofappointment', 'scheduler'); + $remindersel[DAYSECS] = get_string('onedaybefore', 'scheduler'); + for ($i = 2; $i < 7; $i++) { + $remindersel[DAYSECS * $i] = get_string('xdaysbefore', 'scheduler', $i); + } + $remindersel[WEEKSECS] = get_string('oneweekbefore', 'scheduler'); + for ($i = 2; $i < 7; $i++) { + $remindersel[WEEKSECS * $i] = get_string('xweeksbefore', 'scheduler', $i); + } + + $mform->addElement('select', 'emaildaterel', get_string('emailreminder', 'scheduler'), $remindersel); + $mform->setDefault('remindersel', -1); + + $this->add_action_buttons(); + + } + + /** + * 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); + + // Range is negative. + $fordays = 0; + if ($data['rangeend'] > 0) { + $fordays = ($data['rangeend'] - $data['rangestart']) / DAYSECS; + if ($fordays < 0) { + $errors['rangeend'] = get_string('negativerange', 'scheduler'); + } + } + + // Time range is negative. + $starttime = $data['starthour'] * 60 + $data['startminute']; + $endtime = $data['endhour'] * 60 + $data['endminute']; + if ($starttime > $endtime) { + $errors['timerange'] = get_string('negativerange', 'scheduler'); + } + + // First slot is in the past. + if ($data['rangestart'] < time() - DAYSECS) { + $errors['rangestart'] = get_string('startpast', 'scheduler'); + } + + // Break must be nonnegative. + if ($data['break'] < 0) { + $errors['breakgroup'] = get_string('breaknotnegative', 'scheduler'); + } + + // Conflict checks are now being done after submitting the form. + + return $errors; + } +} diff --git a/mod/scheduler/studentview.controller.php b/mod/scheduler/studentview.controller.php new file mode 100644 index 0000000..4b2dd01 --- /dev/null +++ b/mod/scheduler/studentview.controller.php @@ -0,0 +1,308 @@ +<?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/>. + +/** + * Controller for student view + * + * @package mod_scheduler + * @copyright 2015 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/scheduler/mailtemplatelib.php'); + +/** + * scheduler_book_slot + * + * @param scheduler_instance $scheduler + * @param int $slotid + * @param int $userid + * @param int $groupid + * @param scheduler_booking_form $mform + * @param mixed $formdata + * @param mixed $returnurl + * @throws mixed moodle_exception + */ +function scheduler_book_slot($scheduler, $slotid, $userid, $groupid, $mform, $formdata, $returnurl) { + + global $DB, $COURSE, $output; + + $slot = $scheduler->get_slot($slotid); + if (!$slot) { + throw new moodle_exception('error'); + } + + if (!$slot->is_in_bookable_period()) { + throw new moodle_exception('nopermissions'); + } + + $requiredcapacity = 1; + $userstobook = array($userid); + if ($groupid > 0) { + if (!$scheduler->is_group_scheduling_enabled()) { + throw new moodle_exception('error'); + } + $groupmembers = $scheduler->get_available_students($groupid); + $requiredcapacity = count($groupmembers); + $userstobook = array_keys($groupmembers); + } else if ($groupid == 0) { + if (!$scheduler->is_individual_scheduling_enabled()) { + throw new moodle_exception('error'); + } + } else { + // Group scheduling enabled but no group selected. + throw new moodle_exception('error'); + } + + $errormessage = ''; + + $bookinglimit = $scheduler->count_bookable_appointments($userid, false); + if ($bookinglimit == 0) { + $errormessage = get_string('selectedtoomany', 'scheduler', $bookinglimit); + } else { + // Validate our user ids. + $existingstudents = array(); + foreach ($slot->get_appointments() as $app) { + $existingstudents[] = $app->studentid; + } + $userstobook = array_diff($userstobook, $existingstudents); + + $remaining = $slot->count_remaining_appointments(); + // If the slot is already overcrowded... + if ($remaining >= 0 && $remaining < $requiredcapacity) { + if ($requiredcapacity > 1) { + $errormessage = get_string('notenoughplaces', 'scheduler'); + } else { + $errormessage = get_string('slot_is_just_in_use', 'scheduler'); + } + } + } + + if ($errormessage) { + \core\notification::error($errormessage); + redirect($returnurl); + } + + // Create new appointment for each member of the group. + foreach ($userstobook as $studentid) { + $appointment = $slot->create_appointment(); + $appointment->studentid = $studentid; + $appointment->attended = 0; + $appointment->timecreated = time(); + $appointment->timemodified = time(); + $appointment->save(); + + if (($studentid == $userid) && $mform) { + $mform->save_booking_data($formdata, $appointment); + } + + \mod_scheduler\event\booking_added::create_from_slot($slot)->trigger(); + + // Notify the teacher. + if ($scheduler->allownotifications) { + $student = $DB->get_record('user', array('id' => $appointment->studentid), '*', MUST_EXIST); + $teacher = $DB->get_record('user', array('id' => $slot->teacherid), '*', MUST_EXIST); + scheduler_messenger::send_slot_notification($slot, 'bookingnotification', 'applied', + $student, $teacher, $teacher, $student, $COURSE); + } + } + $slot->save(); + redirect($returnurl); + +} + +$returnurlparas = array('id' => $cm->id); +if ($scheduler->is_group_scheduling_enabled()) { + $returnurlparas['appointgroup'] = $appointgroup; +} +$returnurl = new moodle_url('/mod/scheduler/view.php', $returnurlparas); + + +/******************************************** Show the booking form *******************************************/ + +if ($action == 'bookingform') { + require_once($CFG->dirroot.'/mod/scheduler/bookingform.php'); + + require_sesskey(); + require_capability('mod/scheduler:appoint', $context); + + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + + $actionurl = new moodle_url($returnurl, array('what' => 'bookingform', 'slotid' => $slotid)); + + $mform = new scheduler_booking_form($slot, $actionurl); + + if ($mform->is_cancelled()) { + redirect($returnurl); + } else if (($formdata = $mform->get_data()) || $appointgroup < 0) { + // Workaround - call scheduler_book_slot also if no group was selected, to show an error message. + scheduler_book_slot($scheduler, $slotid, $USER->id, $appointgroup, $mform, $formdata, $returnurl); + redirect($returnurl); + } else { + $groupinfo = null; + if ($scheduler->is_group_scheduling_enabled() && $appointgroup == 0) { + $groupinfo = get_string('myself', 'scheduler'); + } else if ($appointgroup > 0) { + $groupinfo = $mygroupsforscheduling[$appointgroup]->name; + } + + echo $output->header(); + echo $output->heading(get_string('bookaslot', 'scheduler')); + echo $output->box(format_text($scheduler->intro, $scheduler->introformat)); + + $info = scheduler_appointment_info::make_from_slot($slot, true, true, $groupinfo); + echo $output->render($info); + $mform->display(); + echo $output->footer(); + exit(); + } + +} + +/************************************************ Book a slot ************************************************/ + +if ($action == 'bookslot') { + + require_sesskey(); + require_capability('mod/scheduler:appoint', $context); + + // Reject this request if the user is required to go through a booking form. + if ($scheduler->uses_bookingform()) { + throw new moodle_exception('error'); + } + + // Get the request parameters. + $slotid = required_param('slotid', PARAM_INT); + + scheduler_book_slot($scheduler, $slotid, $USER->id, $appointgroup, null, null, $returnurl); +} + +/******************************************** Show details of booking *******************************************/ + +if ($action == 'viewbooking') { + require_once($CFG->dirroot.'/mod/scheduler/bookingform.php'); + + require_sesskey(); + require_capability('mod/scheduler:appoint', $context); + + $appointmentid = required_param('appointmentid', PARAM_INT); + list($slot, $appointment) = $scheduler->get_slot_appointment($appointmentid); + + if ($appointment->studentid != $USER->id) { + throw new moodle_exception('nopermissions'); + } + + echo $output->header(); + echo $output->heading(get_string('bookingdetails', 'scheduler')); + echo $output->mod_intro($scheduler); + $info = scheduler_appointment_info::make_from_appointment($slot, $appointment); + echo $output->render($info); + + echo $output->continue_button($returnurl); + echo $output->footer(); + exit(); + +} + +/******************************************** Edit a booking *******************************************/ + +if ($action == 'editbooking') { + require_once($CFG->dirroot.'/mod/scheduler/bookingform.php'); + + require_sesskey(); + require_capability('mod/scheduler:appoint', $context); + + if (!$scheduler->uses_studentdata()) { + throw new moodle_exception('error'); + } + + $appointmentid = required_param('appointmentid', PARAM_INT); + list($slot, $appointment) = $scheduler->get_slot_appointment($appointmentid); + + if ($appointment->studentid != $USER->id) { + throw new moodle_exception('nopermissions'); + } + if (!$slot->is_in_bookable_period()) { + throw new moodle_exception('nopermissions'); + } + + $actionurl = new moodle_url($returnurl, array('what' => 'editbooking', 'appointmentid' => $appointmentid)); + + $mform = new scheduler_booking_form($slot, $actionurl, true); + $mform->set_data($mform->prepare_booking_data($appointment)); + + if ($mform->is_cancelled()) { + redirect($returnurl); + } else if ($formdata = $mform->get_data()) { + $mform->save_booking_data($formdata, $appointment); + redirect($returnurl); + } else { + echo $output->header(); + echo $output->heading(get_string('editbooking', 'scheduler')); + echo $output->box(format_text($scheduler->intro, $scheduler->introformat)); + $info = scheduler_appointment_info::make_from_slot($slot); + echo $output->render($info); + $mform->display(); + echo $output->footer(); + exit(); + } + +} + + +/******************************** Cancel a booking (for the current student or a group) ******************************/ + +if ($action == 'cancelbooking') { + + require_sesskey(); + require_capability('mod/scheduler:appoint', $context); + + // Get the request parameters. + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + if (!$slot) { + throw new moodle_exception('error'); + } + + if (!$slot->is_in_bookable_period()) { + throw new moodle_exception('nopermissions'); + } + + $userstocancel = array($USER->id); + if ($appointgroup) { + $userstocancel = array_keys($scheduler->get_available_students($appointgroup)); + } + + foreach ($userstocancel as $userid) { + if ($appointment = $slot->get_student_appointment($userid)) { + $scheduler->delete_appointment($appointment->id); + + // Notify the teacher. + if ($scheduler->allownotifications) { + $student = $DB->get_record('user', array('id' => $USER->id)); + $teacher = $DB->get_record('user', array('id' => $slot->teacherid)); + scheduler_messenger::send_slot_notification($slot, 'bookingnotification', 'cancelled', + $student, $teacher, $teacher, $student, $COURSE); + } + \mod_scheduler\event\booking_removed::create_from_slot($slot)->trigger(); + } + } + redirect($returnurl); + +} diff --git a/mod/scheduler/studentview.php b/mod/scheduler/studentview.php new file mode 100644 index 0000000..fb204e1 --- /dev/null +++ b/mod/scheduler/studentview.php @@ -0,0 +1,256 @@ +<?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/>. + +/** + * Student scheduler screen (where students choose appointments). + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$appointgroup = optional_param('appointgroup', -1, PARAM_INT); + +\mod_scheduler\event\booking_form_viewed::create_from_scheduler($scheduler)->trigger(); + +$PAGE->set_docs_path('mod/scheduler/studentview'); + +$urlparas = array( + 'id' => $scheduler->cmid, + 'sesskey' => sesskey() +); +if ($appointgroup >= 0) { + $urlparas['appointgroup'] = $appointgroup; +} +$actionurl = new moodle_url('/mod/scheduler/view.php', $urlparas); + + +// General permissions check. +require_capability('mod/scheduler:viewslots', $context); +$canbook = has_capability('mod/scheduler:appoint', $context); +$canseefull = has_capability('mod/scheduler:viewfullslots', $context); + +if ($scheduler->is_group_scheduling_enabled()) { + $mygroupsforscheduling = groups_get_all_groups($scheduler->courseid, $USER->id, $scheduler->bookingrouping, 'g.id, g.name'); + if ($appointgroup > 0 && !array_key_exists($appointgroup, $mygroupsforscheduling)) { + throw new moodle_exception('nopermissions'); + } +} + +if ($scheduler->is_group_scheduling_enabled()) { + $canbook = $canbook && ($appointgroup >= 0); +} else { + $appointgroup = 0; +} + + +require_once($CFG->dirroot.'/mod/scheduler/studentview.controller.php'); + +echo $output->header(); + +// Print intro. +echo $output->mod_intro($scheduler); + + +$showowngrades = $scheduler->uses_grades(); +// Print total grade (if any). +if ($showowngrades) { + $totalgrade = $scheduler->get_user_grade($USER->id); + $gradebookinfo = $scheduler->get_gradebook_info($USER->id); + + $showowngrades = !$gradebookinfo->hidden; + + if ($gradebookinfo && !$gradebookinfo->hidden && ($totalgrade || $gradebookinfo->overridden) ) { + $grademsg = ''; + if ($gradebookinfo->overridden) { + $grademsg = html_writer::tag('p', + get_string('overriddennotice', 'grades'), array('class' => 'overriddennotice') + ); + } else { + $grademsg = get_string('yourtotalgrade', 'scheduler', $output->format_grade($scheduler, $totalgrade)); + } + echo html_writer::div($grademsg, 'totalgrade'); + } +} + +// Print group selection menu if given. +if ($scheduler->is_group_scheduling_enabled()) { + $groupchoice = array(); + if ($scheduler->is_individual_scheduling_enabled()) { + $groupchoice[0] = get_string('myself', 'scheduler'); + } + foreach ($mygroupsforscheduling as $group) { + $groupchoice[$group->id] = $group->name; + } + $select = $output->single_select($actionurl, 'appointgroup', $groupchoice, $appointgroup, + array(-1 => 'choosedots'), 'appointgroupform'); + echo html_writer::div(get_string('appointforgroup', 'scheduler', $select), 'dropdownmenu'); +} + +// Get past (attended) slots. + +$pastslots = $scheduler->get_attended_slots_for_student($USER->id); + +if (count($pastslots) > 0) { + $slottable = new scheduler_slot_table($scheduler, $showowngrades || $scheduler->is_group_scheduling_enabled()); + foreach ($pastslots as $pastslot) { + $appointment = $pastslot->get_student_appointment($USER->id); + + if ($pastslot->is_groupslot() && has_capability('mod/scheduler:seeotherstudentsresults', $context)) { + $others = new scheduler_student_list($scheduler, true); + foreach ($pastslot->get_appointments() as $otherapp) { + $othermark = $scheduler->get_gradebook_info($otherapp->studentid); + $gradehidden = !is_null($othermark) && ($othermark->hidden <> 0); + $others->add_student($otherapp, $otherapp->studentid == $USER->id, false, !$gradehidden); + } + } else { + $others = null; + } + $hasdetails = $scheduler->uses_studentdata(); + $slottable->add_slot($pastslot, $appointment, $others, false, false, $hasdetails); + } + + echo $output->heading(get_string('attendedslots', 'scheduler'), 3); + echo $output->render($slottable); +} + + +$upcomingslots = $scheduler->get_upcoming_slots_for_student($USER->id); + +if (count($upcomingslots) > 0) { + $slottable = new scheduler_slot_table($scheduler, $showowngrades || $scheduler->is_group_scheduling_enabled(), $actionurl); + foreach ($upcomingslots as $slot) { + $appointment = $slot->get_student_appointment($USER->id); + + if ($slot->is_groupslot() && has_capability('mod/scheduler:seeotherstudentsbooking', $context)) { + $showothergrades = has_capability('mod/scheduler:seeotherstudentsresults', $context); + $others = new scheduler_student_list($scheduler); + foreach ($slot->get_appointments() as $otherapp) { + $gradehidden = !$scheduler->uses_grades() || + ($scheduler->get_gradebook_info($otherapp->studentid)->hidden <> 0) || + (!$showothergrades && $otherapp->studentid <> $USER->id); + $others->add_student($otherapp, $otherapp->studentid == $USER->id, false, !$gradehidden); + } + } else { + $others = null; + } + + $cancancel = $slot->is_in_bookable_period(); + $canedit = $cancancel && $scheduler->uses_studentdata(); + $canview = !$cancancel && $scheduler->uses_studentdata(); + if ($scheduler->is_group_scheduling_enabled()) { + $cancancel = $cancancel && ($appointgroup >= 0); + } + $slottable->add_slot($slot, $appointment, $others, $cancancel, $canedit, $canview); + } + + echo $output->heading(get_string('upcomingslots', 'scheduler'), 3); + echo $output->render($slottable); +} + +$bookablecnt = $scheduler->count_bookable_appointments($USER->id, false); +$bookableslots = array_values($scheduler->get_slots_available_to_student($USER->id, $canseefull)); + +if (!$canseefull && $bookablecnt == 0) { + echo html_writer::div(get_string('canbooknofurtherappointments', 'scheduler'), 'studentbookingmessage'); + +} else if (count($bookableslots) == 0) { + + // No slots are available at this time. + $noslots = get_string('noslotsavailable', 'scheduler'); + echo html_writer::div($noslots, 'studentbookingmessage'); + +} else { + // The student can book (or see) further appointments, and slots are available. + // Show the booking form. + + $booker = new scheduler_slot_booker($scheduler, $USER->id, $actionurl, $bookablecnt); + + $pagesize = 25; + $total = count($bookableslots); + $start = ($offset >= 0) ? $offset * $pagesize : 0; + $end = $start + $pagesize; + if ($end > $total) { + $end = $total; + } + + for ($idx = $start; $idx < $end; $idx++) { + $slot = $bookableslots[$idx]; + $canbookthisslot = $canbook && ($bookablecnt != 0); + + if (has_capability('mod/scheduler:seeotherstudentsbooking', $context)) { + $others = new scheduler_student_list($scheduler, false); + foreach ($slot->get_appointments() as $otherapp) { + $others->add_student($otherapp, $otherapp->studentid == $USER->id); + } + $others->expandable = true; + $others->expanded = false; + } else { + $others = null; + } + + // Check what to print as group information... + $remaining = $slot->count_remaining_appointments(); + if ($slot->exclusivity == 0) { + $groupinfo = get_string('yes'); + } else if ($slot->exclusivity == 1 && $remaining == 1) { + $groupinfo = get_string('no'); + } else { + if ($remaining > 0) { + $groupinfo = get_string('limited', 'scheduler', $remaining.'/'.$slot->exclusivity); + } else { // Group info should not be visible to students. + $groupinfo = get_string('complete', 'scheduler'); + $canbookthisslot = false; + } + } + + $booker->add_slot($slot, $canbookthisslot, false, $groupinfo, $others); + } + + + $msgkey = $scheduler->has_slots_for_student($USER->id, true, false) ? 'welcomebackstudent' : 'welcomenewstudent'; + $bookingmsg1 = get_string($msgkey, 'scheduler'); + + $a = $bookablecnt; + if ($bookablecnt == 0) { + $msgkey = 'canbooknofurtherappointments'; + } else if ($bookablecnt == 1) { + $msgkey = ($scheduler->schedulermode == 'oneonly') ? 'canbooksingleappointment' : 'canbook1appointment'; + } else if ($bookablecnt > 1) { + $msgkey = 'canbooknappointments'; + } else { + $msgkey = 'canbookunlimitedappointments'; + } + $bookingmsg2 = get_string($msgkey, 'scheduler', $a); + + echo $output->heading(get_string('availableslots', 'scheduler'), 3); + if ($canbook) { + echo html_writer::div($bookingmsg1, 'studentbookingmessage'); + echo html_writer::div($bookingmsg2, 'studentbookingmessage'); + } + if ($total > $pagesize) { + echo $output->paging_bar($total, $offset, $pagesize, $actionurl, 'offset'); + } + echo $output->render($booker); + if ($total > $pagesize) { + echo $output->paging_bar($total, $offset, $pagesize, $actionurl, 'offset'); + } + +} + +echo $output->footer(); \ No newline at end of file diff --git a/mod/scheduler/styles.css b/mod/scheduler/styles.css new file mode 100644 index 0000000..9dba784 --- /dev/null +++ b/mod/scheduler/styles.css @@ -0,0 +1,217 @@ +.path-mod-scheduler .timelabel { + color: #808080; +} + +.path-mod-scheduler .attended { + color: green; +} + +.path-mod-scheduler div.otherstudent.highlight { + font-weight: bold; +} + +.path-mod-scheduler div.slotnotes { + background-color: #e8e9ee; + border: solid 1px #a7abbe; + font-size: 0.9em; + padding: 2px; + margin: 1px; +} + +div .path-mod-scheduler .appointmentnote { + background-color: #e7efe7; + border: solid 1px #a0c5a4; + font-size: 0.9em; + padding: 2px; + margin: 1px; +} + +.path-mod-scheduler #slotbookertable { + margin-left: auto; + margin-right: auto; +} + +.path-mod-scheduler #slotbookertable { + margin-left: auto; + margin-right: auto; +} + +.path-mod-scheduler div.bookercontrols { + text-align: center; +} + +.path-mod-scheduler div.studentlist.expanded { + display: block; +} + +.path-mod-scheduler div.studentlist.collapsed { + display: none; +} + +.path-mod-scheduler div.commandbar { + width: 100%; + margin-left: auto; + margin-right: auto; + background-color: #eee; + padding: 0.5em; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +/* Reduce space usage by single buttons in table cells */ +.path-mod-scheduler table div.singlebutton div { + margin-bottom: 0; +} +.path-mod-scheduler table div.singlebutton input { + margin: 0; +} + +.path-mod-scheduler div.commandbar span.title { + float: left; + clear: right; + width: 8em; + text-align: left; + font-weight: bold; +} + +.path-mod-scheduler div.commandbar .moodle-actionmenu { + display: inline-block !important; /* stylelint-disable-line declaration-no-important */ +} + +.path-mod-scheduler div.commandbar .moodle-actionmenu.show[data-enhanced] .menu.align-tr-br { + left: 0; + right: auto; +} + +.path-mod-scheduler div.commandbar .moodle-actionmenu .menubar { + width: 12em; +} + +.path-mod-scheduler .moodle-actionmenu img.iconsmall { + width: auto; +} +.path-mod-scheduler .moodle-actionmenu .menu-action-text { + display: inline; +} + + +body.path-mod-scheduler input.slotselect { + display: none; +} +body.path-mod-scheduler.jsenabled input.slotselect { + display: inline; +} + +body.path-mod-scheduler.jsenabled input.studentselectsubmit { + display: none; +} + +.path-mod-scheduler img.statictickbox { + padding-right: 5px; +} + +.path-mod-scheduler .maildisplay { + width: 90%; + margin-left: auto; + margin-right: auto; + background: #eee; + text-align: center; +} + +.path-mod-scheduler div.schedulelist.halfsize { + width: 46%; + display: inline-table; + padding: 3px; +} + +.path-mod-scheduler div.schedulelist.fullsize { + width: 96%; + display: block; + padding: 3px; +} + +.path-mod-scheduler div.schedulelist div.singlebutton, +.path-mod-scheduler div.schedulelist div.singlebutton form { + display: inline; +} + +.path-mod-scheduler div.actionmessage { + width: 50%; + margin-left: auto; + margin-right: auto; + margin-bottom: 10px; + border: solid 2px; + padding: 5px; + display: block; + text-align: center; + font-weight: bold; +} + +.path-mod-scheduler div.actionmessage.success { + background-color: #96fca6; + border-color: #14fa34; +} + +.path-mod-scheduler div.actionmessage.error { + background-color: #ffb2b8; + border-color: #f40000; +} + +.path-mod-scheduler div.totalgrade { + padding-bottom: 25px; +} +.path-mod-scheduler dl.totalgrade dl { + width: 100%; +} +.path-mod-scheduler dl.totalgrade dt { + float: left; + clear: left; + width: 30%; +} +.path-mod-scheduler dl.totalgrade dd { + float: left; + width: 60%; +} + +.path-mod-scheduler div.dropdownmenu { + display: inline-block; + padding-right: 1em; +} + +.path-mod-scheduler div.dropdownmenu select { + vertical-align: middle; +} + +/* Format data fields in vertical rather than horizontal list. */ + +.path-mod-scheduler #id_datafieldhdr .form-group, +.path-mod-scheduler #id_datafieldhdr .fitem_fgroup { + float: left; + clear: none; +} + +.path-mod-scheduler #id_datafieldhdr .col-md-3, +.path-mod-scheduler #id_datafieldhdr fieldset.fgroup { + width: 100%; + text-align: left; + margin-left: 0; +} + +.path-mod-scheduler #id_datafieldhdr .col-md-9 { + float: none; + width: 100%; +} + +.path-mod-scheduler #id_datafieldhdr .col-form-label, +.path-mod-scheduler #id_datafieldhdr .fitemtitle { + font-weight: bold; + text-align: left; +} + +.path-mod-scheduler #id_datafieldhdr .form-group .felement .fitem, +.path-mod-scheduler #id_datafieldhdr fieldset.fgroup > span { + clear: left; + float: left; + margin-left: 0.5em; +} diff --git a/mod/scheduler/teacherview.controller.php b/mod/scheduler/teacherview.controller.php new file mode 100644 index 0000000..3bf4a9b --- /dev/null +++ b/mod/scheduler/teacherview.controller.php @@ -0,0 +1,376 @@ +<?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/>. + +/** + * Controller for all teacher-related views. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Add a session (confirmed action) from data entered into the add session form + * @param \mod_scheduler\model\scheduler $scheduler + * @param mixed $formdata + * @param moodle_url $returnurl the URL to redirect to after the action has been performed + */ +function scheduler_action_doaddsession($scheduler, $formdata, moodle_url $returnurl) { + + global $DB, $output; + + $data = (object) $formdata; + + $fordays = 0; + if ($data->rangeend > 0) { + $fordays = ($data->rangeend - $data->rangestart) / DAYSECS; + } + + // Create as many slots of $duration as will fit between $starttime and $endtime and that do not conflict. + $countslots = 0; + $couldnotcreateslots = ''; + $startfrom = $data->rangestart + ($data->starthour * 60 + $data->startminute) * 60; + $endat = $data->rangestart + ($data->endhour * 60 + $data->endminute) * 60; + $slot = new stdClass(); + $slot->schedulerid = $scheduler->id; + $slot->teacherid = $data->teacherid; + $slot->appointmentlocation = $data->appointmentlocation; + $slot->exclusivity = $data->exclusivityenable ? $data->exclusivity : 0; + if ($data->divide) { + $slot->duration = $data->duration; + } else { + $slot->duration = $data->endhour * 60 + $data->endminute - $data->starthour * 60 - $data->startminute; + }; + $slot->notes = ''; + $slot->notesformat = FORMAT_HTML; + $slot->timemodified = time(); + + for ($d = 0; $d <= $fordays; $d ++) { + $starttime = $startfrom + ($d * DAYSECS); + $eventdate = usergetdate($starttime); + $dayofweek = $eventdate['wday']; + if ((($dayofweek == 1) && ($data->monday == 1)) || + (($dayofweek == 2) && ($data->tuesday == 1)) || + (($dayofweek == 3) && ($data->wednesday == 1)) || + (($dayofweek == 4) && ($data->thursday == 1)) || + (($dayofweek == 5) && ($data->friday == 1)) || + (($dayofweek == 6) && ($data->saturday == 1)) || + (($dayofweek == 0) && ($data->sunday == 1))) { + $slot->starttime = make_timestamp($eventdate['year'], $eventdate['mon'], $eventdate['mday'], + $data->starthour, $data->startminute); + $data->timestart = $slot->starttime; + $data->timeend = make_timestamp($eventdate['year'], $eventdate['mon'], $eventdate['mday'], + $data->endhour, $data->endminute); + + // This corrects around midnight bug. + if ($data->timestart > $data->timeend) { + $data->timeend += DAYSECS; + } + if ($data->hideuntilrel == 0) { + $slot->hideuntil = time(); + } else { + $slot->hideuntil = make_timestamp($eventdate['year'], $eventdate['mon'], $eventdate['mday'], 6, 0) - + $data->hideuntilrel; + } + if ($data->emaildaterel == -1) { + $slot->emaildate = 0; + } else { + $slot->emaildate = make_timestamp($eventdate['year'], $eventdate['mon'], $eventdate['mday'], 0, 0) - + $data->emaildaterel; + } + while ($slot->starttime <= $data->timeend - $slot->duration * 60) { + $conflicts = $scheduler->get_conflicts($data->timestart, $data->timestart + $slot->duration * 60, + $data->teacherid, 0, SCHEDULER_ALL); + $resolvable = (boolean) $data->forcewhenoverlap; + foreach ($conflicts as $conflict) { + $resolvable = $resolvable + && $conflict->isself == 1 // Do not delete slots outside the current scheduler. + && $conflict->numstudents == 0; // Do not delete slots with bookings. + } + + if ($conflicts) { + $conflictmsg = ''; + $cl = new scheduler_conflict_list(); + $cl->add_conflicts($conflicts); + if (!$resolvable) { + $conflictmsg .= get_string('conflictingslots', 'scheduler', userdate($data->timestart)); + $conflictmsg .= $output->doc_link('mod/scheduler/conflict', '', true); + $conflictmsg .= $output->render($cl); + } else { // We force, so delete all conflicting before inserting. + foreach ($conflicts as $conflict) { + $cslot = $scheduler->get_slot($conflict->id); + \mod_scheduler\event\slot_deleted::create_from_slot($cslot, 'addsession-conflict')->trigger(); + $cslot->delete(); + } + $conflictmsg .= get_string('deletedconflictingslots', 'scheduler', userdate($data->timestart)); + $conflictmsg .= $output->doc_link('mod/scheduler/conflict', '', true); + $conflictmsg .= $output->render($cl); + } + \core\notification::warning($conflictmsg); + } + if (!$conflicts || $resolvable) { + $slotid = $DB->insert_record('scheduler_slots', $slot, true, true); + $slotobj = $scheduler->get_slot($slotid); + \mod_scheduler\event\slot_added::create_from_slot($slotobj)->trigger(); + $countslots++; + } + $slot->starttime += ($slot->duration + $data->break) * 60; + $data->timestart += ($slot->duration + $data->break) * 60; + } + } + } + + $messagetype = ($countslots > 0) ? \core\output\notification::NOTIFY_SUCCESS : \core\output\notification::NOTIFY_INFO; + $message = get_string('slotsadded', 'scheduler', $countslots); + \core\notification::add($message, $messagetype); + + redirect($returnurl); +} + +/** + * Send a message (confirmed action) after filling the message form + * + * @param \mod_scheduler\model\scheduler $scheduler + * @param mixed $formdata + * @param moodle_url $returnurl the URL to redirect to after the action has been performed + */ +function scheduler_action_dosendmessage($scheduler, $formdata, $returnurl) { + + global $DB, $USER; + + $data = (object) $formdata; + + $recipients = $data->recipient; + if ($data->copytomyself) { + $recipients[$USER->id] = 1; + } + $rawmessage = $data->body['text']; + $format = $data->body['format']; + $textmessage = format_text_email($rawmessage, $format); + $htmlmessage = null; + if ($format == FORMAT_HTML) { + $htmlmessage = $rawmessage; + } + + $cnt = 0; + foreach ($recipients as $recipientid => $value) { + if ($value) { + $message = new \core\message\message(); + $message->component = 'mod_scheduler'; + $message->name = 'invitation'; + $message->userfrom = $USER; + $message->userto = $recipientid; + $message->subject = $data->subject; + $message->fullmessage = $textmessage; + $message->fullmessageformat = $format; + if ($htmlmessage) { + $message->fullmessagehtml = $htmlmessage; + } + $message->notification = '1'; + + message_send($message); + $cnt++; + } + } + + $message = get_string('messagesent', 'scheduler', $cnt); + $messagetype = \core\output\notification::NOTIFY_SUCCESS; + redirect($returnurl, $message, 0, $messagetype); +} + +/** + * Delete slots (after UI button has been pushed) + * + * @param \mod_scheduler\model\slot[] $slots list of slots to be deleted + * @param string $action description of the action + * @param moodle_url $returnurl the URL to redirect to after the action has been performed + */ +function scheduler_action_delete_slots(array $slots, $action, moodle_url $returnurl) { + + $cnt = 0; + foreach ($slots as $slot) { + \mod_scheduler\event\slot_deleted::create_from_slot($slot, $action)->trigger(); + $slot->delete(); + $cnt++; + } + + if ($cnt == 1) { + $message = get_string('oneslotdeleted', 'scheduler'); + } else { + $message = get_string('slotsdeleted', 'scheduler', $cnt); + } + $messagetype = ($cnt > 0) ? \core\output\notification::NOTIFY_SUCCESS : \core\output\notification::NOTIFY_INFO; + \core\notification::add($message, $messagetype); + redirect($returnurl); +} + +// Require valid session key for all actions. +require_sesskey(); + +// We first have to check whether some action needs to be performed. +// Any of the following actions must issue a redirect when finished. +switch ($action) { + /************************************ Deleting a slot ***********************************************/ + case 'deleteslot': + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $permissions->ensure($permissions->can_edit_slot($slot)); + scheduler_action_delete_slots(array($slot), $action, $viewurl); + break; + /************************************ Deleting multiple slots ***********************************************/ + case 'deleteslots': + $slotids = required_param('items', PARAM_SEQUENCE); + $slotids = explode(",", $slotids); + $slots = array(); + foreach ($slotids as $slotid) { + if ($slotid > 0) { + $slot = $scheduler->get_slot($slotid); + $permissions->ensure($permissions->can_edit_slot($slot)); + $slots[] = $slot; + } + } + scheduler_action_delete_slots($slots, $action, $viewurl); + break; + /************************************ Students were seen ***************************************************/ + case 'saveseen': + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $seen = optional_param_array('seen', array(), PARAM_INT); + + if (is_array($seen)) { + foreach ($slot->get_appointments() as $app) { + $permissions->ensure($permissions->can_edit_attended($app)); + $app->attended = (in_array($app->id, $seen)) ? 1 : 0; + $app->timemodified = time(); + } + } + $slot->save(); + redirect($viewurl); + break; + /************************************ Revoking all appointments to a slot ***************************************/ + case 'revokeall': + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $permissions->ensure($permissions->can_edit_slot($slot)); + + $oldstudents = array(); + foreach ($slot->get_appointments() as $app) { + $oldstudents[] = $app->studentid; + $slot->remove_appointment($app); + } + // Notify the student. + if ($scheduler->allownotifications) { + foreach ($oldstudents as $oldstudent) { + include_once($CFG->dirroot.'/mod/scheduler/mailtemplatelib.php'); + + $student = $DB->get_record('user', array('id' => $oldstudent)); + $teacher = $DB->get_record('user', array('id' => $slot->teacherid)); + + scheduler_messenger::send_slot_notification($slot, 'bookingnotification', 'teachercancelled', + $teacher, $student, $teacher, $student, $COURSE); + } + } + + $slot->save(); + redirect($viewurl); + break; + + /************************************ Toggling to unlimited group ***************************************/ + case 'allowgroup': + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $permissions->ensure($permissions->can_edit_slot($slot)); + + $slot->exclusivity = 0; + $slot->save(); + redirect($viewurl); + break; + + /************************************ Toggling to single student ******************************************/ + case 'forbidgroup': + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $permissions->ensure($permissions->can_edit_slot($slot)); + + $slot->exclusivity = 1; + $slot->save(); + redirect($viewurl); + break; + + /************************************ Deleting all slots ***************************************************/ + case 'deleteall': + $permissions->ensure($permissions->can_edit_all_slots()); + $slots = $scheduler->get_all_slots(); + scheduler_action_delete_slots($slots, $action, $viewurl); + break; + /************************************ Deleting unused slots *************************************************/ + case 'deleteunused': + $permissions->ensure($permissions->can_edit_own_slots()); + $slots = $scheduler->get_slots_without_appointment($USER->id); + scheduler_action_delete_slots($slots, $action, $viewurl); + break; + /************************************ Deleting unused slots (all teachers) ************************************/ + case 'deleteallunused': + $permissions->ensure($permissions->can_edit_all_slots()); + $slots = $scheduler->get_slots_without_appointment(); + scheduler_action_delete_slots($slots, $action, $viewurl); + break; + /************************************ Deleting current teacher's slots ***************************************/ + case 'deleteonlymine': + $permissions->ensure($permissions->can_edit_own_slots()); + $slots = $scheduler->get_slots_for_teacher($USER->id); + scheduler_action_delete_slots($slots, $action, $viewurl); + break; + /************************************ Mark as seen now *******************************************************/ + case 'markasseennow': + $permissions->ensure($permissions->can_edit_own_slots()); + + $slot = new stdClass(); + $slot->schedulerid = $scheduler->id; + $slot->teacherid = $USER->id; + $slot->starttime = time(); + $slot->duration = $scheduler->defaultslotduration; + $slot->exclusivity = 1; + $slot->notes = ''; + $slot->notesformat = FORMAT_HTML; + $slot->hideuntil = time(); + $slot->appointmentlocation = ''; + $slot->emaildate = 0; + $slot->timemodified = time(); + $slotid = $DB->insert_record('scheduler_slots', $slot); + + $appointment = new stdClass(); + $appointment->slotid = $slotid; + $appointment->studentid = required_param('studentid', PARAM_INT); + $appointment->attended = 1; + $appointment->appointmentnote = ''; + $appointment->appointmentnoteformat = FORMAT_HTML; + $appointment->teachernote = ''; + $appointment->teachernoteformat = FORMAT_HTML; + $appointment->timecreated = time(); + $appointment->timemodified = time(); + $DB->insert_record('scheduler_appointment', $appointment); + + $slot = $scheduler->get_slot($slotid); + \mod_scheduler\event\slot_added::create_from_slot($slot)->trigger(); + + redirect($viewurl); + break; +} + +/*************************************************************************************************************/ diff --git a/mod/scheduler/teacherview.php b/mod/scheduler/teacherview.php new file mode 100644 index 0000000..61de6b9 --- /dev/null +++ b/mod/scheduler/teacherview.php @@ -0,0 +1,680 @@ +<?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/>. + +/** + * Contains various sub-screens that a teacher can see. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; + +/** + * Print a selection box of existing slots to be scheduler in + * + * @param scheduler $scheduler + * @param int $studentid student to schedule + * @param int $groupid group to schedule + */ +function scheduler_print_schedulebox(scheduler $scheduler, $studentid, $groupid = 0) { + global $output; + + $availableslots = $scheduler->get_slots_available_to_student($studentid); + + $startdatemem = ''; + $starttimemem = ''; + $availableslotsmenu = array(); + foreach ($availableslots as $slot) { + $startdatecnv = $output->userdate($slot->starttime); + $starttimecnv = $output->usertime($slot->starttime); + + $startdatestr = ($startdatemem != '' and $startdatemem == $startdatecnv) ? "-----------------" : $startdatecnv; + $starttimestr = ($starttimemem != '' and $starttimemem == $starttimecnv) ? '' : $starttimecnv; + + $startdatemem = $startdatecnv; + $starttimemem = $starttimecnv; + + $url = new moodle_url('/mod/scheduler/view.php', + array('id' => $scheduler->cmid, 'slotid' => $slot->id, 'sesskey' => sesskey())); + if ($groupid) { + $url->param('what', 'schedulegroup'); + $url->param('subaction', 'dochooseslot'); + $url->param('groupid', $groupid); + } else { + $url->param('what', 'schedule'); + $url->param('subaction', 'dochooseslot'); + $url->param('studentid', $studentid); + } + $availableslotsmenu[$url->out()] = "$startdatestr $starttimestr"; + } + + $chooser = new url_select($availableslotsmenu); + + if ($availableslots) { + echo $output->box_start(); + echo $output->heading(get_string('chooseexisting', 'scheduler'), 3); + echo $output->render($chooser); + echo $output->box_end(); + } +} + +// Load group restrictions. +$groupmode = groups_get_activity_groupmode($cm); +$currentgroup = false; +if ($groupmode) { + $currentgroup = groups_get_activity_group($cm, true); +} + +// All group arrays in the following are in the format used by groups_get_all_groups. +// The special value '' (empty string) is used to signal "all groups" (no restrictions). + +// Find groups which the current teacher can see ($groupsicansee, $groupsicurrentlysee). +// $groupsicansee contains all groups that a teacher potentially has access to. +// $groupsicurrentlysee may be restricted by the user to one group, using the drop-down box. +$userfilter = $USER->id; +if (has_capability('moodle/site:accessallgroups', $context)) { + $userfilter = 0; +} +$groupsicansee = ''; +$groupsicurrentlysee = ''; +if ($groupmode) { + if ($userfilter) { + $groupsicansee = groups_get_all_groups($COURSE->id, $userfilter, $cm->groupingid); + } + $groupsicurrentlysee = $groupsicansee; + if ($currentgroup) { + if ($userfilter && !groups_is_member($currentgroup, $userfilter)) { + $groupsicurrentlysee = array(); + } else { + $cgobj = groups_get_group($currentgroup); + $groupsicurrentlysee = array($currentgroup => $cgobj); + } + } +} + +// Find groups which the current teacher can schedule as a group ($groupsicanschedule). +$groupsicanschedule = array(); +if ($scheduler->is_group_scheduling_enabled()) { + $groupsicanschedule = groups_get_all_groups($COURSE->id, $userfilter, $scheduler->bookingrouping); +} + +// Find groups which can book an appointment with the current teacher ($groupsthatcanseeme). + +$groupsthatcanseeme = ''; +if ($groupmode) { + $groupsthatcanseeme = groups_get_all_groups($COURSE->id, $USER->id, $cm->groupingid); +} + + +$taburl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid, 'what' => 'view', 'subpage' => $subpage)); + +$baseurl = new moodle_url('/mod/scheduler/view.php', array( + 'id' => $scheduler->cmid, + 'subpage' => $subpage, + 'offset' => $offset +)); + +// The URL that is used for jumping back to the view (e.g., after an action is performed). +$viewurl = new moodle_url($baseurl, array('what' => 'view')); + +$PAGE->set_url($viewurl); + +if ($action != 'view') { + require_once($CFG->dirroot.'/mod/scheduler/slotforms.php'); + require_once($CFG->dirroot.'/mod/scheduler/teacherview.controller.php'); +} + +/************************************ View : New single slot form ****************************************/ +if ($action == 'addslot') { + $permissions->ensure($permissions->can_edit_own_slots()); + + $actionurl = new moodle_url($baseurl, array('what' => 'addslot')); + + if (!$scheduler->has_available_teachers()) { + print_error('needteachers', 'scheduler', viewurl); + } + + $mform = new scheduler_editslot_form($actionurl, $scheduler, $cm, $groupsicansee); + + if ($mform->is_cancelled()) { + redirect($viewurl); + } else if ($formdata = $mform->get_data()) { + $slot = $mform->save_slot(0, $formdata); + \mod_scheduler\event\slot_added::create_from_slot($slot)->trigger(); + redirect($viewurl, + get_string('oneslotadded', 'scheduler'), + 0, + \core\output\notification::NOTIFY_SUCCESS); + } else { + echo $output->header(); + echo $output->heading(get_string('addsingleslot', 'scheduler')); + $mform->display(); + echo $output->footer($course); + die; + } +} +/************************************ View : Update single slot form ****************************************/ +if ($action == 'updateslot') { + + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $permissions->ensure($permissions->can_edit_slot($slot)); + + + if ($slot->starttime % 300 !== 0 || $slot->duration % 5 !== 0) { + $timeoptions = array('step' => 1, 'optional' => false); + } else { + $timeoptions = array('step' => 5, 'optional' => false); + } + + $actionurl = new moodle_url($baseurl, array('what' => 'updateslot', 'slotid' => $slotid)); + + $mform = new scheduler_editslot_form($actionurl, $scheduler, $cm, $groupsicansee, array( + 'slotid' => $slotid, + 'timeoptions' => $timeoptions) + ); + $data = $mform->prepare_formdata($slot); + $mform->set_data($data); + + if ($mform->is_cancelled()) { + redirect($viewurl); + } else if ($formdata = $mform->get_data()) { + $mform->save_slot($slotid, $formdata); + redirect($viewurl, + get_string('slotupdated', 'scheduler'), + 0, + \core\output\notification::NOTIFY_SUCCESS); + } else { + echo $output->header(); + echo $output->heading(get_string('updatesingleslot', 'scheduler')); + $mform->display(); + echo $output->footer($course); + die; + } + +} +/************************************ Add session multiple slots form ****************************************/ +if ($action == 'addsession') { + + $permissions->ensure($permissions->can_edit_own_slots()); + + $actionurl = new moodle_url($baseurl, array('what' => 'addsession')); + + if (!$scheduler->has_available_teachers()) { + print_error('needteachers', 'scheduler', $viewurl); + } + + $mform = new scheduler_addsession_form($actionurl, $scheduler, $cm, $groupsicansee); + + if ($mform->is_cancelled()) { + redirect($viewurl); + } else if ($formdata = $mform->get_data()) { + scheduler_action_doaddsession($scheduler, $formdata, $viewurl); + } else { + echo $output->header(); + echo $output->heading(get_string('addsession', 'scheduler')); + $mform->display(); + echo $output->footer(); + die; + } +} + +/************************************ Schedule a student form ***********************************************/ +if ($action == 'schedule') { + $permissions->ensure($permissions->can_edit_own_slots()); + + echo $output->header(); + + if ($subaction == 'dochooseslot') { + $slotid = required_param('slotid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + $studentid = required_param('studentid', PARAM_INT); + + $actionurl = new moodle_url($baseurl, array('what' => 'updateslot', 'slotid' => $slotid)); + + $repeats = $slot->get_appointment_count() + 1; + $mform = new scheduler_editslot_form($actionurl, $scheduler, $cm, $groupsicansee, + array('slotid' => $slotid, 'repeats' => $repeats)); + $data = $mform->prepare_formdata($slot); + $data->studentid[] = $studentid; + $mform->set_data($data); + + echo $output->heading(get_string('updatesingleslot', 'scheduler'), 2); + $mform->display(); + + } else if (empty($subaction)) { + $studentid = required_param('studentid', PARAM_INT); + $student = $DB->get_record('user', array('id' => $studentid), '*', MUST_EXIST); + + $actionurl = new moodle_url($baseurl, array('what' => 'addslot')); + + $mform = new scheduler_editslot_form($actionurl, $scheduler, $cm, $groupsicansee); + + $data = array(); + $data['studentid'][0] = $studentid; + $mform->set_data($data); + + echo $output->heading(get_string('scheduleappointment', 'scheduler', fullname($student))); + + scheduler_print_schedulebox($scheduler, $studentid); + + echo $output->box_start(); + echo $output->heading(get_string('scheduleinnew', 'scheduler'), 3); + $mform->display(); + echo $output->box_end(); + } + + echo $output->footer(); + die(); +} +/************************************ Schedule a whole group in form ***********************************************/ +if ($action == 'schedulegroup') { + + $permissions->ensure($permissions->can_edit_own_slots()); + + $groupid = required_param('groupid', PARAM_INT); + $group = $DB->get_record('groups', array('id' => $groupid), '*', MUST_EXIST); + $members = groups_get_members($groupid); + + echo $output->header(); + + if ($subaction == 'dochooseslot') { + + $slotid = required_param('slotid', PARAM_INT); + $groupid = required_param('groupid', PARAM_INT); + $slot = $scheduler->get_slot($slotid); + + $actionurl = new moodle_url($baseurl, array('what' => 'updateslot', 'slotid' => $slotid)); + + $repeats = $slot->get_appointment_count() + count($members); + $mform = new scheduler_editslot_form($actionurl, $scheduler, $cm, $groupsicansee, + array('slotid' => $slotid, 'repeats' => $repeats)); + $data = $mform->prepare_formdata($slot); + foreach ($members as $member) { + $data->studentid[] = $member->id; + } + $mform->set_data($data); + + echo $output->heading(get_string('updatesingleslot', 'scheduler'), 3); + $mform->display(); + + } else if (empty($subaction)) { + + $actionurl = new moodle_url($baseurl, array('what' => 'addslot')); + + $data = array(); + $i = 0; + foreach ($members as $member) { + $data['studentid'][$i] = $member->id; + $i++; + } + $data['exclusivity'] = $i; + + $mform = new scheduler_editslot_form($actionurl, $scheduler, $cm, $groupsicansee, array('repeats' => $i)); + $mform->set_data($data); + + echo $output->heading(get_string('scheduleappointment', 'scheduler', $group->name)); + + scheduler_print_schedulebox($scheduler, 0, $groupid); + + echo $output->box_start(); + echo $output->heading(get_string('scheduleinnew', 'scheduler'), 3); + $mform->display(); + echo $output->box_end(); + + } + echo $output->footer(); + die(); +} + +/************************************ Send message to students ****************************************/ +if ($action == 'sendmessage') { + $permissions->ensure($permissions->can_edit_own_slots()); + + require_once($CFG->dirroot.'/mod/scheduler/message_form.php'); + + $template = optional_param('template', 'none', PARAM_ALPHA); + $recipientids = required_param('recipients', PARAM_SEQUENCE); + + $actionurl = new moodle_url('/mod/scheduler/view.php', + array('what' => 'sendmessage', 'id' => $cm->id, 'subpage' => $subpage, + 'template' => $template, 'recipients' => $recipientids)); + + $templatedata = array(); + if ($template != 'none') { + $vars = scheduler_messenger::get_scheduler_variables($scheduler, null, $USER, null, $COURSE, null); + $templatedata['subject'] = scheduler_messenger::compile_mail_template($template, 'subject', $vars); + $templatedata['body'] = scheduler_messenger::compile_mail_template($template, 'html', $vars); + } + $templatedata['recipients'] = $DB->get_records_list('user', 'id', explode(',', $recipientids), 'lastname,firstname'); + + $mform = new scheduler_message_form($actionurl, $scheduler, $templatedata); + + if ($mform->is_cancelled()) { + redirect($viewurl); + } else if ($formdata = $mform->get_data()) { + scheduler_action_dosendmessage($scheduler, $formdata, $viewurl); + } else { + echo $output->header(); + echo $output->heading(get_string('sendmessage', 'scheduler')); + $mform->display(); + echo $output->footer(); + die; + } +} + + +/****************** Standard view ***********************************************/ + + +// Trigger view event. +\mod_scheduler\event\appointment_list_viewed::create_from_scheduler($scheduler)->trigger(); + + +// Print top tabs. + +$actionurl = new moodle_url($viewurl, array('sesskey' => sesskey())); + +$inactive = array(); +if ($DB->count_records('scheduler_slots', array('schedulerid' => $scheduler->id)) <= + $DB->count_records('scheduler_slots', array('schedulerid' => $scheduler->id, 'teacherid' => $USER->id)) ) { + // We are alone in this scheduler. + $inactive[] = 'allappointments'; + if ($subpage = 'allappointments') { + $subpage = 'myappointments'; + } +} + +echo $output->header(); + +echo $output->teacherview_tabs($scheduler, $permissions, $taburl, $subpage, $inactive); +if ($groupmode) { + if ($subpage == 'allappointments') { + groups_print_activity_menu($cm, $taburl); + } else { + $a = new stdClass(); + $a->groupmode = get_string($groupmode == VISIBLEGROUPS ? 'groupsvisible' : 'groupsseparate'); + $groupnames = array(); + foreach ($groupsthatcanseeme as $id => $group) { + $groupnames[] = $group->name; + } + $a->grouplist = implode(', ', $groupnames); + $messagekey = $groupsthatcanseeme ? 'groupmodeyourgroups' : 'groupmodeyourgroupsempty'; + $message = get_string($messagekey, 'scheduler', $a); + echo html_writer::div($message, 'groupmodeyourgroups'); + } +} + +// Print intro. +echo $output->mod_intro($scheduler); + + +if ($subpage == 'allappointments') { + $teacherid = 0; + $slotgroup = $currentgroup; +} else { + $teacherid = $USER->id; + $slotgroup = 0; + $subpage = 'myappointments'; +} +$sqlcount = $scheduler->count_slots_for_teacher($teacherid, $slotgroup); + +$pagesize = 25; +if ($offset == -1) { + if ($sqlcount > $pagesize) { + $offsetcount = $scheduler->count_slots_for_teacher($teacherid, $slotgroup, true); + $offset = floor($offsetcount / $pagesize); + } else { + $offset = 0; + } +} +if ($offset * $pagesize >= $sqlcount && $sqlcount > 0) { + $offset = floor(($sqlcount - 1) / $pagesize); +} + +$slots = $scheduler->get_slots_for_teacher($teacherid, $slotgroup, $offset * $pagesize, $pagesize); + +echo $output->heading(get_string('slots', 'scheduler')); + +// Print instructions and button for creating slots. +$key = ($slots) ? 'addslot' : 'welcomenewteacher'; +echo html_writer::div(get_string($key, 'scheduler')); + + +$commandbar = new scheduler_command_bar(); +$commandbar->title = get_string('actions', 'scheduler'); + +$addbuttons = array(); +$addbuttons[] = $commandbar->action_link(new moodle_url($actionurl, array('what' => 'addsession')), 'addsession', 't/add'); +$addbuttons[] = $commandbar->action_link(new moodle_url($actionurl, array('what' => 'addslot')), 'addsingleslot', 't/add'); +$commandbar->add_group(get_string('addcommands', 'scheduler'), $addbuttons); + +// If slots already exist, also show delete buttons. +if ($slots) { + $delbuttons = array(); + + $delselectedurl = new moodle_url($actionurl, array('what' => 'deleteslots')); + $PAGE->requires->yui_module('moodle-mod_scheduler-delselected', 'M.mod_scheduler.delselected.init', + array($delselectedurl->out(false)) ); + $delselected = $commandbar->action_link($delselectedurl, 'deleteselection', 't/delete', + 'confirmdelete-selected', 'delselected'); + $delselected->formid = 'delselected'; + $delbuttons[] = $delselected; + + if ($permissions->can_edit_all_slots() && $subpage == 'allappointments') { + $delbuttons[] = $commandbar->action_link( + new moodle_url($actionurl, array('what' => 'deleteall')), + 'deleteallslots', 't/delete', 'confirmdelete-all'); + $delbuttons[] = $commandbar->action_link( + new moodle_url($actionurl, array('what' => 'deleteallunused')), + 'deleteallunusedslots', 't/delete', 'confirmdelete-unused'); + } + $delbuttons[] = $commandbar->action_link( + new moodle_url($actionurl, array('what' => 'deleteunused')), + 'deleteunusedslots', 't/delete', 'confirmdelete-myunused'); + $delbuttons[] = $commandbar->action_link( + new moodle_url($actionurl, array('what' => 'deleteonlymine')), + 'deletemyslots', 't/delete', 'confirmdelete-mine'); + + $commandbar->add_group(get_string('deletecommands', 'scheduler'), $delbuttons); +} + +echo $output->render($commandbar); + + +// Some slots already exist - prepare the table of slots. +if ($slots) { + + $slotman = new scheduler_slot_manager($scheduler, $actionurl); + $slotman->showteacher = ($subpage == 'allappointments'); + + foreach ($slots as $slot) { + + $editable = $permissions->can_edit_slot($slot); + + $studlist = new scheduler_student_list($slotman->scheduler); + $studlist->expandable = false; + $studlist->expanded = true; + $studlist->editable = $editable; + $studlist->linkappointment = true; + $studlist->checkboxname = 'seen[]'; + $studlist->buttontext = get_string('saveseen', 'scheduler'); + $studlist->actionurl = new moodle_url($actionurl, array('what' => 'saveseen', 'slotid' => $slot->id)); + foreach ($slot->get_appointments() as $app) { + $studlist->add_student($app, false, $app->is_attended(), true, $scheduler->uses_studentdata(), + $permissions->can_edit_attended($app)); + } + + $slotman->add_slot($slot, $studlist, $editable); + } + + echo $output->render($slotman); + + if ($sqlcount > $pagesize) { + echo $output->paging_bar($sqlcount, $offset, $pagesize, $actionurl, 'offset'); + } + + // Instruction for teacher to click Seen box after appointment. + echo html_writer::div(get_string('markseen', 'scheduler')); + +} + +$groupfilter = ($subpage == 'myappointments') ? $groupsthatcanseeme : $groupsicurrentlysee; +$maxlistsize = get_config('mod_scheduler', 'maxstudentlistsize'); +$students = array(); +$reminderstudents = array(); +if ($groupfilter === '') { + $students = $scheduler->get_students_for_scheduling('', $maxlistsize); + if ($scheduler->allows_unlimited_bookings()) { + $reminderstudents = $scheduler->get_students_for_scheduling('', $maxlistsize, true); + } else { + $reminderstudents = $students; + } +} else if (count($groupfilter) > 0) { + $students = $scheduler->get_students_for_scheduling(array_keys($groupfilter), $maxlistsize); + if ($scheduler->allows_unlimited_bookings()) { + $reminderstudents = $scheduler->get_students_for_scheduling(array_keys($groupfilter), $maxlistsize, true); + } else { + $reminderstudents = $students; + } +} + +if ($students === 0) { + $nostudentstr = get_string('noexistingstudents', 'scheduler'); + if ($COURSE->id == SITEID) { + $nostudentstr .= '<br/>'.get_string('howtoaddstudents', 'scheduler'); + } + echo $output->notification($nostudentstr, 'notifyproblem'); +} else if (is_integer($students)) { + // There are too many students who still have to make appointments, don't display a list. + $toomanystr = get_string('missingstudentsmany', 'scheduler', $students); + echo $output->notification($toomanystr, 'notifymessage'); + +} else if (count($students) > 0) { + + if (count($reminderstudents) > 0) { + $studids = implode(',', array_keys($reminderstudents)); + + $messageurl = new moodle_url($actionurl, array('what' => 'sendmessage', 'recipients' => $studids)); + $invitationurl = new moodle_url($messageurl, array('template' => 'invite')); + $reminderurl = new moodle_url($messageurl, array('template' => 'invitereminder')); + + $maildisplay = ''; + $maildisplay .= html_writer::link($invitationurl, get_string('sendinvitation', 'scheduler')); + $maildisplay .= ' — '; + $maildisplay .= html_writer::link($reminderurl, get_string('sendreminder', 'scheduler')); + + echo $output->box_start('maildisplay'); + // Print number of students who still have to make an appointment. + echo $output->heading(get_string('missingstudents', 'scheduler', count($reminderstudents)), 3); + // Print e-mail addresses and mailto links. + echo $maildisplay; + echo $output->box_end(); + } + + $userfields = scheduler_get_user_fields(null, $context); + $fieldtitles = array(); + foreach ($userfields as $f) { + $fieldtitles[] = $f->title; + } + $studtable = new scheduler_scheduling_list($scheduler, $fieldtitles); + $studtable->id = 'studentstoschedule'; + + foreach ($students as $student) { + $picture = $output->user_picture($student); + $name = $output->user_profile_link($scheduler, $student); + $actions = array(); + $actions[] = new action_menu_link_secondary( + new moodle_url($actionurl, array('what' => 'schedule', 'studentid' => $student->id)), + new pix_icon('e/insert_date', '', 'moodle'), + get_string('scheduleinslot', 'scheduler') ); + $actions[] = new action_menu_link_secondary( + new moodle_url($actionurl, array('what' => 'markasseennow', 'studentid' => $student->id)), + new pix_icon('t/approve', '', 'moodle'), + get_string('markasseennow', 'scheduler') ); + + $userfields = scheduler_get_user_fields($student, $context); + $fieldvals = array(); + foreach ($userfields as $f) { + $fieldvals[] = $f->value; + } + $studtable->add_line($picture, $name, $fieldvals, $actions); + } + + $divclass = 'schedulelist '.($scheduler->is_group_scheduling_enabled() ? 'halfsize' : 'fullsize'); + echo html_writer::start_div($divclass); + echo $output->heading(get_string('schedulestudents', 'scheduler'), 3); + + // Print table of students who still have to make appointments. + echo $output->render($studtable); + echo html_writer::end_div(); + + if ($scheduler->is_group_scheduling_enabled()) { + + // Print list of groups that can be scheduled. + + echo html_writer::start_div('schedulelist halfsize'); + echo $output->heading(get_string('schedulegroups', 'scheduler'), 3); + + if (empty($groupsicanschedule)) { + echo $output->notification(get_string('nogroups', 'scheduler')); + } else { + $grouptable = new scheduler_scheduling_list($scheduler, array()); + $grouptable->id = 'groupstoschedule'; + + $groupcnt = 0; + foreach ($groupsicanschedule as $group) { + $members = groups_get_members($group->id, user_picture::fields('u'), 'u.lastname, u.firstname'); + if (empty($members)) { + continue; + } + if (!$scheduler->has_slots_booked_for_group($group->id, false, $scheduler->schedulermode == 'onetime')) { + + $picture = print_group_picture($group, $course->id, false, true, true); + $name = $group->name; + $groupmembers = array(); + foreach ($members as $member) { + $groupmembers[] = fullname($member); + } + $name .= ' ['. implode(', ', $groupmembers) . ']'; + $actions = array(); + $actions[] = new action_menu_link_secondary( + new moodle_url($actionurl, array('what' => 'schedulegroup', 'groupid' => $group->id)), + new pix_icon('e/insert_date', '', 'moodle'), + get_string('scheduleinslot', 'scheduler') ); + + $grouptable->add_line($picture, $name, array(), $actions); + $groupcnt++; + } + } + // Print table of groups that still need to make appointments. + if ($groupcnt > 0) { + echo $output->render($grouptable); + } else { + echo $output->notification(get_string('nogroups', 'scheduler')); + } + } + echo html_writer::end_div(); + } + +} else { + echo $output->notification(get_string('noexistingstudents', 'scheduler')); +} +echo $output->footer(); \ No newline at end of file diff --git a/mod/scheduler/tests/behat/add_slots.feature b/mod/scheduler/tests/behat/add_slots.feature new file mode 100644 index 0000000..00630a0 --- /dev/null +++ b/mod/scheduler/tests/behat/add_slots.feature @@ -0,0 +1,110 @@ +@mod @mod_scheduler +Feature: Teacher can add slots to a scheduler activity + In order to allow students to book a slot + As a teacher + I need to add slots to the scheduler + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | student4 | Student | 4 | student4@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | scheduler | Test scheduler | n | C1 | scheduler1 | + + @javascript + Scenario: Teacher adds a single, empty slot to the scheduler + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I click on "Add slots" "link" + And I follow "Add single slot" + And I set the following fields to these values: + | starttime[day] | 1 | + | starttime[month] | April | + | starttime[year] | 2050 | + | duration | 30 | + And I click on "Save changes" "button" + Then I should see "1 slot added" + And I should see "Friday, 1 April 2050" + + @javascript + Scenario: Teacher enters invalid values when adding a slot + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I click on "Add slots" "link" + And I follow "Add single slot" + And I set the following fields to these values: + | starttime[day] | 1 | + | starttime[month] | April | + | starttime[year] | 2010 | + And I click on "Save changes" "button" + Then I should see "in the past" + And I set the following fields to these values: + | starttime[year] | 2050 | + | duration | -1 | + When I click on "Save changes" "button" + Then I should see "Slot duration must be between" + And I set the following fields to these values: + | duration | 10 | + | exclusivity | -10 | + And I click on "Save changes" "button" + And I should see "needs to be 1 or more" + And I set the following fields to these values: + | exclusivity | 5 | + And I click on "Save changes" "button" + And I should see "1 slot added" + + @javascript + Scenario: Teacher enters a slot and schedules 3 students + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I click on "Add slots" "link" + And I follow "Add single slot" + And I set the following fields to these values: + | starttime[day] | 1 | + | starttime[month] | April | + | starttime[year] | 2050 | + | exclusivity | 2 | + And I click on "Student 1" item in autocomplete list number 1 + And I click on "Add another student" "button" + And I click on "Student 2" item in autocomplete list number 2 + And I click on "Add another student" "button" + And I click on "Student 3" item in autocomplete list number 3 + And I click on "Save changes" "button" + Then I should see "more than allowed" + And I set the following fields to these values: + | exclusivity | 3 | + And I click on "Save changes" "button" + And I should see "1 slot added" + And I should see "Student 1" + And I should see "Student 2" + And I should see "Student 3" + + @javascript + Scenario: Teacher creates 10 slots at once + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: + | Location | Here | + Then I should see "10 slots have been added" + And I should see "1:00 AM" + And I should see "2:00 AM" + And I should see "10:00 AM" + And I should not see "11:00 AM" diff --git a/mod/scheduler/tests/behat/behat_mod_scheduler.php b/mod/scheduler/tests/behat/behat_mod_scheduler.php new file mode 100644 index 0000000..0825492 --- /dev/null +++ b/mod/scheduler/tests/behat/behat_mod_scheduler.php @@ -0,0 +1,179 @@ +<?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/>. + +/** + * Steps definitions related with the scheduler activity. + * + * @package mod_scheduler + * @category test + * @copyright 2015 Henning Bostelmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Behat\Context\Step\Given as Given, Behat\Behat\Context\Step\When as When, Behat\Gherkin\Node\TableNode as TableNode; +/** + * Scheduler-related steps definitions. + * + * @package mod_scheduler + * @category test + * @copyright 2015 Henning Bostelmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_mod_scheduler extends behat_base { + + /** + * Adds a series of slots to the scheduler + * + * @Given /^I add a slot (\d+) days ahead at (\d+) in "(?P<activityname_string>(?:[^"]|\\")*)" scheduler and I fill the form with:$/ + * + * @param int $daysahead + * @param int $time + * @param string $activityname + * @param TableNode $fielddata + */ + public function i_add_a_slot_days_ahead_at_in_scheduler_and_i_fill_the_form_with( + $daysahead, $time, $activityname, TableNode $fielddata) { + + $hours = floor($time / 100); + $mins = $time - 100 * $hours; + $startdate = time() + $daysahead * DAYSECS; + + $this->execute('behat_general::click_link', $this->escape($activityname)); + $this->execute('behat_general::i_click_on', array('Add slots', 'link')); + $this->execute('behat_general::click_link', 'Add single slot'); + + $this->execute('behat_forms::i_expand_all_fieldsets'); + + $rows = array(); + $rows[] = array('starttime[day]', date("j", $startdate)); + $rows[] = array('starttime[month]', date("F", $startdate)); + $rows[] = array('starttime[year]', date("Y", $startdate)); + $rows[] = array('starttime[hour]', $hours); + $rows[] = array('starttime[minute]', $mins); + $rows[] = array('duration', '45'); + foreach ($fielddata->getRows() as $row) { + if ($row[0] == 'studentid[0]') { + $this->execute('behat_forms::i_open_the_autocomplete_suggestions_list'); + $this->execute('behat_forms::i_click_on_item_in_the_autocomplete_list', $row[1]); + } else { + $rows[] = $row; + } + } + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', new TableNode($rows)); + + $this->execute('behat_general::i_click_on', array('Save changes', 'button')); + } + + + /** + * Adds a series of slots to the scheduler + * + * @Given /^I add (\d+) slots (\d+) days ahead in "(?P<activityname_string>(?:[^"]|\\")*)" scheduler and I fill the form with:$/ + * + * @param int $slotcount + * @param int $daysahead + * @param string $activityname + * @param TableNode $fielddata + */ + public function i_add_slots_days_ahead_in_scheduler_and_i_fill_the_form_with( + $slotcount, $daysahead, $activityname, TableNode $fielddata) { + + $startdate = time() + $daysahead * DAYSECS; + + $this->execute('behat_general::click_link', $this->escape($activityname)); + $this->execute('behat_general::i_click_on', array('Add slots', 'link')); + $this->execute('behat_general::click_link', 'Add repeated slots'); + + $rows = array(); + $rows[] = array('rangestart[day]', date("j", $startdate)); + $rows[] = array('rangestart[month]', date("F", $startdate)); + $rows[] = array('rangestart[year]', date("Y", $startdate)); + $rows[] = array('Saturday', '1'); + $rows[] = array('Sunday', '1'); + $rows[] = array('starthour', '1'); + $rows[] = array('endhour', $slotcount + 1); + $rows[] = array('duration', '45'); + $rows[] = array('break', '15'); + foreach ($fielddata->getRows() as $row) { + $rows[] = $row; + } + + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', new TableNode($rows)); + + $this->execute('behat_general::i_click_on', array('Save changes', 'button')); + + } + + /** + * Add the "upcoming events" block, globally on every page. + * + * This is useful as it provides an easy way of checking a user's calendar entries. + * + * @Given /^I add the upcoming events block globally$/ + */ + public function i_add_the_upcoming_events_block_globally() { + + $home = $this->escape(get_string('sitehome')); + + $this->execute('behat_data_generators::the_following_entities_exist', array('users', + new TableNode(array( + array('username', 'firstname', 'lastname', 'email'), + array('globalmanager1', 'GlobalManager', '1', 'globalmanager1@example.com') + )) ) ); + + $this->execute('behat_data_generators::the_following_entities_exist', array('system role assigns', + new TableNode(array( + array('user', 'role'), + array('globalmanager1', 'manager') + )) ) ); + $this->execute('behat_auth::i_log_in_as', 'globalmanager1'); + $this->execute('behat_general::click_link', $home); + $this->execute('behat_navigation::i_navigate_to_in_current_page_administration', array('Turn editing on')); + $this->execute('behat_blocks::i_add_the_block', 'Upcoming events'); + + $this->execute('behat_blocks::i_open_the_blocks_action_menu', 'Upcoming events'); + $this->execute('behat_general::click_link', 'Configure Upcoming events block'); + $this->execute('behat_forms::i_set_the_following_fields_to_these_values', new TableNode(array( + array('Page contexts', 'Display throughout the entire site') + )) ); + $this->execute('behat_general::i_click_on', array('Save changes', 'button')); + $this->execute('behat_auth::i_log_out'); + + } + + /** + * Select item from the nth autocomplete list. + * + * @Given /^I click on "([^"]*)" item in autocomplete list number (\d+)$/ + * + * @param string $item + * @param int $listnumber + */ + public function i_click_on_item_in_the_nth_autocomplete_list($item, $listnumber) { + + $downarrowtarget = "(//span[contains(@class,'form-autocomplete-downarrow')])[$listnumber]"; + $this->execute('behat_general::i_click_on', [$downarrowtarget, 'xpath_element']); + + $xpathtarget = "(//ul[@class='form-autocomplete-suggestions']//*[contains(concat('|', string(.), '|'),'|" . $item . "|')])[$listnumber]"; + + $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']); + + $this->execute('behat_general::i_press_key_in_element', ['13', 'body', 'xpath_element']); + } +} diff --git a/mod/scheduler/tests/behat/conflicts.feature b/mod/scheduler/tests/behat/conflicts.feature new file mode 100644 index 0000000..867c799 --- /dev/null +++ b/mod/scheduler/tests/behat/conflicts.feature @@ -0,0 +1,198 @@ +@mod @mod_scheduler +Feature: Teachers are warned about scheduling conflicts + In order to create useful slots + As a teacher + I need to take care not to create conflicting schedules. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | manager1 | Manager | 1 | manager1@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + | teacher2 | Teacher | 2 | teacher2@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | student1 | C1 | student | + And the following "system role assigns" exist: + | user | role | + | manager1 | manager | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | schedulermode | maxbookings | + | scheduler | Test scheduler A | n | C1 | schedulerA | 0 | oneonly | 1 | + | scheduler | Test scheduler B | n | C1 | schedulerB | 0 | oneonly | 1 | + + @javascript + Scenario: A teacher edits a single slot and is warned about conflicts + + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I add 5 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: + | Location | My office | + And I am on "Course 1" course homepage + And I add a slot 5 days ahead at 1000 in "Test scheduler B" scheduler and I fill the form with: + | Location | My office | + + When I am on "Course 1" course homepage + And I follow "Test scheduler A" + And I click on "Edit" "link" in the "2:00 AM" "table_row" + And I set the following fields to these values: + | starttime[minute] | 40 | + And I click on "Save changes" "button" + Then I should see "conflict" + And "Save changes" "button" should exist + And I should see "3:00 AM" + And I should not see "2:00 AM" + + When I set the following fields to these values: + | starttime[hour] | 09 | + | starttime[minute] | 55 | + And I click on "Save changes" "button" + Then I should see "conflict" + And I should see "in course C1, scheduler Test scheduler B" + And I should see "10:00 AM" + And I should not see "2:00 AM" + And "Save changes" "button" should exist + + When I set the following fields to these values: + | starttime[hour] | 09 | + | starttime[minute] | 55 | + | Ignore scheduling conflicts | 1 | + And I click on "Save changes" "button" + Then I should see "slot updated" + And "9:55 AM" "table_row" should exist + And I log out + + @javascript + Scenario: A manager edits slots for several teachers, creating conflicts + + Given I log in as "manager1" + And I follow "Site home" + And I navigate to "Turn editing on" in current page administration + And I add the "Navigation" block if not present + And I click on "Courses" "link" in the "Navigation" "block" + And I am on "Course 1" course homepage + And I add 6 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: + | Location | Office T1 | + | Teacher | Teacher 1 | + And I am on "Course 1" course homepage + And I add 5 slots 5 days ahead in "Test scheduler B" scheduler and I fill the form with: + | Location | Office T2 | + | Teacher | Teacher 2 | + + When I am on "Course 1" course homepage + And I follow "Test scheduler A" + And I click on "Edit" "link" in the "3:00 AM" "table_row" + And I set the following fields to these values: + | starttime[hour] | 6 | + | starttime[minute] | 40 | + | duration | 5 | + And I click on "Save changes" "button" + Then I should see "conflict" + And I should see "6:00 AM" + And I should see "in this scheduler" + And I should not see "3:00 AM" + And "Save changes" "button" should exist + + When I set the following fields to these values: + | starttime[hour] | 5 | + | starttime[minute] | 40 | + | duration | 5 | + | Teacher | Teacher 2 | + And I click on "Save changes" "button" + Then I should see "conflict" + And I should see "5:00 AM" + And I should see "in course C1, scheduler Test scheduler B" + And I should not see "3:00 AM" + And "Save changes" "button" should exist + + When I set the following fields to these values: + | starttime[hour] | 6 | + | starttime[minute] | 40 | + | duration | 5 | + | Teacher | Teacher 2 | + And I click on "Save changes" "button" + Then I should not see "conflict" + And I should see "slot updated" + And "6:40 AM" "table_row" should exist + And "Save changes" "button" should not exist + And I log out + + @javascript + Scenario: A teacher adds a series of slots, creating conflicts + + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I add a slot 5 days ahead at 0125 in "Test scheduler A" scheduler and I fill the form with: + | Location | My office | + | duration | 15 | + # Blocks 3 other slots on a 1-hour grid + And I am on "Course 1" course homepage + And I add a slot 5 days ahead at 0225 in "Test scheduler A" scheduler and I fill the form with: + | Location | My office | + | duration | 100 | + # Booked slot - must not be deleted as conflict + And I am on "Course 1" course homepage + And I add a slot 5 days ahead at 0855 in "Test scheduler A" scheduler and I fill the form with: + | Location | My office | + | duration | 10 | + | studentid[0] | Student 1 | + # Slot in other scheduler - must not be deleted as conflict + And I am on "Course 1" course homepage + And I add a slot 5 days ahead at 0605 in "Test scheduler B" scheduler and I fill the form with: + | Location | My office | + | duration | 20 | + + When I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: + | Location | Lecture hall | + Then I should see "conflicting slots" + And I should not see "deleted" + And I should see "4 slots have been added" + And "1:25 AM" "table_row" should exist + And "2:25 AM" "table_row" should exist + And "8:55 AM" "table_row" should exist + And "1:00 AM" "table_row" should not exist + And "2:00 AM" "table_row" should not exist + And "3:00 AM" "table_row" should not exist + And "4:00 AM" "table_row" should not exist + And "5:00 AM" "table_row" should exist + And "6:00 AM" "table_row" should not exist + And "7:00 AM" "table_row" should exist + And "8:00 AM" "table_row" should exist + And "9:00 AM" "table_row" should not exist + And "10:00 AM" "table_row" should exist + And I am on "Course 1" course homepage + And I follow "Test scheduler B" + And "6:05 AM" "table_row" should exist + + When I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: + | Location | Lecture hall | + | Force when overlap | 1 | + Then I should see "conflicting slots" + And I should see "deleted" + And I should see "8 slots have been added" + And "1:25 AM" "table_row" should not exist + And "2:25 AM" "table_row" should not exist + And "9:55 AM" "table_row" should not exist + And "1:00 AM" "table_row" should exist + And "2:00 AM" "table_row" should exist + And "3:00 AM" "table_row" should exist + And "4:00 AM" "table_row" should exist + And "5:00 AM" "table_row" should exist + And "6:00 AM" "table_row" should not exist + And "7:00 AM" "table_row" should exist + And "8:00 AM" "table_row" should exist + And "9:00 AM" "table_row" should not exist + And "10:00 AM" "table_row" should exist + And I am on "Course 1" course homepage + And I follow "Test scheduler B" + And "6:05 AM" "table_row" should exist + + And I log out diff --git a/mod/scheduler/tests/behat/group_availability.feature b/mod/scheduler/tests/behat/group_availability.feature new file mode 100644 index 0000000..4cf5029 --- /dev/null +++ b/mod/scheduler/tests/behat/group_availability.feature @@ -0,0 +1,77 @@ +@mod @mod_scheduler +Feature: As a teacher I need to see an accurate list of users to be scheduled + In order to see who needs to schedule an appointment + As a teacher + I need to view the table of students in the teacher view + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher | Teacher | Teacher | teacher@example.com | + | student1 | Student | 1 | student.1@example.com | + | student2 | Student | 2 | student.2@example.com | + | student3 | Student | 3 | student.3@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G2 | + And the following "groupings" exist: + | name | course | idnumber | + | Grouping 1 | C1 | GG1 | + And the following "grouping groups" exist: + | grouping | group | + | GG1 | G1 | + And the following config values are set as admin: + | enableavailability | 1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | scheduler | Test scheduler | n | C1 | scheduler1 | + And I log in as "teacher" + And I am on "Course 1" course homepage + + @javascript + Scenario: A scheduler that is restricted to a single group + When I follow "Test scheduler" + Then I should see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 2" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + + When I navigate to "Edit settings" in current page administration + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Group" "button" in the "Add restriction..." "dialogue" + And I set the field with xpath "//select[@name='id']" to "Group 2" + And I press "Save and display" + Then I should not see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 2" in the "studentstoschedule" "table" + And I should not see "Student 3" in the "studentstoschedule" "table" + + @javascript + Scenario: A scheduler that is restricted to a grouping + When I follow "Test scheduler" + Then I should see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 2" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + + When I navigate to "Edit settings" in current page administration + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Grouping" "button" in the "Add restriction..." "dialogue" + And I set the field with xpath "//select[@name='id']" to "Grouping 1" + And I press "Save and display" + Then I should see "Student 1" in the "studentstoschedule" "table" + And I should not see "Student 2" in the "studentstoschedule" "table" + And I should not see "Student 3" in the "studentstoschedule" "table" diff --git a/mod/scheduler/tests/behat/groupmode.feature b/mod/scheduler/tests/behat/groupmode.feature new file mode 100644 index 0000000..43ca6f7 --- /dev/null +++ b/mod/scheduler/tests/behat/groupmode.feature @@ -0,0 +1,351 @@ +@mod @mod_scheduler +Feature: Users can only see their own groups if the scheduler is in group mode + In order to see slots + As a user + I must be allowed to see the group of the relevant teacher + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | edteacher1 | Editingteacher | 1 | edteacher1@example.com | + | neteacher1 | Nonedteacher | 1 | neteacher1@example.com | + | neteacher2 | Nonedteacher | 2 | neteacher2@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | student4 | Student | 4 | student4@example.com | + | student5 | Student | 5 | student5@example.com | + | student6 | Student | 6 | student5@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | edteacher1 | C1 | editingteacher | + | neteacher1 | C1 | teacher | + | neteacher2 | C1 | teacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + | student5 | C1 | student | + | student6 | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group A | C1 | GA | + | Group B | C1 | GB | + | Group C | C1 | GC | + | Group D | C1 | GD | + And the following "group members" exist: + | user | group | + | edteacher1 | GA | + | edteacher1 | GB | + | neteacher1 | GB | + | neteacher1 | GC | + | student1 | GA | + | student2 | GA | + | student3 | GB | + | student4 | GB | + | student5 | GD | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | + | scheduler | Test scheduler none | n | C1 | schedulern | 0 | + | scheduler | Test scheduler separate | n | C1 | schedulers | 1 | + | scheduler | Test scheduler visible | n | C1 | schedulerv | 2 | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/site:accessallgroups | Prevent | teacher | Course | C1 | + | mod/scheduler:canseeotherteachersbooking | Allow | teacher | Course | C1 | + And I add the upcoming events block globally + And I log in as "edteacher1" + And I am on "Course 1" course homepage + And I add 5 slots 10 days ahead in "Test scheduler none" scheduler and I fill the form with: + | Location | Here | + And I am on "Course 1" course homepage + And I add 5 slots 11 days ahead in "Test scheduler visible" scheduler and I fill the form with: + | Location | Here | + And I am on "Course 1" course homepage + And I add 5 slots 12 days ahead in "Test scheduler separate" scheduler and I fill the form with: + | Location | Here | + And I log out + And I log in as "neteacher1" + And I am on "Course 1" course homepage + And I add 5 slots 10 days ahead in "Test scheduler none" scheduler and I fill the form with: + | Location | There | + And I am on "Course 1" course homepage + And I add 5 slots 11 days ahead in "Test scheduler visible" scheduler and I fill the form with: + | Location | There | + And I am on "Course 1" course homepage + And I add 5 slots 12 days ahead in "Test scheduler separate" scheduler and I fill the form with: + | Location | There | + And I log out + + @javascript + Scenario: Editing teachers can see all slots and all groups + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + And I should see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + And I should see "Student 5" in the "studentstoschedule" "table" + And I should see "Student 6" in the "studentstoschedule" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Visible groups" + And the "group" select box should contain "All participants" + And the "group" select box should contain "Group A" + And the "group" select box should contain "Group B" + And the "group" select box should contain "Group C" + And the "group" select box should contain "Group D" + When I set the field "group" to "All participants" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group A" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should not see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group B" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group C" + Then I should not see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Separate groups" + And the "group" select box should contain "All participants" + And the "group" select box should contain "Group A" + And the "group" select box should contain "Group B" + And the "group" select box should contain "Group C" + And the "group" select box should contain "Group D" + When I set the field "group" to "All participants" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + And I should see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + And I should see "Student 5" in the "studentstoschedule" "table" + And I should see "Student 6" in the "studentstoschedule" "table" + When I set the field "group" to "Group A" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should not see "Nonedteacher 1" in the "slotmanager" "table" + And I should see "Student 1" in the "studentstoschedule" "table" + And I should not see "Student 3" in the "studentstoschedule" "table" + And I should not see "Student 5" in the "studentstoschedule" "table" + And I should not see "Student 6" in the "studentstoschedule" "table" + When I set the field "group" to "Group B" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + And I should not see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + And I should not see "Student 5" in the "studentstoschedule" "table" + And I should not see "Student 6" in the "studentstoschedule" "table" + When I set the field "group" to "Group C" + Then I should not see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + + # In the "My appointments" tab, the teacher should only see students to schedule from their groups, + # i.e., groups A and B. + # Students outside any group should not be visible. + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "Group mode: Separate groups" + And I should see "Only students in Group A, Group B can book" + And I should see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + And I should not see "Student 5" in the "studentstoschedule" "table" + And I should not see "Student 6" in the "studentstoschedule" "table" + And I log out + + @javascript + Scenario: Nonediting teachers can see groups only if allowed by the group mode + + When I log in as "neteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "6 students still need to make an appointment" + When I follow "All appointments" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "2 students still need to make an appointment" + When I follow "All appointments" + Then I should see "Visible groups" + And the "group" select box should contain "All participants" + And the "group" select box should contain "Group A" + And the "group" select box should contain "Group B" + And the "group" select box should contain "Group C" + And the "group" select box should contain "Group D" + When I set the field "group" to "All participants" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group A" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should not see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group B" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group C" + Then I should not see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "2 students still need to make an appointment" + When I follow "All appointments" + Then I should see "Separate groups" + And the "group" select box should not contain "All participants" + And the "group" select box should not contain "Group A" + And the "group" select box should contain "Group B" + And the "group" select box should contain "Group C" + And the "group" select box should not contain "Group D" + When I set the field "group" to "Group B" + Then I should see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + When I set the field "group" to "Group C" + Then I should not see "Editingteacher 1" in the "slotmanager" "table" + And I should see "Nonedteacher 1" in the "slotmanager" "table" + + When I set the field "group" to "Group B" + And I click on "Edit" "link_or_button" in the "Nonedteacher 1" "table_row" + Then I should see "Appointment 1" + And "Student 1" "option" should not exist in the "studentid[0]" "field" + And "Student 3" "option" should exist in the "studentid[0]" "field" + And I click on "Save changes" "button" + + # In the "My appointments" tab, the teacher should only see students to schedule from their groups, + # i.e., group B (and C). + # Students in group 1 and outside any group should not be visible. + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "Group mode: Separate groups" + And I should see "Only students in Group B, Group C can book" + And I should not see "Student 1" in the "studentstoschedule" "table" + And I should not see "Student 2" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + And I should see "Student 4" in the "studentstoschedule" "table" + And I should not see "Student 5" in the "studentstoschedule" "table" + And I should not see "Student 6" in the "studentstoschedule" "table" + And I log out + + # neteacher2 sees no students for scheduling in group mode, since he's not member of a group + + When I log in as "neteacher2" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "6 students still need to make an appointment" + + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "No students available for scheduling" + And I should see "Group mode: Visible groups" + And I should see "students cannot book appointments with you" + + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "No students available for scheduling" + And I should see "Group mode: Separate groups" + And I should see "students cannot book appointments with you" + + @javascript + Scenario: Students can see slots available to their own groups, or a slots if group mode is off + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + Then I should see "Editingteacher 1" + And I should see "Nonedteacher 1" + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + Then I should see "Editingteacher 1" + And I should not see "Nonedteacher 1" + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + Then I should see "Editingteacher 1" + And I should not see "Nonedteacher 1" + And I log out + + When I log in as "student3" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + Then I should see "Editingteacher 1" + And I should see "Nonedteacher 1" + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + Then I should see "Editingteacher 1" + And I should see "Nonedteacher 1" + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + Then I should see "Editingteacher 1" + And I should see "Nonedteacher 1" + And I log out + + When I log in as "student5" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + Then I should see "Editingteacher 1" + And I should see "Nonedteacher 1" + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + Then I should see "No slots are available" + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + Then I should see "No slots are available" + And I log out + + When I log in as "student6" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + Then I should see "Editingteacher 1" + And I should see "Nonedteacher 1" + When I am on "Course 1" course homepage + And I follow "Test scheduler visible" + Then I should see "No slots are available" + When I am on "Course 1" course homepage + And I follow "Test scheduler separate" + Then I should see "No slots are available" + And I log out + + @javascript + Scenario: Students can see slots available to their own groups in forced group mode + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + And I set the field "Group mode" to "Separate groups" + And I set the field "Force group mode" to "Yes" + And I press "Save and display" + Then I should see "Test scheduler none" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler none" + Then I should see "Editingteacher 1" + And I should not see "Nonedteacher 1" + And I log out \ No newline at end of file diff --git a/mod/scheduler/tests/behat/groupscheduling.feature b/mod/scheduler/tests/behat/groupscheduling.feature new file mode 100644 index 0000000..2fb1310 --- /dev/null +++ b/mod/scheduler/tests/behat/groupscheduling.feature @@ -0,0 +1,153 @@ +@mod @mod_scheduler +Feature: Entire groups can be booked into slots at once + In order to allow booking of entire groups + As a teacher + I need to use a scheduler with group bookings + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | edteacher1 | Editingteacher | 1 | edteacher1@example.com | + | neteacher1 | Nonedteacher | 1 | neteacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | student4 | Student | 4 | student4@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | edteacher1 | C1 | editingteacher | + | neteacher1 | C1 | teacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group A1 | C1 | GA1 | + | Group A2 | C1 | GA2 | + | Group B1 | C1 | GB1 | + | Group B2 | C1 | GB2 | + And the following "groupings" exist: + | name | course | idnumber | + | Grouping A | C1 | GROUPINGA | + | Grouping B | C1 | GROUPINGB | + And the following "group members" exist: + | user | group | + | neteacher1 | GB1 | + | neteacher1 | GA1 | + | student1 | GA1 | + | student2 | GA1 | + | student3 | GA2 | + | student4 | GA2 | + | student1 | GB1 | + | student2 | GB2 | + | student3 | GB1 | + | student4 | GB2 | + And the following "grouping groups" exist: + | grouping | group | + | GROUPINGA | GA1 | + | GROUPINGA | GA2 | + | GROUPINGB | GB1 | + | GROUPINGB | GB2 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | scheduler | Test scheduler no grouping | n | C1 | schedulern | + | scheduler | Test scheduler grouping A | n | C1 | schedulera | + | scheduler | Test scheduler grouping B | n | C1 | schedulerb | + And I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler no grouping" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Booking in groups | Yes, for all groups | + And I click on "Save and return to course" "button" + And I follow "Test scheduler grouping A" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Booking in groups | Yes, in grouping Grouping A | + And I click on "Save and return to course" "button" + And I follow "Test scheduler grouping B" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Booking in groups | Yes, in grouping Grouping B | + And I click on "Save and return to course" "button" + And I log out + + @javascript + Scenario: Editing teachers can see and schedule relevant groups + Given I log in as "edteacher1" + And I am on "Course 1" course homepage + + When I am on "Course 1" course homepage + And I follow "Test scheduler no grouping" + Then I should see "Group A1" in the "groupstoschedule" "table" + And I should see "Group A2" in the "groupstoschedule" "table" + And I should see "Group B1" in the "groupstoschedule" "table" + And I should see "Group B2" in the "groupstoschedule" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler grouping A" + Then I should see "Group A1" in the "groupstoschedule" "table" + And I should see "Group A2" in the "groupstoschedule" "table" + And I should not see "Group B" in the "groupstoschedule" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler grouping B" + Then I should not see "Group A" in the "groupstoschedule" "table" + And I should see "Group B1" in the "groupstoschedule" "table" + And I should see "Group B2" in the "groupstoschedule" "table" + + When I am on "Course 1" course homepage + And I follow "Test scheduler no grouping" + And I click on "Schedule" "link_or_button" in the "Group A1" "table_row" + And I click on "Schedule in slot" "text" in the "Group A1" "table_row" + And I click on "Save changes" "button" + Then I should see "Student 1" in the "slotmanager" "table" + And I should see "Student 2" in the "slotmanager" "table" + And I should see "2 students still need to make an appointment" + And I should not see "Group A1" in the "groupstoschedule" "table" + And I should see "Group A2" in the "groupstoschedule" "table" + And I should not see "Group B1" in the "groupstoschedule" "table" + And I should not see "Group B2" in the "groupstoschedule" "table" + + @javascript + Scenario: Students can book their entire group into a slot + Given I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler no grouping" + And I add 8 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: + | Location | Large office | + | exclusivity | 5 | + And I add 5 slots 6 days ahead in "Test scheduler" scheduler and I fill the form with: + | Location | Small office | + | exclusivity | 1 | + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler no grouping" + Then the "appointgroup" select box should contain "Myself" + And the "appointgroup" select box should contain "Group A1" + And the "appointgroup" select box should contain "Group B1" + And the "appointgroup" select box should not contain "Group A2" + And the "appointgroup" select box should not contain "Group B2" + + When I set the field "appointgroup" to "Group A1" + And I click on "Book slot" "button" in the "8:00 AM" "table_row" + Then I should see "8:00 AM" in the "Large office" "table_row" + And I log out + + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler no grouping" + Then I should see "Student 1" in the "8:00 AM" "table_row" + And I should see "Student 2" in the "8:00 AM" "table_row" + And I should see "2 students still need to make an appointment" + And I should not see "Group A1" in the "groupstoschedule" "table" + And I should see "Group A2" in the "groupstoschedule" "table" + And I should not see "Group B1" in the "groupstoschedule" "table" + And I should not see "Group B2" in the "groupstoschedule" "table" + And I log out \ No newline at end of file diff --git a/mod/scheduler/tests/behat/notes.feature b/mod/scheduler/tests/behat/notes.feature new file mode 100644 index 0000000..a263327 --- /dev/null +++ b/mod/scheduler/tests/behat/notes.feature @@ -0,0 +1,175 @@ +@mod @mod_scheduler +Feature: Teachers can write notes on slots and appointments + In order to record details about a meeting + As a teacher + I need to enter notes for the appointment + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | edteacher1 | Editingteacher | 1 | edteacher1@example.com | + | neteacher1 | Nonedteacher | 1 | neteacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | edteacher1 | C1 | editingteacher | + | neteacher1 | C1 | teacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | usenotes | + | scheduler | Test scheduler | n | C1 | schedulern | 3 | + And I log in as "edteacher1" + And I am on "Course 1" course homepage + And I add 5 slots 10 days ahead in "Test scheduler" scheduler and I fill the form with: + | Location | Here | + And I log out + + @javascript + Scenario: Teachers can enter slot notes and appointment notes for others to see + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + And I click on "Edit" "link" in the "4:00 AM" "table_row" + And I set the following fields to these values: + | Comments | Note-for-slot | + And I click on "Save" "button" + Then I should see "slot updated" + When I click on "Edit" "link" in the "4:00 AM" "table_row" + Then I should see "Note-for-slot" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "Note-for-slot" in the "4:00 AM" "table_row" + When I click on "Book slot" "button" in the "4:00 AM" "table_row" + Then I should see "Note-for-slot" + And I log out + + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" + Then I should see ", 4:00 AM" in the "Date and time" "table_row" + And I should see "4:45 AM" in the "Date and time" "table_row" + And I should see "Editingteacher 1" in the "Teacher" "table_row" + And I set the following fields to these values: + | Attended | 1 | + | Notes for appointment (visible to student) | note-for-appointment | + | Confidential notes (visible to teacher only) | note-confidential | + And I click on "Save changes" "button" + Then I should see "note-for-appointment" + And I should see "note-confidential" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "Attended slots" + And I should see "note-for-appointment" + And I should not see "note-confidential" + And I log out + + @javascript + Scenario: Teachers see only the comments fields specified in the configuration + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I click on "Book slot" "button" in the "4:00 AM" "table_row" + Then I should see "Upcoming slots" + And I log out + + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" + And I set the following fields to these values: + | Notes for appointment (visible to student) | note-for-appointment | + | Confidential notes (visible to teacher only) | note-confidential | + And I click on "Save changes" "button" + Then I should see "note-for-appointment" + And I should see "note-confidential" + + When I follow "Test scheduler" + And I navigate to "Edit settings" in current page administration + And I set the field "Use notes for appointments" to "0" + And I click on "Save and display" "button" + And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" + Then I should not see "Notes for appointment" + And I should not see "note-for-appointment" + And I should not see "Confidential notes" + And I should not see "note-confidential" + And I click on "Save changes" "button" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should not see "note-for-appointment" + And I should not see "note-confidential" + And I log out + + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I navigate to "Edit settings" in current page administration + And I set the field "Use notes for appointments" to "1" + And I click on "Save and display" "button" + And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" + Then I should see "Notes for appointment" + And I should see "note-for-appointment" + And I should not see "Confidential notes" + And I should not see "note-confidential" + And I click on "Save changes" "button" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "note-for-appointment" + And I should not see "note-confidential" + And I log out + + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I navigate to "Edit settings" in current page administration + And I set the field "Use notes for appointments" to "2" + And I click on "Save and display" "button" + And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" + Then I should not see "Notes for appointment" + And I should not see "note-for-appointment" + And I should see "Confidential notes" + And I should see "note-confidential" + And I click on "Save changes" "button" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should not see "note-for-appointment" + And I should not see "note-confidential" + And I log out + + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I navigate to "Edit settings" in current page administration + And I set the field "Use notes for appointments" to "3" + And I click on "Save and display" "button" + And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" + Then I should see "Notes for appointment" + And I should see "note-for-appointment" + And I should see "Confidential notes" + And I should see "note-confidential" + And I log out diff --git a/mod/scheduler/tests/behat/officehours.feature b/mod/scheduler/tests/behat/officehours.feature new file mode 100644 index 0000000..1d8e981 --- /dev/null +++ b/mod/scheduler/tests/behat/officehours.feature @@ -0,0 +1,96 @@ +@mod @mod_scheduler +Feature: Office hours bookings with Scheduler, one booking per student + In order to organize my office hours + As a teacher + I can use a scheduler to let students choose a time slot. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | manager1 | Manager | 1 | manager1@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | student4 | Student | 4 | student4@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + | student4 | C1 | student | + And the following "system role assigns" exist: + | user | role | + | manager1 | manager | + And the following "activities" exist: + | activity | name | intro | course | idnumber | schedulermode | + | scheduler | Test scheduler | n | C1 | scheduler1 | oneonly | + And I add the upcoming events block globally + + @javascript + Scenario: The teacher adds slots, and students book them + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: + | Location | My office | + Then I should see "10 slots have been added" + And I should see "4 students still need to make an appointment" + And I should see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 2" in the "studentstoschedule" "table" + And I should see "Student 3" in the "studentstoschedule" "table" + And I should see "Student 4" in the "studentstoschedule" "table" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "1:00 AM" in the "slotbookertable" "table" + And I should see "10:00 AM" in the "slotbookertable" "table" + When I click on "Book slot" "button" in the "2:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And I should see "Meeting with your Teacher, Teacher 1" in the "Upcoming events" "block" + And I log out + + When I log in as "student3" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "1:00 AM" in the "slotbookertable" "table" + And I should not see "2:00 AM" in the "slotbookertable" "table" + And I should see "10:00 AM" in the "slotbookertable" "table" + When I click on "Book slot" "button" in the "5:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And I should see "Meeting with your Teacher, Teacher 1" in the "Upcoming events" "block" + And I log out + + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "1:00 AM" in the "slotmanager" "table" + And I should see "Student 1" in the "2:00 AM" "table_row" + And I should see "Student 3" in the "5:00 AM" "table_row" + And I should see "10:00 AM" in the "slotmanager" "table" + And I should see "Meeting with your Student, Student 1" in the "Upcoming events" "block" + And I should see "Meeting with your Student, Student 3" in the "Upcoming events" "block" + And I should see "2 students still need to make an appointment" + And I should not see "Student 1" in the "studentstoschedule" "table" + And I should see "Student 2" in the "studentstoschedule" "table" + And I should not see "Student 3" in the "studentstoschedule" "table" + And I should see "Student 4" in the "studentstoschedule" "table" + When I click on "seen[]" "checkbox" in the "2:00 AM" "table_row" + And I follow "Test scheduler" + Then I should not see "Meeting with your Student, Student 1" in the "Upcoming events" "block" + And I should see "Meeting with your Student, Student 3" in the "Upcoming events" "block" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "Attended slots" + And "slotbookertable" "table" should not exist + And I should not see "Cancel booking" + And I should not see "Meeting with your" in the "Upcoming events" "block" + And I log out diff --git a/mod/scheduler/tests/behat/studentdata.feature b/mod/scheduler/tests/behat/studentdata.feature new file mode 100644 index 0000000..d7326c3 --- /dev/null +++ b/mod/scheduler/tests/behat/studentdata.feature @@ -0,0 +1,88 @@ +@mod @mod_scheduler @javascript @_file_upload +Feature: Student-supplied data + In order to collect data from students + As a teacher + I can configure a booking form for the scheduler. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | schedulermode | maxbookings | + | scheduler | Test scheduler | n | C1 | scheduler1 | 0 | oneonly | 0 | + + @javascript + Scenario: A teacher configures a booking form, and students enter data + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I navigate to "Edit settings" in current page administration + And I expand all fieldsets + And I set the field "Use booking form" to "1" + And I set the field "Booking instructions" to "Please enter your first name" + And I set the field "Let students enter a message" to "Yes, student must enter a message" + And I set the field "Maximum number of uploaded files" to "1" + And I click on "Save and display" "button" + And I add 10 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: + | Location | My office | + Then I should see "10 slots have been added" + And I log out + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then I should see "3:00 AM" in the "slotbookertable" "table" + + When I click on "Book slot" "button" in the "3:00 AM" "table_row" + Then I should see "Please enter your first name" + + When I click on "Confirm booking" "button" + Then I should see "You must enter text into this field" + + When I set the field "Your message" to "Joe" + And I click on "Confirm booking" "button" + Then "Cancel booking" "button" should exist + And I log out + + When I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I click on "Book slot" "button" in the "4:00 AM" "table_row" + Then I should see "Please enter your first name" + + When I set the field "Your message" to "Jill" + And I upload "mod/scheduler/tests/fixtures/studentfile.txt" file to "Upload files" filemanager + And I click on "Confirm booking" "button" + Then "Cancel booking" "button" should exist + And I log out + + When I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "My appointments" + Then I should see "Student 1" in the "3:00 AM" "table_row" + And I should see "Student 2" in the "4:00 AM" "table_row" + + When I click on "Student 1" "text" in the "3:00 AM" "table_row" + Then I should see "Student 1" + And I should see "Joe" + And I should not see "studentfile.txt" + + When I click on "Continue" "button" + And I click on "Student 2" "text" in the "4:00 AM" "table_row" + Then I should see "Student 2" + And I should see "Jill" + And I should see "studentfile.txt" + And I log out \ No newline at end of file diff --git a/mod/scheduler/tests/behat/teacherpermissions.feature b/mod/scheduler/tests/behat/teacherpermissions.feature new file mode 100644 index 0000000..7d87c2c --- /dev/null +++ b/mod/scheduler/tests/behat/teacherpermissions.feature @@ -0,0 +1,232 @@ +@mod @mod_scheduler +Feature: Teachers can edit other teacher's appointments only by permission + In order to edit another teacher's appointment + As a teacher + I must have the correct permissions. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | edteacher1 | Editingteacher | 1 | edteacher1@example.com | + | neteacher1 | Nonedteacher | 1 | neteacher1@example.com | + | neteacher2 | Nonedteacher | 2 | neteacher2@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | edteacher1 | C1 | editingteacher | + | neteacher1 | C1 | teacher | + | neteacher2 | C1 | teacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | usenotes | grade | + | scheduler | Test scheduler | n | C1 | schedulert | 0 | 3 | 100 | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | mod/scheduler:canseeotherteachersbooking | Allow | teacher | Course | C1 | + And I log in as "edteacher1" + And I am on "Course 1" course homepage + And I add a slot 10 days ahead at 0300 in "Test scheduler" scheduler and I fill the form with: + | Location | Office ed1 | + | studentid[0] | Student 1 | + And I log out + And I log in as "neteacher1" + And I am on "Course 1" course homepage + And I add a slot 10 days ahead at 0400 in "Test scheduler" scheduler and I fill the form with: + | Location | Office ne1 | + | studentid[0] | Student 2 | + And I log out + And I log in as "neteacher2" + And I am on "Course 1" course homepage + And I add a slot 10 days ahead at 0500 in "Test scheduler" scheduler and I fill the form with: + | Location | Office ne2 | + | studentid[0] | Student 3 | + And I log out + + @javascript + Scenario: Editing teachers edit all appointments, nonediting teachers only their own + When I log in as "edteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Student 1" in the "3:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "3:00 AM" "table_row" + And I should see "Student 2" in the "4:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "4:00 AM" "table_row" + And I should see "Student 3" in the "5:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "5:00 AM" "table_row" + When I click on "//a[text()='Student 3']" "xpath_element" in the "5:00 AM" "table_row" + Then the "Attended" "checkbox" should be enabled + And "Notes for appointment (visible to student)" "field" should exist + And "Confidential notes (visible to teacher only)" "field" should exist + And the "grade" "field" should be enabled + When I set the following fields to these values: + | Attended | 1 | + And I click on "Save changes" "button" + Then the field "Attended" matches value "1" + And I log out + + When I log in as "neteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Student 1" in the "3:00 AM" "table_row" + And "seen[]" "checkbox" should not exist in the "3:00 AM" "table_row" + And I should see "Student 2" in the "4:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "4:00 AM" "table_row" + And I should see "Student 3" in the "5:00 AM" "table_row" + And "seen[]" "checkbox" should not exist in the "5:00 AM" "table_row" + When I click on "//a[text()='Student 2']" "xpath_element" in the "4:00 AM" "table_row" + Then the "Attended" "checkbox" should be enabled + And "Notes for appointment (visible to student)" "field" should exist + And "Confidential notes (visible to teacher only)" "field" should exist + When I set the following fields to these values: + | Attended | 1 | + And I click on "Save changes" "button" + Then the field "Attended" matches value "1" + When I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + When I click on "//a[text()='Student 3']" "xpath_element" in the "5:00 AM" "table_row" + Then the "Attended" "checkbox" should be disabled + And "Notes for appointment (visible to student)" "field" should not exist + And "Confidential notes (visible to teacher only)" "field" should not exist + And "grade" "field" should not exist + And I log out + + @javascript + Scenario: Attended boxes can be edited if the teacher has permission + Given I log in as "admin" + And I set the following system permissions of "Non-editing teacher" role: + | capability | permission | + | mod/scheduler:editallattended | Allow | + And I log out + + When I log in as "neteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Student 1" in the "3:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "3:00 AM" "table_row" + And I should see "Student 2" in the "4:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "4:00 AM" "table_row" + And I should see "Student 3" in the "5:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "5:00 AM" "table_row" + When I click on "//a[text()='Student 2']" "xpath_element" in the "4:00 AM" "table_row" + Then the "Attended" "checkbox" should be enabled + When I set the following fields to these values: + | Attended | 1 | + And I click on "Save changes" "button" + Then the field "Attended" matches value "1" + + When I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + When I click on "//a[text()='Student 3']" "xpath_element" in the "5:00 AM" "table_row" + Then the "Attended" "checkbox" should be enabled + And "Notes for appointment (visible to student)" "field" should not exist + And "Confidential notes (visible to teacher only)" "field" should not exist + And "grade" "field" should not exist + When I set the following fields to these values: + | Attended | 1 | + And I click on "Save changes" "button" + Then the field "Attended" matches value "1" + And I log out + + @javascript + Scenario: Grade boxes can be edited if the teacher has permission + Given I log in as "admin" + And I set the following system permissions of "Non-editing teacher" role: + | capability | permission | + | mod/scheduler:editallgrades | Allow | + And I log out + + When I log in as "neteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Student 1" in the "3:00 AM" "table_row" + And "seen[]" "checkbox" should not exist in the "3:00 AM" "table_row" + And I should see "Student 2" in the "4:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "4:00 AM" "table_row" + And I should see "Student 3" in the "5:00 AM" "table_row" + And "seen[]" "checkbox" should not exist in the "5:00 AM" "table_row" + When I click on "//a[text()='Student 2']" "xpath_element" in the "4:00 AM" "table_row" + Then the "grade" "field" should be enabled + When I set the following fields to these values: + | Grade | 42 | + And I click on "Save changes" "button" + Then the field "Grade" matches value "42" + + When I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + When I click on "//a[text()='Student 3']" "xpath_element" in the "5:00 AM" "table_row" + Then the "grade" "field" should be enabled + And the "Attended" "checkbox" should be disabled + And "Notes for appointment (visible to student)" "field" should not exist + And "Confidential notes (visible to teacher only)" "field" should not exist + When I set the following fields to these values: + | Grade | 33 | + And I click on "Save changes" "button" + Then the field "grade" matches value "33" + And I log out + + @javascript + Scenario: Comment boxes can be edited if the teacher has permission + Given I log in as "admin" + And I set the following system permissions of "Non-editing teacher" role: + | capability | permission | + | mod/scheduler:editallnotes | Allow | + And I log out + + When I log in as "neteacher1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + Then I should see "Student 1" in the "3:00 AM" "table_row" + And "seen[]" "checkbox" should not exist in the "3:00 AM" "table_row" + And I should see "Student 2" in the "4:00 AM" "table_row" + And "seen[]" "checkbox" should exist in the "4:00 AM" "table_row" + And I should see "Student 3" in the "5:00 AM" "table_row" + And "seen[]" "checkbox" should not exist in the "5:00 AM" "table_row" + When I click on "//a[text()='Student 2']" "xpath_element" in the "4:00 AM" "table_row" + Then the "Notes for appointment (visible to student)" "field" should be enabled + And the "Confidential notes (visible to teacher only)" "field" should be enabled + When I set the following fields to these values: + | Notes for appointment (visible to student) | notes-vis | + | Confidential notes (visible to teacher only) | notes-confid | + And I click on "Save changes" "button" + Then I should see "notes-vis" + And I should see "notes-confid" + + When I am on "Course 1" course homepage + And I follow "Test scheduler" + And I follow "Statistics" + And I follow "All appointments" + When I click on "//a[text()='Student 3']" "xpath_element" in the "5:00 AM" "table_row" + Then "grade" "field" should not exist + And the "Attended" "checkbox" should be disabled + And the "Notes for appointment (visible to student)" "field" should be enabled + And the "Confidential notes (visible to teacher only)" "field" should be enabled + When I set the following fields to these values: + | Notes for appointment (visible to student) | notes-vis-3 | + | Confidential notes (visible to teacher only) | notes-confid-3 | + And I click on "Save changes" "button" + Then I should see "notes-vis-3" + And I should see "notes-confid-3" + And I log out diff --git a/mod/scheduler/tests/behat/tutorappointments.feature b/mod/scheduler/tests/behat/tutorappointments.feature new file mode 100644 index 0000000..d7c9cd7 --- /dev/null +++ b/mod/scheduler/tests/behat/tutorappointments.feature @@ -0,0 +1,222 @@ +@mod @mod_scheduler +Feature: Booking of appointments with individual tutors per group + In order to organize appointments with the students in my group + As a tutor + I can use a scheduler to let students choose a time slot. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | manager1 | Manager | 1 | manager1@example.com | + | coor1 | Coordinator | 1 | coor1@example.com | + | tutor2 | Tutor | 2 | tutor2@example.com | + | tutor3 | Tutor | 3 | tutor3@example.com | + | student1a | Student | 1a | student1a@example.com | + | student1b | Student | 1b | student1b@example.com | + | student2a | Student | 2a | student2a@example.com | + | student2b | Student | 2b | student2b@example.com | + | student3a | Student | 3a | student3a@example.com | + | student3b | Student | 3b | student3b@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | coor1 | C1 | editingteacher | + | tutor2 | C1 | teacher | + | tutor3 | C1 | teacher | + | student1a | C1 | student | + | student1b | C1 | student | + | student2a | C1 | student | + | student2b | C1 | student | + | student3a | C1 | student | + | student3b | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + | Group 3 | C1 | G3 | + And the following "group members" exist: + | user | group | + | coor1 | G1 | + | tutor2 | G2 | + | tutor3 | G3 | + | student1a | G1 | + | student1b | G1 | + | student2a | G2 | + | student2b | G2 | + | student3a | G3 | + | student3b | G3 | + And the following "system role assigns" exist: + | user | role | + | manager1 | manager | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/site:accessallgroups | Prevent | teacher | Course | C1 | + | mod/scheduler:canseeotherteachersbooking | Allow | teacher | Course | C1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | schedulermode | maxbookings | + | scheduler | Tutor sessions | n | C1 | scheduler1 | 1 | oneonly | 0 | + And I add the upcoming events block globally + + @javascript + Scenario: A tutor adds slots, and students book them + When I log in as "tutor2" + And I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Tutor sessions" scheduler and I fill the form with: + | Location | My office | + Then I should see "10 slots have been added" + And I should see "2 students still need to make an appointment" + And I should see "Student 2a" in the "studentstoschedule" "table" + And I should see "Student 2b" in the "studentstoschedule" "table" + And I log out + + When I log in as "student2a" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should see "1:00 AM" in the "slotbookertable" "table" + And I should see "10:00 AM" in the "slotbookertable" "table" + When I click on "Book slot" "button" in the "2:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And I should see "Meeting with your Teacher, Tutor 2" in the "Upcoming events" "block" + And I log out + + When I log in as "student2b" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should see "1:00 AM" in the "slotbookertable" "table" + And I should not see "2:00 AM" in the "slotbookertable" "table" + And I should see "10:00 AM" in the "slotbookertable" "table" + When I click on "Book slot" "button" in the "5:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And I should see "Meeting with your Teacher, Tutor 2" in the "Upcoming events" "block" + And I log out + + When I log in as "tutor2" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should see "1:00 AM" in the "slotmanager" "table" + And I should see "Student 2a" in the "2:00 AM" "table_row" + And I should see "Student 2b" in the "5:00 AM" "table_row" + And I should see "10:00 AM" in the "slotmanager" "table" + And I should see "Meeting with your Student, Student 2a" in the "Upcoming events" "block" + And I should see "Meeting with your Student, Student 2b" in the "Upcoming events" "block" + And I should not see "students still need to make an appointment" + And I log out + + @javascript + Scenario: Several tutors add slots, they can be seen only by relevant users + When I log in as "coor1" + And I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Tutor sessions" scheduler and I fill the form with: + | Location | Office 1 | + Then I should see "10 slots have been added" + And I should see "2 students still need to make an appointment" + And I should see "Student 1a" in the "studentstoschedule" "table" + And I should see "Student 1b" in the "studentstoschedule" "table" + And I log out + + When I log in as "tutor2" + And I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Tutor sessions" scheduler and I fill the form with: + | Location | Office 2 | + Then I should see "10 slots have been added" + And I should see "2 students still need to make an appointment" + And I should see "Student 2a" in the "studentstoschedule" "table" + And I should see "Student 2b" in the "studentstoschedule" "table" + And I log out + + When I log in as "tutor3" + And I am on "Course 1" course homepage + And I add 10 slots 5 days ahead in "Tutor sessions" scheduler and I fill the form with: + | Location | Office 2 | + Then I should see "10 slots have been added" + And I should see "2 students still need to make an appointment" + And I should see "Student 3a" in the "studentstoschedule" "table" + And I should see "Student 3b" in the "studentstoschedule" "table" + And I log out + + When I log in as "student1a" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should see "1:00 AM" in the "slotbookertable" "table" + And I should see "10:00 AM" in the "slotbookertable" "table" + And I should see "Coordinator 1" in the "slotbookertable" "table" + And I should not see "Tutor 2" in the "slotbookertable" "table" + And I should not see "Tutor 3" in the "slotbookertable" "table" + When I click on "Book slot" "button" in the "1:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And I should see "Meeting with your Teacher, Coordinator 1" in the "Upcoming events" "block" + And I log out + + When I log in as "student2a" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should see "1:00 AM" in the "slotbookertable" "table" + And I should see "10:00 AM" in the "slotbookertable" "table" + And I should not see "Coordinator 1" in the "slotbookertable" "table" + And I should see "Tutor 2" in the "slotbookertable" "table" + And I should not see "Tutor 3" in the "slotbookertable" "table" + When I click on "Book slot" "button" in the "2:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And I should see "Meeting with your Teacher, Tutor 2" in the "Upcoming events" "block" + And I log out + + When I log in as "coor1" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should not see "Student 2a" in the "slotmanager" "table" + And I should see "Student 1a" in the "Upcoming events" "block" + And I should not see "Student 1b" in the "Upcoming events" "block" + And I should not see "Student 2a" in the "Upcoming events" "block" + When I follow "All appointments" + Then I should see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should see "Student 2a" in the "slotmanager" "table" + And I log out + + When I log in as "tutor2" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should not see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should see "Student 2a" in the "slotmanager" "table" + And I should not see "Student 1a" in the "Upcoming events" "block" + And I should see "Student 2a" in the "Upcoming events" "block" + And I should not see "Student 2b" in the "Upcoming events" "block" + When I follow "All appointments" + Then I should not see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should see "Student 2a" in the "slotmanager" "table" + And I log out + + When I log in as "tutor3" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + Then I should not see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should not see "Student 2a" in the "slotmanager" "table" + When I follow "All appointments" + Then I should not see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should not see "Student 2a" in the "slotmanager" "table" + And I log out + + When I log in as "manager1" + And I follow "Site home" + And I navigate to "Turn editing on" in current page administration + And I add the "Navigation" block if not present + And I click on "Courses" "link" in the "Navigation" "block" + And I am on "Course 1" course homepage + And I follow "Tutor sessions" + And I follow "Statistics" + And I follow "My appointments" + Then "slotmanager" "table" should not exist + And I should see "No students available for scheduling" + When I follow "All appointments" + Then I should see "Student 1a" in the "slotmanager" "table" + And I should not see "Student 1b" in the "slotmanager" "table" + And I should see "Student 2a" in the "slotmanager" "table" + And I log out \ No newline at end of file diff --git a/mod/scheduler/tests/behat/viewslots.feature b/mod/scheduler/tests/behat/viewslots.feature new file mode 100644 index 0000000..4a07df0 --- /dev/null +++ b/mod/scheduler/tests/behat/viewslots.feature @@ -0,0 +1,176 @@ +@mod @mod_scheduler +Feature: Students viewing slots available for booking + In order to view slots that are available for booking + As a student + I need to have appopriate permissions in the student screen. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | manager1 | Manager | 1 | manager1@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "system role assigns" exist: + | user | role | + | manager1 | manager | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | schedulermode | maxbookings | guardtime | + | scheduler | Test scheduler | n | C1 | scheduler1 | 0 | oneonly | 1 | 172800 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + # Slot 1 is available to only 1 student and is not yet booked + And I add a slot 5 days ahead at 0100 in "Test scheduler" scheduler and I fill the form with: + | exclusivity | 1 | + # Slot 2 is available to only 1 student and is already booked + And I add a slot 5 days ahead at 0200 in "Test scheduler" scheduler and I fill the form with: + | exclusivity | 1 | + | studentid[0] | Student 3 | + # Slot 3 is a group slot that is empty + And I add a slot 5 days ahead at 0300 in "Test scheduler" scheduler and I fill the form with: + | exclusivity | 3 | + # Slot 4 is a group slot that is partially booked + And I add a slot 5 days ahead at 0400 in "Test scheduler" scheduler and I fill the form with: + | exclusivity | 2 | + | studentid[0] | Student 3 | + # Slot 5 is an unlimited group slot that is empty + And I add a slot 5 days ahead at 0500 in "Test scheduler" scheduler and I fill the form with: + | exclusivityenable | 0 | + # Slot 6 is an unlimited group slot that is partially booked + And I add a slot 5 days ahead at 0600 in "Test scheduler" scheduler and I fill the form with: + | exclusivityenable | 0 | + | studentid[0] | Student 3 | + # Slot 7 is not yet available to students + And I add a slot 5 days ahead at 0700 in "Test scheduler" scheduler and I fill the form with: + | hideuntil[year] | 2040 | + # Slot 8 is no longer available since the it's too close in the future + And I add a slot 1 days ahead at 0800 in "Test scheduler" scheduler and I fill the form with: + | appointmentlocation | My office | + And I log out + + @javascript + Scenario: A student can see only available upcoming slots (default setting) + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then "Book slot" "button" should exist in the "1:00 AM" "table_row" + And I should not see "2:00 AM" in the "slotbookertable" "table" + And "Book slot" "button" should exist in the "3:00 AM" "table_row" + And "Book slot" "button" should exist in the "4:00 AM" "table_row" + And "Book slot" "button" should exist in the "5:00 AM" "table_row" + And "Book slot" "button" should exist in the "6:00 AM" "table_row" + And I should not see "7:00 AM" in the "slotbookertable" "table" + And I should not see "8:00 AM" in the "slotbookertable" "table" + + When I click on "Book slot" "button" in the "1:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And "slotbookertable" "table" should not exist + + When I click on "Cancel booking" "button" + Then "Book slot" "button" should exist in the "1:00 AM" "table_row" + + When I click on "Book slot" "button" in the "4:00 AM" "table_row" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then "Book slot" "button" should exist in the "3:00 AM" "table_row" + And I should not see "4:00 AM" in the "slotbookertable" "table" + And I log out + + @javascript + Scenario: Students can view all slots, even full ones + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | mod/scheduler:appoint | Allow | student | Course | C1 | + | mod/scheduler:viewslots | Allow | student | Course | C1 | + | mod/scheduler:viewfullslots | Allow | student | Course | C1 | + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then "Book slot" "button" should exist in the "1:00 AM" "table_row" + And I should see "2:00 AM" in the "slotbookertable" "table" + Then "Book slot" "button" should not exist in the "2:00 AM" "table_row" + And "Book slot" "button" should exist in the "3:00 AM" "table_row" + And "Book slot" "button" should exist in the "4:00 AM" "table_row" + And "Book slot" "button" should exist in the "5:00 AM" "table_row" + And "Book slot" "button" should exist in the "6:00 AM" "table_row" + And I should not see "7:00 AM" in the "slotbookertable" "table" + And I should not see "8:00 AM" in the "slotbookertable" "table" + + When I click on "Book slot" "button" in the "1:00 AM" "table_row" + Then "Cancel booking" "button" should exist + And "slotbookertable" "table" should exist + And I should not see "1:00 AM" in the "slotbookertable" "table" + And "Book slot" "button" should not exist + + When I click on "Cancel booking" "button" + Then "Book slot" "button" should exist in the "1:00 AM" "table_row" + + When I click on "Book slot" "button" in the "4:00 AM" "table_row" + And I log out + And I log in as "student2" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then "Book slot" "button" should exist in the "3:00 AM" "table_row" + And I should see "4:00 AM" in the "slotbookertable" "table" + And "Book slot" "button" should not exist in the "4:00 AM" "table_row" + And I log out + + @javascript + Scenario: Students can view all slots, but they cannot book any + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | mod/scheduler:appoint | Prevent | student | Course | C1 | + | mod/scheduler:viewslots | Allow | student | Course | C1 | + | mod/scheduler:viewfullslots | Allow | student | Course | C1 | + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then "Book slot" "button" should not exist + And I should see "1:00 AM" in the "slotbookertable" "table" + And I should see "2:00 AM" in the "slotbookertable" "table" + And I should see "3:00 AM" in the "slotbookertable" "table" + And I should see "4:00 AM" in the "slotbookertable" "table" + And I should see "5:00 AM" in the "slotbookertable" "table" + And I should see "6:00 AM" in the "slotbookertable" "table" + And I should not see "7:00 AM" in the "slotbookertable" "table" + And I should not see "8:00 AM" in the "slotbookertable" "table" + + And I log out + + @javascript + Scenario: Students can view bookable slots, but they cannot book any + Given the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | mod/scheduler:appoint | Prevent | student | Course | C1 | + | mod/scheduler:viewslots | Allow | student | Course | C1 | + | mod/scheduler:viewfullslots | Prevent | student | Course | C1 | + + When I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test scheduler" + Then "Book slot" "button" should not exist + And I should see "1:00 AM" in the "slotbookertable" "table" + And I should not see "2:00 AM" in the "slotbookertable" "table" + And I should see "3:00 AM" in the "slotbookertable" "table" + And I should see "4:00 AM" in the "slotbookertable" "table" + And I should see "5:00 AM" in the "slotbookertable" "table" + And I should see "6:00 AM" in the "slotbookertable" "table" + And I should not see "7:00 AM" in the "slotbookertable" "table" + And I should not see "8:00 AM" in the "slotbookertable" "table" + + And I log out \ No newline at end of file diff --git a/mod/scheduler/tests/fixtures/studentfile.txt b/mod/scheduler/tests/fixtures/studentfile.txt new file mode 100644 index 0000000..4f94cd1 --- /dev/null +++ b/mod/scheduler/tests/fixtures/studentfile.txt @@ -0,0 +1 @@ +Test file to be uploaded by a student diff --git a/mod/scheduler/tests/generator/lib.php b/mod/scheduler/tests/generator/lib.php new file mode 100644 index 0000000..b9749d7 --- /dev/null +++ b/mod/scheduler/tests/generator/lib.php @@ -0,0 +1,123 @@ +<?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/>. + +/** + * mod_scheduler data generator + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Scheduler module PHPUnit data generator class + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_generator extends testing_module_generator { + + /** + * set default + * + * @param stdClass $record + * @param string $property + * @param mixed $value + */ + private function set_default($record, $property, $value) { + if (!isset($record->$property)) { + $record->$property = $value; + } + } + + /** + * Create new scheduler module instance + * @param array|stdClass $record + * @param array $options + * @return stdClass activity record with extra cmid field + */ + public function create_instance($record = null, array $options = null) { + global $CFG, $DB; + require_once("$CFG->dirroot/mod/scheduler/lib.php"); + + $this->instancecount++; + $i = $this->instancecount; + + $record = (object)(array)$record; + $options = (array)$options; + + if (empty($record->course)) { + throw new coding_exception('module generator requires $record->course'); + } + self::set_default($record, 'name', get_string('pluginname', 'scheduler').' '.$i); + self::set_default($record, 'intro', 'Test scheduler '.$i); + self::set_default($record, 'introformat', FORMAT_MOODLE); + self::set_default($record, 'schedulermode', 'onetime'); + self::set_default($record, 'guardtime', 0); + self::set_default($record, 'defaultslotduration', 15); + self::set_default($record, 'staffrolename', ''); + self::set_default($record, 'scale', 0); + if (isset($options['idnumber'])) { + $record->cmidnumber = $options['idnumber']; + } else { + $record->cmidnumber = ''; + } + + $record->coursemodule = $this->precreate_course_module($record->course, $options); + $id = scheduler_add_instance($record); + $modinst = $this->post_add_instance($id, $record->coursemodule); + + if (isset($options['slottimes'])) { + $slottimes = (array) $options['slottimes']; + foreach ($slottimes as $slotkey => $time) { + $slot = new stdClass(); + $slot->schedulerid = $id; + $slot->starttime = $time; + $slot->duration = 10; + $slot->teacherid = isset($options['slotteachers'][$slotkey]) ? $options['slotteachers'][$slotkey] : 2; // Admin user as default. + $slot->appointmentlocation = 'Test Loc'; + $slot->timemodified = time(); + $slot->notes = ''; + $slot->slotnote = ''; + $slot->exclusivity = isset($options['slotexclusivity'][$slotkey]) ? $options['slotexclusivity'][$slotkey] : 0; + $slot->emaildate = 0; + $slot->hideuntil = 0; + $slotid = $DB->insert_record('scheduler_slots', $slot); + + if (isset($options['slotstudents'][$slotkey])) { + $students = (array)$options['slotstudents'][$slotkey]; + foreach ($students as $studentkey => $userid) { + $appointment = new stdClass(); + $appointment->slotid = $slotid; + $appointment->studentid = $userid; + $appointment->attended = isset($options['slotattended'][$slotkey]) && $options['slotattended'][$slotkey]; + $appointment->grade = 0; + $appointment->appointmentnote = ''; + $appointment->teachernote = ''; + $appointment->timecreated = time(); + $appointment->timemodified = time(); + $appointmentid = $DB->insert_record('scheduler_appointment', $appointment); + } + } + } + } + + return $modinst; + } +} diff --git a/mod/scheduler/tests/model_test.php b/mod/scheduler/tests/model_test.php new file mode 100644 index 0000000..7a1111e --- /dev/null +++ b/mod/scheduler/tests/model_test.php @@ -0,0 +1,149 @@ +<?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/>. + +/** + * Unit tests for the MVC model classes + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\appointment_factory; + +global $CFG; +require_once($CFG->dirroot . '/mod/scheduler/locallib.php'); + +/** + * Unit tests for the MVC model classes + * + * @group mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_model_testcase extends advanced_testcase { + + /** + * @var int Course_modules id used for testing + */ + protected $moduleid; + + /** + * @var int Course id used for testing + */ + protected $courseid; + + /** + * @var int Scheduler id used for testing + */ + protected $schedulerid; + + /** + * @var int User id used for testing + */ + protected $userid; + + protected function setUp() { + global $DB, $CFG; + + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $options = array(); + $options['slottimes'] = array(); + $options['slotstudents'] = array(); + for ($c = 0; $c < 4; $c++) { + $options['slottimes'][$c] = time() + ($c + 1) * DAYSECS; + $options['slotstudents'][$c] = array($this->getDataGenerator()->create_user()->id); + } + $options['slottimes'][4] = time() + 10 * DAYSECS; + $options['slottimes'][5] = time() + 11 * DAYSECS; + $options['slotstudents'][5] = array( + $this->getDataGenerator()->create_user()->id, + $this->getDataGenerator()->create_user()->id + ); + + $scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); + $coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid)); + + $this->schedulerid = $scheduler->id; + $this->moduleid = $coursemodule->id; + $this->courseid = $coursemodule->course; + $this->userid = 2; // Admin user. + } + + /** + * Test loading a scheduler instance from the database + */ + public function test_scheduler() { + global $DB; + + $dbdata = $DB->get_record('scheduler', array('id' => $this->schedulerid)); + + $instance = scheduler::load_by_coursemodule_id($this->moduleid); + + $this->assertEquals( $dbdata->name, $instance->get_name()); + + } + + /** + * Test the "appointment" data object + * (basic functionality, with minimal reference to slots) + **/ + public function test_appointment() { + + global $DB; + + $instance = scheduler::load_by_coursemodule_id($this->moduleid); + $slot = array_values($instance->get_slots())[0]; + $factory = new appointment_factory($slot); + + $user = $this->getdataGenerator()->create_user(); + + $app0 = new stdClass(); + $app0->slotid = 1; + $app0->studentid = $user->id; + $app0->attended = 0; + $app0->grade = 0; + $app0->appointmentnote = 'testnote'; + $app0->teachernote = 'confidentialtestnote'; + $app0->timecreated = time(); + $app0->timemodified = time(); + + $id1 = $DB->insert_record('scheduler_appointment', $app0); + + $appobj = $factory->create_from_id($id1); + $this->assertEquals($user->id, $appobj->studentid); + $this->assertEquals(fullname($user), fullname($appobj->get_student())); + $this->assertFalse($appobj->is_attended()); + $this->assertEquals(0, $appobj->grade); + + $app0->attended = 1; + $app0->grade = -7; + $id2 = $DB->insert_record('scheduler_appointment', $app0); + + $appobj = $factory->create_from_id($id2); + $this->assertEquals($user->id, $appobj->studentid); + $this->assertEquals(fullname($user), fullname($appobj->get_student())); + $this->assertTrue($appobj->is_attended()); + $this->assertEquals(-7, $appobj->grade); + + } + +} diff --git a/mod/scheduler/tests/permissions_test.php b/mod/scheduler/tests/permissions_test.php new file mode 100644 index 0000000..138cbc5 --- /dev/null +++ b/mod/scheduler/tests/permissions_test.php @@ -0,0 +1,296 @@ +<?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/>. + +/** + * Unit tests for scheduler permissions + * + * @package mod_scheduler + * @copyright 2019 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; +use \mod_scheduler\permission\scheduler_permissions; + +global $CFG; +require_once($CFG->dirroot . '/mod/scheduler/locallib.php'); + +/** + * Unit tests for the scheduler_permissions class. + * + * @group mod_scheduler + * @copyright 2019 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_permissions_testcase extends advanced_testcase { + + /** + * @var int Course_modules id used for testing + */ + protected $moduleid; + + /** + * @var int Course id used for testing + */ + protected $courseid; + + /** + * @var scheduler Scheduler used for testing + */ + protected $scheduler; + + /** + * @var \context context of the scheduler instance + */ + protected $context; + + /** + * @var int User id of teacher used for testing + */ + protected $edteacher; + /** + * @var int User id of nonediting teacher used for testing + */ + protected $nonedteacher; + + /** + * @var int User id of administrator used for testing + */ + protected $administ; + + /** + * @var slot[] slots used for testing + */ + protected $slots; + + /** + * @var \mod_scheduler\model\appointment[] + */ + protected $appts; + + /** + * @var int[] id of students used for testing + */ + protected $students; + + /** + * Sets up the test case. Common situation for all tests: + * + * - One scheduler in a course + * - Three slots are created for different students + * - There is one editing teacher, with default permissions, who is assigned to slot 1 + * - There is one nonediting teacher, with default permissions, who is assigned to slot 2 + * - There is one "administrator", a user with a custom role that allows only viewing, but not editing, the slots. + */ + protected function setUp() { + global $DB, $CFG; + + $dg = $this->getDataGenerator(); + + $this->resetAfterTest(false); + + $course = $dg->create_course(); + + $this->students = array(); + for ($i = 0; $i < 3; $i++) { + $this->students[$i] = $dg->create_user()->id; + $dg->enrol_user($this->students[$i], $course->id, 'student'); + } + + // An editing teacher. + $this->edteacher = $dg->create_user()->id; + $dg->enrol_user($this->edteacher, $course->id, 'editingteacher'); + + // A nonediting teacher. + $this->nonedteacher = $dg->create_user()->id; + $dg->enrol_user($this->nonedteacher, $course->id, 'teacher'); + + // An administrator. + $adminrole = $dg->create_role(); + assign_capability('mod/scheduler:canseeotherteachersbooking', CAP_ALLOW, $adminrole, \context_system::instance()->id); + $this->administ = $dg->create_user()->id; + $dg->enrol_user($this->administ, $course->id, $adminrole); + + $options = array(); + $options['slottimes'] = [time() + DAYSECS, time() + 2 * DAYSECS, time() + 3 * DAYSECS]; + $options['slotstudents'] = array_values($this->students); + $options['slotteachers'] = [$this->edteacher, $this->nonedteacher]; + + $schedrec = $this->getDataGenerator()->create_module('scheduler', ['course' => $course->id], $options); + $coursemodule = $DB->get_record('course_modules', array('id' => $schedrec->cmid)); + $this->scheduler = scheduler::load_by_coursemodule_id($coursemodule->id); + + $this->moduleid = $coursemodule->id; + $this->courseid = $coursemodule->course; + $this->context = $this->scheduler->context; + $slotids = array_keys($DB->get_records('scheduler_slots', array('schedulerid' => $this->scheduler->id), 'starttime ASC')); + $this->slots = array(); + $this->appts = array(); + foreach ($slotids as $key => $id) { + $this->slots[$key] = $this->scheduler->get_slot($id); + $this->appts[$key] = array_values($this->slots[$key]->get_appointments())[0]; + } + } + + + public function test_teacher_can_see_slot() { + + // Editing teacher sees all slots. + $p = new scheduler_permissions($this->context, $this->edteacher); + $this->assertTrue($p->teacher_can_see_slot($this->slots[0])); + $this->assertTrue($p->teacher_can_see_slot($this->slots[1])); + $this->assertTrue($p->teacher_can_see_slot($this->slots[2])); + + // Nonediting teacher sees only his own slot. + $p = new scheduler_permissions($this->context, $this->nonedteacher); + $this->assertFalse($p->teacher_can_see_slot($this->slots[0])); + $this->assertTrue ($p->teacher_can_see_slot($this->slots[1])); + $this->assertFalse($p->teacher_can_see_slot($this->slots[2])); + + // Adminstrator sees all slots. + $p = new scheduler_permissions($this->context, $this->administ); + $this->assertTrue($p->teacher_can_see_slot($this->slots[0])); + $this->assertTrue($p->teacher_can_see_slot($this->slots[1])); + $this->assertTrue($p->teacher_can_see_slot($this->slots[2])); + + // Student don't ever see the teacher side of things. + $p = new scheduler_permissions($this->context, $this->students[1]); + $this->assertFalse($p->teacher_can_see_slot($this->slots[0])); + $this->assertFalse($p->teacher_can_see_slot($this->slots[1])); + $this->assertFalse($p->teacher_can_see_slot($this->slots[2])); + + } + + public function test_can_edit_slot() { + + // Editing teacher can edit all slots. + $p = new scheduler_permissions($this->context, $this->edteacher); + $this->assertTrue($p->can_edit_slot($this->slots[0])); + $this->assertTrue($p->can_edit_slot($this->slots[1])); + $this->assertTrue($p->can_edit_slot($this->slots[2])); + + // Nonediting teacher can only edit his own slot. + $p = new scheduler_permissions($this->context, $this->nonedteacher); + $this->assertFalse($p->can_edit_slot($this->slots[0])); + $this->assertTrue ($p->can_edit_slot($this->slots[1])); + $this->assertFalse($p->can_edit_slot($this->slots[2])); + + // Adminstrator cannot edit any slots. + $p = new scheduler_permissions($this->context, $this->administ); + $this->assertFalse($p->can_edit_slot($this->slots[0])); + $this->assertFalse($p->can_edit_slot($this->slots[1])); + $this->assertFalse($p->can_edit_slot($this->slots[2])); + + // Student can't ever edit slots. + $p = new scheduler_permissions($this->context, $this->students[1]); + $this->assertFalse($p->can_edit_slot($this->slots[0])); + $this->assertFalse($p->can_edit_slot($this->slots[1])); + $this->assertFalse($p->can_edit_slot($this->slots[2])); + + } + + public function test_can_edit_own_slots() { + + // Both teachers can edit their own slots. + $p = new scheduler_permissions($this->context, $this->edteacher); + $this->assertTrue($p->can_edit_own_slots()); + $p = new scheduler_permissions($this->context, $this->nonedteacher); + $this->assertTrue($p->can_edit_own_slots()); + + // Adminstrator and student cannot edit any slots. + $p = new scheduler_permissions($this->context, $this->administ); + $this->assertFalse($p->can_edit_own_slots()); + $p = new scheduler_permissions($this->context, $this->students[1]); + $this->assertFalse($p->can_edit_own_slots()); + + } + + public function test_can_edit_all_slots() { + + // Editing teachers can edit all slots. + $p = new scheduler_permissions($this->context, $this->edteacher); + $this->assertTrue($p->can_edit_all_slots()); + + // Nonediting teacher, adminstrator and student cannot edit all slots. + $p = new scheduler_permissions($this->context, $this->nonedteacher); + $this->assertFalse($p->can_edit_all_slots()); + $p = new scheduler_permissions($this->context, $this->administ); + $this->assertFalse($p->can_edit_all_slots()); + $p = new scheduler_permissions($this->context, $this->students[1]); + $this->assertFalse($p->can_edit_all_slots()); + + } + + + public function test_can_see_all_slots() { + + // Editing teachers can see all slots. + $p = new scheduler_permissions($this->context, $this->edteacher); + $this->assertTrue($p->can_see_all_slots()); + + // Nonediting teacher cannot see all slots. + $p = new scheduler_permissions($this->context, $this->nonedteacher); + $this->assertFalse($p->can_see_all_slots()); + + // Administrator can see (though not edit) all slots. + $p = new scheduler_permissions($this->context, $this->administ); + $this->assertTrue($p->can_see_all_slots()); + + // Students cannot see all slots. + $p = new scheduler_permissions($this->context, $this->students[1]); + $this->assertFalse($p->can_see_all_slots()); + + } + + + public function test_can_see_appointment() { + + // Editing teacher can all appointments. + $p = new scheduler_permissions($this->context, $this->edteacher); + $this->assertTrue($p->can_see_appointment($this->appts[0])); + $this->assertTrue($p->can_see_appointment($this->appts[1])); + $this->assertTrue($p->can_see_appointment($this->appts[2])); + + // Nonediting teacher can only see his own appointment. + $p = new scheduler_permissions($this->context, $this->nonedteacher); + $this->assertFalse($p->can_see_appointment($this->appts[0])); + $this->assertTrue ($p->can_see_appointment($this->appts[1])); + $this->assertFalse($p->can_see_appointment($this->appts[2])); + + // Administrator can see all appointments. + $p = new scheduler_permissions($this->context, $this->administ); + $this->assertTrue($p->can_see_appointment($this->appts[0])); + $this->assertTrue($p->can_see_appointment($this->appts[1])); + $this->assertTrue($p->can_see_appointment($this->appts[2])); + + // Student can see only his own appointment. + for ($i = 0; $i < 3; $i++) { + $p = new scheduler_permissions($this->context, $this->students[$i]); + for ($j = 0; $j < 3; $j++) { + $actual = $p->can_see_appointment($this->appts[$j]); + $expected = ($i == $j); + $msg = "Student $i with id {$this->students[$i]} tested on appointment $j booked by {$this->appts[$j]->studentid}"; + $this->assertEquals($expected, $actual, $msg); + } + } + + } + + +} diff --git a/mod/scheduler/tests/privacy_test.php b/mod/scheduler/tests/privacy_test.php new file mode 100644 index 0000000..16bbe88 --- /dev/null +++ b/mod/scheduler/tests/privacy_test.php @@ -0,0 +1,240 @@ +<?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/>. + +/** + * Data provider tests. + * + * @package mod_scheduler + * @category test + * @copyright 2018 Henning Bostelmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_privacy\tests\provider_testcase; +use mod_scheduler\privacy\provider; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\writer; + +require_once($CFG->dirroot.'/mod/scheduler/locallib.php'); + +/** + * Data provider testcase class. + * + * @group mod_scheduler + * @copyright 2018 Henning Bostelmann + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_privacy_testcase extends provider_testcase { + + /** + * @var int course_module id used for testing + */ + protected $moduleid; + + /** + * @var the module context used for testing + */ + protected $context; + + /** + * @var int Course id used for testing + */ + protected $courseid; + + /** + * @var int Scheduler id used for testing + */ + protected $schedulerid; + + /** + * @var int One of the slots used for testing + */ + protected $slotid; + + /** + * @var int first student used in testing - a student that has an appointment + */ + protected $student1; + + /** + * @var int second student used in testing - a student that has an appointment + */ + protected $student2; + + /** + * @var array all students (only id) involved in the scheduler + */ + protected $allstudents; + + protected function setUp() { + global $DB, $CFG; + + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $this->courseid = $course->id; + + $this->student1 = $this->getDataGenerator()->create_user(); + $this->student2 = $this->getDataGenerator()->create_user(); + $this->allstudents = [$this->student1->id, $this->student2->id]; + + $options = array(); + $options['slottimes'] = array(); + $options['slotstudents'] = array(); + for ($c = 0; $c < 4; $c++) { + $options['slottimes'][$c] = time() + ($c + 1) * DAYSECS; + $stud = $this->getDataGenerator()->create_user()->id; + $this->allstudents[] = $stud; + $options['slotstudents'][$c] = array($stud); + } + $options['slottimes'][4] = time() + 10 * DAYSECS; + $options['slottimes'][5] = time() + 11 * DAYSECS; + $options['slotstudents'][5] = array( + $this->student1->id, + $this->student2->id + ); + + $scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); + $coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid)); + + $this->schedulerid = $scheduler->id; + $this->moduleid = $coursemodule->id; + $this->context = context_module::instance($scheduler->cmid); + + $recs = $DB->get_records('scheduler_slots', array('schedulerid' => $scheduler->id), 'id DESC'); + $this->slotid = array_keys($recs)[0]; + $this->appointmentids = array_keys($DB->get_records('scheduler_appointment', array('slotid' => $this->slotid))); + } + + /** + * Asserts whether or not an appointment exists in a scheduler for a certian student. + * + * @param int $schedulerid the id of the scheduler to test + * @param int $studentid the user id of the student to test + * @param boolean $expected whether an appointment is expected to exist or not + */ + private function assert_appointment_status($schedulerid, $studentid, $expected) { + global $DB; + + $sql = "SELECT * FROM {scheduler} s + JOIN {scheduler_slots} t ON t.schedulerid = s.id + JOIN {scheduler_appointment} a ON a.slotid = t.id + WHERE s.id = :schedulerid AND a.studentid = :studentid"; + + $params = array('schedulerid' => $schedulerid, 'studentid' => $studentid); + $actual = $DB->record_exists_sql($sql, $params); + $this->assertEquals($expected, $actual, "Checking whether student $studentid has appointment in scheduler $schedulerid"); + } + + /** + * Test getting the contexts for a user. + */ + public function test_get_contexts_for_userid() { + + // Get contexts for the first user. + $contextids = provider::get_contexts_for_userid($this->student1->id)->get_contextids(); + $this->assertEquals([$this->context->id], $contextids, '', 0.0, 10, true); + } + + /** + * Test getting the users within a context. + */ + public function test_get_users_in_context() { + global $DB; + $component = 'mod_scheduler'; + + // Ensure userlist for context contains all users. + $userlist = new \core_privacy\local\request\userlist($this->context, $component); + provider::get_users_in_context($userlist); + + $expected = $this->allstudents; + $expected[] = 2; // The teacher involved. + $actual = $userlist->get_userids(); + sort($expected); + sort($actual); + $this->assertEquals($expected, $actual); + } + + + /** + * Export test for teacher data. + */ + public function test_export_teacher_data() { + global $DB; + + // Export all contexts for the teacher. + $contextids = [$this->context->id]; + $teacher = $DB->get_record('user', array('id' => 2)); + $appctx = new approved_contextlist($teacher, 'mod_scheduler', $contextids); + provider::export_user_data($appctx); + $data = writer::with_context($this->context)->get_data([]); + $this->assertNotEmpty($data); + } + + /** + * Export test for student1's data. + */ + public function test_export_user_data1() { + + // Export all contexts for the first user. + $contextids = [$this->context->id]; + $appctx = new approved_contextlist($this->student1, 'mod_scheduler', $contextids); + provider::export_user_data($appctx); + $data = writer::with_context($this->context)->get_data([]); + $this->assertNotEmpty($data); + } + + /** + * Test for delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + provider::delete_data_for_all_users_in_context($this->context); + + foreach ($this->allstudents as $u) { + $this->assert_appointment_status($this->schedulerid, $u, false); + } + } + + /** + * Test for delete_data_for_user(). + */ + public function test_delete_data_for_user() { + $appctx = new approved_contextlist($this->student1, 'mod_scheduler', [$this->context->id]); + provider::delete_data_for_user($appctx); + + $this->assert_appointment_status($this->schedulerid, $this->student1->id, false); + $this->assert_appointment_status($this->schedulerid, $this->student2->id, true); + + } + + /** + * Test for delete_data_for_users(). + */ + public function test_delete_data_for_users() { + $component = 'mod_scheduler'; + + $approveduserids = [$this->student1->id, $this->student2->id]; + $approvedlist = new approved_userlist($this->context, $component, $approveduserids); + provider::delete_data_for_users($approvedlist); + + $this->assert_appointment_status($this->schedulerid, $this->student1->id, false); + $this->assert_appointment_status($this->schedulerid, $this->student2->id, false); + } +} diff --git a/mod/scheduler/tests/scheduler_test.php b/mod/scheduler/tests/scheduler_test.php new file mode 100644 index 0000000..175baa5 --- /dev/null +++ b/mod/scheduler/tests/scheduler_test.php @@ -0,0 +1,511 @@ +<?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/>. + +/** + * Unit tests for the scheduler class. + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; +use \mod_scheduler\model\appointment; + +global $CFG; +require_once($CFG->dirroot . '/mod/scheduler/locallib.php'); + +/** + * Unit tests for the scheduler class. + * + * @group mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_scheduler_testcase extends advanced_testcase { + + /** + * @var int Course_module id used for testing + */ + protected $moduleid; + + /** + * @var int Course id used for testing + */ + protected $courseid; + + /** + * @var int Scheduler id used for testing + */ + protected $schedulerid; + + /** + * @var int One of the slots used for testing + */ + protected $slotid; + + protected function setUp() { + global $DB, $CFG; + + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $this->courseid = $course->id; + + $options = array(); + $options['slottimes'] = array(); + $options['slotstudents'] = array(); + for ($c = 0; $c < 4; $c++) { + $options['slottimes'][$c] = time() + ($c + 1) * DAYSECS; + $options['slotstudents'][$c] = array($this->getDataGenerator()->create_user()->id); + } + $options['slottimes'][4] = time() + 10 * DAYSECS; + $options['slottimes'][5] = time() + 11 * DAYSECS; + $options['slotstudents'][5] = array( + $this->getDataGenerator()->create_user()->id, + $this->getDataGenerator()->create_user()->id + ); + + $scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); + $coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid)); + + $this->schedulerid = $scheduler->id; + $this->moduleid = $coursemodule->id; + + $recs = $DB->get_records('scheduler_slots', array('schedulerid' => $scheduler->id), 'id DESC'); + $this->slotid = array_keys($recs)[0]; + $this->appointmentids = array_keys($DB->get_records('scheduler_appointment', array('slotid' => $this->slotid))); + } + + /** + * Create a student record and enrol him in a course. + * + * @param int $courseid + * @return int user id + */ + private function create_student($courseid = 0) { + if ($courseid == 0) { + $courseid = $this->courseid; + } + $userid = $this->getDataGenerator()->create_user()->id; + $this->getDataGenerator()->enrol_user($userid, $courseid); + return $userid; + } + + /** + * Assert a record count in the database. + * + * @param string $table table name to test + * @param string $field field name + * @param string $value value to look for + * @param int $expect expected record count where that field has that value + */ + private function assert_record_count($table, $field, $value, $expect) { + global $DB; + + $act = $DB->count_records($table, array($field => $value)); + $this->assertEquals($expect, $act, "Checking whether table $table has $expect records with $field equal to $value"); + } + + /** + * Test a scheduler instance + */ + public function test_scheduler() { + global $DB; + + $dbdata = $DB->get_record('scheduler', array('id' => $this->schedulerid)); + + $instance = scheduler::load_by_coursemodule_id($this->moduleid); + + $this->assertEquals($dbdata->name, $instance->get_name()); + + } + + /** + * Test the loading of slots + */ + public function test_load_slots() { + global $DB; + + $instance = scheduler::load_by_coursemodule_id($this->moduleid); + + /* test slot retrieval */ + + $slotcount = $instance->get_slot_count(); + $this->assertEquals(6, $slotcount); + + $slots = $instance->get_all_slots(2, 3); + $this->assertEquals(3, count($slots)); + + $slots = $instance->get_slots_without_appointment(); + $this->assertEquals(1, count($slots)); + + $allslots = $instance->get_all_slots(); + $this->assertEquals(6, count($allslots)); + + $cnt = 0; + foreach ($allslots as $slot) { + $this->assertTrue($slot instanceof slot); + + if ($cnt == 5) { + $expectedapp = 2; + } else if ($cnt == 4) { + $expectedapp = 0; + } else { + $expectedapp = 1; + } + $this->assertEquals($expectedapp, $slot->get_appointment_count()); + + $apps = $slot->get_appointments(); + $this->assertEquals($expectedapp, count($apps)); + + foreach ($apps as $app) { + $this->assertTrue($app instanceof appointment); + } + $cnt++; + } + + } + + /** + * Test adding slots to a scheduler + */ + public function test_add_slot() { + + $scheduler = scheduler::load_by_coursemodule_id($this->moduleid); + + $newslot = $scheduler->create_slot(); + $newslot->teacherid = $this->getDataGenerator()->create_user()->id; + $newslot->starttime = time() + MINSECS; + $newslot->duration = 10; + + $allslots = $scheduler->get_slots(); + $this->assertEquals(7, count($allslots)); + + $scheduler->save(); + + } + + /** + * Test deleting a scheduler + */ + public function test_delete_scheduler() { + + $options = array(); + $options['slottimes'] = array(); + $options['slotstudents'] = array(); + for ($c = 0; $c < 10; $c++) { + $options['slottimes'][$c] = time() + ($c + 1) * DAYSECS; + $options['slotstudents'][$c] = array($this->getDataGenerator()->create_user()->id); + } + + $delrec = $this->getDataGenerator()->create_module('scheduler', array('course' => $this->courseid), $options); + $delid = $delrec->id; + + $delsched = scheduler::load_by_id($delid); + + $this->assert_record_count('scheduler', 'id', $this->schedulerid, 1); + $this->assert_record_count('scheduler_slots', 'schedulerid', $this->schedulerid, 6); + $this->assert_record_count('scheduler_appointment', 'slotid', $this->slotid, 2); + + $this->assert_record_count('scheduler', 'id', $delid, 1); + $this->assert_record_count('scheduler_slots', 'schedulerid', $delid, 10); + + $delsched->delete(); + + $this->assert_record_count('scheduler', 'id', $this->schedulerid, 1); + $this->assert_record_count('scheduler_slots', 'schedulerid', $this->schedulerid, 6); + $this->assert_record_count('scheduler_appointment', 'slotid', $this->slotid, 2); + + $this->assert_record_count('scheduler', 'id', $delid, 0); + $this->assert_record_count('scheduler_slots', 'schedulerid', $delid, 0); + + } + + /** + * Assert that slot times have certain values + * @param array $expected list of expected slots + * @param array $actual list of actual slots + * @param array $options expected attributes of slots + * @param string $message + */ + private function assert_slot_times($expected, $actual, $options, $message) { + $this->assertEquals(count($expected), count($actual), "Slot count - $message"); + $slottimes = array(); + foreach ($expected as $e) { + $slottimes[] = $options['slottimes'][$e]; + } + foreach ($actual as $a) { + $this->assertTrue( in_array($a->starttime, $slottimes), "Slot at {$a->starttime} - $message"); + } + } + + /** + * Check slots in the scheduler for certain patterns. + * + * @param int $schedulerid id of the scheduler + * @param unknown $studentid + * @param array $slotoptions expected attributes of slots + * @param array $expattended which slots are expected to be "attended" + * @param array $expupcoming which slots are expected to be "upcoming" + * @param unknown $expavailable which slots are expected to be "available" (including already booked ones) + * @param unknown $expbookable which slots are expected to be "bookable" + */ + private function check_timed_slots($schedulerid, $studentid, $slotoptions, + $expattended, $expupcoming, $expavailable, $expbookable) { + + $sched = scheduler::load_by_id($schedulerid); + + $attended = $sched->get_attended_slots_for_student($studentid); + $this->assert_slot_times($expattended, $attended, $slotoptions, 'Attended slots'); + + $upcoming = $sched->get_upcoming_slots_for_student($studentid); + $this->assert_slot_times($expupcoming, $upcoming, $slotoptions, 'Upcoming slots'); + + $available = $sched->get_slots_available_to_student($studentid, false); + $this->assert_slot_times($expavailable, $available, $slotoptions, 'Available slots (incl. booked)'); + + $bookable = $sched->get_slots_available_to_student($studentid, true); + $this->assert_slot_times($expbookable, $bookable, $slotoptions, 'Booked slots'); + + } + + /** + * Test slot timings when parameters of the scheduler are altered. + */ + public function test_load_slot_timing() { + + global $DB; + + $currentstud = $this->getDataGenerator()->create_user()->id; + $otherstud = $this->getDataGenerator()->create_user()->id; + + $options = array(); + $options['slottimes'] = array(); + $options['slotstudents'] = array(); + $options['slotattended'] = array(); + + // Create slots 0 to 5, n days in the future, booked by the student but not attended. + for ($c = 0; $c <= 5; $c++) { + $options['slottimes'][$c] = time() + $c * DAYSECS + 12 * HOURSECS; + $options['slotstudents'][$c] = $currentstud; + $options['slotattended'][$c] = false; + } + + // Create slot 6, located in the past, booked by the student but not attended. + $options['slottimes'][6] = time() - 3 * DAYSECS; + $options['slotstudents'][6] = $currentstud; + $options['slotattended'][6] = false; + + // Create slot 7, located in the past, booked by the student and attended. + $options['slottimes'][7] = time() - 4 * DAYSECS; + $options['slotstudents'][7] = $currentstud; + $options['slotattended'][7] = true; + + // Create slot 8, located less than one day in the future but marked attended. + $options['slottimes'][8] = time() + 8 * HOURSECS; + $options['slotstudents'][8] = $currentstud; + $options['slotattended'][8] = true; + + // Create slot 9, located in the future but already booked by another student. + $options['slottimes'][9] = time() + 10 * DAYSECS + 9 * HOURSECS; + $options['slotstudents'][9] = $otherstud; + $options['slotattended'][9] = false; + $options['slotexclusivity'][9] = 1; + + // Create slots 10 to 14, (n-10) days in the future, open for booking. + for ($c = 10; $c <= 14; $c++) { + $options['slottimes'][$c] = time() + ($c - 10) * DAYSECS + 10 * HOURSECS; + } + + $schedrec = $this->getDataGenerator()->create_module('scheduler', array('course' => $this->courseid), $options); + $schedid = $schedrec->id; + + $schedrec->guardtime = 0; + $DB->update_record('scheduler', $schedrec); + + $this->check_timed_slots($schedid, $currentstud, $options, + array(7, 8), + array(0, 1, 2, 3, 4, 5, 6), + array(10, 11, 12, 13, 14), + array(10, 11, 12, 13, 14, 9) ); + + $schedrec->guardtime = DAYSECS; + $DB->update_record('scheduler', $schedrec); + + $this->check_timed_slots($schedid, $currentstud, $options, + array(7, 8), + array(0, 1, 2, 3, 4, 5, 6), + array(11, 12, 13, 14), + array(11, 12, 13, 14, 9) ); + + $schedrec->guardtime = 4 * DAYSECS; + $DB->update_record('scheduler', $schedrec); + + $this->check_timed_slots($schedid, $currentstud, $options, + array(7, 8), + array(0, 1, 2, 3, 4, 5, 6), + array(14), + array(14, 9) ); + + $schedrec->guardtime = 20 * DAYSECS; + $DB->update_record('scheduler', $schedrec); + + $this->check_timed_slots($schedid, $currentstud, $options, + array(7, 8), + array(0, 1, 2, 3, 4, 5, 6), + array(), + array() ); + + } + + /** + * Assert the number of appointments for a student with certain properties. + * + * @param int $expectedwithchangeables expected number of bookable appointments, including changeable ones + * @param int $expectedwithoutchangeables expected number of bookable appointments, excluding changeable ones + * @param int $schedid scheduler id + * @param int $studentid student id + */ + private function assert_bookable_appointments($expectedwithchangeables, $expectedwithoutchangeables, + $schedid, $studentid) { + $scheduler = scheduler::load_by_id($schedid); + + $actualwithchangeables = $scheduler->count_bookable_appointments($studentid, true); + $this->assertEquals($expectedwithchangeables, $actualwithchangeables, + 'Checking number of bookable appointments (including changeable bookings)'); + + $actualwithoutchangeables = $scheduler->count_bookable_appointments($studentid, false); + $this->assertEquals($expectedwithoutchangeables, $actualwithoutchangeables, + 'Checking number of bookable appointments (excluding changeable bookings)'); + + $studs = $scheduler->get_students_for_scheduling(); + if ($expectedwithoutchangeables != 0) { + $this->assertTrue(is_array($studs), 'Checking that get_students_for_scheduling returns an array'); + } + $actualnum = count($studs); + $expectednum = ($expectedwithoutchangeables > 0) ? 3 : 2; + $this->assertEquals($expectednum, $actualnum, 'Checking number of students available for scheduling'); + } + + /** + * Creates a scheduler with certain settings, + * having 10 appointments, from 1 hour in the future to 9 days, 1 hour in the future, + * and booking a given student into these slots - either unattended bookings ($bookedslots) + * or attended bookings ($attendedslots). + * + * The scheduler is created in a new course, into which the given student is enrolled. + * Also, two other students (without any slot bookings) is created in the course. + * + * @param int $schedulermode scheduler mode + * @param int $maxbookings max number of bookings per student + * @param int $guardtime guard time + * @param int $studentid student to book into slots + * @param array $bookedslots slots to book the student in + * @param array $attendedslots slots which the student has attended + */ + private function create_data_for_bookable_appointments($schedulermode, $maxbookings, $guardtime, $studentid, + array $bookedslots, array $attendedslots) { + + global $DB; + + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($studentid, $course->id); + + $options['slottimes'] = array(); + for ($c = 0; $c < 10; $c++) { + $options['slottimes'][$c] = time() + $c * DAYSECS + HOURSECS; + if (in_array($c, $bookedslots) || in_array($c, $attendedslots)) { + $options['slotstudents'][$c] = $studentid; + } + } + + $schedrec = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); + + $scheduler = scheduler::load_by_id($schedrec->id); + + $scheduler->schedulermode = $schedulermode; + $scheduler->maxbookings = $maxbookings; + $scheduler->guardtime = $guardtime; + $scheduler->save(); + + $slotrecs = $DB->get_records('scheduler_slots', array('schedulerid' => $scheduler->id), 'starttime ASC'); + $slotrecs = array_values($slotrecs); + + foreach ($attendedslots as $id) { + $DB->set_field('scheduler_appointment', 'attended', 1, array('slotid' => $slotrecs[$id]->id)); + } + + for ($i = 0; $i < 2; $i++) { + $dummystud = $this->create_student($course->id); + } + + return $scheduler->id; + } + + /** + * Test the retrieveal routines for bookable appointments. + */ + public function test_bookable_appointments() { + + $studid = $this->create_student(); + + $sid = $this->create_data_for_bookable_appointments('oneonly', 1, 0, $studid, array(), array()); + $this->assert_bookable_appointments(1, 1, $sid, $studid); + + $sid = $this->create_data_for_bookable_appointments('oneonly', 1, 0, $studid, array(5), array()); + $this->assert_bookable_appointments(1, 0, $sid, $studid); + + $sid = $this->create_data_for_bookable_appointments('oneonly', 1, 0, $studid, array(5, 6, 7), array()); + $this->assert_bookable_appointments(1, 0, $sid, $studid); + + $sid = $this->create_data_for_bookable_appointments('oneonly', 1, 0, $studid, array(5, 6), array(8)); + $this->assert_bookable_appointments(0, 0, $sid, $studid); + + // One booking inside guard time, cannot be rebooked. + $sid = $this->create_data_for_bookable_appointments('oneonly', 1, 5 * DAYSECS, $studid, array(1), array()); + $this->assert_bookable_appointments(0, 0, $sid, $studid); + + // Five bookings allowed, three booked, one of which attended. + $sid = $this->create_data_for_bookable_appointments('oneonly', 5, 0, $studid, array(2, 3), array(4)); + $this->assert_bookable_appointments(4, 2, $sid, $studid); + + // Five bookings allowed, three booked, one of which inside guard time. + $sid = $this->create_data_for_bookable_appointments('oneonly', 5, 5 * DAYSECS, $studid, array(2, 7, 8), array()); + $this->assert_bookable_appointments(4, 2, $sid, $studid); + + // Five bookings allowed, four booked, of which two inside guard time (one attended), two outside guard time (one attended). + $sid = $this->create_data_for_bookable_appointments('oneonly', 5, 5 * DAYSECS, $studid, array(2, 7), array(1, 8)); + $this->assert_bookable_appointments(2, 1, $sid, $studid); + + // One booking allowed at a time. Two attended already present (one inside GT, one outside GT). + $sid = $this->create_data_for_bookable_appointments('onetime', 1, 5 * DAYSECS, $studid, array(), array(3, 7)); + $this->assert_bookable_appointments(1, 1, $sid, $studid); + + // One booking allowed at a time. One booked outside GT. + $sid = $this->create_data_for_bookable_appointments('onetime', 1, 5 * DAYSECS, $studid, array(7), array()); + $this->assert_bookable_appointments(1, 0, $sid, $studid); + + // One booking allowed at a time. One booked inside GT. + $sid = $this->create_data_for_bookable_appointments('onetime', 1, 5 * DAYSECS, $studid, array(2), array()); + $this->assert_bookable_appointments(0, 0, $sid, $studid); + + } + +} diff --git a/mod/scheduler/tests/slot_test.php b/mod/scheduler/tests/slot_test.php new file mode 100644 index 0000000..8adab10 --- /dev/null +++ b/mod/scheduler/tests/slot_test.php @@ -0,0 +1,313 @@ +<?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/>. + +/** + * Unit tests for scheduler slots + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \mod_scheduler\model\scheduler; +use \mod_scheduler\model\slot; + +global $CFG; +require_once($CFG->dirroot . '/mod/scheduler/locallib.php'); + +/** + * Unit tests for the scheduler_slots class. + * + * @group mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_scheduler_slot_testcase extends advanced_testcase { + + /** + * @var int Course_modules id used for testing + */ + protected $moduleid; + + /** + * @var int Course id used for testing + */ + protected $courseid; + + /** + * @var int Scheduler id used for testing + */ + protected $schedulerid; + + /** + * @var int User id of teacher used for testing + */ + protected $teacherid; + + /** + * @var int a slot used for testing + */ + protected $slotid; + + /** + * @var int[] appointments used for testing + */ + protected $appointmentids; + + /** + * @var int[] id of students used for testing + */ + protected $students; + + protected function setUp() { + global $DB, $CFG; + + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + + $this->students = array(); + for ($i = 0; $i < 3; $i++) { + $this->students[$i] = $this->getDataGenerator()->create_user()->id; + } + + $options = array(); + $options['slottimes'] = array(); + $options['slotstudents'] = array(); + $options['slottimes'][0] = time() + DAYSECS; + $options['slotstudents'][0] = $this->students; + + $scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); + $coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid)); + + $this->schedulerid = $scheduler->id; + $this->moduleid = $coursemodule->id; + $this->courseid = $coursemodule->course; + $this->teacherid = 2; // Admin user. + $this->slotid = $DB->get_field('scheduler_slots', 'id', array('schedulerid' => $scheduler->id), MUST_EXIST); + $this->appointmentids = array_keys($DB->get_records('scheduler_appointment', array('slotid' => $this->slotid))); + } + + /** + * Assert that a record is present in the DB + * + * @param string $table name of table to test + * @param int $id id of record to look for + * @param string $msg message + */ + private function assert_record_present($table, $id, $msg = "") { + global $DB; + + $ex = $DB->record_exists($table, array('id' => $id)); + $this->assertTrue($ex, "Checking whether record $id is present in table $table: $msg"); + } + + /** + * Assert that a record is absent from the DB + * + * @param string $table name of table to test + * @param int $id id of record to look for + * @param string $msg message + */ + private function assert_record_absent($table, $id, $msg = "") { + global $DB; + + $ex = $DB->record_exists($table, array('id' => $id)); + $this->assertFalse($ex, "Checking whether record $id is absent in table $table: $msg"); + } + + /** + * Test creating a slot with appointments + */ + public function test_create() { + + global $DB; + + $scheduler = scheduler::load_by_id($this->schedulerid); + $slot = $scheduler->create_slot(); + + $slot->teacherid = $this->getDataGenerator()->create_user()->id; + $slot->starttime = time(); + $slot->duration = 60; + + $newapp1 = $slot->create_appointment(); + $newapp1->studentid = $this->getDataGenerator()->create_user()->id; + $newapp2 = $slot->create_appointment(); + $newapp2->studentid = $this->getDataGenerator()->create_user()->id; + + $slot->save(); + + $newid = $slot->get_id(); + $this->assertNotEquals(0, $newid, "Checking slot id after creation"); + + $newcnt = $DB->count_records('scheduler_appointment', array('slotid' => $newid)); + $this->assertEquals(2, $newcnt, "Counting number of appointments after addition"); + + } + + + /** + * Test deleting a slot and associated data + */ + public function test_delete() { + + $scheduler = scheduler::load_by_id($this->schedulerid); + + // Make sure calendar events are all created. + $slot = slot::load_by_id($this->slotid, $scheduler); + $start = $slot->starttime; + $slot->save(); + + // Load again, to delete. + $slot = slot::load_by_id($this->slotid, $scheduler); + $slot->delete(); + + $this->assert_record_absent('scheduler_slots', $this->slotid); + foreach ($this->appointmentids as $id) { + $this->assert_record_absent('scheduler_appointment', $id); + } + + $this->assert_event_absent($this->teacherid, $start, ""); + foreach ($this->students as $student) { + $this->assert_event_absent($student, $start, ""); + } + + } + + /** + * Test adding an appointment to a slot. + */ + public function test_add_appointment() { + + global $DB; + + $scheduler = scheduler::load_by_id($this->schedulerid); + $slot = slot::load_by_id($this->slotid, $scheduler); + + $oldcnt = $DB->count_records('scheduler_appointment', array('slotid' => $slot->get_id())); + $this->assertEquals(3, $oldcnt, "Counting number of appointments before addition"); + + $newapp = $slot->create_appointment(); + $newapp->studentid = $this->getDataGenerator()->create_user()->id; + + $slot->save(); + + $newcnt = $DB->count_records('scheduler_appointment', array('slotid' => $slot->get_id())); + $this->assertEquals(4, $newcnt, "Counting number of appointments after addition"); + + } + + /** + * Test removing an appointment from a slot. + */ + public function test_remove_appointment() { + + global $DB; + + $scheduler = scheduler::load_by_id($this->schedulerid); + $slot = slot::load_by_id($this->slotid, $scheduler); + + $apps = $slot->get_appointments(); + $appointment = array_pop($apps); + $delid = $appointment->get_id(); + + $this->assert_record_present('scheduler_appointment', $delid); + + $slot->remove_appointment($appointment); + $slot->save(); + + $this->assert_record_absent('scheduler_appointment', $delid); + } + + /** + * Test presence or absence of event records when appointments are modified. + */ + public function test_calendar_events() { + global $DB; + + $scheduler = scheduler::load_by_id($this->schedulerid); + $slot = slot::load_by_id($this->slotid, $scheduler); + $slot->save(); + + $oldstart = $slot->starttime; + + $this->assert_event_exists($this->teacherid, $slot->starttime, "Meeting with your Students"); + foreach ($this->students as $student) { + $this->assert_event_exists($student, $slot->starttime, "Meeting with your Teacher"); + } + + $newstart = time() + 3 * DAYSECS; + $slot->starttime = $newstart; + $slot->save(); + + foreach ($this->students as $student) { + $this->assert_event_absent($student, $oldstart); + $this->assert_event_exists($student, $newstart, "Meeting with your Teacher"); + } + $this->assert_event_absent($this->teacherid, $oldstart); + $this->assert_event_exists($this->teacherid, $newstart, "Meeting with your Students"); + + // Delete one of the appointments. + $app = $slot->get_appointment($this->appointmentids[0]); + $slot->remove_appointment($app); + $slot->save(); + + $this->assert_event_absent($this->students[0], $newstart); + $this->assert_event_exists($this->students[1], $newstart, "Meeting with your Teacher"); + $this->assert_event_exists($this->teacherid, $newstart, "Meeting with your Students"); + + // Delete all appointments. + $DB->delete_records('scheduler_appointment', array('slotid' => $this->slotid)); + $slot = slot::load_by_id($this->slotid, $scheduler); + $slot->save(); + + foreach ($this->students as $student) { + $this->assert_event_absent($student, $newstart); + } + $this->assert_event_absent($this->teacherid, $newstart); + + } + + /** + * Assert that a calendar event exists in the DB. + * + * @param int $userid user associated with event + * @param int $time start time of the event + * @param string $titlestart beginning of the title of the event + */ + private function assert_event_exists($userid, $time, $titlestart) { + global $DB; + $events = calendar_get_events($time - MINSECS, $time + HOURSECS, $userid, false, false); + $this->assertEquals(1, count($events), "Expecting exactly one event at time $time for user $userid"); + $event = array_pop($events); + $this->assertEquals($time, $event->timestart); + $this->assertEquals('scheduler', $event->modulename); + $this->assertTrue(strpos($event->name, $titlestart) === 0, "Checking event title start: $titlestart"); + } + + /** + * Assert that a calendar event at a certain time is absent from the DB. + * + * @param int $userid user id associated with event + * @param int $time start time of the event + */ + private function assert_event_absent($userid, $time) { + $events = calendar_get_events($time - MINSECS, $time + HOURSECS, $userid, false, false); + $this->assertEquals(0, count($events), "Expecting no event at time $time for user $userid"); + } +} diff --git a/mod/scheduler/version.php b/mod/scheduler/version.php new file mode 100644 index 0000000..12297ee --- /dev/null +++ b/mod/scheduler/version.php @@ -0,0 +1,35 @@ +<?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/>. + +/** + * Version information for mod/scheduler + * + * @package mod_scheduler + * @copyright 2018 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/* + * This is the development branch (master) of the scheduler module. + */ + +$plugin->component = 'mod_scheduler'; // Full name of the plugin (used for diagnostics). +$plugin->version = 2019120200; // The current module version (Date: YYYYMMDDXX). +$plugin->release = '3.7.0'; // Human-friendly version name. +$plugin->requires = 2019052000; // requires Moodle 3.7. +$plugin->maturity = MATURITY_STABLE; // Stable branch MOODLE_37_STABLE diff --git a/mod/scheduler/view.php b/mod/scheduler/view.php new file mode 100644 index 0000000..1edfed2 --- /dev/null +++ b/mod/scheduler/view.php @@ -0,0 +1,100 @@ +<?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/>. + +/** + * This page prints a particular instance of scheduler and handles top level interactions + * + * @package mod_scheduler + * @copyright 2014 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use \mod_scheduler\model\scheduler; + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->dirroot.'/mod/scheduler/lib.php'); +require_once($CFG->dirroot.'/mod/scheduler/locallib.php'); +require_once($CFG->dirroot.'/mod/scheduler/renderable.php'); + +// Read common request parameters. +$id = optional_param('id', '', PARAM_INT); // Course Module ID - if it's not specified, must specify 'a', see below. +$action = optional_param('what', 'view', PARAM_ALPHA); +$subaction = optional_param('subaction', '', PARAM_ALPHA); +$offset = optional_param('offset', -1, PARAM_INT); + +if ($id) { + $cm = get_coursemodule_from_id('scheduler', $id, 0, false, MUST_EXIST); + $scheduler = scheduler::load_by_coursemodule_id($id); +} else { + $a = required_param('a', PARAM_INT); // Scheduler ID. + $scheduler = scheduler::load_by_id($a); + $cm = $scheduler->get_cm(); +} +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + +require_login($course->id, true, $cm); +$context = context_module::instance($cm->id); +$permissions = new \mod_scheduler\permission\scheduler_permissions($context, $USER->id); + +// Initialize $PAGE, compute blocks. +$PAGE->set_url('/mod/scheduler/view.php', array('id' => $cm->id)); + +$output = $PAGE->get_renderer('mod_scheduler'); + +if (groups_get_activity_groupmode($cm) || !$permissions->can_see_all_slots()) { + $defaultsubpage = 'myappointments'; +} else { + $defaultsubpage = 'allappointments'; +} +$subpage = optional_param('subpage', $defaultsubpage, PARAM_ALPHA); + + +// Print the page header. + +$title = $course->shortname . ': ' . format_string($scheduler->name); +$PAGE->set_title($title); +$PAGE->set_heading($course->fullname); + +// Route to screen. + +$teachercaps = ['mod/scheduler:manage', 'mod/scheduler:manageallappointments', 'mod/scheduler:canseeotherteachersbooking']; +$isteacher = has_any_capability($teachercaps, $context); +$isstudent = has_capability('mod/scheduler:viewslots', $context); +if ($isteacher) { + // Teacher side. + if ($action == 'viewstatistics') { + include($CFG->dirroot.'/mod/scheduler/viewstatistics.php'); + } else if ($action == 'viewstudent') { + include($CFG->dirroot.'/mod/scheduler/viewstudent.php'); + } else if ($action == 'export') { + include($CFG->dirroot.'/mod/scheduler/export.php'); + } else if ($action == 'datelist') { + include($CFG->dirroot.'/mod/scheduler/datelist.php'); + } else { + include($CFG->dirroot.'/mod/scheduler/teacherview.php'); + } + +} else if ($isstudent) { + // Student side. + include($CFG->dirroot.'/mod/scheduler/studentview.php'); + +} else { + // For guests. + echo $OUTPUT->header(); + echo $OUTPUT->box(get_string('guestscantdoanything', 'scheduler'), 'generalbox'); + echo $OUTPUT->footer($course); +} diff --git a/mod/scheduler/viewstatistics.php b/mod/scheduler/viewstatistics.php new file mode 100644 index 0000000..4379bfb --- /dev/null +++ b/mod/scheduler/viewstatistics.php @@ -0,0 +1,295 @@ +<?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/>. + +/** + * Statistics report for the scheduler + * + * @package mod_scheduler + * @copyright 2011 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * A function utility for sorting stat results. + * + * @param array $a + * @param array $b + * @return int + */ +function byname($a, $b) { + return strcasecmp($a[0], $b[0]); +} + +$taburl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid, + 'what' => 'viewstatistics', 'subpage' => $subpage)); +$PAGE->set_url($taburl); + +echo $OUTPUT->header(); + +// Display navigation tabs. + +echo $output->teacherview_tabs($scheduler, $permissions, $taburl, $subpage); + +// Find active group in case that group mode is in use. +$currentgroupid = 0; +$groupmode = groups_get_activity_groupmode($scheduler->cm); +if ($groupmode) { + $currentgroupid = groups_get_activity_group($scheduler->cm, true); + groups_print_activity_menu($scheduler->cm, $taburl); +} + +// Display correct type of statistics by request. + +$usergroups = ($currentgroupid > 0) ? array($currentgroupid) : ''; +$attendees = $scheduler->get_available_students($usergroups); + +switch ($subpage) { + case 'overall': + $sql = ' + SELECT + COUNT(DISTINCT(a.studentid)) + FROM + {scheduler_slots} s, + {scheduler_appointment} a + WHERE + s.id = a.slotid AND + s.schedulerid = ? AND + a.attended = 1 + '; + $attended = $DB->count_records_sql($sql, array($scheduler->id)); + + $sql = ' + SELECT + COUNT(DISTINCT(a.studentid)) + FROM + {scheduler_slots} s, + {scheduler_appointment} a + WHERE + s.id = a.slotid AND + s.schedulerid = ? AND + a.attended = 0 + '; + $registered = $DB->count_records_sql($sql, array($scheduler->id)); + + $sql = ' + SELECT + COUNT(DISTINCT(s.id)) + FROM + {scheduler_slots} s + LEFT JOIN + {scheduler_appointment} a + ON + s.id = a.slotid + WHERE + s.schedulerid = ? AND + s.teacherid = ? AND + a.attended IS NULL + '; + $freeowned = $DB->count_records_sql($sql, array($scheduler->id, $USER->id)); + + $sql = ' + SELECT + COUNT(DISTINCT(s.id)) + FROM + {scheduler_slots} s + LEFT JOIN + {scheduler_appointment} a + ON + s.id = a.slotid + WHERE + s.schedulerid = ? AND + s.teacherid != ? AND + a.attended IS NULL + '; + $freenotowned = $DB->count_records_sql($sql, array($scheduler->id, $USER->id)); + + $allattendees = ($attendees) ? count($attendees) : 0; + + $str = '<h3>'.get_string('attendable', 'scheduler').'</h3>'; + $str .= '<strong>'.get_string('attendablelbl', 'scheduler').'</strong>: ' . $allattendees . '<br/>'; + $str .= '<h3>'.get_string('attended', 'scheduler').'</h3>'; + $str .= '<strong>'.get_string('attendedlbl', 'scheduler').'</strong>: ' . $attended . '<br/><br/>'; + $str .= '<h3>'.get_string('unattended', 'scheduler').'</h3>'; + $str .= '<strong>'.get_string('registeredlbl', 'scheduler').'</strong>: ' . $registered . '<br/>'; + $str .= '<strong>'.get_string('unregisteredlbl', 'scheduler').'</strong>: ' . + ($allattendees - $registered - $attended) . '<br/>'; + $str .= '<h3>'.get_string('availableslots', 'scheduler').'</h3>'; + $str .= '<strong>'.get_string('availableslotsowned', 'scheduler').'</strong>: ' . $freeowned . '<br/>'; + $str .= '<strong>'.get_string('availableslotsnotowned', 'scheduler').'</strong>: ' . $freenotowned . '<br/>'; + $str .= '<strong>'.get_string('availableslotsall', 'scheduler').'</strong>: ' . ($freeowned + $freenotowned) . '<br/>'; + + echo $OUTPUT->box($str); + + break; + case 'studentbreakdown': + // Display the amount of time each student has received. + + if (!empty($attendees)) { + $table = new html_table(); + $table->head = array (get_string('student', 'scheduler'), get_string('duration', 'scheduler')); + $table->align = array ('LEFT', 'CENTER'); + $table->width = '70%'; + $table->data = array(); + $sql = ' + SELECT + a.studentid, + SUM(s.duration) as totaltime + FROM + {scheduler_slots} s, + {scheduler_appointment} a + WHERE + s.id = a.slotid AND + a.studentid > 0 AND + s.schedulerid = ? + GROUP BY + a.studentid + '; + if ($statrecords = $DB->get_records_sql($sql, array($scheduler->id))) { + foreach ($statrecords as $arecord) { + if (array_key_exists($arecord->studentid, $attendees)) { + $table->data[] = array (fullname($attendees[$arecord->studentid]), $arecord->totaltime); + } + } + uasort($table->data, 'byname'); + } + echo html_writer::table($table); + } else { + echo $OUTPUT->box(get_string('nostudents', 'scheduler'), 'center', '70%'); + } + break; + case 'staffbreakdown': + // Display break down by member of staff. + $sql = "SELECT s.teacherid, + SUM(s.duration) as totaltime + FROM {scheduler_slots} s + LEFT JOIN {scheduler_appointment} a + ON a.slotid = s.id + WHERE + s.schedulerid = :sid + AND a.studentid IS NOT NULL"; + $params = array('sid' => $scheduler->id); + if ($currentgroupid > 0) { + $sql .= " AND EXISTS (SELECT 1 FROM {groups_members} gm WHERE gm.userid = a.studentid AND gm.groupid = :gid)"; + $params['gid'] = $currentgroupid; + } + $sql .= " GROUP BY s.teacherid"; + if ($statrecords = $DB->get_records_sql($sql, $params)) { + $table = new html_table(); + $table->width = '70%'; + $table->head = array (s($scheduler->get_teacher_name()), get_string('cumulatedduration', 'scheduler')); + $table->align = array ('LEFT', 'CENTER'); + foreach ($statrecords as $arecord) { + $ateacher = $DB->get_record('user', array('id' => $arecord->teacherid)); + $table->data[] = array (fullname($ateacher), $arecord->totaltime); + } + uasort($table->data, 'byname'); + echo html_writer::table($table); + } + break; + case 'lengthbreakdown': + // Display by number of atendees to one member of staff. + $sql = ' + SELECT + s.starttime, + COUNT(*) as groupsize, + MAX(s.duration) as duration + FROM + {scheduler_slots} s + LEFT JOIN + {scheduler_appointment} a + ON + a.slotid = s.id + WHERE + a.studentid IS NOT NULL AND + schedulerid = :sid'; + $params = array('sid' => $scheduler->id); + if ($currentgroupid > 0) { + $sql .= " AND EXISTS (SELECT 1 FROM {groups_members} gm WHERE gm.userid = a.studentid AND gm.groupid = :gid)"; + $params['gid'] = $currentgroupid; + } + $sql .= " GROUP BY s.starttime ORDER BY groupsize DESC"; + if ($groupslots = $DB->get_records_sql($sql, $params)) { + $table = new html_table(); + $table->head = array (get_string('duration', 'scheduler'), get_string('appointments', 'scheduler')); + $table->align = array ('LEFT', 'CENTER'); + $table->width = '70%'; + + $durationcount = array(); + foreach ($groupslots as $slot) { + if (array_key_exists($slot->duration, $durationcount)) { + $durationcount[$slot->duration] ++; + } else { + $durationcount[$slot->duration] = 1; + } + } + foreach ($durationcount as $key => $duration) { + $table->data[] = array ($key, $duration); + } + echo html_writer::table($table); + } + break; + case 'groupbreakdown': + // Display by number of atendees to one member of staff. + $sql = " + SELECT + s.starttime, + COUNT(*) as groupsize, + MAX(s.duration) as duration + FROM + {scheduler_slots} s + LEFT JOIN + {scheduler_appointment} a + ON + a.slotid = s.id + WHERE + a.studentid IS NOT NULL AND + s.schedulerid = :sid"; + $params = array('sid' => $scheduler->id); + if ($currentgroupid > 0) { + $sql .= " AND EXISTS (SELECT 1 FROM {groups_members} gm WHERE gm.userid = s.teacherid AND gm.groupid = :gid)"; + $params['gid'] = $currentgroupid; + } + $sql .= " GROUP BY s.starttime + ORDER BY groupsize DESC"; + if ($groupslots = $DB->get_records_sql($sql, $params)) { + $table = new html_table(); + $table->head = array (get_string('groupsize', 'scheduler'), get_string('occurrences', 'scheduler'), + get_string('cumulatedduration', 'scheduler')); + $table->align = array ('LEFT', 'CENTER', 'CENTER'); + $table->width = '70%'; + $grouprows = array(); + foreach ($groupslots as $agroup) { + if (!array_key_exists($agroup->groupsize, $grouprows)) { + $grouprows[$agroup->groupsize] = new stdClass(); + $grouprows[$agroup->groupsize]->occurrences = 0; + $grouprows[$agroup->groupsize]->duration = 0; + } + $grouprows[$agroup->groupsize]->occurrences++; + $grouprows[$agroup->groupsize]->duration += $agroup->duration; + } + foreach (array_keys($grouprows) as $agroupsize) { + $table->data[] = array ($agroupsize, $grouprows[$agroupsize]->occurrences, $grouprows[$agroupsize]->duration); + } + echo html_writer::table($table); + } +} +echo '<br/>'; +echo $OUTPUT->continue_button("$CFG->wwwroot/mod/scheduler/view.php?id=".$cm->id); +// Finish the page. +echo $OUTPUT->footer($course); +exit; diff --git a/mod/scheduler/viewstudent.php b/mod/scheduler/viewstudent.php new file mode 100644 index 0000000..23d57e2 --- /dev/null +++ b/mod/scheduler/viewstudent.php @@ -0,0 +1,153 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Prints the screen that displays a single student to a teacher. + * + * @package mod_scheduler + * @copyright 2016 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/scheduler/locallib.php'); + +$appointmentid = required_param('appointmentid', PARAM_INT); +list($slot, $appointment) = $scheduler->get_slot_appointment($appointmentid); +$studentid = $appointment->studentid; + +$permissions->ensure($permissions->can_see_appointment($appointment)); + +$urlparas = array('what' => 'viewstudent', + 'id' => $scheduler->cmid, + 'appointmentid' => $appointmentid, + 'course' => $scheduler->courseid); +$taburl = new moodle_url('/mod/scheduler/view.php', $urlparas); +$PAGE->set_url($taburl); + +$appts = $scheduler->get_appointments_for_student($studentid); + +$pages = array('thisappointment'); +if ($slot->get_appointment_count() > 1) { + $pages[] = 'otherstudents'; +} +if (count($appts) > 1) { + $pages[] = 'otherappointments'; +} + +if (!in_array($subpage, $pages) ) { + $subpage = 'thisappointment'; +} + +// Process edit form before page output starts. +if ($subpage == 'thisappointment') { + require_once($CFG->dirroot.'/mod/scheduler/appointmentforms.php'); + + $actionurl = new moodle_url($taburl, array('page' => 'thisappointment')); + $returnurl = new moodle_url($taburl, array('page' => 'thisappointment')); + + $distribute = ($slot->get_appointment_count() > 1); + $gradeedit = $permissions->can_edit_grade($appointment); + $mform = new scheduler_editappointment_form($appointment, $actionurl, $permissions, $distribute); + $mform->set_data($mform->prepare_appointment_data($appointment)); + + if ($mform->is_cancelled()) { + redirect($returnurl); + } else if ($formdata = $mform->get_data()) { + $mform->save_appointment_data($formdata, $appointment); + redirect($returnurl); + } +} + +echo $output->header(); + +// Print user summary. + +scheduler_print_user($DB->get_record('user', array('id' => $appointment->studentid)), $course); + +// Print tabs. +$tabrows = array(); +$row = array(); + +if (count($pages) > 1) { + foreach ($pages as $tabpage) { + $tabname = get_string('tab-'.$tabpage, 'scheduler'); + $row[] = new tabobject($tabpage, new moodle_url($taburl, array('subpage' => $tabpage)), $tabname); + } + $tabrows[] = $row; + print_tabs($tabrows, $subpage); +} + +$totalgradeinfo = new scheduler_totalgrade_info($scheduler, $scheduler->get_gradebook_info($appointment->studentid)); + +if ($subpage == 'thisappointment') { + + $ai = scheduler_appointment_info::make_for_teacher($slot, $appointment); + echo $output->render($ai); + + $mform->display(); + + if ($scheduler->uses_grades()) { + echo $output->render($totalgradeinfo); + } + +} else if ($subpage == 'otherappointments') { + // Print table of other appointments of the same student. + + $studenturl = new moodle_url($taburl, array('page' => 'thisappointment')); + $table = new scheduler_slot_table($scheduler, true, $studenturl); + $table->showattended = true; + $table->showteachernotes = true; + $table->showeditlink = true; + $table->showlocation = false; + + foreach ($appts as $appt) { + $table->add_slot($appt->get_slot(), $appt, null, false); + } + + echo $output->render($table); + + if ($scheduler->uses_grades()) { + $totalgradeinfo->showtotalgrade = true; + $totalgradeinfo->totalgrade = $scheduler->get_user_grade($appointment->studentid); + echo $output->render($totalgradeinfo); + } + +} else if ($subpage == 'otherstudents') { + // Print table of other students in the same slot. + + $ai = scheduler_appointment_info::make_from_slot($slot, false); + echo $output->render($ai); + + $studenturl = new moodle_url($taburl, array('page' => 'thisappointment')); + $table = new scheduler_slot_table($scheduler, true, $studenturl); + $table->showattended = true; + $table->showslot = false; + $table->showstudent = true; + $table->showteachernotes = true; + $table->showeditlink = true; + + foreach ($slot->get_appointments() as $otherappointment) { + $table->add_slot($otherappointment->get_slot(), $otherappointment, null, false); + } + + echo $output->render($table); +} + +echo $output->continue_button(new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid))); +echo $output->footer($course); +exit; diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-debug.js b/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-debug.js new file mode 100644 index 0000000..35ab9b1 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-debug.js @@ -0,0 +1,44 @@ +YUI.add('moodle-mod_scheduler-delselected', function (Y, NAME) { + +// ESLint directives. +/* eslint-disable camelcase */ + +var SELECTORS = { + DELACTION: 'div.commandbar a#delselected', + SELECTBOX: 'table#slotmanager input.slotselect' +}, + MOD; + +M.mod_scheduler = M.mod_scheduler || {}; +MOD = M.mod_scheduler.delselected = {}; + +/** + * Copy the selected boexs into an input parameter of the respective form + * + * @param {String} link + * @param {String} baseurl + */ +MOD.collect_selection = function(link, baseurl) { + + var sellist = ''; + Y.all(SELECTORS.SELECTBOX).each(function(box) { + if (box.get('checked')) { + if (sellist.length > 0) { + sellist += ','; + } + sellist += box.get('value'); + } + }); + link.setAttribute('href', baseurl + '&items=' + sellist); +}; + +MOD.init = function(baseurl) { + var link = Y.one(SELECTORS.DELACTION); + if (link !== null) { + link.on('click', function() { + M.mod_scheduler.delselected.collect_selection(link, baseurl); + }); + } +}; + +}, '@VERSION@', {"requires": ["base", "node", "event"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-min.js b/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-min.js new file mode 100644 index 0000000..98fee20 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_scheduler-delselected",function(e,t){var n={DELACTION:"div.commandbar a#delselected",SELECTBOX:"table#slotmanager input.slotselect"},r;M.mod_scheduler=M.mod_scheduler||{},r=M.mod_scheduler.delselected={},r.collect_selection=function(t,r){var i="";e.all(n.SELECTBOX).each(function(e){e.get("checked")&&(i.length>0&&(i+=","),i+=e.get("value"))}),t.setAttribute("href",r+"&items="+i)},r.init=function(t){var r=e.one(n.DELACTION);r!==null&&r.on("click",function(){M.mod_scheduler.delselected.collect_selection(r,t)})}},"@VERSION@",{requires:["base","node","event"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected.js b/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected.js new file mode 100644 index 0000000..35ab9b1 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected.js @@ -0,0 +1,44 @@ +YUI.add('moodle-mod_scheduler-delselected', function (Y, NAME) { + +// ESLint directives. +/* eslint-disable camelcase */ + +var SELECTORS = { + DELACTION: 'div.commandbar a#delselected', + SELECTBOX: 'table#slotmanager input.slotselect' +}, + MOD; + +M.mod_scheduler = M.mod_scheduler || {}; +MOD = M.mod_scheduler.delselected = {}; + +/** + * Copy the selected boexs into an input parameter of the respective form + * + * @param {String} link + * @param {String} baseurl + */ +MOD.collect_selection = function(link, baseurl) { + + var sellist = ''; + Y.all(SELECTORS.SELECTBOX).each(function(box) { + if (box.get('checked')) { + if (sellist.length > 0) { + sellist += ','; + } + sellist += box.get('value'); + } + }); + link.setAttribute('href', baseurl + '&items=' + sellist); +}; + +MOD.init = function(baseurl) { + var link = Y.one(SELECTORS.DELACTION); + if (link !== null) { + link.on('click', function() { + M.mod_scheduler.delselected.collect_selection(link, baseurl); + }); + } +}; + +}, '@VERSION@', {"requires": ["base", "node", "event"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-debug.js b/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-debug.js new file mode 100644 index 0000000..bddee9d --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-debug.js @@ -0,0 +1,69 @@ +YUI.add('moodle-mod_scheduler-saveseen', function (Y, NAME) { + +// ESLint directives. +/* eslint-disable camelcase */ + +var SELECTORS = { + CHECKBOXES: 'table#slotmanager form.studentselectform input.studentselect' +}, + MOD; + +M.mod_scheduler = M.mod_scheduler || {}; +MOD = M.mod_scheduler.saveseen = {}; + +/** + * Save the "seen" status. + * + * @param {Number} cmid the coursemodule id + * @param {Number} appid the id of the relevant appointment + * @param {Boolean} newseen + * @param {Object} spinner The spinner icon shown while saving + */ +MOD.save_status = function(cmid, appid, newseen, spinner) { + + Y.io(M.cfg.wwwroot + '/mod/scheduler/ajax.php', { + // The request paramaters. + data: { + action: 'saveseen', + id: cmid, + appointmentid: appid, + seen: newseen, + sesskey: M.cfg.sesskey + }, + + timeout: 5000, // 5 seconds of timeout. + + // Define the events. + on: { + start: function() { + spinner.show(); + }, + success: function() { + window.setTimeout(function() { + spinner.hide(); + }, 250); + }, + failure: function(transactionid, xhr) { + var msg = { + name: xhr.status + ' ' + xhr.statusText, + message: xhr.responseText + }; + spinner.hide(); + return new M.core.exception(msg); + } + }, + context: this + }); +}; + +MOD.init = function(cmid) { + Y.all(SELECTORS.CHECKBOXES).each(function(box) { + box.on('change', function() { + var spinner = M.util.add_spinner(Y, box.ancestor('div')); + M.mod_scheduler.saveseen.save_status(cmid, box.get('value'), box.get('checked'), spinner); + }); + }); +}; + + +}, '@VERSION@', {"requires": ["base", "node", "event"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-min.js b/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-min.js new file mode 100644 index 0000000..3d41746 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_scheduler-saveseen",function(e,t){var n={CHECKBOXES:"table#slotmanager form.studentselectform input.studentselect"},r;M.mod_scheduler=M.mod_scheduler||{},r=M.mod_scheduler.saveseen={},r.save_status=function(t,n,r,i){e.io(M.cfg.wwwroot+"/mod/scheduler/ajax.php",{data:{action:"saveseen",id:t,appointmentid:n,seen:r,sesskey:M.cfg.sesskey},timeout:5e3,on:{start:function(){i.show()},success:function(){window.setTimeout(function(){i.hide()},250)},failure:function(e,t){var n={name:t.status+" "+t.statusText,message:t.responseText};return i.hide(),new M.core.exception(n)}},context:this})},r.init=function(t){e.all(n.CHECKBOXES).each(function(n){n.on("change",function(){var r=M.util.add_spinner(e,n.ancestor("div"));M.mod_scheduler.saveseen.save_status(t,n.get("value"),n.get("checked"),r)})})}},"@VERSION@",{requires:["base","node","event"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen.js b/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen.js new file mode 100644 index 0000000..bddee9d --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen.js @@ -0,0 +1,69 @@ +YUI.add('moodle-mod_scheduler-saveseen', function (Y, NAME) { + +// ESLint directives. +/* eslint-disable camelcase */ + +var SELECTORS = { + CHECKBOXES: 'table#slotmanager form.studentselectform input.studentselect' +}, + MOD; + +M.mod_scheduler = M.mod_scheduler || {}; +MOD = M.mod_scheduler.saveseen = {}; + +/** + * Save the "seen" status. + * + * @param {Number} cmid the coursemodule id + * @param {Number} appid the id of the relevant appointment + * @param {Boolean} newseen + * @param {Object} spinner The spinner icon shown while saving + */ +MOD.save_status = function(cmid, appid, newseen, spinner) { + + Y.io(M.cfg.wwwroot + '/mod/scheduler/ajax.php', { + // The request paramaters. + data: { + action: 'saveseen', + id: cmid, + appointmentid: appid, + seen: newseen, + sesskey: M.cfg.sesskey + }, + + timeout: 5000, // 5 seconds of timeout. + + // Define the events. + on: { + start: function() { + spinner.show(); + }, + success: function() { + window.setTimeout(function() { + spinner.hide(); + }, 250); + }, + failure: function(transactionid, xhr) { + var msg = { + name: xhr.status + ' ' + xhr.statusText, + message: xhr.responseText + }; + spinner.hide(); + return new M.core.exception(msg); + } + }, + context: this + }); +}; + +MOD.init = function(cmid) { + Y.all(SELECTORS.CHECKBOXES).each(function(box) { + box.on('change', function() { + var spinner = M.util.add_spinner(Y, box.ancestor('div')); + M.mod_scheduler.saveseen.save_status(cmid, box.get('value'), box.get('checked'), spinner); + }); + }); +}; + + +}, '@VERSION@', {"requires": ["base", "node", "event"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-debug.js b/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-debug.js new file mode 100644 index 0000000..252d975 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-debug.js @@ -0,0 +1,42 @@ +YUI.add('moodle-mod_scheduler-studentlist', function (Y, NAME) { + +// ESLint directives. +/* eslint-disable camelcase */ + +var CSS = { + EXPANDED: 'expanded', + COLLAPSED: 'collapsed' +}; + +M.mod_scheduler = M.mod_scheduler || {}; +var MOD = M.mod_scheduler.studentlist = {}; + +MOD.setState = function(id, expanded) { + var image = Y.one('#' + id); + var content = Y.one('#list' + id); + if (expanded) { + content.removeClass(CSS.COLLAPSED); + content.addClass(CSS.EXPANDED); + image.set('src', M.util.image_url('t/expanded')); + } else { + content.removeClass(CSS.EXPANDED); + content.addClass(CSS.COLLAPSED); + image.set('src', M.util.image_url('t/collapsed')); + } +}; + +MOD.toggleState = function(id) { + var content = Y.one('#list' + id); + var isVisible = content.hasClass(CSS.EXPANDED); + this.setState(id, !isVisible); +}; + +MOD.init = function(imageid, expanded) { + this.setState(imageid, expanded); + Y.one('#' + imageid).on('click', function() { + M.mod_scheduler.studentlist.toggleState(imageid); + }); +}; + + +}, '@VERSION@', {"requires": ["base", "node", "event", "io"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-min.js b/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-min.js new file mode 100644 index 0000000..1829128 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-min.js @@ -0,0 +1 @@ +YUI.add("moodle-mod_scheduler-studentlist",function(e,t){var n={EXPANDED:"expanded",COLLAPSED:"collapsed"};M.mod_scheduler=M.mod_scheduler||{};var r=M.mod_scheduler.studentlist={};r.setState=function(t,r){var i=e.one("#"+t),s=e.one("#list"+t);r?(s.removeClass(n.COLLAPSED),s.addClass(n.EXPANDED),i.set("src",M.util.image_url("t/expanded"))):(s.removeClass(n.EXPANDED),s.addClass(n.COLLAPSED),i.set("src",M.util.image_url("t/collapsed")))},r.toggleState=function(t){var r=e.one("#list"+t),i=r.hasClass(n.EXPANDED);this.setState(t,!i)},r.init=function(t,n){this.setState(t,n),e.one("#"+t).on("click",function(){M.mod_scheduler.studentlist.toggleState(t)})}},"@VERSION@",{requires:["base","node","event","io"]}); diff --git a/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist.js b/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist.js new file mode 100644 index 0000000..252d975 --- /dev/null +++ b/mod/scheduler/yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist.js @@ -0,0 +1,42 @@ +YUI.add('moodle-mod_scheduler-studentlist', function (Y, NAME) { + +// ESLint directives. +/* eslint-disable camelcase */ + +var CSS = { + EXPANDED: 'expanded', + COLLAPSED: 'collapsed' +}; + +M.mod_scheduler = M.mod_scheduler || {}; +var MOD = M.mod_scheduler.studentlist = {}; + +MOD.setState = function(id, expanded) { + var image = Y.one('#' + id); + var content = Y.one('#list' + id); + if (expanded) { + content.removeClass(CSS.COLLAPSED); + content.addClass(CSS.EXPANDED); + image.set('src', M.util.image_url('t/expanded')); + } else { + content.removeClass(CSS.EXPANDED); + content.addClass(CSS.COLLAPSED); + image.set('src', M.util.image_url('t/collapsed')); + } +}; + +MOD.toggleState = function(id) { + var content = Y.one('#list' + id); + var isVisible = content.hasClass(CSS.EXPANDED); + this.setState(id, !isVisible); +}; + +MOD.init = function(imageid, expanded) { + this.setState(imageid, expanded); + Y.one('#' + imageid).on('click', function() { + M.mod_scheduler.studentlist.toggleState(imageid); + }); +}; + + +}, '@VERSION@', {"requires": ["base", "node", "event", "io"]}); diff --git a/mod/scheduler/yui/src/delselected/build.json b/mod/scheduler/yui/src/delselected/build.json new file mode 100644 index 0000000..f4b97f0 --- /dev/null +++ b/mod/scheduler/yui/src/delselected/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_scheduler-delselected", + "builds": { + "moodle-mod_scheduler-delselected": { + "jsfiles": [ + "delselected.js" + ] + } + } +} diff --git a/mod/scheduler/yui/src/delselected/js/delselected.js b/mod/scheduler/yui/src/delselected/js/delselected.js new file mode 100644 index 0000000..3f24417 --- /dev/null +++ b/mod/scheduler/yui/src/delselected/js/delselected.js @@ -0,0 +1,40 @@ +// ESLint directives. +/* eslint-disable camelcase */ + +var SELECTORS = { + DELACTION: 'div.commandbar a#delselected', + SELECTBOX: 'table#slotmanager input.slotselect' +}, + MOD; + +M.mod_scheduler = M.mod_scheduler || {}; +MOD = M.mod_scheduler.delselected = {}; + +/** + * Copy the selected boexs into an input parameter of the respective form + * + * @param {String} link + * @param {String} baseurl + */ +MOD.collect_selection = function(link, baseurl) { + + var sellist = ''; + Y.all(SELECTORS.SELECTBOX).each(function(box) { + if (box.get('checked')) { + if (sellist.length > 0) { + sellist += ','; + } + sellist += box.get('value'); + } + }); + link.setAttribute('href', baseurl + '&items=' + sellist); +}; + +MOD.init = function(baseurl) { + var link = Y.one(SELECTORS.DELACTION); + if (link !== null) { + link.on('click', function() { + M.mod_scheduler.delselected.collect_selection(link, baseurl); + }); + } +}; \ No newline at end of file diff --git a/mod/scheduler/yui/src/delselected/meta/delselected.json b/mod/scheduler/yui/src/delselected/meta/delselected.json new file mode 100644 index 0000000..87b1f1a --- /dev/null +++ b/mod/scheduler/yui/src/delselected/meta/delselected.json @@ -0,0 +1,7 @@ +{ + "moodle-mod_scheduler-delselected": { + "requires": [ + "base", "node", "event" + ] + } +} diff --git a/mod/scheduler/yui/src/saveseen/build.json b/mod/scheduler/yui/src/saveseen/build.json new file mode 100644 index 0000000..665cece --- /dev/null +++ b/mod/scheduler/yui/src/saveseen/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_scheduler-saveseen", + "builds": { + "moodle-mod_scheduler-saveseen": { + "jsfiles": [ + "saveseen.js" + ] + } + } +} diff --git a/mod/scheduler/yui/src/saveseen/js/saveseen.js b/mod/scheduler/yui/src/saveseen/js/saveseen.js new file mode 100644 index 0000000..3eff0bb --- /dev/null +++ b/mod/scheduler/yui/src/saveseen/js/saveseen.js @@ -0,0 +1,64 @@ +// ESLint directives. +/* eslint-disable camelcase */ + +var SELECTORS = { + CHECKBOXES: 'table#slotmanager form.studentselectform input.studentselect' +}, + MOD; + +M.mod_scheduler = M.mod_scheduler || {}; +MOD = M.mod_scheduler.saveseen = {}; + +/** + * Save the "seen" status. + * + * @param {Number} cmid the coursemodule id + * @param {Number} appid the id of the relevant appointment + * @param {Boolean} newseen + * @param {Object} spinner The spinner icon shown while saving + */ +MOD.save_status = function(cmid, appid, newseen, spinner) { + + Y.io(M.cfg.wwwroot + '/mod/scheduler/ajax.php', { + // The request paramaters. + data: { + action: 'saveseen', + id: cmid, + appointmentid: appid, + seen: newseen, + sesskey: M.cfg.sesskey + }, + + timeout: 5000, // 5 seconds of timeout. + + // Define the events. + on: { + start: function() { + spinner.show(); + }, + success: function() { + window.setTimeout(function() { + spinner.hide(); + }, 250); + }, + failure: function(transactionid, xhr) { + var msg = { + name: xhr.status + ' ' + xhr.statusText, + message: xhr.responseText + }; + spinner.hide(); + return new M.core.exception(msg); + } + }, + context: this + }); +}; + +MOD.init = function(cmid) { + Y.all(SELECTORS.CHECKBOXES).each(function(box) { + box.on('change', function() { + var spinner = M.util.add_spinner(Y, box.ancestor('div')); + M.mod_scheduler.saveseen.save_status(cmid, box.get('value'), box.get('checked'), spinner); + }); + }); +}; diff --git a/mod/scheduler/yui/src/saveseen/meta/saveseen.json b/mod/scheduler/yui/src/saveseen/meta/saveseen.json new file mode 100644 index 0000000..b5cd040 --- /dev/null +++ b/mod/scheduler/yui/src/saveseen/meta/saveseen.json @@ -0,0 +1,7 @@ +{ + "moodle-mod_scheduler-saveseen": { + "requires": [ + "base", "node", "event" + ] + } +} diff --git a/mod/scheduler/yui/src/studentlist/build.json b/mod/scheduler/yui/src/studentlist/build.json new file mode 100644 index 0000000..7340899 --- /dev/null +++ b/mod/scheduler/yui/src/studentlist/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-mod_scheduler-studentlist", + "builds": { + "moodle-mod_scheduler-studentlist": { + "jsfiles": [ + "studentlist.js" + ] + } + } +} diff --git a/mod/scheduler/yui/src/studentlist/js/studentlist.js b/mod/scheduler/yui/src/studentlist/js/studentlist.js new file mode 100644 index 0000000..f00e602 --- /dev/null +++ b/mod/scheduler/yui/src/studentlist/js/studentlist.js @@ -0,0 +1,37 @@ +// ESLint directives. +/* eslint-disable camelcase */ + +var CSS = { + EXPANDED: 'expanded', + COLLAPSED: 'collapsed' +}; + +M.mod_scheduler = M.mod_scheduler || {}; +var MOD = M.mod_scheduler.studentlist = {}; + +MOD.setState = function(id, expanded) { + var image = Y.one('#' + id); + var content = Y.one('#list' + id); + if (expanded) { + content.removeClass(CSS.COLLAPSED); + content.addClass(CSS.EXPANDED); + image.set('src', M.util.image_url('t/expanded')); + } else { + content.removeClass(CSS.EXPANDED); + content.addClass(CSS.COLLAPSED); + image.set('src', M.util.image_url('t/collapsed')); + } +}; + +MOD.toggleState = function(id) { + var content = Y.one('#list' + id); + var isVisible = content.hasClass(CSS.EXPANDED); + this.setState(id, !isVisible); +}; + +MOD.init = function(imageid, expanded) { + this.setState(imageid, expanded); + Y.one('#' + imageid).on('click', function() { + M.mod_scheduler.studentlist.toggleState(imageid); + }); +}; diff --git a/mod/scheduler/yui/src/studentlist/meta/studentlist.json b/mod/scheduler/yui/src/studentlist/meta/studentlist.json new file mode 100644 index 0000000..d4c64dd --- /dev/null +++ b/mod/scheduler/yui/src/studentlist/meta/studentlist.json @@ -0,0 +1,7 @@ +{ + "moodle-mod_scheduler-studentlist": { + "requires": [ + "base", "node", "event", "io" + ] + } +} diff --git a/theme/adaptable/.eslintignore b/theme/adaptable/.eslintignore new file mode 100644 index 0000000..b3a4726 --- /dev/null +++ b/theme/adaptable/.eslintignore @@ -0,0 +1,3 @@ +*/**/build/ +node_modules/ +vendor/ diff --git a/theme/adaptable/.eslintrc b/theme/adaptable/.eslintrc new file mode 100644 index 0000000..6c3376e --- /dev/null +++ b/theme/adaptable/.eslintrc @@ -0,0 +1,240 @@ +{ + 'plugins': [ + 'babel', + 'promise', + ], + 'env': { + 'browser': true, + 'amd': true + }, + 'globals': { + 'M': true, + 'Y': true + }, + 'rules': { + // See http://eslint.org/docs/rules/ for all rules and explanations of all + // rules. + + // === Possible Errors === + 'comma-dangle': 'off', + 'no-compare-neg-zero': 'error', + 'no-cond-assign': 'error', + 'no-console': 'error', + 'no-constant-condition': 'error', + 'no-control-regex': 'error', + 'no-debugger': 'error', + 'no-dupe-args': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty': 'warn', + 'no-empty-character-class': 'error', + 'no-ex-assign': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-parens': 'off', + 'no-extra-semi': 'error', + 'no-func-assign': 'error', + 'no-inner-declarations': 'error', + 'no-invalid-regexp': 'error', + 'no-irregular-whitespace': 'error', + 'no-obj-calls': 'error', + 'no-prototype-builtins': 'off', + 'no-regex-spaces': 'error', + 'no-sparse-arrays': 'error', + 'no-unexpected-multiline': 'error', + 'no-unreachable': 'warn', + 'no-unsafe-finally': 'error', + 'no-unsafe-negation': 'error', + 'use-isnan': 'error', + 'valid-jsdoc': ['warn', { 'requireReturn': false, 'requireParamDescription': false, 'requireReturnDescription': false }], + 'valid-typeof': 'error', + + // === Best Practices === + // (these mostly match our jshint config) + 'array-callback-return': 'warn', + 'block-scoped-var': 'warn', + 'complexity': 'warn', + 'consistent-return': 'warn', + 'curly': 'error', + 'dot-notation': 'warn', + 'no-alert': 'warn', + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-div-regex': 'error', + 'no-empty-pattern': 'error', + 'no-empty-function': 'warn', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'warn', + 'no-fallthrough': 'error', + 'no-floating-decimal': 'warn', + 'no-global-assign': 'warn', + 'no-implied-eval': 'error', + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-labels': 'error', + 'no-loop-func': 'error', + 'no-multi-spaces': 'warn', + 'no-multi-str': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-octal': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-redeclare': 'warn', + 'no-return-assign': 'error', + 'no-script-url': 'error', + 'no-self-assign': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'warn', + 'no-throw-literal': 'warn', + 'no-unmodified-loop-condition': 'error', + 'no-unused-expressions': 'error', + 'no-unused-labels': 'error', + 'no-useless-call': 'warn', + 'no-useless-escape': 'warn', + 'no-with': 'error', + 'wrap-iife': ['error', 'any'], + + // === Variables === + 'no-delete-var': 'error', + 'no-undef': 'error', + 'no-undef-init': 'error', + 'no-unused-vars': ['error', { 'caughtErrors': 'none' }], + + // === Stylistic Issues === + 'array-bracket-spacing': 'warn', + 'block-spacing': 'warn', + 'brace-style': ['warn', '1tbs'], + 'camelcase': 'warn', + 'capitalized-comments': ['warn', 'always', { 'ignoreConsecutiveComments': true }], + 'comma-spacing': ['warn', { 'before': false, 'after': true }], + 'comma-style': ['warn', 'last'], + 'computed-property-spacing': 'error', + 'consistent-this': 'off', + 'eol-last': 'off', + 'func-call-spacing': ['warn', 'never'], + 'func-names': 'off', + 'func-style': 'off', + // indent currently not doing well with our wrapping style, so disabled. + 'indent': ['off', 4, { 'SwitchCase': 1 }], + 'key-spacing': ['warn', { 'beforeColon': false, 'afterColon': true, 'mode': minimum }], + 'keyword-spacing': 'warn', + 'linebreak-style': ['error', 'unix'], + 'lines-around-comment': 'off', + 'max-len': ['error', 132], + 'max-lines': 'off', + 'max-depth': 'warn', + 'max-nested-callbacks': ['warn', 5], + 'max-params': 'off', + 'max-statements': 'off', + 'max-statements-per-line': ['warn', { max: 2 }], + 'new-cap': ['warn', { 'properties': false }], + 'new-parens': 'warn', + 'newline-after-var': 'off', + 'newline-before-return': 'off', + 'newline-per-chained-call': 'off', + 'no-array-constructor': 'off', + 'no-bitwise': 'error', + 'no-continue': 'off', + 'no-inline-comments': 'off', + 'no-lonely-if': 'off', + 'no-mixed-operators': 'off', + 'no-mixed-spaces-and-tabs': 'error', + 'no-multiple-empty-lines': 'warn', + 'no-negated-condition': 'off', + 'no-nested-ternary': 'warn', + 'no-new-object': 'off', + 'no-plusplus': 'off', + 'no-tabs': 'error', + 'no-ternary': 'off', + 'no-trailing-spaces': 'error', + 'no-underscore-dangle': 'off', + 'no-unneeded-ternary': 'off', + 'no-whitespace-before-property': 'warn', + 'object-curly-newline': 'off', + 'object-curly-spacing': 'warn', + 'object-property-newline': 'off', + 'one-var': 'off', + 'one-var-declaration-per-line': ['warn', 'initializations'], + 'operator-assignment': 'off', + 'operator-linebreak': 'off', + 'padded-blocks': 'off', + 'quote-props': ['warn', 'as-needed', {'unnecessary': false, 'keywords': true, 'numbers': true}], + 'quotes': 'off', + 'require-jsdoc': 'warn', + 'semi': 'error', + 'semi-spacing': ['warn', {'before': false, 'after': true}], + 'sort-vars': 'off', + 'space-before-blocks': 'warn', + 'space-before-function-paren': ['warn', 'never'], + 'space-in-parens': 'warn', + 'space-infix-ops': 'warn', + 'space-unary-ops': 'warn', + 'spaced-comment': 'warn', + 'unicode-bom': 'error', + 'wrap-regex': 'off', + + // === Promises === + 'promise/always-return': 'warn', + 'promise/no-return-wrap': 'warn', + 'promise/param-names': 'warn', + 'promise/catch-or-return': ['warn', {terminationMethod: ['catch', 'fail']}], + 'promise/no-native': 'warn', + 'promise/no-promise-in-callback': 'warn', + 'promise/no-callback-in-promise': 'warn', + 'promise/avoid-new': 'warn', + + // === Deprecations === + "no-restricted-properties": ['warn', { + 'object': 'M', + 'property': 'str', + 'message': 'Use AMD module "core/str" or M.util.get_string()' + }], + + }, + overrides: [ + { + files: ["**/amd/src/*.js", "**/amd/src/**/*.js", "Gruntfile*.js", "babel-plugin-add-module-to-define.js"], + // We support es6 now. Woot! + env: { + es6: true + }, + // We're using babel transpiling so use their parser + // for linting. + parser: 'babel-eslint', + // Check AMD with some slightly stricter rules. + rules: { + 'no-unused-vars': 'error', + 'no-implicit-globals': 'error', + // Disable all of the rules that have babel versions. + 'new-cap': 'off', + // Not using this rule for the time being because it isn't + // compatible with jQuery and ES6. + 'no-invalid-this': 'off', + 'object-curly-spacing': 'off', + 'quotes': 'off', + 'semi': 'off', + 'no-unused-expressions': 'off', + // Enable all of the babel version of these rules. + 'babel/new-cap': ['warn', { 'properties': false }], + // Not using this rule for the time being because it isn't + // compatible with jQuery and ES6. + 'babel/no-invalid-this': 'off', + 'babel/object-curly-spacing': 'warn', + // This is off in the original style int. + 'babel/quotes': 'off', + 'babel/semi': 'error', + 'babel/no-unused-expressions': 'error', + // === Promises === + // We have Promise now that we're using ES6. + 'promise/no-native': 'off', + 'promise/avoid-new': 'off' + }, + parserOptions: { + 'ecmaVersion': 9, + 'sourceType': 'module' + } + } + ] +} diff --git a/theme/adaptable/.travis.yml b/theme/adaptable/.travis.yml new file mode 100644 index 0000000..534240b --- /dev/null +++ b/theme/adaptable/.travis.yml @@ -0,0 +1,62 @@ +language: php + +cache: + directories: + - $HOME/.composer/cache + - $HOME/.npm + +matrix: + allow_failures: + fast_finish: true + include: + - php: 7.2 + env: DB=mysqli + services: mysql + - php: 7.2 + env: DB=pgsql + services: postgresql + addons: + postgresql: 9.5 + - php: 7.4 + env: DB=mysqli + services: mysql + - php: 7.4 + env: DB=pgsql + services: postgresql + addons: + postgresql: 9.5 +env: + global: + - MOODLE_BRANCH=MOODLE_39_STABLE + - IGNORE_NAMES=*.txt,fallback.css,activity_navigation.mustache,message_drawer.mustache,overlaymenu.mustache,overlaymenuitem.mustache,savediscard.mustache,tabs.mustache,tourstep.mustache,admin_settingspage_tabs.php + +before_install: +# This disables XDebug which should speed up the build. One reason to remove this +# line is if you are trying to generate code coverage with PHPUnit. + - phpenv config-rm xdebug.ini +# Currently we are inside of the clone of your repository. We move up two +# directories to build the project. + - cd ../.. +# Update Composer. + - composer selfupdate +# Install this project into a directory called "ci". + - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 +# Update the $PATH so scripts from this project can be called easily. + - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" + +install: + - moodle-plugin-ci install +# npm + - npm install eslint@latest + - npm install eslint-plugin-babel@latest + - npm install eslint-plugin-promise@latest + +script: + - moodle-plugin-ci phplint + - moodle-plugin-ci phpcpd + - moodle-plugin-ci phpmd + - moodle-plugin-ci codechecker + - moodle-plugin-ci validate + - moodle-plugin-ci mustache + - moodle-plugin-ci phpdoc + - moodle-plugin-ci phpunit diff --git a/theme/adaptable/CONTRIBUTING.txt b/theme/adaptable/CONTRIBUTING.txt new file mode 100644 index 0000000..c2742ed --- /dev/null +++ b/theme/adaptable/CONTRIBUTING.txt @@ -0,0 +1,28 @@ +Contributing to Adaptable +========================= + +Adaptable is open to your contributions. Feel free to fork it. + +You can contribute to Adaptable in several ways: fixing bugs, adding new features or translating the theme to your language. + +To approve and merge your Pull Request (PR) for bugs and improvements you must follow this requirements: + -Open an issue first and link it to the PR + -ALL the code must follow the Moodle code guideline (https://docs.moodle.org/dev/Coding_style) and pass Moodle Code Checker +without errors or warnings. This is mandatory to approve the PR. + -Add your code attribution as a single line comment before the code modified. + -Comment your code as much as you can. This will help other developers to understand your code and will help also in +the approval. + -Use space instead tab for indentation. Tab is not allowed. Use four spaces for indentation. + -Use only the default Bootstrap 2 media queries are: 1200px, 979px, 767px and 480px. Other values are not valid + -The PR must include only the issue related. If you want to submit a PR with several fixes then you must create an issue for +each one and submit a different PR for each one. + -Add the needed strings ONLY in English (British). DO NOT translate to other languages. The strings added will be translated +later to the rest of languages available by the translators. + + +If you want to submit a translation file then follow these rules: + -Follow the Moodle Coding Style (https://docs.moodle.org/dev/Coding_style) + -Use Unicode and UNIX format for files (Windows format is not allowed) + +Or you can use Moodle AMOS to add your translation. Note that translations submitted through AMOS are not official and will not +recognized as an Adaptable translation because we can't verify it. diff --git a/theme/adaptable/COPYING.txt b/theme/adaptable/COPYING.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/theme/adaptable/COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/theme/adaptable/Changes.md b/theme/adaptable/Changes.md new file mode 100644 index 0000000..8ed1dfb --- /dev/null +++ b/theme/adaptable/Changes.md @@ -0,0 +1,313 @@ +Change Log in version 3.0.4 (2020073105) +======================================== +1. Fix 'Header issues with logo and background image'. +2. Fix 'Custom menu items not shown even when "disablecustommenu" is false'. +3. Fix 'Header style two does not show logo when there is no site title'. +4. Fix 'Undefined property: stdClass::$tickertext1 when running behat tests' - #159. + +Change Log in version 3.0.3 (2020073104) +======================================== +1. Fix 'Cope when there is no first or full name' when showing a user profile. +2. Fix 'Frontpage tiles do not show course contacts' - #184. +3. Due date label doesn't honor overridden dates for mod_assign - #186, + thanks to https://github.com/golenkovm for the original patch in Collapsed Topics. +4. Fix 'adaptable_setting_confightmleditor does not set setting as empty when there is no content' - #187. +5. Fix 'Sub sub menus and below show all at once' - #188. +6. Fix the ability for Behat to run without '$CFG->forced_plugin_settings' being set - dashboard.php issue only - #159. +7. Fix 'admin_setting_configselect defaults should use the index and not the value' - #189. +8. Fix 'regression - background colour on dashboard' - #190. +9. Add 'Book printing to PDF' - #173. +10. Fix 'Dashboard dropdown hover makes text unreadable' - #194. +11. Add 'Not submitted confusing when student can no longer submit' - #195. +12. Fix 'buttondropshadow does not use lang strings' - #152. +13. Port of Collapsed Topics accessible colours for the activity meta - https://github.com/gjb2048/moodle-format_topcoll/issues/88. +14. Tabbed settings and fixed use of $PAGE which gives invalid variable values when Adaptable is not the set theme. +15. Fix 'PHPUnit install fails' - #197. +16. Fix 'Install fails on Moodle 3.9' - #198 - thanks to https://gitlab.com/kiklopgs for the patch in https://gitlab.com/jezhops/moodle-theme_adaptable/-/merge_requests/34. +17. Fix 'Gradebook: Edit link not working' - #201. +18. Fix 'edit_button in renderers.php is not used' - #202. +19. Fix 'Redundant CSS' - #96. +20. Fix 'theme_adaptable_get_html_for_settings() is not used!' - #27. +21. Fix '$hasmiddle is not used!' - #26. +22. Fix '$hasfootnote is not used!' - #203. +23. Fix '$responsivealerts = $PAGE->theme->settings->responsivealerts; not used!' - #204. +24. Fix 'Improve Activity Completion Icons' - #8. +25. Fix 'User menu available when using "Full screen pop-up with some Javascript scurity" in Quiz' - #210. +26. Fix 'Adding Activity with Safari in Moodle 3.9' - #211. +27. Fix 'Second level links do not work when using 3 levels of sub-menu in custom menus' - #117. +28. Fix 'Course formatting in Safari and Moodle 3.9' - #212. +29. Fix ''About me' tab should be the default for the user profile page' - #206. +30. Add version information to settings pages. +31. Fix 'Calendar links on the page'. +32. Fix 'Navigation tweaks'. +33. Fix 'User details not visible on profile page' - #119. +34. Tabs update in line with MDL-69301. +35. Fix block header icons. +36. Fix block hide / show icon size. +37. Fix 'Wrong display of date user profile fields' - #214. +38. Fix property display can cause markup to be interpreted. +39. Allow Import / Export settings to work by separating from tabbed settings. +40. Fix 'Impossible to enter a course with Coventry tiles' - #156. +41. Remove setting to control activities in chooser - M3.9+ only - #135. +42. Fix 'stickynavbar' JS error on login page. +43. Fix 'Login page cookies popup not working' - #217. +44. Fix 'Onetopic: background color in tabs' - #215. +45. Fix 'Theme does not respect the before_footer callback' - #216. +46. Fix 'No multilang support in headers and footer' - #132. +47. Fix 'Drag&Drop Image question behaviour problem' - #220. +48. Fix 'Duplicate code in get_logo_title()' - #208. +49. Fix 'responsivealerts used?' - #205. +50. Fix 'fonttitlecolorcourse setting not used' - #154. +51. Fix 'enablealertcoursepages setting used?' - #151. + +Change Log in version 3.0.2 (2020073103) +======================================== +1. Fix 'Error in function quiz_num_submissions_ungraded' - #176. +2. Fix 'course_participant_count inaccurate' - #179. +3. Fix 'Lesson status inaccurate' - #180. + +Change Log in version 3.0.1 (2020073102) +======================================== +1. Fix 'Too few arguments to function theme_adaptable_core_renderer::render_mycourses(), + 0 passed in [dirroot]/lib/outputrenderers.php on line 497 and exactly 1 expected' - #172. +2. Fix navbar is not showing on the frontpage. +3. Fix 'Book module has two icons' - #174. +4. Fix 'Course in category' - https://moodle.org/mod/forum/discuss.php?d=408081#p1656297. +5. Fix 'Ungraded assign' - https://moodle.org/mod/forum/discuss.php?d=410681. +6. Fix btn-secondary text colour when link. +7. Fix 'Filter not applied' - https://moodle.org/mod/forum/discuss.php?d=408081#p1657138. +8. Fix 'Support for Embedded Questions filter' - #177. + +Change Log in version 3.0.0 (2020073101) +======================================== + +Release candidate for Moodle 3.9. + +1. Fix licence from GPLv2 to GPLv3 as is incorrect - Moodle plugins must be GPLv3. +2. Fix message drawer closure. +3. Fix 'Regression - Frontpage marketing blocks don't display on desktop' - #139. +4. Moodle 3.9 New Activity Chooser styling needs work - #131. +5. Fix 'Blocks - My Home recently accessed course' - #9. +6. Fix rubic icons -> https://moodle.org/mod/forum/discuss.php?d=408081#p1646693. +7. Fix 'Searchbox conflict with Advanced Forum (hsuforum)' - #133. +8. Fix 'Bullet list display in Collapsed Topics course format' - #81. +9. Fix 'Block settings are left justified' - #82. +10. Improve 'Improve Onetopic course format tab rendering' - #115. +11. Fix 'Missing action menu (like editing button / cog button) on content bank page' - #140. +12. Fix 'Button/link 'Turn editing on' missing on Moodle 3.9' - #129. +13. Fix '.btn declaired after .hidden' - #130. +14. Fix message drawer hover. +15. Improve position of #82. +16. Make 'side-post' have no padding on the right so that the page is symmetrical. +17. Fix 'H5P iframe element too small in content bank page' - #146. +18. Fix 'Navbar Custom Menu does not fit' - #128. +19. Fix 'Editing cog colour not consistent' - #149. +20. Fix 'Whitespace Below Header in Course Pages' - #38. +21. Fix 'Use of "$setting->set_updatedcallback('theme_reset_all_caches');" not needed on some settings' - #25. +22. Fix 'lib.php preg_match logic flaw' - #150. +23. Fix 'wrong rtl css' - #142. +24. Fix 'Impossible to enter a course with Coventry tiles' - #156. +25. Update to version 3 https://moodlehq.github.io/moodle-plugin-ci/UPGRADE-3.0.html - #158. +26. Assignment with restricting grouping shows all users or groups - #161. +27. Fix 'Regression? / Inconsistent cog positioning in content bank' - #166. +28. Fix 'Quiz attempt: no breadcrumbs' - #123. +29. Fix '$fontname can never be 'custom'' - #104. +30. Implement 'Update google fonts list ' - #42 - thanks to 'Sal Zaydon' - https://gitlab.com/szaydon for the list. +31. Fix no such font-family as 'default' - #42. +32. Fix 'Topic header text now black' - #167. + +Change Log in version 2.2.2 (2019112601) +======================================== + +Main fixes & Enhancements done in this release. + +- Fix mobile responsive settings in "layout responsive" settings page +- Fix ability to set general box color in forums +- Fix issues with login page when no header in use +- Fix issue of footer riding up on short pages with little content +- Fix close icon for activity chooser in Moodle 3.8 +- Fix combo list on mobile, now collapses into single column + +What's new? + +- Layout responsive settings page +- Setting to control color of forum "general box" background where forum description is displayed + + +HTML/CSS sample code for block areas +------------------------------------ +Here you will find some code samples to help you to customize the Info Box and the Marketing Blocks. + +You can insert any HTML tag to customize the front page blocks. Use a <div> tag as a main container and add the height to keep the +same value in all the blocks. + +The Font Awesome icons set is available in +http://fortawesome.github.io/Font-Awesome/icons/. + +You can insert any of them following the examples +http://fortawesome.github.io/Font-Awesome/examples/ + + +Front Page Slider Styles +------------------------ +Add images with at least 1900px x 400px. +If you want to reduce or increase the height, Adaptable will resize the image automatically. +There are two possible slider styles each with different markup required: + + +Original BCU Slider Markup: + +<div class="span9"> + <h4>Information</h4> + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore + et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + </div> + + <div class="span3"> + <a href="#" class="submit">2016/17 Courses <i class="fa-chevron-right fa"></i></a> +</div> + + +Coventry Style Slider Markup + +<div class="span6 col-sm-6"> +<h3>Hand-crafted</h3> <h4>pixels and code for the Moodle community</h4> +<a href="#" class="submit">Check out our custom theme pricing!</a> +</div> + + +Frontpage Marketing Block HTML structure Coventry + +<div><img src="http://somewebsite.com/2.jpg" class="marketimage"></div> +<h4><a href="#">International Courses</a></h4> +<p>Some text below the link....</p> + + +Front page Info Box and Marketing Blocks +---------------------------------------- + +There are two Info blocks in the front page located above and below the Marketing Blocks. These are just for compatibility with the +old BCU. + +It is recommended to use the new marketing blocks builder that allows you to create your own layout and add much more blocks. + +There are 8 rows where you can add up to 4 blocks in each with a total of 32 block of different size. See pix/layout.png for +more information. + +You can enter any HTML code to the block, include FA icons, images, videos and apply in-line styles. + +Some samples: + + +Block with solid background, FA icon and some text: + +<div style="text-align:center; background: #e6e6e6; height: 350px; padding: 7px;"> + <i class="fa fa-paint-brush fa-5x" style="color: #3A454b;"></i> + <h3>Title </h3> + <div style="text-align:center;">Add your text here.</div> +</div> + + +Block with border and transparent background: + +<div style="text-align:center; height: 350px; padding: 7px; border: 1px solid #3A454b;"> + <i class="fa fa-list fa-5x" style="color: #3A454b;"></i> + <h3>Heading</h3> + <div style="text-align:center; padding: 5px; color: #3A454b;">Add your text here.</div> +</div> + + +Block with an image: + +<div style="height: 350px;"> + <img src="http://yoursite/yourimage.jpg" style="vertical-align:text-bottom; margin: 0 .5em;" height="auto" width="100%"> + <p style="margin-top: 5px; color: #333333; text-align: center;"><strong>Add your text here</strong></p> +</div> + + +Block with a video: + +<div style="background: #606060; height: 350px"> + <center> + <iframe src="https://www.youtube.com/embed/wop3FMhoLGs" allowfullscreen="" frameborder="0" height="315" width="560"></iframe> + </center> +</div> + + +Block using multi-lang filter: + +<div style="width: 100%; height: 240px; background-color: #cccccc;"> +<h1 style="text-align: center; line-height: 120px;"> + <span class="multilang" lang="en">text in english</span> + <span class="multilang" lang="es">texto en español</span> + <span class="multilang" lang="fr">texte en français</span> + <span class="multilang" lang="ca">text en català </span> +</div> + + +Footer Blocks +------------- +You can apply the same HTML/CSS in the footer blocks. + +Some samples: + +Contact information + +<i class="fa fa-building"></i> High St. 100<br> +<span style="margin-left: 20px;">123456 City</span><br><br> +<i class="fa fa-phone"></i> +12 (3)456 78 90<br> +<i class="fa fa-envelope"></i> info@mail.com<br> +<i class="fa fa-globe"></i> www.example.com + + +List with Chevron + +<ul class="block-list"> + <li><a href="http://moodle.org/"><span class="fa fa-chevron-right"></span><span>Accessibility</span></a></li> + <li><a href="http://moodle.org/"><span class="fa fa-chevron-right"></span><span>Moodle Help</span></a></li> + <li><a href="http://moodle.org/"><span class="fa fa-chevron-right"></span><span>Moodle Feedback</span></a></li> + <li><a href="http://moodle.org/"><span class="fa fa-chevron-right"></span><span>IT Help</span></a></li> + <li><a href="http://moodle.org/"><span class="fa fa-chevron-right"></span><span>IT Feedback</span></a></li> +</ul> + + +Copyright text +-------------- +A sample of copyright text using FA icon + +Made with <i class="fa fa-heart" style="color: #ff0000;"></i> in Europe + + +News Ticker +----------- +From version 1.3 the news ticker do not need to create an unordered list. Just add paragraphs using <p> tags: + +<p>Configure all the theme colors</p> +<p>Use any Google Font for the content, headings and site title</p> +<p>Display a logo or a configurable title site</p> +<p>Configurable Slideshow</p> +<p>Display up to 12 marketing blocks in the front page</p> + + +Messages / Notifications +------------------------ +Moodle 3.2 includes a new system to display messages and notifications in the screen. + +The new system displays a hard coded black icons that are difficult to see when using dark background color in the top header. +In that case, you can use an alternate icons pack using white color. + +Login the server by FTP or SFTP and open /theme/adaptable/pix_core/i and +delete notifications.png and rename notifications-white.png to notifications.png + +Then open /theme/adaptable/pix_core/t and delete message.png and +rename message-white.png to message.png + +From moodle 3.6 the messages and notifications has been changed to the called "Messages Drawer". + + +Activities icons +------------------------ +From version 1.4, Adaptable includes its own icons pack that replace the default moodle icons. +If you don't want to use the icons just remove adaptable/pix_plugins and adaptable/pix_core/f +You can enable this icons from the administration. \ No newline at end of file diff --git a/theme/adaptable/Gruntfile.js b/theme/adaptable/Gruntfile.js new file mode 100644 index 0000000..4bb8629 --- /dev/null +++ b/theme/adaptable/Gruntfile.js @@ -0,0 +1,151 @@ +/** + * Gruntfile for the Adaptable theme. + * + * This file configures tasks to be run by Grunt + * http://gruntjs.com/ for the current theme. + * + * + * Requirements: + * ------------- + * nodejs, npm, grunt-cli. + * + * Installation: + * ------------- + * node and npm: instructions at http://nodejs.org/ + * + * grunt-cli: `[sudo] npm install -g grunt-cli` + * + * node dependencies: run `npm install` in the root directory. + * + * + * Usage: + * ------ + * Call tasks from the theme root directory. Default behaviour + * (calling only `grunt`) is to run the watch task detailed below. + * + * + * Porcelain tasks: + * ---------------- + * The nice user interface intended for everyday use. Provide a + * high level of automation and convenience for specific use-cases. + * + * grunt css Create CSS from the SCSS. + * + * grunt amd Create the Asynchronous Module Definition JavaScript files. See: MDL-49046. + * Done here as core Gruntfile.js currently *nix only. + * + * @package theme_adaptable. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @author Based on code originally written by Andrew Nicols, Joby Harding, Bas Brands, David Scotson and many other contributors. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +module.exports = function(grunt) { // jshint ignore:line + var path = require('path'), + tasks = {}, + cwd = process.env.PWD || process.cwd(), // jshint ignore:line + semver = require('semver'); + + // Verify the node version is new enough. + var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node); + var actual = semver.valid(process.version); // jshint ignore:line + if (!semver.satisfies(actual, expected)) { + grunt.fail.fatal('Node version too old. Require ' + expected + ', version installed: ' + actual); + } + + /* Windows users can't run grunt in a subdirectory, so allow them to set + the root by passing --root=path/to/dir. */ + if (grunt.option('root')) { + var root = grunt.option('root'); + if (grunt.file.exists(__dirname, root)) { // jshint ignore:line + cwd = path.join(__dirname, root); // jshint ignore:line + grunt.log.ok('Setting root to ' + cwd); + } else { + grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist'); + } + } + + // PHP strings for exec task. + var moodleroot = path.dirname(path.dirname(__dirname)), // jshint ignore:line + dirrootopt = grunt.option('dirroot') || process.env.MOODLE_DIR || ''; // jshint ignore:line + + // Allow user to explicitly define Moodle root dir. + if ('' !== dirrootopt) { + moodleroot = path.resolve(dirrootopt); + } + + var configfile = path.join(moodleroot, 'config.php'); + + var decachephp = 'define(\'CLI_SCRIPT\', true);'; + decachephp += 'require(\'' + configfile + '\');'; + decachephp += 'purge_all_caches();'; + + // Project configuration. + grunt.initConfig({ + sass: { + dist: { + files: { + "style/cardblocks.css": "scss/card-blocks.scss" + } + }, + options: { + includePaths: ["scss/"] + } + }, + stylelint: { + scss: { + options: {syntax: 'scss'}, + src: ['scss/**/*.scss'] + } + }, + exec: { + decache: { + cmd: 'php -r "' + decachephp + '"', + callback: function(error) { + // The 'exec' process will output error messages, just add one to confirm success. + if (!error) { + grunt.log.writeln("Moodle cache reset."); + } + } + } + }, + jshint: { + options: {jshintrc: moodleroot + '/.jshintrc'}, + files: ['**/amd/src/*.js'] + }, + uglify: { + dynamic_mappings: { + files: grunt.file.expandMapping( + ['**/src/*.js', '!**/node_modules/**'], + '', + { + cwd: cwd, + rename: function(destBase, destPath) { + destPath = destPath.replace('src', 'build'); + destPath = destPath.replace('.js', '.min.js'); + destPath = path.resolve(cwd, destPath); + return destPath; + } + } + ) + } + } + }); + + // Register tasks. + grunt.loadNpmTasks("grunt-exec"); + grunt.loadNpmTasks('grunt-sass'); + grunt.loadNpmTasks('grunt-stylelint'); + grunt.registerTask("decache", ["exec:decache"]); + + // Load core tasks. + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.registerTask("amd", ["jshint", "uglify", "decache"]); + + // Register CSS taks. + grunt.registerTask('css', ['stylelint:scss', 'sass']); + + // Register the default task. + grunt.registerTask('default', ['amd']); +}; diff --git a/theme/adaptable/README.md b/theme/adaptable/README.md new file mode 100644 index 0000000..bb9d487 --- /dev/null +++ b/theme/adaptable/README.md @@ -0,0 +1,130 @@ +Adaptable - the most adaptable moodle theme +=========================================== + +Adaptable is a highly customizable responsive two column moodle theme based on the popular BCU theme adding: + +- Customizable fonts (Google Fonts) +- Fully customizable colors +- Fully customizable block styles (including FA icons) +- Fully customizable buttons +- Additional header navigation +- News Ticker +- Alternative jQuery slider +- Alternative front page course styles +- Additional customizable marketing blocks +- Additional layout settings for width, slider width, padding of + various elements +- Social icons +- Mobile settings (customize how theme looks on mobile devices) +- Dismissible bootstrap alerts +- Option to add login form in header on front page +- Logo and Favicon uploader +- Modern emojis (thanks to EmojiOne) +- Front Page layout builder +- Dashboard layout builder +- Course layout builder +- Activities status +- Privacy API (compatible with GDPR) +- 2 and 3 row header style options +- 2 User profile layouts + +In addition many fields (menus, news items, alerts and help links) can be targeted using custom profile fields, thus it is possible +to present different users with different navigation items and notices. It is also possible for individual users to customize where +they want top menu navigation to appear (disable, home pages only, site wide) using custom profile fields. + +Adaptable has a lot of settings and may seem daunting at first, our advice is to simply install with the default settings and play +with it afterwards. + +With a little time you should be able to setup an attractive Moodle site with a high degree of individuality without without +knowing any CSS. + +This theme has been developed by: +Lead Developers +Jeremy Hopkins (Coventry University) +Fernando Acedo (3bits elearning solutions) +Manoj Solanki (Coventry University) +G J Barnard MSc. BSc(Hons)(Sndw). MBCS. CEng. CITP. PGCE. Moodle profile | http://moodle.org/user/profile.php?id=442195. Web profile | http://about.me/gjbarnard + +Required version of Moodle +========================== +This version works with Moodle 3.9 version 2020061500.00 (Build: 20200615) and above within the 3.9 branch until the +next release. + +Please ensure that your hardware and software complies with 'Requirements' in 'Installing Moodle' on +'docs.moodle.org/39/en/Installing_Moodle'. + + +Versioning +========== +Adaptable is maintained under the Semantic Versioning guidelines as much as possible. Releases will be numbered with the +following format: + +major.minor.patch + +and following these guidelines: +- Breaking backward compatibility bumps the major (and resets the minor and patch) +- New additions without breaking backward compatibility bumps the minor (and resets the patch) +- Bug fixes and misc changes bumps the patch + +For more information on SemVer, please visit http://semver.org. + + +Acknowledgments +=============== +Big thanks to all the volunteers that are collaborating and testing Adaptable continuously. We really appreciate your help and +support to develop the most adaptable theme for moodle. + +Development: +- Justin Hunt +- Leonid Chernyavskiy +- COMETE (Université Paris Nanterre) +- Marina Glancy +- Nick Phillips +- Björn Bettzüche +- Michael Milette +- Bas Brands +- Gareth Barnard +- Konrád LÅ‘rinczi +- Mathieu Domingo + +Testing: +- Andrew Walding +- Alexander Goryntsev + +Translation: +- Germán Valero (Español - México) +- Jordi Rodilla (Català - Andorra) + + +Contributions +============= +You are welcome to collaborate in the project. You can fix bugs, add new features or help in the translation to your language. +See CONTRIBUTING.txt for more information + + +Licenses +======== +Adaptable is licensed under: +GPL v3 (GNU General Public License) - http://www.gnu.org/licenses + +Google Fonts released under: +SIL Open Font License v1.1 - http://scripts.sil.org/OFL +Apache 2 license - https://www.apache.org/licenses/LICENSE-2.0 +The Ubuntu fonts use the Ubuntu Font License v1.0 - http://font.ubuntu.com/ufl/ubuntu-font-licence-1.0.txt + +The Font Awesome font (by Dave Gandy) http://fontawesome.io) is licensed under: +SIL Open Font License v1.1 - http://scripts.sil.org/OFL + +Font Awesome CSS, LESS, and SASS files are licensed under: +MIT License - https://opensource.org/licenses/mit-license.html + +Emoji icons provided free by EmojiOne (http://emojione.com) released under: +Creative Commons Attribution 4.0 International - https://creativecommons.org/licenses/by/4.0/ + + +Follow Us +========= +Twitter - https://twitter.com/adaptable_theme +Facebook - https://www.facebook.com/adaptable.theme + +Modify it! - Improve it! - Share it! diff --git a/theme/adaptable/amd/build/adaptable.min.js b/theme/adaptable/amd/build/adaptable.min.js new file mode 100644 index 0000000..1dac2e3 --- /dev/null +++ b/theme/adaptable/amd/build/adaptable.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable AMD"),{init:function(){a(document).ready(function(a){b.debug("Adaptable AMD init"),a(".close").click(function(){var b=a(this).data("alertindex"),c=a(this).data("alertkey");"undismissable"!=c&&"undefined"!=c&&c&&M.util.set_user_preference("theme_adaptable_alertkey"+b,c)}),a(".breadcrumb li:not(:last-child) span").not(".separator").addClass(""),a(".breadcrumb li:last-child").addClass("lastli");try{a('.context-header-settings-menu .dropdown-menu .dropdown-item a[href*="edit"], #edittingbutton a').click(function(b){b.preventDefault();var c=a(window).scrollTop();sessionStorage.setItem("scrollTo",c);var d=a(this).prop("href");return window.location.replace(d),!1});var c=sessionStorage.getItem("scrollTo");null!=c&&(window.scrollTo(0,c),sessionStorage.removeItem("scrollTo"))}catch(d){b.debug("Adaptable: Session storage exception: "+d.name)}var e,f=50,g=500;e=!(a(window).scrollTop()>f);var h=function(){a(window).scrollTop()>f?0==e&&(e=!0,a("#back-to-top").fadeIn(g)):1==e&&(e=!1,a("#back-to-top").fadeOut(g))};if(h(),a(window).scroll(function(){h()}),a("#back-to-top").click(function(b){return b.preventDefault(),a("html, body").animate({scrollTop:0},g),!1}),window.location.hash&&a("body").hasClass("pagelayout-course")){var i=a(window.location.hash).offset().top;a("html, body").animate({scrollTop:i-102},g)}})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/bsoptions.min.js b/theme/adaptable/amd/build/bsoptions.min.js new file mode 100644 index 0000000..afb97d5 --- /dev/null +++ b/theme/adaptable/amd/build/bsoptions.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable Bootstrap AMD opt in functions"),{init:function(b){if(a(document).ready(function(a){function c(){f>0&&(window.pageYOffset>=f?0==g&&(e.classList.add("adaptable-navbar-sticky"),g=!0):1==g&&(e.classList.remove("adaptable-navbar-sticky"),g=!1))}function d(){0==f&&(f=e.offsetTop,g=window.pageYOffset<f,c())}var e=document.getElementById("main-navbar");if(b.stickynavbar&&null!=e){var f=e.offsetTop;window.onscroll=function(){c()},window.onresize=function(){d()};var g=window.pageYOffset<f;c()}var h=992,i=0;a(window).width()<=h?(a("#adaptable-page-header-wrapper").addClass("fixed-top"),a("body").addClass("page-header-margin"),i=1):(a("#adaptable-page-header-wrapper").removeClass("fixed-top"),a("body").removeClass("page-header-margin")),a(window).resize(function(){a(window).width()<=h?0==i&&(a("#adaptable-page-header-wrapper").addClass("fixed-top"),a("body").addClass("page-header-margin"),i=1):1==i&&(a("#adaptable-page-header-wrapper").removeClass("fixed-top"),a("body").removeClass("page-header-margin"),i=0)});var j=a("#showsidebaricon");j.length&&j.css({top:a(window).height()/2+"px"}),a(window).resize(function(){if(a(window).width()>h){var b=a("#nav-drawer");if(b.length&&!b.hasClass("closed")){b.addClass("closed"),b.attr("aria-hidden","true"),a("#drawer").attr("aria-expanded","false");var c=a("#drawer").attr("data-side");a("body").removeClass("drawer-open-"+c)}}j.length&&j.css({top:a(window).height()/2+"px"})}),a(".moodlewidth").click(function(){a("#page").hasClass("fullin")?(a("#page").removeClass("fullin"),M.util.set_user_preference("theme_adaptable_full","nofull")):(a("#page").addClass("fullin"),M.util.set_user_preference("theme_adaptable_full","fullin"))}),a("#openoverlaymenu").click(function(){a("#conditionalmenu").toggleClass("open")}),a("#overlaymenuclose").click(function(){a("#conditionalmenu").toggleClass("open")}),a(".dropdown-menu a.dropdown-toggle").on("click",function(b){var c=a(this),d=a(this).offsetParent(".dropdown-menu");a(this).next().hasClass("show")||a(this).parents(".dropdown-menu").first().find(".show").removeClass("show");var e=a(this).next(".dropdown-menu");return e.toggleClass("show"),a(this).parent("li").toggleClass("show"),a(this).parents("li.nav-item.dropdown.show").on("hidden.bs.dropdown",function(b){a(".dropdown-menu .show").removeClass("show")}),d.parent().hasClass("navbar-nav")||c.next().css({top:c[0].offsetTop,left:d.outerWidth()-4}),!1})}),b.stickynavbar){var c=function(){scrollBy(0,-50)};location.hash&&c(),window.addEventListener("hashchange",c)}}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/drawer.min.js b/theme/adaptable/amd/build/drawer.min.js new file mode 100644 index 0000000..4f4d6be --- /dev/null +++ b/theme/adaptable/amd/build/drawer.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable Drawer AMD"),{init:function(){a(document).ready(function(a){var c=a("body"),d=a("#drawer").attr("data-side");a("#drawer").click(function(){var b=a("#nav-drawer");b.hasClass("closed")?(b.removeClass("closed"),c.addClass("drawer-open-"+d),b.attr("aria-hidden","false"),a(this).attr("aria-expanded","true")):(b.addClass("closed"),c.removeClass("drawer-open-"+d),b.attr("aria-hidden","true"),a(this).attr("aria-expanded","false"))}),c.addClass("drawer-ease"),b.debug("Adaptable Drawer AMD init")})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/savebutton.min.js b/theme/adaptable/amd/build/savebutton.min.js new file mode 100644 index 0000000..319e95a --- /dev/null +++ b/theme/adaptable/amd/build/savebutton.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable savebutton.js function called"),{init:function(){a(document).ready(function(a){a("#savediscardsection").hide(),a("#adminsettings :input").on("change input",function(){a("#savediscardsection").fadeIn("slow")}),a("#adminsubmitbutton").click(function(){window.onbeforeunload=null,a("#adminsettings").submit()}),a("#adminresetbutton").click(function(){var b=confirm("This resets any changes made since loading this page. Are you sure?");1==b&&(a("#adminsettings")[0].reset(),a("#savediscardsection").hide())}),a(".colourdialogue").click(function(){a("#savediscardsection").fadeIn("slow")})})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/search-input.min.js b/theme/adaptable/amd/build/search-input.min.js new file mode 100644 index 0000000..eaa1d6f --- /dev/null +++ b/theme/adaptable/amd/build/search-input.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){var c=null,d=function(){c.find("form").submit()},e=function(b){var e=a(document).width();if("keydown"!==b.type||13===b.keyCode||32===b.keyCode)return e<=767&&("click"===b.type||"keydown"===b.type)?void d():void(e<=767||("keydown"===b.type&&b.preventDefault(),c.addClass("expanded"),c.find("form").addClass("expanded"),c.find("input").focus()))},f=function(){c.removeClass("expanded"),c.find("form").removeClass("expanded")},g=function(b){var e=a(document).width();if("keydown"!==b.type||13===b.keyCode||32===b.keyCode)return e<=767&&("click"===b.type||"keydown"===b.type)?void d():void(e<=767||("keydown"===b.type&&b.preventDefault(),c.find("input").focus()))},h=function(a){c.hasClass("expanded")?f():e(a)};return{init:function(d){b.debug("Adaptable Search Input AMD Init"),c=a("#"+d.id),1==d.expandable?c.on("click mouseover keydown","div",h):c.on("click mouseover keydown","div",g)}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/showsidebar.min.js b/theme/adaptable/amd/build/showsidebar.min.js new file mode 100644 index 0000000..5cf8877 --- /dev/null +++ b/theme/adaptable/amd/build/showsidebar.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable AMD Show sidebar"),{init:function(){a(document).ready(function(a){b.debug("Adaptable AMD Show sidebar init");var c=!0,d=a("#block-region-side-post"),e=a("#showsidebaricon i.fa"),f=a("body");"undefined"!=typeof d&&a("#showsidebaricon").click(function(){c===!0?(d.addClass("sidebarshown"),f.addClass("sidebarshown"),e.removeClass("fa-angle-left"),e.addClass("fa-angle-right"),c=!1):(d.removeClass("sidebarshown"),f.removeClass("sidebarshown"),e.removeClass("fa-angle-right"),e.addClass("fa-angle-left"),c=!0)})})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/templatepreview.min.js b/theme/adaptable/amd/build/templatepreview.min.js new file mode 100644 index 0000000..50f7e41 --- /dev/null +++ b/theme/adaptable/amd/build/templatepreview.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable Template Preview AMD"),{init:function(){a(document).ready(function(a){b.debug("Adaptable Template Preview AMD init")})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/utils.min.js b/theme/adaptable/amd/build/utils.min.js new file mode 100644 index 0000000..3030faf --- /dev/null +++ b/theme/adaptable/amd/build/utils.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){"use strict";return{init:function(b,c){a(document).ready(function(a){if("coursepage"==b){var d,e,f="sessionStorage"in window&&window.sessionStorage,g=document.location.toString();if(f&&g.indexOf("course/view.php?id=")!=-1){var h=JSON.parse(sessionStorage.getItem("tabValues"))||{},i=JSON.parse(sessionStorage.getItem("tabTimestamp")),j=a("#coursetabcontainer :radio");i?(d=new Date,e=new Date(i),e.setMinutes(e.getMinutes()+parseInt(c)),d.getTime()>e.getTime()&&(console.log("Expired"),sessionStorage.removeItem("tabTimestamp"),sessionStorage.removeItem("tabValues"),h={}),sessionStorage.setItem("tabTimestamp",JSON.stringify(new Date))):(sessionStorage.setItem("tabTimestamp",JSON.stringify(new Date)),console.log("Setting timestamp"));var k=new URL(document.location).searchParams,l=k.get("id");j.on("change",function(){j.each(function(){this.checked&&(h[l]=this.id)}),sessionStorage.setItem("tabValues",JSON.stringify(h))});var m=!1;a.each(h,function(b,c){b==l&&(a("#"+c).prop("checked",!0),m=!0)}),0==m&&a("input:radio[name=tabs]:first").attr("checked",!0),a("label.coursetab").show()}}})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/build/zoomin.min.js b/theme/adaptable/amd/build/zoomin.min.js new file mode 100644 index 0000000..1f87b29 --- /dev/null +++ b/theme/adaptable/amd/build/zoomin.min.js @@ -0,0 +1 @@ +define(["jquery","core/log"],function(a,b){"use strict";return b.debug("Adaptable AMD Zoom in"),{init:function(){a(document).ready(function(a){b.debug("Adaptable AMD Zoom in init");var c=a("#zoominicon");if(c.length){var d=a("#block-region-side-post"),e=a("#zoominicon i.fa"),f=a("body"),g=!1;c.hasClass("left")&&(g=!0);var h=c.data("hidetitle"),i=c.data("showtitle"),j=a(".showhideblocksdesc");"undefined"!=typeof d&&c.click(function(){f.hasClass("zoomin")?(f.removeClass("zoomin"),g?(e.removeClass("fa-indent"),e.addClass("fa-outdent")):(e.removeClass("fa-outdent"),e.addClass("fa-indent")),M.util.set_user_preference("theme_adaptable_zoom","nozoom"),c.prop("title",h),j.length&&j.text(h)):(f.addClass("zoomin"),g?(e.removeClass("fa-outdent"),e.addClass("fa-indent")):(e.removeClass("fa-indent"),e.addClass("fa-outdent")),M.util.set_user_preference("theme_adaptable_zoom","zoomin"),c.prop("title",i),j.length&&j.text(i))})}})}}}); \ No newline at end of file diff --git a/theme/adaptable/amd/src/adaptable.js b/theme/adaptable/amd/src/adaptable.js new file mode 100644 index 0000000..cf08767 --- /dev/null +++ b/theme/adaptable/amd/src/adaptable.js @@ -0,0 +1,127 @@ +// +// This file is part of Adaptable theme for moodle +// +// 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/>. +// +// +// Adaptable main JS file +// +// @package theme_adaptable +// @copyright 2015-2019 Jeremy Hopkins (Coventry University) +// @copyright 2015-2019 Fernando Acedo (3-bits.com) +// @copyright 2018-2019 Manoj Solanki (Coventry University) +// +// @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($ , log) { + "use strict"; // ...jshint ;_; !!! + + log.debug('Adaptable AMD'); + + return { + init: function() { + $(document).ready(function($) { + + log.debug('Adaptable AMD init'); + + // Dismiss Alerts + // + // Bootstrap will close the alert because it spots the data-dismiss="alert" attribute + // Here we also handle the alert close event. We have added two custom tags data-alertindex + // and data-alertkey. e.g Alert1 has alertindex1. The alertkey value identifies the + // alert content, since Alert1 (2 and 3) will be reused. We use a YUI function to set + // the user preference for the key to the last dismissed key for the alertindex. + // alertkey undismissable is a special case for "loginas" alert which shouldn't really + // be permanently dismissed. + // Justin 2015/12/05. + + $('.close').click(function() { + var alertindex = $(this).data('alertindex'); + var alertkey = $(this).data('alertkey'); + if (alertkey != 'undismissable' && alertkey != 'undefined' && alertkey) { + M.util.set_user_preference('theme_adaptable_alertkey' + alertindex, alertkey); + } + }); + + // Breadcrumb. + $(".breadcrumb li:not(:last-child) span").not('.separator').addClass(''); + $(".breadcrumb li:last-child").addClass("lastli"); + + // Edit button keep position. Needs session storage! + try { + $('.context-header-settings-menu .dropdown-menu .dropdown-item a[href*="edit"], #edittingbutton a').click(function(event) { + event.preventDefault(); + + var to = $(window).scrollTop(); + sessionStorage.setItem('scrollTo', to); + var url = $(this).prop('href'); + window.location.replace(url); + + return false; + }); + var scrollTo = sessionStorage.getItem('scrollTo'); + if (scrollTo != null) { + window.scrollTo(0, scrollTo); + sessionStorage.removeItem('scrollTo'); + } + } catch(e) { + log.debug('Adaptable: Session storage exception: ' + e.name); + } + + // Scroll to top. + var offset = 50; + var duration = 500; + var bttOn; + if ($(window).scrollTop() > offset) { + bttOn = false; + } else { + bttOn = true; + } + var scrollCheck = function() { + if ($(window).scrollTop() > offset) { + if (bttOn == false) { + bttOn = true; + $('#back-to-top').fadeIn(duration); + } + } else { + if (bttOn == true) { + bttOn = false; + $('#back-to-top').fadeOut(duration); + } + } + }; + scrollCheck(); + $(window).scroll(function () { + scrollCheck(); + }); + + $('#back-to-top').click(function(event) { + event.preventDefault(); + $('html, body').animate({scrollTop: 0}, duration); + return false; + }) + + // Anchor. + if (window.location.hash) { + if ($("body").hasClass("pagelayout-course")) { + var anchorTop = $(window.location.hash).offset().top; + $('html, body').animate({scrollTop: anchorTop - 102}, duration); + } + } + }); + } + }; +}); +/* jshint ignore:end */ \ No newline at end of file diff --git a/theme/adaptable/amd/src/bsoptions.js b/theme/adaptable/amd/src/bsoptions.js new file mode 100644 index 0000000..080bda3 --- /dev/null +++ b/theme/adaptable/amd/src/bsoptions.js @@ -0,0 +1,181 @@ +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($, log) { + "use strict"; // ...jshint ;_; !!! + + log.debug('Adaptable Bootstrap AMD opt in functions'); + + return { + init: function(data) { + $(document).ready(function($) { + + // Get the navbar, if present. + var navbar = document.getElementById("main-navbar"); + + if (data.stickynavbar && navbar != null) { + + // New way to handle sticky navbar requirement. + // Simply taken from https://www.w3schools.com/howto/howto_js_navbar_sticky.asp. + + // Initial sticky position. + var sticky = navbar.offsetTop; + + // When the user scrolls the page, execute makeNavbarSticky(). + window.onscroll = function() {makeNavbarSticky()}; + + // When the page changes size, check the sticky. + window.onresize = function() {checkSticky()}; + + // Changed? + var isSticky = (window.pageYOffset < sticky); // Initial inverse logic to cause first check to work. + + // Check if we are already down the page because of an anchor etc. + makeNavbarSticky(); + + // Add the sticky class to the navbar when you reach its scroll position. Remove "sticky" when you leave the scroll position + function makeNavbarSticky() { + if (sticky > 0) { + if (window.pageYOffset >= sticky) { + if (isSticky == false) { + navbar.classList.add("adaptable-navbar-sticky") + isSticky = true; + } + } else { + if (isSticky == true) { + navbar.classList.remove("adaptable-navbar-sticky"); + isSticky = false; + } + } + } + } + + // Adjust sticky if 0 when window resizes. + function checkSticky() { + if (sticky == 0) { + sticky = navbar.offsetTop; + isSticky = (window.pageYOffset < sticky); + // Check if we are already down the page because of an anchor etc. + makeNavbarSticky(); + } + } + } + + var screenmd = 992; + + var isFixed = 0; + /* Ok, here's an odd one... desktops need to use the 'inner' variables and mobiles the 'outer' to be accurate! + But... I've (GB) found that the jQuery height and width functions adapt and report close to correct values + regardless of device, so use them instead without complicated device detection here! + Update: postion:fixed does not work on mobiles at the moment so won't be for such, left comment for future info. */ + + // Top navbar stickyness. + // As per above comments, some issues noted with using CSS position: fixed, but these seem to mostly be constrained + // to older browsers (inc. mobile browsers). May need to revisit! + // https://caniuse.com/#feat=css-fixed + if ($(window).width() <= screenmd) { + $("#adaptable-page-header-wrapper").addClass("fixed-top"); + $("body").addClass("page-header-margin"); + isFixed = 1; + } else { + $("#adaptable-page-header-wrapper").removeClass("fixed-top"); + $("body").removeClass("page-header-margin") + } + + // If you want these classes to toggle when a desktop user shrinks the browser width to an xs width - or from xs to larger. + $(window).resize(function() { + if ($(window).width() <= screenmd) { + if (isFixed == 0) { + $("#adaptable-page-header-wrapper").addClass("fixed-top"); + $("body").addClass("page-header-margin"); + isFixed = 1; + } + } else { + if (isFixed == 1) { + $("#adaptable-page-header-wrapper").removeClass("fixed-top"); + $("body").removeClass("page-header-margin"); + isFixed = 0; + } + } + }); + + var showsidebaricon = $("#showsidebaricon"); + if (showsidebaricon.length) { + // Using 'css' and not 'offset' function as latter seems unreliable on mobiles as changes the value! + showsidebaricon.css({ top: ($(window).height() / 2) + 'px'}); + } + + $(window).resize(function() { + if ($(window).width() > screenmd) { + var navDrawer = $("#nav-drawer"); + if (navDrawer.length) { + if (!navDrawer.hasClass("closed")) { + navDrawer.addClass("closed"); + navDrawer.attr("aria-hidden", "true"); + $("#drawer").attr("aria-expanded", "false"); + var side = $('#drawer').attr('data-side'); + $("body").removeClass("drawer-open-" + side); + } + } + } + if (showsidebaricon.length) { + showsidebaricon.css({ top: ($(window).height() / 2) + 'px'}); + } + }); + + $('.moodlewidth').click(function() { + if ($('#page').hasClass('fullin') ) { + $('#page').removeClass('fullin'); + M.util.set_user_preference('theme_adaptable_full', 'nofull'); + } else { + $('#page').addClass('fullin'); + M.util.set_user_preference('theme_adaptable_full', 'fullin'); + } + }); + + $('#openoverlaymenu').click(function() { + $('#conditionalmenu').toggleClass('open'); + }); + $('#overlaymenuclose').click(function() { + $('#conditionalmenu').toggleClass('open'); + }); + + // Bootstrap sub-menu functionality. + // See: https://bootstrapthemes.co/demo/resource/bootstrap-4-multi-dropdown-hover-navbar/. + + $( '.dropdown-menu a.dropdown-toggle' ).on( 'click', function ( e ) { + var $el = $( this ); + var $parent = $( this ).offsetParent( ".dropdown-menu" ); + if ( !$( this ).next().hasClass( 'show' ) ) { + $( this ).parents( '.dropdown-menu' ).first().find( '.show' ).removeClass( "show" ); + } + var $subMenu = $( this ).next( ".dropdown-menu" ); + $subMenu.toggleClass( 'show' ); + + $( this ).parent( "li" ).toggleClass( 'show' ); + + $( this ).parents( 'li.nav-item.dropdown.show' ).on( 'hidden.bs.dropdown', function ( e ) { + $( '.dropdown-menu .show' ).removeClass( "show" ); + } ); + + if ( !$parent.parent().hasClass( 'navbar-nav' ) ) { + $el.next().css( { "top": $el[0].offsetTop, "left": $parent.outerWidth() - 4 } ); + } + + return false; + } ); + + }); + + // Conditional javascript to resolve anchor link clicking issue with sticky navbar. + // in old bootstrap version. Re: issue #919. + // Original issue / solution discussion here: https://github.com/twbs/bootstrap/issues/1768. + if (data.stickynavbar) { + var shiftWindow = function() { scrollBy(0, -50) }; + if (location.hash) { + shiftWindow(); + } + window.addEventListener("hashchange", shiftWindow); + } + } + }; +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/amd/src/drawer.js b/theme/adaptable/amd/src/drawer.js new file mode 100644 index 0000000..66a2122 --- /dev/null +++ b/theme/adaptable/amd/src/drawer.js @@ -0,0 +1,36 @@ +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($, log) { + + "use strict"; // jshint ;_; + + log.debug('Adaptable Drawer AMD'); + + return { + init: function() { + $(document).ready(function($) { + var body = $('body'); + var side = $('#drawer').attr('data-side'); + $('#drawer').click(function() { + var drawer = $('#nav-drawer'); + + if (drawer.hasClass('closed')) { + // Drawer closed -> open. + drawer.removeClass('closed'); + body.addClass('drawer-open-' + side); + drawer.attr('aria-hidden', 'false'); + $(this).attr('aria-expanded', 'true'); + } else { + // Drawer open -> closed. + drawer.addClass('closed'); + body.removeClass('drawer-open-' + side); + drawer.attr('aria-hidden', 'true'); + $(this).attr('aria-expanded', 'false'); + } + }); + body.addClass('drawer-ease'); + log.debug('Adaptable Drawer AMD init'); + }); + } + } +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/amd/src/savebutton.js b/theme/adaptable/amd/src/savebutton.js new file mode 100644 index 0000000..4fa72ae --- /dev/null +++ b/theme/adaptable/amd/src/savebutton.js @@ -0,0 +1,37 @@ +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($, log) { + + "use strict"; // ... jshint ;_;. + + log.debug('Adaptable savebutton.js function called'); + + return { + init: function() { + $(document).ready(function($) { + + $("#savediscardsection").hide(); + + $('#adminsettings :input').on('change input', function() { + $("#savediscardsection").fadeIn('slow'); + }); + + $("#adminsubmitbutton").click(function() { + window.onbeforeunload = null; + $("#adminsettings").submit(); + }); + $("#adminresetbutton").click(function() { + var result = confirm("This resets any changes made since loading this page. Are you sure?") + if (result == true) { + $('#adminsettings')[0].reset(); + $("#savediscardsection").hide(); + } + }); + + $(".colourdialogue").click(function() { + $("#savediscardsection").fadeIn('slow'); + }); + }); + } + }; +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/amd/src/search-input.js b/theme/adaptable/amd/src/search-input.js new file mode 100644 index 0000000..734e92e --- /dev/null +++ b/theme/adaptable/amd/src/search-input.js @@ -0,0 +1,161 @@ +// 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/>. + +/** + * Search box. + * + * @module theme_adaptable/search-input + * @class search-input + * @package theme_adaptable + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @copyright Adaptable changes 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/log'], function($, log) { + + /** + * This search box div node. + * + * @private + */ + var wrapper = null; + + /** + * Submits the form. + * + * @param {Event} ev + * @method submitForm + * @private + */ + var submitForm = function() { + wrapper.find('form').submit(); + }; + + /** + * Shows the form or submits it depending on the window size. + * + * @param {Event} ev + * @method showForm + * @private + */ + var showForm = function(ev) { + + var windowWidth = $(document).width(); + + // We are only interested in enter and space keys (accessibility). + if (ev.type === 'keydown' && ev.keyCode !== 13 && ev.keyCode !== 32) { + return; + } + + if (windowWidth <= 767 && (ev.type === 'click' || ev.type === 'keydown')) { + // Move to the search page when using small window sizes as the input requires too much space. + submitForm(); + return; + } else if (windowWidth <= 767) { + // Ignore mousedown events in while using small window sizes. + return; + } + + if (ev.type === 'keydown') { + // We don't want to submit the form unless the user hits enter. + ev.preventDefault(); + } + + wrapper.addClass('expanded'); + wrapper.find('form').addClass('expanded'); + wrapper.find('input').focus(); + }; + + /** + * Hides the form. + * + * @method hideForm + * @private + */ + var hideForm = function() { + wrapper.removeClass('expanded'); + wrapper.find('form').removeClass('expanded'); + }; + + /** + * Processes the form when not expanding / collapsing and submits it depending on the window size. + * + * @param {Event} ev + * @method showForm + * @private + */ + var processForm = function(ev) { + var windowWidth = $(document).width(); + + // We are only interested in enter and space keys (accessibility). + if (ev.type === 'keydown' && ev.keyCode !== 13 && ev.keyCode !== 32) { + return; + } + + if (windowWidth <= 767 && (ev.type === 'click' || ev.type === 'keydown')) { + // Move to the search page when using small window sizes as the input requires too much space. + submitForm(); + return; + } else if (windowWidth <= 767) { + // Ignore mousedown events in while using small window sizes. + return; + } + + if (ev.type === 'keydown') { + // We don't want to submit the form unless the user hits enter. + ev.preventDefault(); + } + + wrapper.find('input').focus(); + }; + + /** + * Toggles the form visibility. + * + * @param {Event} ev + * @method toggleForm + * @private + */ + var toggleForm = function(ev) { + + if (wrapper.hasClass('expanded')) { + hideForm(); + } else { + showForm(ev); + } + }; + + return /** @alias module:theme_adaptable/search-input */ { + // Public variables and functions. + + /** + * Assigns listeners to the requested select box. + * + * @method init + * @param {Array} data Containing: + * @param {Number} id The search wrapper div id + * @param {Boolean} expandable If the search box expands and collapses. + */ + init: function(data) { + log.debug('Adaptable Search Input AMD Init'); + wrapper = $('#' + data.id); + if (data.expandable == true) { + wrapper.on('click mouseover keydown', 'div', toggleForm); + } else { + wrapper.on('click mouseover keydown', 'div', processForm); + } + } + }; +}); diff --git a/theme/adaptable/amd/src/showsidebar.js b/theme/adaptable/amd/src/showsidebar.js new file mode 100644 index 0000000..d55ea6f --- /dev/null +++ b/theme/adaptable/amd/src/showsidebar.js @@ -0,0 +1,37 @@ +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($, log) { + "use strict"; // ...jshint ;_; !!! + + log.debug('Adaptable AMD Show sidebar'); + + return { + init: function() { + $(document).ready(function($) { + log.debug('Adaptable AMD Show sidebar init'); + + var sidePostClosed = true; + var sidePost = $('#block-region-side-post'); + var showSideBarIcon = $('#showsidebaricon i.fa'); + var body = $('body'); + if (typeof sidePost != 'undefined') { + $('#showsidebaricon').click(function() { + if (sidePostClosed === true) { + sidePost.addClass('sidebarshown'); + body.addClass('sidebarshown'); + showSideBarIcon.removeClass('fa-angle-left'); + showSideBarIcon.addClass('fa-angle-right'); + sidePostClosed = false; + } else { + sidePost.removeClass('sidebarshown'); + body.removeClass('sidebarshown'); + showSideBarIcon.removeClass('fa-angle-right'); + showSideBarIcon.addClass('fa-angle-left'); + sidePostClosed = true; + } + }); + } + }); + } + }; +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/amd/src/templatepreview.js b/theme/adaptable/amd/src/templatepreview.js new file mode 100644 index 0000000..0b99613 --- /dev/null +++ b/theme/adaptable/amd/src/templatepreview.js @@ -0,0 +1,16 @@ +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($, log) { + + "use strict"; // jshint ;_; + + log.debug('Adaptable Template Preview AMD'); + + return { + init: function() { + $(document).ready(function($) { + log.debug('Adaptable Template Preview AMD init'); + }); + } + } +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/amd/src/utils.js b/theme/adaptable/amd/src/utils.js new file mode 100644 index 0000000..d61197f --- /dev/null +++ b/theme/adaptable/amd/src/utils.js @@ -0,0 +1,73 @@ +/* jshint ignore:start */ +define(['jquery'], function($) { + + "use strict"; // ... jshint ;_;. + return { + init: function(currentpage, tabpersistencetime) { + + $(document).ready(function($) { + if (currentpage == 'coursepage') { + var hasStorage = ("sessionStorage" in window && window.sessionStorage); + var now, expiration; + var currentUrl = document.location.toString(); + + if ( (hasStorage) && (currentUrl.indexOf('course/view.php?id=') != -1) ) { + + var tabValues = JSON.parse(sessionStorage.getItem('tabValues')) || {}; + var tabTimestamp = JSON.parse(sessionStorage.getItem('tabTimestamp')); + var $radiobuttons = $("#coursetabcontainer :radio"); + + // Check timestamp for session. + if (tabTimestamp) { + // calculate expiration time for content, + // to force periodic refresh after 30 minutes + now = new Date(); + expiration = new Date(tabTimestamp); + expiration.setMinutes(expiration.getMinutes() + parseInt(tabpersistencetime)); + if (now.getTime() > expiration.getTime()) { + console.log ('Expired'); + sessionStorage.removeItem('tabTimestamp'); + sessionStorage.removeItem('tabValues'); + tabValues = {}; + } + + // Reset timestamp anyway as user is still active. + sessionStorage.setItem("tabTimestamp", JSON.stringify(new Date())); + } else { + sessionStorage.setItem("tabTimestamp", JSON.stringify(new Date())); + console.log ('Setting timestamp'); + } + + var params = (new URL(document.location)).searchParams; + var courseid = params.get("id"); + + $radiobuttons.on("change", function() { + + $radiobuttons.each(function(){ + if (this.checked) { + tabValues[courseid] = this.id; + } + }); + sessionStorage.setItem("tabValues", JSON.stringify(tabValues)); + }); + + var tabhasbeenset = false; + $.each(tabValues, function(key, value) { + if (key == courseid) { + $("#" + value).prop('checked', true); + tabhasbeenset = true; + } + }); + if (tabhasbeenset == false) { + $("input:radio[name=tabs]:first").attr('checked', true); + } + + $('label.coursetab').show(); + } + + } + }); + } + }; +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/amd/src/zoomin.js b/theme/adaptable/amd/src/zoomin.js new file mode 100644 index 0000000..c22b12d --- /dev/null +++ b/theme/adaptable/amd/src/zoomin.js @@ -0,0 +1,63 @@ +/* jshint ignore:start */ +define(['jquery', 'core/log'], function($, log) { + "use strict"; // ...jshint ;_; !!! + + log.debug('Adaptable AMD Zoom in'); + + return { + init: function() { + $(document).ready(function($) { + log.debug('Adaptable AMD Zoom in init'); + + var zoomInIcon = $('#zoominicon'); + if (zoomInIcon.length) { + var sidePost = $('#block-region-side-post'); + var zoomInFaIcon = $('#zoominicon i.fa'); + var body = $('body'); + var zoomLeft = false; + if (zoomInIcon.hasClass('left')) { + zoomLeft = true; + } + var hidestring = zoomInIcon.data('hidetitle'); + var showstring = zoomInIcon.data('showtitle'); + var showhideblocksdesc = $('.showhideblocksdesc'); + + if (typeof sidePost != 'undefined') { + zoomInIcon.click(function() { + if (body.hasClass('zoomin') ) { // Blocks not shown. + body.removeClass('zoomin'); + if (zoomLeft) { + zoomInFaIcon.removeClass('fa-indent'); + zoomInFaIcon.addClass('fa-outdent'); + } else { + zoomInFaIcon.removeClass('fa-outdent'); + zoomInFaIcon.addClass('fa-indent'); + } + M.util.set_user_preference('theme_adaptable_zoom', 'nozoom'); + zoomInIcon.prop('title', hidestring); + if (showhideblocksdesc.length) { + showhideblocksdesc.text(hidestring); + } + } else { + body.addClass('zoomin'); + if (zoomLeft) { + zoomInFaIcon.removeClass('fa-outdent'); + zoomInFaIcon.addClass('fa-indent'); + } else { + zoomInFaIcon.removeClass('fa-indent'); + zoomInFaIcon.addClass('fa-outdent'); + } + M.util.set_user_preference('theme_adaptable_zoom', 'zoomin'); + zoomInIcon.prop('title', showstring); + if (showhideblocksdesc.length) { + showhideblocksdesc.text(showstring); + } + } + }); + } + } + }); + } + }; +}); +/* jshint ignore:end */ diff --git a/theme/adaptable/classes/activity.php b/theme/adaptable/classes/activity.php new file mode 100644 index 0000000..be2eb73 --- /dev/null +++ b/theme/adaptable/classes/activity.php @@ -0,0 +1,903 @@ +<?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/>. + +/** + * Activity-related functions. + * + * This defines the activity class that is used to retrieve activity-related information, such as submission status, + * due dates etc. + * + * @package theme_adaptable + * @copyright 2018 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable; + +defined('MOODLE_INTERNAL') || die(); + +use \cm_info; + +require_once($CFG->dirroot.'/mod/assign/locallib.php'); + +/** + * Activity functions. + * + * These functions are in a class purely for auto loading convenience. + * + * @package theme_adaptable + * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) + * @copyright Copyright (c) 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity { + + /** + * + * Main method that calls relevant activity-related method based on the mod name. + * + * @param \cm_info $mod + * @return activity_meta + */ + public static function module_meta(cm_info $mod) { + $methodname = $mod->modname . '_meta'; + if (method_exists('theme_adaptable\\activity', $methodname)) { + $meta = call_user_func('theme_adaptable\\activity::' . $methodname, $mod); + } else { + $meta = new activity_meta(); // Return empty activity meta. + } + + if ((!empty($meta->timeclose)) && ($meta->timeclose < time())) { + $meta->expired = true; + } + + return $meta; + } + + /** + * Return standard meta data for module + * + * @param cm_info $mod + * @param string $timeopenfld + * @param string $timeclosefld + * @param string $keyfield + * @param string $submissiontable + * @param string $submittedonfld + * @param string $submitstrkey + * @param bool $isgradeable + * @param string $submitselect - sql to further filter submission row select statement - e.g. st.status='finished' + * @param bool $submissionnotrequired + * @return activity_meta + */ + protected static function std_meta(cm_info $mod, + $timeopenfld, + $timeclosefld, + $keyfield, + $submissiontable, + $submittedonfld, + $submitstrkey, + $isgradeable = false, + $submitselect = '', + $submissionnotrequired = false + ) { + global $USER; + + $courseid = $mod->course; + + // Create meta data object. Use set_default for when is_set(true) so that only changed metas are valid. + $meta = new activity_meta(); + + // If module is not visible to the user then don't bother getting meta data. + if (!$mod->uservisible) { + return $meta; + } + + $meta->set_default('submissionnotrequired', $submissionnotrequired); + $meta->set_default('submitstrkey', $submitstrkey); + $meta->set_default('submittedstr', get_string($submitstrkey, 'theme_adaptable')); + $meta->set_default('notsubmittedstr', get_string('not'.$submitstrkey, 'theme_adaptable')); + $meta->set_default('draftstr', get_string('draft', 'theme_adaptable')); + $meta->set_default('reopenedstr', get_string('reopened', 'theme_adaptable')); + $meta->set_default('expiredstr', get_string('expired', 'theme_adaptable')); + + $activitydates = self::instance_activity_dates($courseid, $mod, $timeopenfld, $timeclosefld); + $meta->timeopen = $activitydates->timeopen; + $meta->timeclose = $activitydates->timeclose; + if (isset($activitydates->extension)) { + $meta->extension = $activitydates->extension; + } + + // If role has specific "teacher" capabilities. + if (has_capability('mod/assign:grade', $mod->context)) { + $meta->isteacher = true; + + if ($mod->modname === 'assign') { + list( + 'participants' => $meta->numparticipants, + 'submissions' => $meta->numsubmissions, + 'ungraded' => $meta->numrequiregrading, + ) = self::assign_nums($courseid, $mod); + } else { + // Teacher - useful teacher meta data. + $methodnsubmissions = $mod->modname.'_num_submissions'; + $methodnumgraded = $mod->modname.'_num_submissions_ungraded'; + $methodparticipants = $mod->modname.'_num_participants'; + + if (method_exists('theme_adaptable\\activity', $methodnsubmissions)) { + $meta->numsubmissions = call_user_func('theme_adaptable\\activity::'. + $methodnsubmissions, $courseid, $mod); + } + if (method_exists('theme_adaptable\\activity', $methodnumgraded)) { + $meta->numrequiregrading = call_user_func('theme_adaptable\\activity::'. + $methodnumgraded, $courseid, $mod); + } + if (method_exists('theme_adaptable\\activity', $methodparticipants)) { + $meta->numparticipants = call_user_func('theme_adaptable\\activity::'. + $methodparticipants, $courseid, $mod); + } else { + $meta->numparticipants = self::course_participant_count($courseid, $mod); + } + } + + } else { + // Student - useful student meta data - only display if activity is available. + if (empty($activitydates->timeopen) || $activitydates->timeopen <= time()) { // TODO User time needed??? + + $submissionrow = self::get_submission_row($courseid, $mod, $submissiontable, $keyfield, $submitselect); + + if (!empty($submissionrow)) { + if ($mod->modname === 'assign' && !empty($submissionrow->status)) { + switch ($submissionrow->status) { + case ASSIGN_SUBMISSION_STATUS_DRAFT: + $meta->draft = true; + break; + + case ASSIGN_SUBMISSION_STATUS_REOPENED: + $meta->reopened = true; + break; + + case ASSIGN_SUBMISSION_STATUS_SUBMITTED: + $meta->submitted = true; + break; + } + } else { + $meta->submitted = true; + $meta->timesubmitted = !empty($submissionrow->$submittedonfld) ? $submissionrow->$submittedonfld : null; + } + // If submitted on field uses modified field then fall back to timecreated if modified is 0. + if (empty($meta->timesubmitted) && $submittedonfld = 'timemodified') { + if (isset($submissionrow->timemodified)) { + $meta->timesubmitted = $submissionrow->timemodified; + } else { + $meta->timesubmitted = $submissionrow->timecreated; + } + } + } else if ($mod->modname === 'assign') { + return null; + } + } + + $graderow = false; + if ($isgradeable) { + $graderow = self::grade_row($courseid, $mod); + } + + if ($graderow) { + $gradeitem = \grade_item::fetch(array( + 'itemtype' => 'mod', + 'itemmodule' => $mod->modname, + 'iteminstance' => $mod->instance, + 'outcomeid' => null + )); + + $grade = new \grade_grade(array('itemid' => $gradeitem->id, 'userid' => $USER->id)); + + $coursecontext = \context_course::instance($courseid); + $canviewhiddengrade = has_capability('moodle/grade:viewhidden', $coursecontext); + + if (!$grade->is_hidden() || $canviewhiddengrade) { + $meta->grade = true; + } + } + } + + if (!empty($meta->timeclose)) { + // Submission required? + $subreqd = empty($meta->submissionnotrequired); + + // Overdue? + $meta->overdue = $subreqd && empty($meta->submitted) && (time() > $meta->timeclose); + } + + return $meta; + } + + /** + * Get assignment meta data + * + * @param cm_info $modinst - module instance + * @return activity_meta + */ + protected static function assign_meta(cm_info $modinst) { + global $DB, $USER; + static $submissionsenabled; + + $courseid = $modinst->course; + + /* Get count of enabled submission plugins grouped by assignment id. + Note, under normal circumstances we only run this once but with PHP unit tests, assignments are being + created one after the other and so this needs to be run each time during a PHP unit test. */ + if (empty($submissionsenabled) || PHPUNIT_TEST) { + $sql = "SELECT a.id, count(1) AS submissionsenabled + FROM {assign} a + JOIN {assign_plugin_config} ac ON ac.assignment = a.id + WHERE a.course = ? + AND ac.name='enabled' + AND ac.value = '1' + AND ac.subtype='assignsubmission' + AND plugin!='comments' + GROUP BY a.id;"; + $submissionsenabled = $DB->get_records_sql($sql, array($courseid)); + } + + $submitselect = ''; + + // If there aren't any submission plugins enabled for this module, then submissions are not required. + if (empty($submissionsenabled[$modinst->instance])) { + $submissionnotrequired = true; + } else { + $submissionnotrequired = false; + } + + $meta = self::std_meta($modinst, 'allowsubmissionsfromdate', 'duedate', 'assignment', 'submission', + 'timemodified', 'submitted', true, $submitselect, $submissionnotrequired); + + if (!empty($meta)) { + // Check assignment due date in user and group overrides. + $context = \context_module::instance($modinst->id); + $assign = new \assign($context, $modinst, $courseid); + $assign->update_effective_access($USER->id); + $submissionstatus = $assign->get_assign_submission_status_renderable($USER, false); + if (!empty($submissionstatus->duedate) && $submissionstatus->duedate != $meta->timeclose) { + $meta->timeclose = $submissionstatus->duedate; + } + } + + return ($meta); + } + + /** + * Get choice module meta data + * + * @param cm_info $modinst - module instance + * @return string + */ + protected static function choice_meta(cm_info $modinst) { + return self::std_meta($modinst, 'timeopen', 'timeclose', 'choiceid', 'answers', 'timeseen', 'answered'); + } + + /** + * Get database module meta data + * + * @param cm_info $modinst - module instance + * @return string + */ + protected static function data_meta(cm_info $modinst) { + return self::std_meta($modinst, 'timeavailablefrom', 'timeavailableto', 'dataid', 'records', 'timemodified', 'contributed'); + } + + /** + * Get feedback module meta data + * + * @param cm_info $modinst - module instance + * @return string + */ + protected static function feedback_meta(cm_info $modinst) { + return self::std_meta($modinst, 'timeopen', 'timeclose', 'feedback', 'completed', 'timemodified', 'submitted'); + } + + /** + * Get lesson module meta data + * + * @param cm_info $modinst - module instance + * @return string + */ + protected static function lesson_meta(cm_info $modinst) { + $meta = self::std_meta($modinst, 'available', 'deadline', 'lessonid', 'timer', 'lessontime', 'attempted', true); + // TO BE DELETED: $meta->submissionnotrequired = true; .......... + return $meta; + } + + /** + * Get quiz module meta data + * + * @param cm_info $modinst - module instance + * @return string + */ + protected static function quiz_meta(cm_info $modinst) { + return self::std_meta($modinst, 'timeopen', 'timeclose', 'quiz', + 'attempts', 'timemodified', 'attempted', true, 'AND st.state=\'finished\''); + } + + // The lesson_ungraded function has been removed as it was very tricky to implement. + // This was because it creates a grade record as soon as a student finishes the lesson. + + /** + * Standard function for getting number of submissions (where sql is not complicated and pretty much standard) + * + * @param int $courseid + * @param cm_info $mod + * @param string $maintable + * @param string $mainkey + * @param string $submittable + * @param string $extraselect + * + * @return int + */ + protected static function std_num_submissions( + $courseid, + $mod, + $maintable, + $mainkey, + $submittable, + $extraselect = '') { + global $DB; + + static $modtotalsbyid = array(); + + if (!isset($modtotalsbyid[$maintable][$courseid])) { + // Results are not cached, so lets get them. + + // Get people who are typically not students (people who can view grader report) so that we can exclude them! + list($graderids, $params) = get_enrolled_sql(\context_course::instance($courseid), 'moodle/grade:viewall'); + $params['courseid'] = $courseid; + + // Get the number of submissions for all $maintable activities in this course. + $sql = "-- Snap sql + SELECT m.id, COUNT(DISTINCT sb.userid) as totalsubmitted + FROM {".$maintable."} m + JOIN {".$submittable."} sb ON m.id = sb.$mainkey + WHERE m.course = :courseid + AND sb.userid NOT IN ($graderids) + $extraselect + GROUP BY m.id"; + $modtotalsbyid[$maintable][$courseid] = $DB->get_records_sql($sql, $params); + } + $totalsbyid = $modtotalsbyid[$maintable][$courseid]; + + if (!empty($totalsbyid)) { + $instanceid = $mod->instance; + if (isset($totalsbyid[$instanceid])) { + return intval($totalsbyid[$instanceid]->totalsubmitted); + } + } + return 0; + } + + /** + * Get assignment number information. + * + * @param int $courseid + * @param cm_info $mod + * @return array + */ + protected static function assign_nums($courseid, $mod) { + // Ref: get_assign_grading_summary_renderable(). + $coursemodulecontext = \context_module::instance($mod->id); + $course = get_course($courseid); + $assign = new \assign($coursemodulecontext, $mod, $course); + $activitygroup = groups_get_activity_group($mod); + $instance = $assign->get_default_instance(); + if ($instance->teamsubmission) { + $participants = $assign->count_teams($activitygroup); + } else { + $participants = $assign->count_participants($activitygroup); + } + $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + + return array( + 'participants' => $participants, + 'submissions' => $assign->count_submissions_with_status($submitted, $activitygroup), + 'ungraded' => $assign->count_submissions_need_grading($activitygroup) + ); + } + + /** + * Data module function for getting number of contributions. + * + * @param int $courseid + * @param cm_info $mod + * @return int + */ + protected static function data_num_submissions($courseid, $mod) { + global $DB; + + $modid = $mod->id; + + static $modtotalsbyid = array(); + + if (!isset($modtotalsbyid['data'][$modid])) { + $params['dataid'] = $modid; + + // Get the number of contributions for this data activity. + $sql = ' + SELECT d.id, count(dataid) as total FROM {data_records} r, {data} d + WHERE r.dataid = d.id AND r.dataid = :dataid GROUP BY d.id'; + + $modtotalsbyid['data'][$modid] = $DB->get_records_sql($sql, $params); + } + $totalsbyid = $modtotalsbyid['data'][$modid]; + // TO BE DELETED echo '<br>' . print_r($totalsbyid, 1) . '<br>'; .... + if (!empty($totalsbyid)) { + if (isset($totalsbyid[$modid])) { + return intval($totalsbyid[$modid]->total); + } + } + return 0; + } + + /** + * Get number of answers for specific choice. + * + * @param int $courseid + * @param cm_info $mod + * @return int + */ + protected static function choice_num_submissions($courseid, $mod) { + return self::std_num_submissions($courseid, $mod, 'choice', 'choiceid', 'choice_answers'); + } + + /** + * Get number of submissions for feedback activity. + * + * @param int $courseid + * @param cm_info $mod + * @return int + */ + protected static function feedback_num_submissions($courseid, $mod) { + return self::std_num_submissions($courseid, $mod, 'feedback', 'feedback', 'feedback_completed'); + } + + /** + * Get number of submissions for lesson activity. + * + * @param int $courseid + * @param cm_info $mod + * @return int + */ + protected static function lesson_num_submissions($courseid, $mod) { + return self::std_num_submissions($courseid, $mod, 'lesson', 'lessonid', 'lesson_timer'); + } + + /** + * Get number of attempts for specific quiz. + * + * @param int $courseid + * @param cm_info $mod + * @return int + */ + protected static function quiz_num_submissions($courseid, $mod) { + return self::std_num_submissions($courseid, $mod, 'quiz', 'quiz', 'quiz_attempts'); + } + + /** + * Get number of ungraded quiz attempts for specific quiz. + * + * @param int $courseid + * @param cm_info $mod + * @return int + */ + protected static function quiz_num_submissions_ungraded($courseid, $mod) { + global $DB; + + static $totalsbyquizid; + + $coursecontext = \context_course::instance($courseid); + // Get people who are typically not students (people who can view grader report) so that we can exclude them! + list($graderids, $params) = get_enrolled_sql($coursecontext, 'moodle/grade:viewall'); + $params['courseid'] = $courseid; + + if (!isset($totalsbyquizid)) { + // Results are not cached. + $sql = "-- Snap sql + SELECT q.id, count(DISTINCT qa.userid) as total + FROM {quiz} q + + -- Get ALL ungraded attempts for this quiz + + JOIN {quiz_attempts} qa ON qa.quiz = q.id + AND qa.sumgrades IS NULL + + -- Exclude those people who can grade quizzes + + WHERE qa.userid NOT IN ($graderids) + AND qa.state = 'finished' + AND q.course = :courseid + GROUP BY q.id"; + $totalsbyquizid = $DB->get_records_sql($sql, $params); + } + + if (!empty($totalsbyquizid)) { + $quizid = $mod->instance; + if (isset($totalsbyquizid[$quizid])) { + return intval($totalsbyquizid[$quizid]->total); + } + } + + return 0; + } + + /** + * Get activity submission row. + * + * This method gets all of the possible submission rows for the user for the given module type + * and then caches them so that there is only one database read for the user per module type + * regardless of the number on the course. + * + * @param int $courseid + * @param cm_info $mod + * @param string $submissiontable + * @param string $modfield + * @param string $extraselect + * + * @return mixed + */ + protected static function get_submission_row($courseid, $mod, $submissiontable, $modfield, $extraselect='') { + global $DB, $USER; + + // Note: Caches all submissions to minimise database transactions. + static $submissions = array(); + + // Pull from cache? + if (!PHPUNIT_TEST) { + if (isset($submissions[$courseid.'_'.$mod->modname])) { + if (isset($submissions[$courseid.'_'.$mod->modname][$mod->instance])) { + return $submissions[$courseid.'_'.$mod->modname][$mod->instance]; + } else { + return false; + } + } + } + + $submissiontable = $mod->modname.'_'.$submissiontable; + + if ($mod->modname === 'assign') { + $params = [$courseid]; + $sql = "-- Snap sql + SELECT a.id, st.* + FROM {".$submissiontable."} st + + JOIN {".$mod->modname."} a + ON a.id = st.$modfield + + WHERE a.course = ? + AND st.latest = 1 $extraselect + ORDER BY $modfield DESC, st.id DESC"; + } else { + // Less effecient general purpose for other module types. + $params = [$USER->id, $courseid, $USER->id]; + $sql = "-- Snap sql + SELECT a.id AS instanceid, st.* + FROM {".$submissiontable."} st + + JOIN {".$mod->modname."} a + ON a.id = st.$modfield + + -- Get only the most recent submission. + JOIN (SELECT $modfield AS modid, MAX(id) AS maxattempt + FROM {".$submissiontable."} + WHERE userid = ? + GROUP BY $modfield) AS smx + ON smx.modid = st.$modfield + AND smx.maxattempt = st.id + + WHERE a.course = ? + AND st.userid = ? $extraselect + ORDER BY $modfield DESC, st.id DESC"; + } + + /* Not every activity has a status field... + Add one if it is missing so code assuming there is a status property doesn't explode. */ + $results = $DB->get_records_sql($sql, $params); + if (!$results) { + unset($submissions[$courseid.'_'.$mod->modname]); + return false; + } + + foreach ($results as $r) { + if (!isset($r->status)) { + $r->status = null; + } + } + + if ($mod->modname === 'assign') { + /* Assignment submissions can either be against the user's id or a group they are in. + Make a simple list of the groups the user is in. */ + $usergroups = array(); + foreach ($USER->groupmember as $grouparray) { + foreach ($grouparray as $group) { + $usergroups[] = $group; + } + } + + $theresults = array(); + foreach ($results as $r) { + if (!empty($r->userid)) { // User id of 0 means that there should be a groupid. + if ($r->userid == $USER->id) { // This record is for us. + $theresults[$r->assignment] = $r; + } + } else if (!empty($r->groupid)) { + if (in_array($r->groupid, $usergroups)) { // This record is in one of our groups. + $theresults[$r->assignment] = $r; + } + } + } + } else { + $theresults = $results; + } + + $submissions[$courseid.'_'.$mod->modname] = $theresults; + + if (isset($submissions[$courseid.'_'.$mod->modname][$mod->instance])) { + return $submissions[$courseid.'_'.$mod->modname][$mod->instance]; + } else { + return false; + } + } + + /** + * Get the activity dates for a specific module instance. + * + * @param int $courseid + * @param cm_info $mod + * @param string $timeopenfld + * @param string $timeclosefld + * + * @return bool|stdClass + */ + protected static function instance_activity_dates($courseid, $mod, $timeopenfld = '', $timeclosefld = '') { + global $DB, $USER; + // Note: Caches all moduledates to minimise database transactions. + static $moddates = array(); + if (!isset($moddates[$courseid . '_' . $mod->modname][$mod->instance]) || PHPUNIT_TEST) { + $timeopenfld = $mod->modname === 'quiz' ? 'timeopen' : ($mod->modname === 'lesson' ? 'available' : $timeopenfld); + $timeclosefld = $mod->modname === 'quiz' ? 'timeclose' : ($mod->modname === 'lesson' ? 'deadline' : $timeclosefld); + $sql = "-- Snap sql + SELECT + module.id, + module.$timeopenfld AS timeopen, + module.$timeclosefld AS timeclose"; + if ($mod->modname === 'assign') { + $sql .= ", + auf.extensionduedate AS extension + "; + } + if ($mod->modname === 'quiz' || $mod->modname === 'lesson') { + $id = $mod->modname === 'quiz' ? $mod->modname : 'lessonid'; + $groups = groups_get_user_groups($courseid); + $groupbysql = ''; + $params = array(); + if ($groups[0]) { + list ($groupsql, $params) = $DB->get_in_or_equal($groups[0]); + if ($DB->get_dbfamily() === 'mysql') { + $sql .= ", + CASE + WHEN ovrd1.$timeopenfld IS NULL + THEN MIN(ovrd2.$timeopenfld) + ELSE ovrd1.$timeopenfld + END AS timeopenover, + CASE + WHEN ovrd1.$timeclosefld IS NULL + THEN MAX(ovrd2.$timeclosefld) + ELSE ovrd1.$timeclosefld + END AS timecloseover + FROM {" . $mod->modname . "} module"; + } else { + $sql .= ", + MIN ( + CASE + WHEN ovrd1.$timeopenfld IS NULL + THEN ovrd2.$timeopenfld + ELSE ovrd1.$timeopenfld + END + ) AS timeopenover, + MAX ( + CASE + WHEN ovrd1.$timeclosefld IS NULL + THEN ovrd2.$timeclosefld + ELSE ovrd1.$timeclosefld + END + ) AS timecloseover + FROM {" . $mod->modname . "} module"; + } + array_unshift($params, $USER->id); // Add userid to start of params. + $sql .= " + LEFT JOIN {" . $mod->modname . "_overrides} ovrd1 + ON module.id=ovrd1.$id + AND ovrd1.userid = ? + LEFT JOIN {" . $mod->modname . "_overrides} ovrd2 + ON module.id=ovrd2.$id + AND ovrd2.groupid $groupsql"; + $groupbysql = " + GROUP BY module.id, module.$timeopenfld, module.$timeclosefld"; + + } else { + $params[] = $USER->id; + $sql .= ", ovrd1.$timeopenfld AS timeopenover, ovrd1.$timeclosefld AS timecloseover + FROM {" . $mod->modname . "} module + LEFT JOIN {" . $mod->modname . "_overrides} ovrd1 + ON module.id=ovrd1.$id AND ovrd1.userid = ?"; + } + $sql .= " WHERE module.course = ?"; + $sql .= $groupbysql; + $params[] = $courseid; + $result = $DB->get_records_sql($sql, $params); + } else { + $params = []; + $sql .= " FROM {" . $mod->modname . "} module"; + if ($mod->modname === 'assign') { + $params[] = $USER->id; + $sql .= " + LEFT JOIN {assign_user_flags} auf + ON module.id = auf.assignment + AND auf.userid = ? + "; + } + $params[] = $courseid; + $sql .= " WHERE module.course = ?"; + $result = $DB->get_records_sql($sql, $params); + } + $moddates[$courseid . '_' . $mod->modname] = $result; + } + $modinst = $moddates[$courseid.'_'.$mod->modname][$mod->instance]; + if (!empty($modinst->timecloseover)) { + $modinst->timeclose = $modinst->timecloseover; + if ($modinst->timeopenover) { + $modinst->timeopen = $modinst->timeopenover; + } + } + return $modinst; + + } + + /** + * Return grade row for specific module instance. + * + * @param int $courseid + * @param cm_info $mod + * + * @return bool + */ + protected static function grade_row($courseid, $mod) { + global $DB, $USER; + + static $grades = array(); + + if (isset($grades[$courseid.'_'.$mod->modname]) + && isset($grades[$courseid.'_'.$mod->modname][$mod->instance]) + ) { + return $grades[$courseid.'_'.$mod->modname][$mod->instance]; + } + + $sql = "-- Snap sql + SELECT m.id AS instanceid, gg.* + + FROM {".$mod->modname."} m + + JOIN {grade_items} gi + ON m.id = gi.iteminstance + AND gi.itemtype = 'mod' + AND gi.itemmodule = :modname + AND gi.courseid = :courseid1 + AND gi.outcomeid IS NULL + + JOIN {grade_grades} gg + ON gi.id = gg.itemid + + WHERE m.course = :courseid2 + AND gg.userid = :userid + AND ( + gg.rawgrade IS NOT NULL + OR gg.finalgrade IS NOT NULL + OR gg.feedback IS NOT NULL + ) + "; + $params = array( + 'modname' => $mod->modname, + 'courseid1' => $courseid, + 'courseid2' => $courseid, + 'userid' => $USER->id + ); + $grades[$courseid.'_'.$mod->modname] = $DB->get_records_sql($sql, $params); + + if (isset($grades[$courseid.'_'.$mod->modname][$mod->instance])) { + return $grades[$courseid.'_'.$mod->modname][$mod->instance]; + } else { + return false; + } + } + + /** + * Get total participant count for specific courseid and module. + * + * @param int $courseid + * @param cm_info $mod + * + * @return int + */ + protected static function course_participant_count($courseid, $mod) { + static $modulecount = array(); // 3D array on course id then module id. + static $studentroles = null; + if (empty($studentroles)) { + $studentarch = get_archetype_roles('student'); + $studentroles = array(); + foreach ($studentarch as $role) { + $studentroles[] = $role->shortname; + } + } + + if (!isset($modulecount[$courseid])) { + $modulecount[$courseid] = array(); + + // Initialise to zero in case of no enrolled students on the course. + $modinfo = get_fast_modinfo($courseid, -1); + $cms = $modinfo->get_cms(); // Array of cm_info objects. + foreach ($cms as $themod) { + $modulecount[$courseid][$themod->id] = 0; + } + + $context = \context_course::instance($courseid); + $users = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true); + $users = array_keys($users); + $alluserroles = get_users_roles($context, $users, false); + + foreach ($users as $userid) { + $usershortnames = array(); + foreach ($alluserroles[$userid] as $userrole) { + $usershortnames[] = $userrole->shortname; + } + $isstudent = false; + foreach ($studentroles as $studentrole) { + if (in_array($studentrole, $usershortnames)) { + // User is in a role that is based on a student archetype on the course. + $isstudent = true; + break; + } + } + if (!$isstudent) { + // Don't go any further. + continue; + } + + $modinfo = get_fast_modinfo($courseid, $userid); + $cms = $modinfo->get_cms(); // Array of cm_info objects for the user on the course. + foreach ($cms as $usermod) { + // From course_section_cm() in M3.8 - is_visible_on_course_page for M3.9+. + if (((method_exists($usermod, 'is_visible_on_course_page')) && ($usermod->is_visible_on_course_page())) + || ((!empty($usermod->availableinfo)) && ($usermod->url))) { + // From course_section_cm_name_title(). + if ($usermod->uservisible) { + $modulecount[$courseid][$usermod->id]++; + } + } + } + } + } + + return $modulecount[$courseid][$mod->id]; + } +} diff --git a/theme/adaptable/classes/activity_meta.php b/theme/adaptable/classes/activity_meta.php new file mode 100644 index 0000000..b3bc705 --- /dev/null +++ b/theme/adaptable/classes/activity_meta.php @@ -0,0 +1,153 @@ +<?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/>. + +/** + * Activity-related meta data. + * + * This defines the activity_meta class that is used to store information such as submission status, + * due dates etc. + * + * @package theme_adaptable + * @copyright 2018 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable; + +defined('MOODLE_INTERNAL') || die(); + +use \theme_adaptable\traits\null_object; + +/** + * Activity meta data. + * + * @package theme_adaptable + * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class activity_meta { + + use null_object; + + // Strings. + /** + * @var string $submittedstr - string to use when submitted. + */ + public $submittedstr; + + /** + * @var string $notsubmittedstr - string to use when not submitted + */ + public $notsubmittedstr; + + /** + * @var string $submitstrkey - language string key. + */ + public $submitstrkey; + + /** + * @var string $draftstr - string for draft status. + */ + public $draftstr; + + /** + * @var string $reopenedstr - string for reopened status. + */ + public $reopenedstr; + + /** + * @var string $expiredstr - string for expired status. + */ + public $expiredstr; + + // General meta data. + /** + * @var int $timeopen - unix time stamp for time open. + */ + public $timeopen; + + /** + * @var int $timeclose - unix time stamp for time closed. + */ + public $timeclose; + + /** + * @var int $extension - unix time stamp for extended due dates. + */ + public $extension; + + /** + * @var bool $isteacher - true if meta data is intended for teacher. + */ + public $isteacher = false; + /** + * @var bool $submissionnotrequired - true if a submission is not required. + */ + public $submissionnotrequired = false; + + // Student meta data. + /** + * @var bool $submitted - true if submission has been made. + */ + public $submitted = false; // Consider collapsing this variable + draft variable into one 'status' variable? + + /** + * @var bool $draft - true if activity submission is in draft status. + */ + public $draft = false; + + /** + * @var bool $reopened - true if reopened. + */ + public $reopened = false; + + /** + * @var bool $expired - true if expired. + */ + public $expired = false; + + /** + * @var int $timesubmitted - unix time stamp for time submitted. + */ + public $timesubmitted; + + /** + * @var bool $grade - has the submission been graded. + */ + public $grade = false; + + /** + * @var bool $overdue - is the submission overdue. + */ + public $overdue = false; + + // Teacher meta data. + /** + * @var int $numsubmissions - number of submissions. + */ + public $numsubmissions = 0; + + /** + * @var int $numrequiregrading - number of submissions requiring grading. + */ + public $numrequiregrading = 0; + + /** + * @var int $numparticipants - number of participants. + */ + public $numparticipants = 0; +} diff --git a/theme/adaptable/classes/admin_settingspage_tabs.php b/theme/adaptable/classes/admin_settingspage_tabs.php new file mode 100644 index 0000000..6abefa0 --- /dev/null +++ b/theme/adaptable/classes/admin_settingspage_tabs.php @@ -0,0 +1,110 @@ +<?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/>. + +/** + * @package theme_adaptable + * @copyright © 2020 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * @package theme_adaptable + * @copyright © 2020 G J Barnard. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_adaptable_admin_settingspage_tabs extends theme_boost_admin_settingspage_tabs { + + /** @var int The branch this Adaptable is for. */ + protected $mbranch; + + /** + * see admin_settingpage for details of this function + * + * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects. + * @param string $visiblename The displayed name for this external page. Usually obtained through get_string(). + * @param string $version The version info. + * @param int $mbranch The branch this Adaptable is for. + * @param int $aversion Adaptable version. + * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'. + * @param boolean $hidden Is this external page hidden in admin tree block? Default false. + * @param stdClass $context The context the page relates to. + */ + public function __construct($name, $visiblename, $mbranch, $req_capability = 'moodle/site:config', $hidden = false, $context = NULL) { + $this->mbranch = $mbranch; + return parent::__construct($name, $visiblename, $req_capability, $hidden, $context); + } + + /** + * Generate the HTML output. + * + * @return string + */ + public function output_html() { + global $CFG, $OUTPUT; + + $activetab = optional_param('activetab', '', PARAM_TEXT); + $context = array('tabs' => array()); + $havesetactive = false; + + foreach ($this->get_tabs() as $tab) { + $active = false; + + // Default to first tab it not told otherwise. + if (empty($activetab) && !$havesetactive) { + $active = true; + $havesetactive = true; + } else if ($activetab === $tab->name) { + $active = true; + } + + $context['tabs'][] = array( + 'name' => $tab->name, + 'displayname' => $tab->visiblename, + 'html' => $tab->output_html(), + 'active' => $active, + ); + } + + if (empty($context['tabs'])) { + return ''; + } + + $plugininfo = core_plugin_manager::instance()->get_plugin_info('theme_adaptable'); + $context['versioninfo'] = get_string('versioninfo', 'theme_adaptable', + array( + 'moodle' => $CFG->release, + 'release' => $plugininfo->release, + 'version' => $plugininfo->versiondisk + ) + ); + if ($CFG->branch != $this->mbranch) { + $context['versioncheck'] = 'Release '.$plugininfo->release.', version '.$plugininfo->versiondisk.' is incompatible with Moodle '.$CFG->release; + $context['versioncheck'] .= ', please get the correct version from '; + $context['versioncheck'] .= '<a href="https://moodle.org/plugins/theme_adaptable" target="_blank">Moodle.org</a>. '; + $context['versioncheck'] .= 'If none is available, then please consider supporting the theme by funding it. '; + $context['versioncheck'] .= 'Please contact me via \'gjbarnard at gmail dot com\' or my '; + $context['versioncheck'] .= '<a href="http://moodle.org/user/profile.php?id=442195">Moodle dot org profile</a>. '; + $context['versioncheck'] .= 'This is my <a href="http://about.me/gjbarnard">\'Web profile\'</a> if you want '; + $context['versioncheck'] .= 'to know more about me.'; + } + + return $OUTPUT->render_from_template('theme_adaptable/adaptable_admin_setting_tabs', $context); + } + +} diff --git a/theme/adaptable/classes/output/core/core_renderer.php b/theme/adaptable/classes/output/core/core_renderer.php new file mode 100644 index 0000000..3093f9b --- /dev/null +++ b/theme/adaptable/classes/output/core/core_renderer.php @@ -0,0 +1,41 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output; + +defined('MOODLE_INTERNAL') || die; + +/** + * Renderers to align Moodle's HTML with that expected by Bootstrap + * + * Note: This class is required to avoid inheriting Boost's core_renderer + * + * @copyright Copyright (c) 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_renderer extends \core_renderer { +} diff --git a/theme/adaptable/classes/output/core/course_renderer.php b/theme/adaptable/classes/output/core/course_renderer.php new file mode 100644 index 0000000..d5a859d --- /dev/null +++ b/theme/adaptable/classes/output/core/course_renderer.php @@ -0,0 +1,1017 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\core; + +defined('MOODLE_INTERNAL') || die(); + +/****************************************************************************************** + * + * Overridden Core Course Renderer for Adaptable theme + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @copyright 2015 Moodlerooms Inc. (http://www.moodlerooms.com) (activity further information functionality) + * @copyright 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +use cm_info; +use core_text; +use html_writer; +use context_course; +use moodle_url; +use coursecat_helper; +use lang_string; +use core_course_list_element; +use stdClass; +use renderable; +use action_link; + +/** + * Course renderer implementation. + * + * @package theme_adaptable + * @copyright 2017 Manoj Solanki (Coventry University) + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_renderer extends \core_course_renderer { + + /** + * Build the HTML for the module chooser javascript popup + * + * @param array $modules A set of modules as returned form + * @see get_module_metadata + * @param object $course The course that will be displayed + * @return string The composed HTML for the module + */ + public function course_modchooser($modules, $course) { + if (!$this->page->requires->should_create_one_time_item_now('core_course_modchooser')) { + return ''; + } + $modchooser = new \theme_adaptable\output\core_course\output\modchooser($course, $modules); + return $this->render($modchooser); + } + + /** + * Render course tiles in the fron page + * + * @param coursecat_helper $chelper + * @param string $course + * @param string $additionalclasses + * @return string + */ + protected function coursecat_coursebox(coursecat_helper $chelper, $course, $additionalclasses = '') { + $type = theme_adaptable_get_setting('frontpagerenderer'); + if ($type == 3 || $this->output->body_id() != 'page-site-index') { + return parent::coursecat_coursebox($chelper, $course, $additionalclasses = ''); + } + + $additionalcss = ''; + + if ($type == 2) { + $additionalcss = 'hover'; + } + + if ($type == 4) { + $additionalcss = 'hover covtiles'; + } + + if (!isset($this->strings->summary)) { + $this->strings->summary = get_string('summary'); + } + + $showcourses = $chelper->get_show_courses(); + + if ($showcourses <= self::COURSECAT_SHOW_COURSES_COUNT) { + return ''; + } + + if ($course instanceof stdClass) { + $course = new core_course_list_element($course); + } + + $content = ''; + $classes = trim($additionalclasses); + + if ($showcourses < self::COURSECAT_SHOW_COURSES_EXPANDED) { + $classes .= ' collapsed'; + } + + // Number of tiles per row: 12=1 tile / 6=2 tiles / 4 (default)=3 tiles / 3=4 tiles / 2=6 tiles. + $spanclass = $this->page->theme->settings->frontpagenumbertiles; + + // Display course tiles depending the number per row. + $content .= html_writer::start_tag('div', + array('class' => 'col-xs-12 col-sm-'.$spanclass.' panel panel-default coursebox '.$additionalcss)); + + // Add the course name. + $coursename = $chelper->get_course_formatted_name($course); + if (($type == 1) || ($showcourses < self::COURSECAT_SHOW_COURSES_EXPANDED)) { + $content .= html_writer::start_tag('div', array('class' => 'panel-heading')); + $content .= html_writer::link(new moodle_url('/course/view.php', array('id' => $course->id)), + $coursename, array('class' => $course->visible ? '' : 'dimmed', 'title' => $coursename)); + } + + // If we display course in collapsed form but the course has summary or course contacts, display the link to the info page. + if ($showcourses < self::COURSECAT_SHOW_COURSES_EXPANDED) { + $arrow = html_writer::tag('span', '', array('class' => 'fa fp-chevron ml-1')); + $content .= html_writer::link('#coursecollapse' . $course->id , '' . $arrow, + array('class' => 'fpcombocollapse collapsed', 'data-toggle' => 'collapse', + 'data-parent' => '#frontpage-category-combo')); + } + + if ($type == 1) { + $content .= $this->coursecat_coursebox_enrolmenticons($course); + } + + if (($type == 1) || ($showcourses < self::COURSECAT_SHOW_COURSES_EXPANDED)) { + $content .= html_writer::end_tag('div'); // End .panel-heading. + } + + if ($showcourses < self::COURSECAT_SHOW_COURSES_EXPANDED) { + $content .= html_writer::start_tag('div', array('id' => 'coursecollapse' . $course->id, + 'class' => 'panel-collapse collapse')); + } + + $content .= html_writer::start_tag('div', array('class' => 'panel-body clearfix')); + + // This gets the course image or files. + $content .= $this->coursecat_coursebox_content($chelper, $course, $type); + + if ($showcourses >= self::COURSECAT_SHOW_COURSES_EXPANDED) { + $icondirection = 'left'; + if ('ltr' === get_string('thisdirection', 'langconfig')) { + $icondirection = 'right'; + } + $arrow = html_writer::tag('span', '', array('class' => 'fa fa-chevron-'.$icondirection)); + $btn = html_writer::tag('span', get_string('course', 'theme_adaptable') . ' ' . + $arrow, array('class' => 'get_stringlink')); + + if (($type != 4) || (empty($this->page->theme->settings->covhidebutton))) { + $content .= html_writer::link(new moodle_url('/course/view.php', + array('id' => $course->id)), $btn, array('class' => " coursebtn submit btn btn-info btn-sm")); + } + } + + $content .= html_writer::end_tag('div'); // End .panel-body. + + if ($showcourses < self::COURSECAT_SHOW_COURSES_EXPANDED) { + $content .= html_writer::end_tag('div'); // End .collapse. + } + + $content .= html_writer::end_tag('div'); // End .panel. + + return $content; + } + + /** + * Returns enrolment icons + * + * @param string $course + * @return string + */ + protected function coursecat_coursebox_enrolmenticons($course) { + $content = ''; + if ($icons = enrol_get_course_info_icons($course)) { + $content .= html_writer::start_tag('div', array('class' => 'enrolmenticons')); + foreach ($icons as $pixicon) { + $content .= $this->render($pixicon); + } + $content .= html_writer::end_tag('div'); // Enrolmenticons. + } + return $content; + } + + /** + * Returns course box content for categories + * + * Type - 1 = No Overlay. + * Type - 2 = Overlay. + * Type - 3 = Moodle default. + * Type - 4 = Coventry tiles. + * + * @param coursecat_helper $chelper + * @param string $course + * @param int $type = 3 + * @return string + */ + protected function coursecat_coursebox_content(coursecat_helper $chelper, $course, $type = 3) { + global $CFG; + + if ($course instanceof stdClass) { + require_once($CFG->libdir. '/coursecatlib.php'); + $course = new core_course_list_element($course); + } + if ($type == 3 || $this->output->body_id() != 'page-site-index') { + return parent::coursecat_coursebox_content($chelper, $course); + } + if ($type == 4) { + // From this method's perspective 2 and 4 are the same. + $type = 2; + } + $content = ''; + + // Display course overview files. + $contentimages = ''; + $contentfiles = ''; + foreach ($course->get_course_overviewfiles() as $file) { + $isimage = $file->is_valid_image(); + $url = file_encode_url("$CFG->wwwroot/pluginfile.php", + '/'. $file->get_contextid(). '/'. $file->get_component(). '/'. + $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage); + if ($isimage) { + if ($type == 1) { + $contentimages .= html_writer::start_tag('div', array('class' => 'courseimage')); + $link = new moodle_url('/course/view.php', array('id' => $course->id)); + $contentimages .= html_writer::link($link, html_writer::empty_tag('img', array('src' => $url))); + $contentimages .= html_writer::end_tag('div'); + } else { + $contentimages .= html_writer::tag('div', '', array('class' => 'cimbox', + 'style' => 'background-image: url(\''.$url.'\');')); + } + } else { + $image = $this->output->pix_icon(file_file_icon($file, 24), $file->get_filename(), 'moodle'); + $filename = html_writer::tag('span', $image, array('class' => 'fp-icon')). + html_writer::tag('span', $file->get_filename(), array('class' => 'fp-filename')); + $contentfiles .= html_writer::tag('span', + html_writer::link($url, $filename), + array('class' => 'coursefile fp-filename-icon')); + } + } + if (strlen($contentimages) == 0 && $type == 2) { + // Default image. + $cimboxattr = array('class' => 'cimbox'); + $url = $this->page->theme->setting_file_url('frontpagerendererdefaultimage', 'frontpagerendererdefaultimage'); + if (!empty($url)) { + $cimboxattr['style'] = 'background-image: url(\''.$url.'\');'; + } + $contentimages .= html_writer::tag('div', '', $cimboxattr); + } + $content .= $contentimages.$contentfiles; + + if ($type == 2) { + $content .= $this->coursecat_coursebox_enrolmenticons($course); + } + + if ($type == 2) { + $content .= html_writer::start_tag('a', array( + 'class' => 'coursebox-content', + 'href' => new moodle_url('/course/view.php', array('id' => $course->id)) + )); + $coursename = $chelper->get_course_formatted_name($course); + $content .= html_writer::tag('h3', $coursename, array('class' => $course->visible ? '' : 'dimmed')); + } + $content .= html_writer::start_tag('div', array('class' => 'summary')); + // Display course summary. + if ($course->has_summary()) { + $summs = $chelper->get_course_formatted_summary($course, array('overflowdiv' => false, 'noclean' => true, + 'para' => false)); + $summs = strip_tags($summs); + $truncsum = mb_strimwidth($summs, 0, 70, "...", 'utf-8'); + $content .= html_writer::tag('span', $truncsum, array('title' => $summs)); + } + $coursecontacts = theme_adaptable_get_setting('tilesshowcontacts'); + if ($coursecontacts) { + $coursecontacttitle = theme_adaptable_get_setting('tilescontactstitle'); + // Display course contacts. See ::get_course_contacts(). + if ($course->has_course_contacts()) { + $content .= html_writer::start_tag('ul', array('class' => 'teachers')); + foreach ($course->get_course_contacts() as $userid => $coursecontact) { + $name = ($coursecontacttitle ? $coursecontact['rolename'].': ' : html_writer::tag('i', ' ', + array('class' => 'fa fa-graduation-cap')) ). + html_writer::link(new moodle_url('/user/view.php', + array('id' => $userid, 'course' => SITEID)), + $coursecontact['username']); + $content .= html_writer::tag('li', $name); + } + $content .= html_writer::end_tag('ul'); // Teachers. + } + } + $content .= html_writer::end_tag('div'); // Summary. + + // Display course category if necessary (for example in search results). + if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT) { + require_once($CFG->libdir. '/coursecatlib.php'); + if ($cat = core_course_category::get($course->category, IGNORE_MISSING)) { + $content .= html_writer::start_tag('div', array('class' => 'coursecat')); + $content .= get_string('category').': '. + html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)), + $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed')); + $content .= html_writer::end_tag('div'); // Coursecat. + } + } + if ($type == 2) { + $content .= html_writer::end_tag('a'); + // End coursebox-content. + } + $content .= html_writer::tag('div', '', array('class' => 'boxfooter')); // Coursecat. + + return $content; + } + + /** + * Course search form + * + * @param string $value + * @param string $format + * @return string + */ + public function course_search_form($value = '', $format = 'plain') { + static $count = 0; + $formid = 'coursesearch'; + if ((++$count) > 1) { + $formid .= $count; + } + $inputid = 'coursesearchbox'; + $inputsize = 30; + + if ($format === 'navbar') { + $formid = 'coursesearchnavbar'; + $inputid = 'navsearchbox'; + } + + $strsearchcourses = get_string("searchcourses", "theme_adaptable"); + $searchurl = new moodle_url('/course/search.php'); + + $form = array('id' => $formid, 'action' => $searchurl, 'method' => 'get', 'class' => "form-inline", 'role' => 'form'); + $output = html_writer::start_tag('form', $form); + $output .= html_writer::start_div('form-group'); + $output .= html_writer::tag('label', $strsearchcourses, array('for' => $inputid, 'class' => 'sr-only')); + $search = array('type' => 'text', 'id' => $inputid, 'size' => $inputsize, 'name' => 'search', + 'class' => 'form-control', 'value' => s($value), 'placeholder' => $strsearchcourses); + $output .= html_writer::empty_tag('input', $search); + $button = array('type' => 'submit', 'class' => 'btn btn-default'); + $output .= html_writer::tag('button', get_string('go'), $button); + $output .= html_writer::end_div(); // Close form-group. + $output .= html_writer::end_tag('form'); + + return $output; + } + + /** + * Frontpage course list + * + * @return string + */ + public function frontpage_my_courses() { + global $CFG, $DB; + $output = ''; + if (!isloggedin() or isguestuser()) { + return ''; + } + // Calls a core renderer method (render_mycourses) to get list of a user's current courses that they are enrolled on. + $sortedcourses = $this->render_mycourses(); + + if (!empty($sortedcourses) || !empty($rcourses) || !empty($rhosts)) { + $chelper = new coursecat_helper(); + if (count($sortedcourses) > $CFG->frontpagecourselimit) { + // There are more enrolled courses than we can display, display link to 'My courses'. + $totalcount = count($sortedcourses); + $courses = array_slice($sortedcourses, 0, $CFG->frontpagecourselimit, true); + $chelper->set_courses_display_options(array( + 'viewmoreurl' => new moodle_url('/my/'), + 'viewmoretext' => new lang_string('mycourses') + )); + } else { + // All enrolled courses are displayed, display link to 'All courses' if there are more courses in system. + $chelper->set_courses_display_options(array( + 'viewmoreurl' => new moodle_url('/course/index.php'), + 'viewmoretext' => new lang_string('fulllistofcourses') + )); + $totalcount = $DB->count_records('course') - 1; + } + $chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED)->set_attributes( + array('class' => 'frontpage-course-list-enrolled')); + $output .= $this->coursecat_courses($chelper, $sortedcourses, $totalcount); + + if (!empty($rcourses)) { + $output .= html_writer::start_tag('div', array('class' => 'courses')); + foreach ($rcourses as $course) { + $output .= $this->frontpage_remote_course($course); + } + $output .= html_writer::end_tag('div'); + } else if (!empty($rhosts)) { + $output .= html_writer::start_tag('div', array('class' => 'courses')); + foreach ($rhosts as $host) { + $output .= $this->frontpage_remote_host($host); + } + $output .= html_writer::end_tag('div'); + } + } + return $output; + } + + /** + * Return the navbar content so that it can be echoed out by the layout + * + * @return string XHTML navbar + */ + public function navbar() { + $items = $this->page->navbar->get_items(); + $itemcount = count($items); + if ($itemcount === 0) { + return ''; + } + + $htmlblocks = array(); + // Iterate the navarray and display each node. + $separator = get_separator(); + for ($i = 0; $i < $itemcount; $i++) { + $item = $items[$i]; + $item->hideicon = true; + if ($i === 0) { + $content = html_writer::tag('li', $this->render($item)); + } else { + $content = html_writer::tag('li', $separator.$this->render($item)); + } + $htmlblocks[] = $content; + } + + // Accessibility: heading for navbar list (MDL-20446). + $navbarcontent = html_writer::tag('span', get_string('pagepath'), array('class' => 'accesshide')); + $navbarcontent .= html_writer::tag('ul', join('', $htmlblocks), array('role' => 'navigation')); + return $navbarcontent; + } + + /** + * Renders a navigation node object. + * + * @param navigation_node $item The navigation node to render. + * @return string HTML fragment + */ + protected function render_navigation_node(navigation_node $item) { + $content = $item->get_content(); + $title = $item->get_title(); + if ($item->icon instanceof renderable && !$item->hideicon) { + $icon = $this->render($item->icon); + $content = $icon.$content; // Use CSS for spacing of icons. + } + if ($item->helpbutton !== null) { + $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class' => 'clearhelpbutton', + 'tabindex' => '0')); + } + if ($content === '') { + return ''; + } + if ($item->action instanceof action_link) { + $link = $item->action; + if ($item->hidden) { + $link->add_class('dimmed'); + } + if (!empty($content)) { + // Providing there is content we will use that for the link content. + $link->text = $content; + } + $content = $this->render($link); + } else if ($item->action instanceof moodle_url) { + $attributes = array(); + if ($title !== '') { + $attributes['title'] = $title; + } + if ($item->hidden) { + $attributes['class'] = 'dimmed_text'; + } + $content = html_writer::link($item->action, $content, $attributes); + + } else if (is_string($item->action) || empty($item->action)) { + $attributes = array('tabindex' => '0'); // Add tab support to span but still maintain character stream sequence. + if ($title !== '') { + $attributes['title'] = $title; + } + if ($item->hidden) { + $attributes['class'] = 'dimmed_text'; + } + $content = html_writer::tag('span', $content, $attributes); + } + return $content; + } + + /** + * Overridden. Renders html to display a name with the link to the course module on a course page + * + * If module is unavailable for user but still needs to be displayed + * in the list, just the name is returned without a link. + * + * Note that for course modules that never have separate pages (i.e. labels) + * this function return an empty string. + * + * This method has only been overriden in order to strip -24 and similar from icon image filenames + * to allow using of local theme icons in /pix_core/f. + * + * @param cm_info $mod + * @param array $displayoptions + * @return string + */ + public function course_section_cm_name(cm_info $mod, $displayoptions = array()) { + // If use adaptable icons is set to false, then just run parent method as normal. + if (empty($this->page->theme->settings->coursesectionactivityuseadaptableicons)) { + return parent::course_section_cm_name($mod, $displayoptions); + } + + if (!$mod->uservisible && empty($mod->availableinfo)) { + // Nothing to be displayed to the user. + return ''; + } + + if (!$mod->url) { + return ''; + } + + $templateclass = new \core_course\output\course_module_name($mod, $this->page->user_is_editing(), $displayoptions); + $data = $this->adaptable_course_section_cm_name($mod, $templateclass); + + return $this->output->render_from_template('core/inplace_editable', $data['templatedata']); + } + + /** + * Common course_section_cm_name code. + * + * @param cm_info $mod + * @param course_module_name $templateclass + * + * @return array('templatedata', 'groupinglabel'). + */ + protected function adaptable_course_section_cm_name(cm_info $mod, $templateclass) { + $url = $mod->url; + + // Accessibility: for files get description via icon, this is very ugly hack! + $instancename = $mod->get_formatted_name(); + $altname = $mod->modfullname; + /* Avoid unnecessary duplication: if e.g. a forum name already + includes the word forum (or Forum, etc) then it is unhelpful + to include that in the accessible description that is added. */ + if (false !== strpos(core_text::strtolower($instancename), + core_text::strtolower($altname))) { + $altname = ''; + } + + // File type after name, for alphabetic lists (screen reader). + if ($altname) { + $altname = get_accesshide(' '.$altname); + } + + /* For items which are hidden but available to current user + ($mod->uservisible), we show those as dimmed only if the user has + viewhiddenactivities, so that teachers see 'items which might not + be available to some students' dimmed but students do not see 'item + which is actually available to current student' dimmed. */ + $linkclasses = ''; + $accesstext = ''; + $textclasses = ''; + if ($mod->uservisible) { + + $conditionalhidden = $this->is_cm_conditionally_hidden($mod); + $accessiblebutdim = (!$mod->visible || $conditionalhidden) && + has_capability('moodle/course:viewhiddenactivities', $mod->context); + if ($accessiblebutdim) { + $linkclasses .= ' dimmed'; + $textclasses .= ' dimmed_text'; + if ($conditionalhidden) { + $linkclasses .= ' conditionalhidden'; + $textclasses .= ' conditionalhidden'; + } + // Show accessibility note only if user can access the module himself. + $accesstext = get_accesshide(get_string('hiddenfromstudents').':'. $mod->modfullname); + } + + } else { + $linkclasses .= ' dimmed'; + $textclasses .= ' dimmed_text'; + } + + /* Get on-click attribute value if specified and decode the onclick - it + has already been encoded for display. */ + $onclick = htmlspecialchars_decode($mod->onclick, ENT_QUOTES); + + $groupinglabel = $mod->get_grouping_label($textclasses); + + /* Display link itself. + Get icon url, but strip -24, -64, -256 etc from the end of filetype icons so we + only need to provide one SVG, see MDL-47082. (Used from snap theme). */ + $imageurl = \preg_replace('/-\d\d\d?$/', '', $mod->get_icon_url()); + + $activitylink = html_writer::empty_tag('img', array('src' => $imageurl, + 'class' => 'iconlarge activityicon', 'alt' => ' ', 'role' => 'presentation')) . $accesstext . + html_writer::tag('span', $instancename . $altname, array('class' => 'instancename')); + + $outputlink = ''; + if ($mod->uservisible) { + $outputlink .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick)) . + $groupinglabel; + } else { + /* We may be displaying this just in order to show information + about visibility, without the actual link ($mod->uservisible).*/ + $outputlink .= html_writer::tag('div', $activitylink, array('class' => $textclasses)) . + $groupinglabel; + } + + $templatedata = $templateclass->export_for_template($this->output); + + // Variable displayvalue element is purposely overriden below with link including custom icon created above. + $templatedata['displayvalue'] = $outputlink; + + return array('templatedata' => $templatedata, 'groupinglabel' => $groupinglabel); + } + + // New methods added for activity styling below. Adapted from snap theme by Moodleroooms. + + /** + * Overridden. Customise display. Renders HTML to display one course module in a course section + * + * This includes link, content, availability, completion info and additional information + * that module type wants to display (i.e. number of unread forum posts) + * + * This function calls: + * core_course_renderer::course_section_cm_name() + * core_course_renderer::course_section_cm_text() + * core_course_renderer::course_section_cm_availability() + * core_course_renderer::course_section_cm_completion() + * course_get_cm_edit_actions() + * core_course_renderer::course_section_cm_edit_actions() + * + * @param stdClass $course + * @param completion_info $completioninfo + * @param cm_info $mod + * @param int|null $sectionreturn + * @param array $displayoptions + * @return string + */ + public function course_section_cm($course, &$completioninfo, cm_info $mod, $sectionreturn, $displayoptions = array()) { + $output = ''; + // We return empty string (because course module will not be displayed at all) if + // 1) The activity is not visible to users and + // 2) The 'availableinfo' is empty, i.e. the activity was hidden in a way that leaves no info, such as using the + // eye icon. + + if ( (method_exists($mod, 'is_visible_on_course_page')) && (!$mod->is_visible_on_course_page()) + || (!$mod->uservisible && empty($mod->availableinfo)) ) { + return $output; + } + + $indentclasses = 'mod-indent'; + if (!empty($mod->indent)) { + $indentclasses .= ' mod-indent-'.$mod->indent; + if ($mod->indent > 15) { + $indentclasses .= ' mod-indent-huge'; + } + } + + $output .= html_writer::start_tag('div'); + + if ($this->page->user_is_editing()) { + $output .= course_get_cm_move($mod, $sectionreturn); + } + + $output .= html_writer::start_tag('div', array('class' => 'mod-indent-outer')); + + // This div is used to indent the content. + $output .= html_writer::div('', $indentclasses); + + // Start a wrapper for the actual content to keep the indentation consistent. + $output .= html_writer::start_tag('div', array('class' => 'ad-activity-wrapper')); + + // Display the link to the module (or do nothing if module has no url). + $cmname = $this->course_section_cm_name($mod, $displayoptions); + + if (!empty($cmname)) { + // Start the div for the activity title, excluding the edit icons. + $output .= html_writer::start_tag('div', array('class' => 'activityinstance')); + $output .= $cmname; + + // Module can put text after the link (e.g. forum unread). + $output .= $mod->afterlink; + + // Closing the tag which contains everything but edit icons. Content part of the module should not be part of this. + $output .= html_writer::end_tag('div'); // .activityinstance class. + } + + // If there is content but NO link (eg label), then display the + // content here (BEFORE any icons). In this case icons must be + // displayed after the content so that it makes more sense visually + // and for accessibility reasons, e.g. if you have a one-line label + // it should work similarly (at least in terms of ordering) to an + // activity. + $contentpart = $this->course_section_cm_text($mod, $displayoptions); + $url = $mod->url; + if (empty($url)) { + $output .= $contentpart; + } + + $modicons = ''; + if ($this->page->user_is_editing()) { + $editactions = course_get_cm_edit_actions($mod, $mod->indent, $sectionreturn); + $modicons .= ' '. $this->course_section_cm_edit_actions($editactions, $mod, $displayoptions); + $modicons .= $mod->afterediticons; + } + + $modicons .= $this->course_section_cm_completion($course, $completioninfo, $mod, $displayoptions); + + if (!empty($modicons)) { + $output .= html_writer::start_tag('div', array('class' => 'actions-right')); + $output .= html_writer::span($modicons, 'actions'); + $output .= html_writer::end_tag('div'); + } + + // Get further information. + $settingname = 'coursesectionactivityfurtherinformation'. $mod->modname; + if (isset ($this->page->theme->settings->$settingname) && $this->page->theme->settings->$settingname == true) { + $output .= html_writer::start_tag('div', array('class' => 'ad-activity-meta-container')); + $output .= $this->course_section_cm_get_meta($mod); + $output .= html_writer::end_tag('div'); + // TO BE DELETED $output .= '<div style="clear: both;"></div>'; ???? + } + + // If there is content AND a link, then display the content here. + // (AFTER any icons). Otherwise it was displayed before. + if (!empty($url)) { + $output .= $contentpart; + } + + // Show availability info (if module is not available). + $output .= $this->course_section_cm_availability($mod, $displayoptions); + + $output .= html_writer::end_tag('div'); + + // End of indentation div. + $output .= html_writer::end_tag('div'); + + $output .= html_writer::end_tag('div'); + return $output; + } + + /** + * Get the module meta data for a specific module. + * + * @param cm_info $mod + * @return string + */ + protected function course_section_cm_get_meta(cm_info $mod) { + global $COURSE; + + $content = ''; + + if (is_guest(context_course::instance($COURSE->id))) { + return ''; + } + + // If module is not visible to the user then don't bother getting meta data. + if (!$mod->uservisible) { + return ''; + } + + // Do we have an activity function for this module for returning meta data? + $meta = \theme_adaptable\activity::module_meta($mod); + if (($meta == null) || (!$meta->is_set(true))) { + // Can't get meta data for this module. + return ''; + } + $content .= ''; + + $warningclass = ''; + if ($meta->submitted) { + $warningclass = ' ad-activity-date-submitted '; + } + + $activitycontent = $this->submission_cta($mod, $meta); + + if (!(empty($activitycontent))) { + if ( ($mod->modname == 'assign') && ($meta->submitted) ) { + $content .= html_writer::start_tag('span', array('class' => 'ad-activity-due-date'.$warningclass)); + $content .= $activitycontent; + $content .= html_writer::end_tag('span') . '<br>'; + } else { + // Only display if this is really a student on the course (i.e. not anyone who can grade an assignment). + if (!has_capability('mod/assign:grade', $mod->context)) { + $content .= html_writer::start_tag('div', array('class' => 'ad-activity-mod-engagement'.$warningclass)); + $content .= $activitycontent; + $content .= html_writer::end_tag('div'); + } + } + } + + // Activity due date. + if (!empty($meta->extension) || !empty($meta->timeclose)) { + if (!empty($meta->extension)) { + $field = 'extension'; + } else if (!empty($meta->timeclose)) { + $field = 'timeclose'; + } + + // Create URL for due date. + $url = new \moodle_url("/mod/{$mod->modname}/view.php", ['id' => $mod->id]); + $dateformat = get_string('strftimedate', 'langconfig'); + $labeltext = get_string('due', 'theme_adaptable', userdate($meta->$field, $dateformat)); + $warningclass = ''; + + // Display assignment status (due, nearly due, overdue), as long as it hasn't been submitted, + // or submission not required. + if ((!$meta->submitted) && (!$meta->submissionnotrequired)) { + $warningclass = ''; + $labeltext = ''; + + // If assignment due in 7 days or less, display in amber, if overdue, then in red, or if submitted, turn to green. + + // If assignment is 7 days before date due(nearly due). + $time = time(); + $timedue = $meta->$field - (86400 * 7); + if (($time > $timedue) && ($time <= $meta->$field)) { + if ($mod->modname == 'assign') { + $warningclass = ' ad-activity-date-nearly-due'; + } + } else if ($time > $meta->$field) { // If assignment is actually overdue. + if ($mod->modname == 'assign') { + $warningclass = ' ad-activity-date-overdue'; + } + $labeltext .= $this->output->pix_icon('i/warning', get_string('warning', 'theme_adaptable')); + } + + $labeltext .= get_string('due', 'theme_adaptable', userdate($meta->$field, $dateformat)); + + $activityclass = ''; + if ($mod->modname == 'assign') { + $activityclass = 'ad-activity-due-date'; + } + $duedate = html_writer::start_tag('span', array('class' => $activityclass.$warningclass)); + $duedate .= html_writer::link($url, $labeltext); + $duedate .= html_writer::end_tag('span'); + $content .= html_writer::start_tag('div', array('class' => 'ad-activity-mod-engagement')); + $content .= $duedate . html_writer::end_tag('div'); + } + } + + if ($meta->isteacher) { + // Teacher - useful teacher meta data. + $engagementmeta = array(); + + // Below, !== false means we get 0 out of x submissions. + if (!$meta->submissionnotrequired && $meta->numparticipants !== false) { + $engagementmeta[] = get_string('xofy'.$meta->submitstrkey, 'theme_adaptable', + (object) array( + 'completed' => $meta->numsubmissions, + 'participants' => $meta->numparticipants + ) + ); + } + + if ($meta->numrequiregrading) { + $engagementmeta[] = get_string('xungraded', 'theme_adaptable', $meta->numrequiregrading); + } + if (!empty($engagementmeta)) { + $engagementstr = implode(', ', $engagementmeta); + + $params = array( + 'action' => 'grading', + 'id' => $mod->id, + 'tsort' => 'timesubmitted', + 'filter' => 'require_grading' + ); + $url = new moodle_url("/mod/{$mod->modname}/view.php", $params); + + $icon = html_writer::tag('i', ' ', array('class' => 'fa fa-info-circle')); + $content .= html_writer::start_tag('div', array('class' => 'ad-activity-mod-engagement')); + $content .= html_writer::link($url, $icon.$engagementstr, array('class' => 'ad-activity-action')); + $content .= html_writer::end_tag('div'); + } + + } else { + // Feedback meta. + if (!empty($meta->grade)) { + $url = new \moodle_url('/grade/report/user/index.php', ['id' => $COURSE->id]); + if (in_array($mod->modname, ['quiz', 'assign'])) { + $url = new \moodle_url('/mod/'.$mod->modname.'/view.php?id='.$mod->id); + } + $content .= html_writer::start_tag('span', array('class' => 'ad-activity-mod-feedback')); + $feedbackavailable = html_writer::tag('i', ' ', array('class' => 'fa fa-commenting-o')) . + get_string('feedbackavailable', 'theme_adaptable'); + $content .= html_writer::link($url, $feedbackavailable); + $content .= html_writer::end_tag('span'); + } + + // If submissions are not allowed, return the content. + if (!empty($meta->timeopen) && $meta->timeopen > time()) { + // TODO - spit out a 'submissions allowed from' tag. + return $content; + } + + } + + return $content; + } + + /** + * Submission call to action. + * + * @param cm_info $mod + * @param activity_meta $meta + * @return string + * @throws coding_exception + */ + public function submission_cta(cm_info $mod, \theme_adaptable\activity_meta $meta) { + global $CFG; + + if (empty($meta->submissionnotrequired)) { + + $url = $CFG->wwwroot.'/mod/'.$mod->modname.'/view.php?id='.$mod->id; + + if ($meta->submitted) { + if (empty($meta->timesubmitted)) { + $submittedonstr = ''; + } else { + $submittedonstr = ' '.userdate($meta->timesubmitted, get_string('strftimedate', 'langconfig')); + } + $message = $this->output->pix_icon('i/checked', get_string('checked', 'theme_adaptable')).$meta->submittedstr.$submittedonstr; + } else { + if ($meta->expired) { + $warningstr = $meta->expiredstr; + $warningicon = 't/locked'; + } else if ($meta->reopened) { + $warningstr = $meta->reopenedstr; + $warningicon = 't/unlocked'; + } else if ($meta->draft) { + $warningstr = $meta->draftstr; + $warningicon = 'i/warning'; + } else { + $warningstr = $meta->notsubmittedstr; + $warningicon = 'i/warning'; + } + + $message = $this->output->pix_icon($warningicon, get_string('warning', 'theme_adaptable')).$warningstr; + } + + return html_writer::link($url, $message, array('class' => 'ad-activity-action')); + } + return ''; + } + + /** + * Renders the activity navigation. + * + * Defer to template. + * + * @param \core_course\output\activity_navigation $page + * @return string html for the page + */ + public function render_activity_navigation(\core_course\output\activity_navigation $page) { + $data = $page->export_for_template($this->output); + + /* Add in extra data for our own overridden activity_navigation template. + So manipulating the 'classes' and 'text' properties in 'action_link' and 'classes' in 'urlselect'. */ + if (!empty($data->prevlink)) { + $data->prevlink->classes = 'previous_activity prevnext'; // Override the button! + + $icon = html_writer::tag('i', '', array('class' => 'fa fa-angle-double-left')); + $previouslink = html_writer::tag('span', $icon, array('class' => 'nav_icon')); + $activityname = html_writer::tag('span', get_string('previousactivity', 'theme_adaptable'), + array('class' => 'nav_guide')).'<br>'; + $activityname .= $data->prevlink->attributes[0]['value']; + $previouslink .= html_writer::tag('span', $activityname, array('class' => 'text')); + $data->prevlink->text = $previouslink; + } + + if (!empty($data->nextlink)) { + $data->nextlink->classes = 'next_activity prevnext'; // Override the button! + + $activityname = html_writer::tag('span', get_string('nextactivity', 'theme_adaptable'), + array('class' => 'nav_guide')).'<br>'; + $activityname .= $data->nextlink->attributes[0]['value']; + $nextlink = html_writer::tag('span', $activityname, array('class' => 'text')); + $icon = html_writer::tag('i', '', array('class' => 'fa fa-angle-double-right')); + $nextlink .= html_writer::tag('span', $icon, array('class' => 'nav_icon')); + $data->nextlink->text = $nextlink; + } + + if (!empty($data->activitylist)) { + $data->activitylist->classes = 'jumpmenu'; + } + + return $this->output->render_from_template('core_course/activity_navigation', $data); + } +} diff --git a/theme/adaptable/classes/output/core_user/myprofile/editprofile.php b/theme/adaptable/classes/output/core_user/myprofile/editprofile.php new file mode 100644 index 0000000..d875c0c --- /dev/null +++ b/theme/adaptable/classes/output/core_user/myprofile/editprofile.php @@ -0,0 +1,370 @@ +<?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/>. + +/** + * myprofile edit profile. + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\core_user\myprofile; + +defined('MOODLE_INTERNAL') || die; + +/** + * myprofile editprofile. + * + * @package theme_adaptable + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @copyright Copyright (c) 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class editprofile { + + /** + * Generate form. + * + * @return array + */ + public static function generate_form() { + global $CFG, $DB, $PAGE, $USER; + + $userid = optional_param('id', 0, PARAM_INT); + $userid = $userid ? $userid : $USER->id; + $user = \core_user::get_user($userid); + + $courseid = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + + // User interests. + $user->interests = \core_tag_tag::get_item_tags_array('core', 'user', $userid); + + require_once($CFG->dirroot.'/lib/formslib.php'); + $usercontext = \context_user::instance($user->id); + $editoroptions = array( + 'maxfiles' => EDITOR_UNLIMITED_FILES, + 'maxbytes' => $CFG->maxbytes, + 'trusttext' => false, + 'forcehttps' => false, + 'context' => $usercontext + ); + $user = file_prepare_standard_editor($user, 'description', $editoroptions, $usercontext, 'user', 'profile', 0); + + // Prepare filemanager draft area. + $draftitemid = 0; + $filemanagercontext = $editoroptions['context']; + $filemanageroptions = array( + 'maxbytes' => $CFG->maxbytes, + 'subdirs' => 0, + 'maxfiles' => 1, + 'accepted_types' => 'web_image'); + \file_prepare_draft_area($draftitemid, $filemanagercontext->id, 'user', 'newicon', 0, $filemanageroptions); + $user->imagefile = $draftitemid; + + $editprofileform = new editprofile_form( + new \moodle_url( + $PAGE->url, + array( + 'id' => $user->id, + 'course' => $course->id, + 'aep' => 'aep' + ) + ), + array( + 'editoroptions' => $editoroptions, + 'filemanageroptions' => $filemanageroptions, + 'user' => $user) + ); + + return array( + 'form' => $editprofileform, + 'user' => $user, + 'course' => $course, + 'editoroptions' => $editoroptions, + 'filemanageroptions' => $filemanageroptions, + 'usercontext' => $usercontext + ); + } + + /** + * Process form. + * + * @param bool $redirect + * @param array $editprofile + */ + public static function process_form($redirect = true, $editprofile = null) { + global $CFG, $DB, $SITE, $USER; + + if (is_null($editprofile)) { + $editprofile = self::generate_form(); + } + + $editprofileform = $editprofile['form']; + $user = $editprofile['user']; + $course = $editprofile['course']; + $editoroptions = $editprofile['editoroptions']; + $filemanageroptions = $editprofile['filemanageroptions']; + $usercontext = $editprofile['usercontext']; + + // Deciding where to send the user back in most cases. + if ($redirect) { + if ($course->id != SITEID) { + $returnurl = new \moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)); + } else { + $returnurl = new \moodle_url('/user/profile.php', array('id' => $user->id)); + } + } + + if ($editprofileform->is_cancelled()) { + if ($redirect) { + redirect($returnurl); + } + } else if ($usernew = $editprofileform->get_data()) { + $usercreated = false; + if (empty($usernew->auth)) { + // User editing self. + $authplugin = get_auth_plugin($user->auth); + unset($usernew->auth); // Can not change/remove. + } else { + $authplugin = get_auth_plugin($usernew->auth); + } + + $usernew->timemodified = time(); + $createpassword = false; + + if ($usernew->id == -1) { + unset($usernew->id); + $createpassword = !empty($usernew->createpassword); + unset($usernew->createpassword); + $usernew = file_postupdate_standard_editor($usernew, 'description', $editoroptions, null, 'user', 'profile', null); + $usernew->mnethostid = $CFG->mnet_localhost_id; // Always local user. + $usernew->confirmed = 1; + $usernew->timecreated = time(); + if ($authplugin->is_internal()) { + if ($createpassword or empty($usernew->newpassword)) { + $usernew->password = ''; + } else { + $usernew->password = hash_internal_user_password($usernew->newpassword); + } + } else { + $usernew->password = AUTH_PASSWORD_NOT_CACHED; + } + $usernew->id = user_create_user($usernew, false, false); + + if (!$authplugin->is_internal() and $authplugin->can_change_password() and !empty($usernew->newpassword)) { + if (!$authplugin->user_update_password($usernew, $usernew->newpassword)) { + // Do not stop here, we need to finish user creation. + debugging(get_string('cannotupdatepasswordonextauth', '', '', $usernew->auth), DEBUG_NONE); + } + } + $usercreated = true; + } else { + $usernew = file_postupdate_standard_editor($usernew, 'description', $editoroptions, + $usercontext, 'user', 'profile', 0); + // Pass a true old $user here. + if (!$authplugin->user_update($user, $usernew)) { + // Auth update failed. + print_error('cannotupdateuseronexauth', '', '', $user->auth); + } + user_update_user($usernew, false, false); + + // Set new password if specified. + if (!empty($usernew->newpassword)) { + if ($authplugin->can_change_password()) { + if (!$authplugin->user_update_password($usernew, $usernew->newpassword)) { + print_error('cannotupdatepasswordonextauth', '', '', $usernew->auth); + } + unset_user_preference('create_password', $usernew); // Prevent cron from generating the password. + + if (!empty($CFG->passwordchangelogout)) { + // We can use SID of other user safely here because they are unique, + // the problem here is we do not want to logout admin here when changing own password. + \core\session\manager::kill_user_sessions($usernew->id, session_id()); + } + if (!empty($usernew->signoutofotherservices)) { + webservice::delete_user_ws_tokens($usernew->id); + } + } + } + + // Force logout if user just suspended. + if (isset($usernew->suspended) and $usernew->suspended and !$user->suspended) { + \core\session\manager::kill_user_sessions($user->id); + } + } + + $usercontext = \context_user::instance($usernew->id); + + // Update preferences. + useredit_update_user_preference($usernew); + + // Update tags. + if (empty($USER->newadminuser) && isset($usernew->interests)) { + useredit_update_interests($usernew, $usernew->interests); + } + + // Update user picture. + if (empty($USER->newadminuser)) { + \core_user::update_picture($usernew, $filemanageroptions); + } + + // Update mail bounces. + useredit_update_bounces($user, $usernew); + + // Update forum track preference. + useredit_update_trackforums($user, $usernew); + + // Save custom profile fields data. + profile_save_data($usernew); + + // Reload from db. + $usernew = $DB->get_record('user', array('id' => $usernew->id)); + + if ($createpassword) { + setnew_password_and_mail($usernew); + unset_user_preference('create_password', $usernew); + set_user_preference('auth_forcepasswordchange', 1, $usernew); + } + + // Trigger update/create event, after all fields are stored. + if ($usercreated) { + \core\event\user_created::create_from_userid($usernew->id)->trigger(); + } else { + \core\event\user_updated::create_from_userid($usernew->id)->trigger(); + } + + if ($user->id == $USER->id) { + // Override old $USER session variable. + foreach ((array)$usernew as $variable => $value) { + if ($variable === 'description' or $variable === 'password') { + // These are not set for security nad perf reasons. + continue; + } + $USER->$variable = $value; + } + // Preload custom fields. + profile_load_custom_fields($USER); + + if (!empty($USER->newadminuser)) { + unset($USER->newadminuser); + // Apply defaults again - some of them might depend on admin user info, backup, roles, etc. + admin_apply_default_settings(null, false); + // Admin account is fully configured - set flag here in case the redirect does not work. + unset_config('adminsetuppending'); + // Redirect to admin/ to continue with installation. + if ($redirect) { + self::redirect("$CFG->wwwroot/$CFG->admin/"); + } + } else if (empty($SITE->fullname)) { + // Somebody double clicked when editing admin user during install. + if ($redirect) { + self::redirect("$CFG->wwwroot/$CFG->admin/"); + } + } else { + if ($redirect) { + self::redirect($returnurl); + } + } + } else { + \core\session\manager::gc(); // Remove stale sessions. + if ($redirect) { + self::redirect("$CFG->wwwroot/$CFG->admin/user.php"); + } + } + // Never reached if redirect is true. + } + } + + /** + * Redirect function. + * + * @param string $url + */ + public static function redirect($url) { + global $CFG, $OUTPUT, $PAGE; + + // Adapted from function of same name in lib/weblib.php. + if ($url instanceof moodle_url) { + $url = $url->out(false); + } + + // Prevent debug errors - make sure context is properly initialised. + if ($PAGE) { + $PAGE->set_context(null); + $PAGE->set_pagelayout('redirect'); // No header and footer needed. + $PAGE->set_title(get_string('pageshouldredirect', 'moodle')); + } + + /* Technically, HTTP/1.1 requires Location: header to contain the absolute path. + (In practice browsers accept relative paths - but still, might as well do it properly.) + This code turns relative into absolute. */ + if (!preg_match('|^[a-z]+:|i', $url)) { + // Get host name http://www.wherever.com. + $hostpart = preg_replace('|^(.*?[^:/])/.*$|', '$1', $CFG->wwwroot); + if (preg_match('|^/|', $url)) { + // URLs beginning with / are relative to web server root so we just add them in. + $url = $hostpart.$url; + } else { + // URLs not beginning with / are relative to path of current script, so add that on. + $url = $hostpart.preg_replace('|\?.*$|', '', me()).'/../'.$url; + } + // Replace all ..s. + while (true) { + $newurl = preg_replace('|/(?!\.\.)[^/]*/\.\./|', '/', $url); + if ($newurl == $url) { + break; + } + $url = $newurl; + } + } + + /* Sanitise url - we can not rely on moodle_url or our URL cleaning + because they do not support all valid external URLs. */ + $url = preg_replace('/[\x00-\x1F\x7F]/', '', $url); + $url = str_replace('"', '%22', $url); + $encodedurl = preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&", $url); + $encodedurl = preg_replace('/^.*href="([^"]*)".*$/', "\\1", clean_text('<a href="'.$encodedurl.'" />', FORMAT_HTML)); + $url = str_replace('&', '&', $encodedurl); + + /* Make sure the session is closed properly, this prevents problems in IIS + and also some potential PHP shutdown issues. */ + \core\session\manager::write_close(); + + if (!headers_sent()) { + // 302 might not work for POST requests, 303 is ignored by obsolete clients. + @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other'); + @header('Location: '.$url); + echo \bootstrap_renderer::plain_redirect_message($encodedurl); + exit; + } + + // Include a redirect message, even with a HTTP redirect, because that is recommended practice. + if ($PAGE) { + $CFG->docroot = false; // To prevent the link to moodle docs from being displayed on redirect page. + echo $OUTPUT->adaptable_redirect($encodedurl); + exit; + } else { + echo \bootstrap_renderer::early_redirect_message($encodedurl, '', '0'); + exit; + } + } +} diff --git a/theme/adaptable/classes/output/core_user/myprofile/editprofile_form.php b/theme/adaptable/classes/output/core_user/myprofile/editprofile_form.php new file mode 100644 index 0000000..f5ddd39 --- /dev/null +++ b/theme/adaptable/classes/output/core_user/myprofile/editprofile_form.php @@ -0,0 +1,232 @@ +<?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/>. + +/** + * myprofile edit profile. + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\core_user\myprofile; + +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. +} +require_once($CFG->dirroot.'/lib/formslib.php'); +require_once($CFG->dirroot.'/user/lib.php'); +require_once($CFG->dirroot.'/user/editlib.php'); + +/** + * Class editprofile_form. + * + * @package theme_adaptable + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @copyright Copyright (c) 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class editprofile_form extends \moodleform { + + /** + * Define the form. + */ + public function definition() { + global $COURSE; + + $mform = $this->_form; + $editoroptions = null; + $filemanageroptions = null; + + if (!is_array($this->_customdata)) { + throw new coding_exception('invalid custom data for user_edit_form'); + } + $editoroptions = $this->_customdata['editoroptions']; + $filemanageroptions = $this->_customdata['filemanageroptions']; + $user = $this->_customdata['user']; + $userid = $user->id; + + // Add some extra hidden fields. + $mform->addElement('hidden', 'id'); + $mform->setType('id', \core_user::get_property_type('id')); + $mform->addElement('hidden', 'course', $COURSE->id); + $mform->setType('course', PARAM_INT); + + // Print the required moodle fields first. + $mform->addElement('static', 'moodle', '<h3>'.get_string('general').'</h3>'); + + // Fields. + $this->editprofile_definition($mform, $editoroptions, $filemanageroptions, $user); + + if ($userid == -1) { + $btnstring = get_string('createuser'); // Should never happen, but leave as an indicator. + } else { + $btnstring = get_string('updatemyprofile'); + } + + $this->add_action_buttons(true, $btnstring); + + $this->set_data($user); + } + + /** + * Extend the form definition after data has been parsed. + */ + public function definition_after_data() { + global $DB, $OUTPUT, $USER; + + $mform = $this->_form; + + // Trim required name fields. + foreach (useredit_get_required_name_fields() as $field) { + $mform->applyFilter($field, 'trim'); + } + + if ($userid = $mform->getElementValue('id')) { + $user = $DB->get_record('user', array('id' => $userid)); + } else { + $user = false; + } + + // Require password for new users. + if ($userid > 0) { + if ($mform->elementExists('createpassword')) { + $mform->removeElement('createpassword'); + } + } + + if ($user and is_mnet_remote_user($user)) { + // Only local accounts can be suspended. + if ($mform->elementExists('suspended')) { + $mform->removeElement('suspended'); + } + } + if ($user and ($user->id == $USER->id or is_siteadmin($user))) { + // Prevent self and admin mess ups. + if ($mform->elementExists('suspended')) { + $mform->hardFreeze('suspended'); + } + } + + // Print picture. + if (empty($USER->newadminuser)) { + if ($user) { + $context = \context_user::instance($user->id, MUST_EXIST); + $fs = get_file_storage(); + $hasuploadedpicture = ($fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f2.png') + || $fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f2.jpg')); + + if (!empty($user->picture) && $hasuploadedpicture) { + $imagevalue = $OUTPUT->user_picture($user, array('courseid' => SITEID, 'size' => 64)); + } else { + $imagevalue = get_string('none'); + } + } else { + $imagevalue = get_string('none'); + } + + $imageelement = $mform->getElement('currentpicture'); + $imageelement->setValue($imagevalue); + + if ($user && $mform->elementExists('deletepicture') && !$hasuploadedpicture) { + $mform->removeElement('deletepicture'); + } + } + + // Next the customisable profile fields. + profile_definition_after_data($mform, $userid); + } + + /** + * Validate the form data. + * @param array $usernew + * @param array $files + * @return array|bool + */ + public function validation($usernew, $files) { + $usernew = (object)$usernew; + + $err = array(); + // Next the customisable profile fields. + $err += profile_validation($usernew, $files); + + if (count($err) == 0) { + return true; + } else { + return $err; + } + } + + /** + * Powerful function that is used by edit and editadvanced to add common form elements/rules/etc. + * + * @param moodleform $mform + * @param array $editoroptions + * @param array $filemanageroptions + * @param stdClass $user + */ + public function editprofile_definition(&$mform, $editoroptions, $filemanageroptions, $user) { + global $CFG, $USER; + + if ($user->id > 0) { + useredit_load_preferences($user, false); + } + + $mform->addElement('editor', 'description_editor', get_string('userdescription'), + 'class="adaptablemyeditprofile"', $editoroptions); + $mform->setType('description_editor', PARAM_RAW); + $mform->addHelpButton('description_editor', 'userdescription'); + + $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="54" class="adaptablemyeditprofile"'); + $mform->setType('city', PARAM_TEXT); + if (!empty($CFG->defaultcity)) { + $mform->setDefault('city', $CFG->defaultcity); + } + + if (\core_tag_tag::is_enabled('core', 'user') and empty($USER->newadminuser)) { + $mform->addElement('static', 'moodle_interests', '<h3>'.get_string('interests').'</h3>'); + $mform->addElement('tags', 'interests', get_string('interestslist'), + array('itemtype' => 'user', 'component' => 'core')); + $mform->addHelpButton('interests', 'interestslist'); + } + + if (empty($USER->newadminuser)) { + $mform->addElement('static', 'moodle_picture', '<h3>'.get_string('pictureofuser').'</h3>'); + + if (!empty($CFG->enablegravatar)) { + $mform->addElement('html', \html_writer::tag('p', get_string('gravatarenabled'))); + } + + $mform->addElement('static', 'currentpicture', get_string('currentpicture')); + + $mform->addElement('checkbox', 'deletepicture', get_string('deletepicture')); + $mform->setDefault('deletepicture', 0); + + $mform->addElement('filemanager', 'imagefile', get_string('newpicture'), + 'class="adaptablemyeditprofile"', $filemanageroptions); + $mform->addHelpButton('imagefile', 'newpicture'); + + $mform->addElement('text', 'imagealt', get_string('imagealt'), + 'maxlength="100" size="54" class="adaptablemyeditprofile"'); + $mform->setType('imagealt', PARAM_TEXT); + } + } + +} diff --git a/theme/adaptable/classes/output/core_user/myprofile/renderer.php b/theme/adaptable/classes/output/core_user/myprofile/renderer.php new file mode 100644 index 0000000..1360a48 --- /dev/null +++ b/theme/adaptable/classes/output/core_user/myprofile/renderer.php @@ -0,0 +1,709 @@ +<?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/>. + +/** + * myprofile renderer. + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\core_user\myprofile; + +defined('MOODLE_INTERNAL') || die; + +use core_user\output\myprofile\category; +use core_user\output\myprofile\node; +use core_user\output\myprofile\tree; +use html_writer; + +require_once($CFG->dirroot.'/user/lib.php'); + +/** + * myprofile renderer. + * @copyright Copyright (c) 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \core_user\output\myprofile\renderer { + + /** @var Obj $user U*/ + private $user = null; + + /** @var Obj $course */ + private $course = null; + + /** @var bool $enabletabbedprofile */ + private $enabletabbedprofile = true; + + /** + * Constructor for class. + * + * @param moodle_page $page + * @param string $target + * + * @return Obj + */ + public function __construct(\moodle_page $page, $target) { + $this->enabletabbedprofile = get_config('theme_adaptable', 'enabletabbedprofile'); + + if ($this->enabletabbedprofile) { + /* We need the user id! + From user/profile.php - technically by the time we are instantiated then the user id will have been validated. */ + global $DB, $USER; + $userid = optional_param('id', 0, PARAM_INT); + $userid = $userid ? $userid : $USER->id; + $this->user = \core_user::get_user($userid); + + $courseid = optional_param('course', SITEID, PARAM_INT); // Course id (defaults to Site). + $this->course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + + /* Using this as the function copes with hidden fields and capabilities. For example: + * If the you're allowed to see the description. + * + * This way because the DB user record from get_user can contain the description that + * the function user_get_user_details can exclude! */ + $this->user->userdetails = user_get_user_details($this->user, $this->course); + } + + parent::__construct($page, $target); + } + + /** + * Render the whole tree. + * + * @param tree $tree + * + * @return string + */ + public function render_tree(tree $tree) { + if (!$this->enabletabbedprofile) { + return parent::render_tree($tree); + } + + $categories = array(); + foreach ($tree->categories as $category) { + $categories[$category->name] = $category; + } + + $output = html_writer::start_tag('div', array('id' => 'adaptable_profile_tree', 'class' => 'profile_tree row')); + + $output .= html_writer::start_tag('div', array('class' => 'ucol1 col-md-4')); // Col one. + + $output .= html_writer::start_tag('div', array('class' => 'row')); + $output .= html_writer::start_tag('div', array('class' => 'col-12 contact')); + $contactcategory = $this->transform_contact_category($categories['contact']); + $output .= $this->render($contactcategory); + unset($categories['contact']); + $output .= html_writer::end_tag('div'); + + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('div'); + + $output .= html_writer::start_tag('div', array('class' => 'ucol2 col-md-8')); // Col two. + $output .= $this->tabs($categories); + $output .= html_writer::end_tag('div'); + + $output .= html_writer::end_tag('div'); + + return $output; + } + + /** + * Transform contact category. + * + * @param Obj $oldcontactcategory + * + * @return Obj + */ + protected function transform_contact_category($oldcontactcategory) { + $contactcategory = new category('contact', ''); + + $node = new node('contact', 'userimage', '', null, null, + $this->userimage()); + $contactcategory->add_node($node); + + if ((empty($this->user->userdetails['firstname'])) || (empty($this->user->userdetails['lastname']))) { + $node = new node('contact', 'fullname', '', null, null, + $this->user->userdetails['fullname']); + $contactcategory->add_node($node); + } else { + $node = new node('contact', 'firstname', '', null, null, + $this->user->userdetails['firstname']); + $contactcategory->add_node($node); + + $node = new node('contact', 'lastname', '', null, null, + $this->user->userdetails['lastname']); + $contactcategory->add_node($node); + } + + if (theme_adaptable_get_setting('enabledtabbedprofileuserpreferenceslink')) { + $node = new node('contact', 'userpreferences', '', null, null, + $this->userpreferences()); + $contactcategory->add_node($node); + } + + if (theme_adaptable_get_setting('enabledtabbedprofileeditprofilelink')) { + if (!empty($oldcontactcategory->nodes['editprofile'])) { + $contactcategory->add_node($oldcontactcategory->nodes['editprofile']); + } + } + + if (!empty($this->user->userdetails['email'])) { + $node = new node('contact', 'email', get_string('email'), null, null, + $this->user->userdetails['email']); + $contactcategory->add_node($node); + } + + if (!empty($this->user->userdetails['city'])) { + $node = new node('contact', 'city', get_string('city'), null, null, + $this->user->userdetails['city']); + $contactcategory->add_node($node); + } + if (!empty($this->user->userdetails['country'])) { + $node = new node('contact', 'country', get_string('country'), null, null, + $this->user->userdetails['country']); + $contactcategory->add_node($node); + } + + $messagebuttons = $this->message_user(); + if (!empty($messagebuttons)) { + foreach ($messagebuttons as $button) { + $node = new node('contact', $button['type'], '', null, null, $button['content']); + $contactcategory->add_node($node); + } + } + + return $contactcategory; + } + + /** + * Message user. + * + * @return array + */ + protected function message_user() { + global $CFG, $USER; + $output = array(); + + // Use Moodle code just in case! + $course = ($this->page->context->contextlevel == CONTEXT_COURSE) ? $this->page->course : null; + $user = $this->user; + + if (user_can_view_profile($user, $course)) { + $context = \context_user::instance($user->id, IGNORE_MISSING); + // Check to see if we can display a message button. + if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) { + $userbuttons = array(); + if (($USER->id != $user->id) || ($CFG->branch > 36)) { + if ($CFG->branch < 37) { + $linkattributes = array('role' => 'button'); + } else { + $linkattributes = \core_message\helper::messageuser_link_params($user->id); + // Change the id to us instead of copying the method. + $linkattributes['id'] = 'adaptable-message-user-button'; + $this->adaptable_messageuser_requirejs(); + } + $userbuttons['messages'] = array( + 'buttontype' => 'message', + 'title' => get_string('message', 'message'), + 'url' => new \moodle_url('/message/index.php', array('id' => $user->id)), + 'image' => 't/message', + 'linkattributes' => $linkattributes, + 'page' => $this->page + ); + } + + if ($USER->id != $user->id) { + $iscontact = \core_message\api::is_contact($USER->id, $user->id); + $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts'; + $contacturlaction = $iscontact ? 'removecontact' : 'addcontact'; + $contactimage = $iscontact ? 'removecontact' : 'addcontact'; + $userbuttons['togglecontact'] = array( + 'buttontype' => 'togglecontact', + 'title' => get_string($contacttitle, 'message'), + 'url' => new \moodle_url('/message/index.php', array( + 'user1' => $USER->id, + 'user2' => $user->id, + $contacturlaction => $user->id, + 'sesskey' => sesskey()) + ), + 'image' => 't/'.$contactimage, + 'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact), + 'page' => $this->page + ); + \core_message\helper::togglecontact_requirejs(); + } + + $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle'); + foreach ($userbuttons as $button) { + $image = $this->pix_icon($button['image'], $button['title'], 'moodle', array( + 'class' => 'iconsmall', + 'role' => 'presentation' + )); + $image .= html_writer::span($button['title'], 'header-button-title'); + $output[] = array( + 'type' => $button['buttontype'], + 'content' => html_writer::link($button['url'], html_writer::tag('span', $image), $button['linkattributes']) + ); + } + } + } + + return $output; + } + + /** + * Requires the JS libraries for the Adaptable message user button. + * + * This is needed so that we have a different id to the core button that is hidden and the core JS picks up. + * Thus the 'trigger' of the message box would only happen on the 'first' '#message-user-button' the JS sees and + * not ours that comes afterwards. So by having a different id we can solve this and not invoke the core JS with + * the core button's id in the first place. + * + * @return void + */ + public function adaptable_messageuser_requirejs() { + static $done = false; + if ($done) { + return; + } + + $this->page->requires->js_call_amd('core_message/message_user_button', 'send', array('#adaptable-message-user-button')); + $done = true; + } + + /** + * Render a category. + * + * @param category $category + * + * @return string + */ + public function render_category(category $category) { + if (!$this->enabletabbedprofile) { + return parent::render_category($category); + } + + $nodes = $category->nodes; + if (empty($nodes)) { + // No nodes, nothing to render. + return ''; + } + + $classes = $category->classes; + if (empty($classes)) { + $output = html_writer::start_tag('section', array('class' => 'node_category '.$category->name)); + } else { + $output = html_writer::start_tag('section', array('class' => 'node_category '.$category->name.' '.$classes)); + } + if ((empty($category->notitle)) && ($category->title)) { + $output .= html_writer::tag('h3', $category->title); + } + $output .= html_writer::start_tag('ul'); + foreach ($nodes as $node) { + $output .= $this->render($node); + } + $output .= html_writer::end_tag('ul'); + $output .= html_writer::end_tag('section'); + + return $output; + } + + /** + * Render a node. + * + * @param node $node + * + * @return string + */ + public function render_node(node $node) { + if (!$this->enabletabbedprofile) { + return parent::render_node($node); + } + $return = ''; + if (is_object($node->url)) { + $header = \html_writer::link($node->url, $node->title); + } else { + $header = $node->title; + } + $icon = $node->icon; + if (!empty($icon)) { + $header .= $this->render($icon); + } + $content = $node->content; + $classes = $node->classes; + if (!empty($content)) { + if ($header) { + // There is some content to display below this make this a header. + $return = \html_writer::tag('dt', $header); + $return .= \html_writer::tag('dd', $content); + + $return = \html_writer::tag('dl', $return); + } else { + $return = \html_writer::span($content); + } + if ($classes) { + $return = \html_writer::tag('li', $return, array('class' => 'contentnode '.$node->name.' '.$classes)); + } else { + $return = \html_writer::tag('li', $return, array('class' => 'contentnode '.$node->name)); + } + } else { + $return = \html_writer::span($header); + $return = \html_writer::tag('li', $return, array('class' => $classes)); + } + + return $return; + } + + /** + * Output user image. + * + * @return string + */ + protected function userimage() { + $output = ''; + + if (!empty($this->user)) { + $output .= html_writer::start_tag('li', array('class' => 'adaptableuserpicture')); + $output .= $this->output->user_picture($this->user, array('size' => '1')); + $output .= html_writer::end_tag('li'); + } + + return $output; + } + + /** + * Get user preferences. + * + * @return string + */ + protected function userpreferences() { + global $USER; + + $output = ''; + if ($USER->id == $this->user->id) { + $output = html_writer::start_tag('li', array('class' => 'contentnode adaptableuserpreferences')); + $output .= html_writer::link(new \moodle_url('/user/preferences.php'), get_string('preferences', 'moodle')); + $output .= html_writer::end_tag('li'); + } + + return $output; + } + + /** + * About me. + * + * @return category Obj + */ + protected function create_aboutme() { + $aboutme = new category('aboutme', get_string('aboutme', 'theme_adaptable')); + + // Description. + if (!empty($this->user->userdetails['description'])) { + $description = $this->user->userdetails['description']; + } else { + $description = get_string('usernodescription', 'theme_adaptable'); + } + $node = new node('aboutme', 'description', get_string('description'), null, null, + $description); + $aboutme->add_node($node); + + // Interests. + if (!empty($this->user->userdetails['interests'])) { + // Odd but just the way things can be! + $interests = $this->output->tag_list(\core_tag_tag::get_item_tags('core', 'user', $this->user->id), ''); + } else { + $interests = get_string('usernointerests', 'theme_adaptable'); + } + $node = new node('aboutme', 'interests', get_string('interests'), null, null, + $interests); + $aboutme->add_node($node); + + // Optional. + $this->optional_fields($aboutme, 'aboutme'); + + return $aboutme; + } + + /** + * Add the optional fields to the stated category if they are populated. + * + * @param category $category Category object to add to. + * @param string $categoryname Category name to add to. + */ + protected function optional_fields(category $category, $categoryname) { + if (!empty($this->user->userdetails['url'])) { + $node = new node($categoryname, 'url', get_string('url'), null, null, + $this->user->userdetails['url'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['icq'])) { + $node = new node($categoryname, 'icq', get_string('icqnumber'), null, null, + $this->user->userdetails['icq'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['skype'])) { + $node = new node($categoryname, 'skype', get_string('skypeid'), null, null, + $this->user->userdetails['skype'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['yahoo'])) { + $node = new node($categoryname, 'yahoo', get_string('yahooid'), null, null, + $this->user->userdetails['yahoo'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['aim'])) { + $node = new node($categoryname, 'aim', get_string('aimid'), null, null, + $this->user->userdetails['aim'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['msn'])) { + $node = new node($categoryname, 'msn', get_string('msnid'), null, null, + $this->user->userdetails['msn'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['address'])) { + $node = new node($categoryname, 'address', get_string('address'), null, null, + $this->user->userdetails['address'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['phone1'])) { + $node = new node($categoryname, 'phone1', get_string('phone1'), null, null, + $this->user->userdetails['phone1'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['phone2'])) { + $node = new node($categoryname, 'phone2', get_string('phone2'), null, null, + $this->user->userdetails['phone2'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['idnumber'])) { + $node = new node($categoryname, 'idnumber', get_string('idnumber'), null, null, + $this->user->userdetails['idnumber'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['institution'])) { + $node = new node($categoryname, 'institution', get_string('institution'), null, null, + $this->user->userdetails['institution'], null, 'aduseropt'); + $category->add_node($node); + } + if (!empty($this->user->userdetails['department'])) { + $node = new node($categoryname, 'department', get_string('department'), null, null, + $this->user->userdetails['department'], null, 'aduseropt'); + $category->add_node($node); + } + } + + /** + * Custom user profile. + * + * TODO: May need to change, somehow, to use display_data(), see: userprofilefields(). + * + * @return category Obj or null if not created. + */ + protected function customuserprofile() { + $category = null; + + $customcoursetitleprofilefield = get_config('theme_adaptable', 'customcoursetitle'); + $customcoursesubtitleprofilefield = get_config('theme_adaptable', 'customcoursesubtitle'); + + if ((!empty($this->user->userdetails['customfields'])) && + ((!empty($customcoursetitleprofilefield)) || (!empty($customcoursesubtitleprofilefield)))) { + $customcoursetitle = ''; + $customcoursesubtitle = ''; + $searcharray = array(); + foreach ($this->user->userdetails['customfields'] as $cfield) { + $searcharray[$cfield['shortname']] = $cfield; + } + + if (!empty($customcoursetitleprofilefield)) { + if (array_key_exists($customcoursetitleprofilefield, $searcharray)) { + $customcoursetitle = $searcharray[$customcoursetitleprofilefield]['value']; + } + } + if (!empty($customcoursesubtitleprofilefield)) { + if (array_key_exists($customcoursesubtitleprofilefield, $searcharray)) { + $customcoursesubtitle = $searcharray[$customcoursesubtitleprofilefield]['value']; + } + } + + if ((!empty($customcoursetitle)) || (!empty($customcoursesubtitle))) { + $category = new category('customuserprofile', get_string('course', 'theme_adaptable')); + + if (!empty($customcoursetitle)) { + $node = new node('customuserprofile', 'customcoursetitle', '', null, null, $customcoursetitle); + $category->add_node($node); + } + if (!empty($customcoursesubtitle)) { + $node = new node('customuserprofile', 'customcoursesubtitle', '', null, null, $customcoursesubtitle); + $category->add_node($node); + } + } + } + + return $category; + } + + /** + * User profile fields. + * + * @return string + */ + protected function userprofilefields() { + $output = ''; + + if (!empty($this->user->userdetails['customfields'])) { + $customcoursetitleprofilefield = get_config('theme_adaptable', 'customcoursetitle'); + $customcoursesubtitleprofilefield = get_config('theme_adaptable', 'customcoursesubtitle'); + + $customfieldscat = new category('customfields', ''); + $hasnodes = false; + + $categories = profile_get_user_fields_with_data_by_category($this->user->id); + foreach ($categories as $categoryid => $fields) { + foreach ($fields as $formfield) { + if ((!empty($customcoursetitleprofilefield)) && ($formfield->field->shortname == $customcoursetitleprofilefield)) { + continue; + } + if ((!empty($customcoursesubtitleprofilefield)) && ($formfield->field->shortname == $customcoursesubtitleprofilefield)) { + continue; + } + if ($formfield->is_visible() and !$formfield->is_empty()) { + $node = new node('customfields', 'custom_field_' . $formfield->field->shortname, + format_string($formfield->field->name), null, null, $formfield->display_data()); + $customfieldscat->add_node($node); + $hasnodes = true; + } + } + } + + if ($hasnodes) { + $output .= $this->render($customfieldscat); + } + } + + return $output; + } + + /** + * Create edit profile form. + * + * @return string + */ + protected function create_editprofile() { + $editprofile = new category('editprofile', get_string('editmyprofile')); + + $editprofileform = editprofile::generate_form(); + $node = new node('editprofile', 'editprofile', '', null, null, $editprofileform['form']->render()); + $editprofile->add_node($node); + + // Process the form. + editprofile::process_form(true, $editprofileform); + + return $editprofile; + } + + /** + * Create tabs. + * + * @param array $categories + * + * @return string + */ + protected function tabs($categories) { + global $USER; + + static $tabcategories = array('coursedetails'); + + $tabdata = new \stdClass; + $tabdata->containerid = 'userprofiletabs'; + $tabdata->tabs = array(); + + // Aboutme tab. + $category = $this->create_aboutme(); + $aboutmetab = new \stdClass; + $aboutmetab->name = $category->name; + $aboutmetab->displayname = $category->title; + $customuserprofilecat = $this->customuserprofile(); + $aboutmetab->content = ''; + if (!is_null($customuserprofilecat)) { + $aboutmetab->content .= $this->render($customuserprofilecat); + } else { + $category->notitle = true; + } + $aboutmetab->content .= $this->render($category); + // Custom fields on About me tab. + $aboutmetab->content .= $this->userprofilefields(); + $tabdata->tabs[] = $aboutmetab; + + foreach ($tabcategories as $categoryname) { + if (!empty($categories[$categoryname])) { + $category = $categories[$categoryname]; + if ($category->name == 'coursedetails') { + $category->notitle = true; + } + $markup = $this->render($category); + if (!empty($markup)) { + $tab = new \stdClass; + $tab->name = $category->name; + if ($category->name == 'coursedetails') { + $tab->displayname = get_string('courses', 'theme_adaptable'); + } else { + $tab->displayname = $category->title; + } + $tab->content = $markup; + $tabdata->tabs[] = $tab; + } + unset($categories[$categoryname]); + } + } + + // More tab. + $misccontent = html_writer::start_tag('div', array('class' => 'row')); + foreach ($categories as $categoryname => $category) { + $misccontent .= html_writer::start_tag('div', array('class' => 'col-12 '.$categoryname)); + $misccontent .= $this->render($category); + $misccontent .= html_writer::end_tag('div'); + } + $misccontent .= html_writer::end_tag('div'); + $tab = new \stdClass; + $tab->name = 'more'; + $tab->displayname = get_string('more', 'theme_adaptable'); + $tab->content = $misccontent; + $tabdata->tabs[] = $tab; + + if ((is_siteadmin()) || + (($USER->id == $this->user->id)) && + (has_capability('moodle/user:editownprofile', \context_user::instance($this->user->id)))) { + // Edit profile tab. + $category = $this->create_editprofile(); + $editprofiletab = new \stdClass; + $editprofiletab->name = $category->name; + $editprofiletab->displayname = $category->title; + $category->notitle = true; + $editprofiletab->content = $this->render($category); + $tabdata->tabs[] = $editprofiletab; + } + $aboutmetab->selected = true; + + return $this->render_from_template('theme_adaptable/tabs', $tabdata); + } +} diff --git a/theme/adaptable/classes/output/mod_forum/email/renderer_htmlemail.php b/theme/adaptable/classes/output/mod_forum/email/renderer_htmlemail.php new file mode 100644 index 0000000..4d93821 --- /dev/null +++ b/theme/adaptable/classes/output/mod_forum/email/renderer_htmlemail.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * Forum post renderable. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\mod_forum\email; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum post renderable. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_htmlemail extends \mod_forum\output\email\renderer { + + /** + * Display a forum post in the relevant context. + * + * @param \mod_forum\output\forum_post_email $post The post to display. + * @return string + */ + public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { + // Was ($this, $this->target === RENDERER_TARGET_TEXTEMAIL) and as we are already 'htmlemail' it will always be false. + $data = $post->export_for_template($this, false); + + $templatename = $this->forum_post_template(); + $themeoverride = \theme_adaptable\toolbox::apply_template_override('mod_forum/'.$templatename, $data); + if ($themeoverride !== false) { + $output = $themeoverride; + } else { + // Use core mechanism. + $output = $this->render_from_template('mod_forum/'.$templatename, $data); + } + + return $output; + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mod_forum/email/renderer_textemail.php b/theme/adaptable/classes/output/mod_forum/email/renderer_textemail.php new file mode 100644 index 0000000..ee23e8d --- /dev/null +++ b/theme/adaptable/classes/output/mod_forum/email/renderer_textemail.php @@ -0,0 +1,59 @@ +<?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/>. + +/** + * Forum post renderable. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable\output\mod_forum\email; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum post renderable. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_textemail extends \mod_forum\output\email\renderer_textemail { + + /** + * Display a forum post in the relevant context. + * + * @param \mod_forum\output\forum_post_email $post The post to display. + * @return string + */ + public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { + // Was ($this, $this->target === RENDERER_TARGET_TEXTEMAIL) and as we are already 'textemail' it will always be false. + $data = $post->export_for_template($this, false); + + $templatename = $this->forum_post_template(); + $themeoverride = \theme_adaptable\toolbox::apply_template_override('mod_forum/'.$templatename, $data); + if ($themeoverride !== false) { + $output = $themeoverride; + } else { + // Use core mechanism. + $output = $this->render_from_template('mod_forum/'.$templatename, $data); + } + + return $output; + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_htmlemail.php b/theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_htmlemail.php new file mode 100644 index 0000000..c41e70d --- /dev/null +++ b/theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_htmlemail.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * Forum post renderable. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\mod_forum\emaildigestbasic; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum post renderable. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_htmlemail extends \mod_forum\output\emaildigestbasic\renderer { + + /** + * Display a forum post in the relevant context. + * + * @param \mod_forum\output\forum_post_email $post The post to display. + * @return string + */ + public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { + // Was ($this, $this->target === RENDERER_TARGET_TEXTEMAIL) and as we are already 'htmlemail' it will always be false. + $data = $post->export_for_template($this, false); + + $templatename = $this->forum_post_template(); + $themeoverride = \theme_adaptable\toolbox::apply_template_override('mod_forum/'.$templatename, $data); + if ($themeoverride !== false) { + $output = $themeoverride; + } else { + // Use core mechanism. + $output = $this->render_from_template('mod_forum/'.$templatename, $data); + } + + return $output; + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_textemail.php b/theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_textemail.php new file mode 100644 index 0000000..1ea0107 --- /dev/null +++ b/theme/adaptable/classes/output/mod_forum/emaildigestbasic/renderer_textemail.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * Forum post renderable. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\mod_forum\emaildigestbasic; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum post renderable. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_textemail extends \mod_forum\output\emaildigestbasic\renderer_textemail { + + /** + * Display a forum post in the relevant context. + * + * @param \mod_forum\output\forum_post_email $post The post to display. + * @return string + */ + public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { + // Was ($this, $this->target === RENDERER_TARGET_TEXTEMAIL) and as we are already 'textemail' it will always be false. + $data = $post->export_for_template($this, false); + + $templatename = $this->forum_post_template(); + $themeoverride = \theme_adaptable\toolbox::apply_template_override('mod_forum/'.$templatename, $data); + if ($themeoverride !== false) { + $output = $themeoverride; + } else { + // Use core mechanism. + $output = $this->render_from_template('mod_forum/'.$templatename, $data); + } + + return $output; + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_htmlemail.php b/theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_htmlemail.php new file mode 100644 index 0000000..d50057f --- /dev/null +++ b/theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_htmlemail.php @@ -0,0 +1,61 @@ +<?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/>. + +/** + * Forum post renderable. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\mod_forum\emaildigestfull; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum post renderable. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_htmlemail extends \mod_forum\output\emaildigestfull\renderer { + + /** + * Display a forum post in the relevant context. + * + * @param \mod_forum\output\forum_post_email $post The post to display. + * + * @return string + */ + public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { + // Was ($this, $this->target === RENDERER_TARGET_TEXTEMAIL) and as we are already 'htmlemail' it will always be false. + $data = $post->export_for_template($this, false); + + $templatename = $this->forum_post_template(); + $themeoverride = \theme_adaptable\toolbox::apply_template_override('mod_forum/'.$templatename, $data); + if ($themeoverride !== false) { + $output = $themeoverride; + } else { + // Use core mechanism. + $output = $this->render_from_template('mod_forum/'.$templatename, $data); + } + + return $output; + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_textemail.php b/theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_textemail.php new file mode 100644 index 0000000..05e6bec --- /dev/null +++ b/theme/adaptable/classes/output/mod_forum/emaildigestfull/renderer_textemail.php @@ -0,0 +1,60 @@ +<?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/>. + +/** + * Forum post renderable. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +namespace theme_adaptable\output\mod_forum\emaildigestfull; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum post renderable. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer_textemail extends \mod_forum\output\emaildigestfull\renderer_textemail { + + /** + * Display a forum post in the relevant context. + * + * @param \mod_forum\output\forum_post_email $post The post to display. + * @return string + */ + public function render_forum_post_email(\mod_forum\output\forum_post_email $post) { + // Was ($this, $this->target === RENDERER_TARGET_TEXTEMAIL) and as we are already 'textemail' it will always be false. + $data = $post->export_for_template($this, false); + + $templatename = $this->forum_post_template(); + $themeoverride = \theme_adaptable\toolbox::apply_template_override('mod_forum/'.$templatename, $data); + if ($themeoverride !== false) { + $output = $themeoverride; + } else { + // Use core mechanism. + $output = $this->render_from_template('mod_forum/'.$templatename, $data); + } + + return $output; + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mustache_filesystemstring_loader.php b/theme/adaptable/classes/output/mustache_filesystemstring_loader.php new file mode 100644 index 0000000..190dfe3 --- /dev/null +++ b/theme/adaptable/classes/output/mustache_filesystemstring_loader.php @@ -0,0 +1,67 @@ +<?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/>. + +/** + * Mustache file system loader. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable\output; + +use coding_exception; + +/** + * Mustache file system loader. + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mustache_filesystemstring_loader extends \core\output\mustache_filesystem_loader { + + /** + * @var $templates array of templates. + */ + private $templates = array(); + + /** + * Provide a default no-args constructor (we don't really need anything). + */ + public function __construct() { + } + + /** + * Load a Template by name. + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) { + if (!isset($this->templates[$name])) { + $templateoverride = \theme_adaptable\toolbox::get_template_override($name); + if ($templateoverride !== false) { + $this->templates[$name] = $templateoverride; + } else { + $this->templates[$name] = $this->loadFile($name); + } + } + + return $this->templates[$name]; + } +} diff --git a/theme/adaptable/classes/output/mustache_renderer.php b/theme/adaptable/classes/output/mustache_renderer.php new file mode 100644 index 0000000..5102bbc --- /dev/null +++ b/theme/adaptable/classes/output/mustache_renderer.php @@ -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/>. + +/** + * The mustache renderer. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mustache renderer. + * + * @copyright © 2020-onwards G J Barnard. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class mustache_renderer extends \renderer_base { + + /** + * @var Mustache_Loader $stringloader The mustache string loader. + */ + protected $stringloader; + + /** + * Renders a template by name with the given context. + * + * The provided data needs to be array/stdClass made up of only simple types. + * Simple types are array,stdClass,bool,int,float,string + * + * @since 2.9 + * @param string $templatename + * @param array|stdClass $context Context containing data for the template. + * @return string|boolean + */ + public function render_from_template($templatename, $context) { + if ($this->stringloader === null) { + // Change loaders! + $mustache = $this->get_mustache(); + $this->stringloader = new \Mustache_Loader_StringLoader(); + $mustache->setLoader($this->stringloader); + // Needed to get the partials from the setting or the file system, otherwise they are not processed. + $partialsloader = new mustache_filesystemstring_loader(); + $mustache->setPartialsLoader($partialsloader); + } + return parent::render_from_template($templatename, $context); + } +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/mustachesource_renderer.php b/theme/adaptable/classes/output/mustachesource_renderer.php new file mode 100644 index 0000000..d1eb391 --- /dev/null +++ b/theme/adaptable/classes/output/mustachesource_renderer.php @@ -0,0 +1,49 @@ +<?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/>. + +/** + * The mustache renderer. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mustache renderer. + * + * @copyright © 2020-onwards G J Barnard. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class mustachesource_renderer extends \renderer_base { + + /** + * Gets the template source by name. + * + * @param string $templatename Template name. + * @return Mustache Source string. + */ + public function get_template($templatename) { + $mustache = $this->get_mustache(); + + return $mustache->getLoader()->load($templatename); + } + +} \ No newline at end of file diff --git a/theme/adaptable/classes/output/topcoll_course_renderer.php b/theme/adaptable/classes/output/topcoll_course_renderer.php new file mode 100644 index 0000000..17f1a45 --- /dev/null +++ b/theme/adaptable/classes/output/topcoll_course_renderer.php @@ -0,0 +1,86 @@ +<?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/>. + +/** + * Overridden Collapsed Topics Core Course Renderer for Adaptable theme + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +namespace theme_adaptable\output; + +defined('MOODLE_INTERNAL') || die(); + +use cm_info; +use core_text; +use html_writer; + +/** + * Collapsed Topics Course renderer implementation. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class topcoll_course_renderer extends \theme_adaptable\output\core\course_renderer { + + /** + * Overridden. Renders html to display a name with the link to the course module on a course page + * + * If module is unavailable for user but still needs to be displayed + * in the list, just the name is returned without a link. + * + * Note that for course modules that never have separate pages (i.e. labels) + * this function return an empty string. + * + * This method has only been overriden in order to strip -24 and similar from icon image filenames + * to allow using of local theme icons in /pix_core/f. + * + * @param cm_info $mod + * @param array $displayoptions + * @return string + */ + public function course_section_cm_name(cm_info $mod, $displayoptions = array()) { + if (!$mod->uservisible && empty($mod->availableinfo)) { + // Nothing to be displayed to the user. + return ''; + } + + if (!$mod->url) { + return ''; + } + + // If use adaptable icons is set to false, then just run CT version of the method. + if (empty($this->page->theme->settings->coursesectionactivityuseadaptableicons)) { + list($linkclasses, $textclasses) = $this->course_section_cm_classes($mod); + $groupinglabel = $mod->get_grouping_label($textclasses); + + /* Render element that allows to edit activity name inline. It calls course_section_cm_name_title() + to get the display title of the activity. */ + $tmpl = new \format_topcoll\output\course_module_name($mod, $this->page->user_is_editing(), $displayoptions); + return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output)). + $groupinglabel; + } + + $templateclass = new \format_topcoll\output\course_module_name($mod, $this->page->user_is_editing(), $displayoptions); + $data = $this->adaptable_course_section_cm_name($mod, $templateclass); + + // Not sure about groupinglabel at end as same as CT but not Adaptable, need to see what happens. + return $this->output->render_from_template('core/inplace_editable', $data['templatedata']).$data['groupinglabel']; + } +} diff --git a/theme/adaptable/classes/privacy/provider.php b/theme/adaptable/classes/privacy/provider.php new file mode 100644 index 0000000..1f9b4d3 --- /dev/null +++ b/theme/adaptable/classes/privacy/provider.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Provider class file. As required for any data privacy information required. + * + * @package theme_adaptable + * @copyright 2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace theme_adaptable\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Provider class. + * + * @package theme_adaptable + * @copyright 2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This theme does not store any personal user data. + \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/theme/adaptable/classes/toolbox.php b/theme/adaptable/classes/toolbox.php new file mode 100644 index 0000000..6538a50 --- /dev/null +++ b/theme/adaptable/classes/toolbox.php @@ -0,0 +1,567 @@ +<?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/>. + +/** + * + * File for toolbox class. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable; + +defined('MOODLE_INTERNAL') || die; + +/** + * + * Class definition for toolbox. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class toolbox { + + /** + * Gets the setting moodle_url for the given setting if it exists and set. + * + * See: https://moodle.org/mod/forum/discuss.php?d=371252#p1516474 and change if theme_config::setting_file_url + * changes. + * My need to do: $url = preg_replace('|^https?://|i', '//', $url->out(false)); separately. + * + * @param string $setting Setting + * @param Obj $theconfig + * + * @return string Setting url + */ + static public function get_setting_moodle_url($setting, $theconfig = null) { + $settingurl = null; + + if (empty($theconfig)) { + $theconfig = \theme_config::load('adaptable'); + } + if ($theconfig != null) { + $thesetting = $theconfig->settings->$setting; + if (!empty($thesetting)) { + global $CFG; + $itemid = \theme_get_revision(); + $syscontext = \context_system::instance(); + + $settingurl = \moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", + "/$syscontext->id/theme_$theconfig->name/$setting/$itemid".$thesetting); + } + } + return $settingurl; + } + + /** + * Finds the given setting in the theme using the get_config core function for when the + * theme_config object has not been created. + * + * @param string $setting Setting name. + * @param themename $themename null(default of 'adaptable' used)|theme name. + * + * @return any false|value of setting. + */ + static public function get_config_setting($setting, $themename = null) { + if (empty($themename)) { + $themename = 'adaptable'; + } + return \get_config('theme_'.$themename, $setting); + } + + + /** + * Get top level categories. + * + * @return array category ids + */ + public static function get_top_level_categories() { + $categoryids = array(); + $categories = \core_course_category::get(0)->get_children(); // Parent = 0 i.e. top-level categories only. + + foreach ($categories as $category) { + $categoryids[$category->id] = $category->name; + } + + return $categoryids; + } + + /** + * Get the current top level category. + * + * @return int category id + */ + static public function get_current_top_level_catetgory() { + global $PAGE; + $catid = false; + + if (is_array($PAGE->categories)) { + $catids = array_keys($PAGE->categories); + if (!empty($catids)) { + // The last entry in the array is the top level category. + $catid = $catids[(count($catids) - 1)]; + } + } else if (!empty($PAGE->course->category)) { + $catid = $PAGE->course->category; + // See if the course category is a top level one. + if (!array_key_exists($catid, self::get_top_level_categories())) { + $catid = false; + } + } + + return $catid; + } + + /** + * Get top level categories with sub-categories. + * + * @return array category list + */ + static public function get_top_categories_with_children() { + static $catlist = null; + static $dbcatlist = null; + + if (empty($catlist)) { + global $DB; + $dbcatlist = $DB->get_records('course_categories', null, 'sortorder', 'id, name, depth, path'); + $catlist = array(); + + foreach ($dbcatlist as $category) { + if ($category->depth > 1 ) { + $path = preg_split('|/|', $category->path, -1, PREG_SPLIT_NO_EMPTY); + $top = $path[0]; + if (empty($catlist[$top])) { + $catlist[$top] = array('name' => $dbcatlist[$top]->name, 'children' => array()); + } + unset($path[0]); + foreach ($path as $id) { + if (!array_key_exists($id, $catlist[$top]['children'])) { + $catlist[$top]['children'][$id] = $category->name; + } + } + } else if (empty($catlist[$category->id])) { + $catlist[$category->id] = array('name' => $category->name, 'children' => array()); + } + } + } + + return $catlist; + } + + /** + * Compile properties. + * + * @param string $themename Theme name + * @param bool $array Is this an array (confusing variable name) + * + * @return array properties + */ + static public function compile_properties($themename, $array = true) { + global $CFG, $DB; + + $props = array(); + $themeprops = $DB->get_records('config_plugins', array('plugin' => 'theme_'.$themename)); + + if ($array) { + $props['moodle_version'] = $CFG->version; + // Put the theme version next so that it will be at the top of the table. + foreach ($themeprops as $themeprop) { + if ($themeprop->name == 'version') { + $props['theme_version'] = $themeprop->value; + unset($themeprops[$themeprop->id]); + break; + } + } + + foreach ($themeprops as $themeprop) { + $props[$themeprop->name] = $themeprop->value; + } + } else { + $data = new \stdClass(); + $data->id = 0; + $data->value = $CFG->version; + $props['moodle_version'] = $data; + // Convert 'version' to 'theme_version'. + foreach ($themeprops as $themeprop) { + if ($themeprop->name == 'version') { + $data = new \stdClass(); + $data->id = $themeprop->id; + $data->name = 'theme_version'; + $data->value = $themeprop->value; + $props['theme_version'] = $data; + unset($themeprops[$themeprop->id]); + break; + } + } + foreach ($themeprops as $themeprop) { + $data = new \stdClass(); + $data->id = $themeprop->id; + $data->value = $themeprop->value; + $props[$themeprop->name] = $data; + } + } + + return $props; + } + + /** + * Store properties. + * + * @param string $themename Theme name + * @param string $props Properties + * @return string + */ + static public function put_properties($themename, $props) { + global $DB; + + // Get the current properties as a reference and for theme version information. + $currentprops = self::compile_properties($themename, false); + + // Build the report. + $report = get_string('putpropertyreport', 'theme_adaptable').PHP_EOL; + $report .= get_string('putpropertyproperties', 'theme_adaptable').' \'Moodle\' '. + get_string('putpropertyversion', 'theme_adaptable').' '.$props['moodle_version'].'.'.PHP_EOL; + unset($props['moodle_version']); + $report .= get_string('putpropertyour', 'theme_adaptable').' \'Moodle\' '. + get_string('putpropertyversion', 'theme_adaptable').' '.$currentprops['moodle_version']->value.'.'.PHP_EOL; + unset($currentprops['moodle_version']); + $report .= get_string('putpropertyproperties', 'theme_adaptable').' \''.ucfirst($themename).'\' '. + get_string('putpropertyversion', 'theme_adaptable').' '.$props['theme_version'].'.'.PHP_EOL; + unset($props['theme_version']); + $report .= get_string('putpropertyour', 'theme_adaptable').' \''.ucfirst($themename).'\' '. + get_string('putpropertyversion', 'theme_adaptable').' '.$currentprops['theme_version']->value.'.'.PHP_EOL.PHP_EOL; + unset($currentprops['theme_version']); + + // Pre-process files - using 'theme_adaptable_pluginfile' in lib.php as a reference. + $filestoreport = ''; + $preprocessfilesettings = array('logo', 'homebk', 'pagebackground', 'iphoneicon', 'iphoneretinaicon', + 'ipadicon', 'ipadretinaicon', 'fontfilettfheading', 'fontfilettfbody', 'adaptablemarkettingimages'); + + // Slide show. + for ($propslide = 1; $propslide <= $props['slidercount']; $propslide++) { + $preprocessfilesettings[] = 'p'.$propslide; + } + + // Process the file properties. + foreach ($preprocessfilesettings as $preprocessfilesetting) { + self::put_prop_file_preprocess($preprocessfilesetting, $props, $filestoreport); + unset($currentprops[$preprocessfilesetting]); + } + + if ($filestoreport) { + $report .= get_string('putpropertiesreportfiles', 'theme_adaptable').PHP_EOL.$filestoreport.PHP_EOL; + } + + // Need to ignore and report on any unknown settings. + $report .= get_string('putpropertiessettingsreport', 'theme_adaptable').PHP_EOL; + $changed = ''; + $unchanged = ''; + $added = ''; + $ignored = ''; + $settinglog = ''; + foreach ($props as $propkey => $propvalue) { + $settinglog = '\''.$propkey.'\' '.get_string('putpropertiesvalue', 'theme_adaptable').' \''.$propvalue.'\''; + if (array_key_exists($propkey, $currentprops)) { + if ($propvalue != $currentprops[$propkey]->value) { + $settinglog .= ' '.get_string('putpropertiesfrom', 'theme_adaptable').' \''.$currentprops[$propkey]->value.'\''; + $changed .= $settinglog.'.'.PHP_EOL; + $DB->update_record('config_plugins', array('id' => $currentprops[$propkey]->id, 'value' => $propvalue), true); + } else { + $unchanged .= $settinglog.'.'.PHP_EOL; + } + } else if (self::to_add_property($propkey)) { + // Properties that have an index and don't already exist. + $DB->insert_record('config_plugins', array( + 'plugin' => 'theme_'.$themename, 'name' => $propkey, 'value' => $propvalue), true); + $added .= $settinglog.'.'.PHP_EOL; + } else { + $ignored .= $settinglog.'.'.PHP_EOL; + } + } + + if (!empty($changed)) { + $report .= get_string('putpropertieschanged', 'theme_adaptable').PHP_EOL.$changed.PHP_EOL; + } + if (!empty($added)) { + $report .= get_string('putpropertiesadded', 'theme_adaptable').PHP_EOL.$added.PHP_EOL; + } + if (!empty($unchanged)) { + $report .= get_string('putpropertiesunchanged', 'theme_adaptable').PHP_EOL.$unchanged.PHP_EOL; + } + if (!empty($ignored)) { + $report .= get_string('putpropertiesignored', 'theme_adaptable').PHP_EOL.$ignored.PHP_EOL; + } + + return $report; + } + + /** + * Property to add + * + * @param int $propkey + + * @return array matches + */ + static protected function to_add_property($propkey) { + static $matches = '('. + // Slider .... + '^p[1-9][0-9]?url$|'. + '^p[1-9][0-9]?cap$|'. + '^sliderh3color$|'. + '^sliderh4color$|'. + '^slidersubmitcolor$|'. + '^slidersubmitbgcolor$|'. + '^slider2h3color$|'. + '^slider2h3bgcolor$|'. + '^slider2h4color$|'. + '^slider2h4bgcolor$|'. + '^slideroption2submitcolor$|'. + '^slideroption2color$|'. + '^slideroption2a$|'. + // Alerts.... + '^enablealert[1-9][0-9]?$|'. + '^alertkey[1-9][0-9]?$|'. + '^alerttext[1-9][0-9]?$|'. + '^alerttype[1-9][0-9]?$|'. + '^alertaccess[1-9][0-9]?$|'. + '^alertprofilefield[1-9][0-9]?$|'. + // Analytics.... + '^analyticstext[1-9][0-9]?$|'. + '^analyticsprofilefield[1-9][0-9]?$|'. + // Header menu.... + '^newmenu[1-9][0-9]?title$|'. + '^newmenu[1-9][0-9]?$|'. + '^newmenu[1-9][0-9]?requirelogin$|'. + '^newmenu[1-9][0-9]?field$|'. + // Marketing blocks.... + '^market[1-9][0-9]?$|'. + '^marketlayoutrow[1-9][0-9]?$|'. + // Navbar menu.... + '^toolsmenu[1-9][0-9]?title$|'. + '^toolsmenu[1-9][0-9]?$|'. + // Ticker text.... + '^tickertext[1-9][0-9]?$|'. + '^tickertext[1-9][0-9]?profilefield$'. + ')'; + + return (preg_match($matches, $propkey) === 1); + } + + /** + * Pre process properties file. + * + * @param int $key + * @param array $props + * @param string $filestoreport + * + */ + static private function put_prop_file_preprocess($key, &$props, &$filestoreport) { + if (!empty($props[$key])) { + $filestoreport .= '\''.$key.'\' '.get_string('putpropertiesvalue', 'theme_adaptable').' \''. + \core_text::substr($props[$key], 1).'\'.'.PHP_EOL; + } + unset($props[$key]); + } + + /** + * States if the Kaltura plugin is installed. + * Ref: https://moodle.org/plugins/view.php?id=447 + * + * @return boolean true or false. + */ + static public function kalturaplugininstalled() { + global $CFG; + + static $paths = array( + 'local/kalturamediagallery', + 'local/mymedia' + ); + + $hascount = 0; + foreach ($paths as $path) { + if (file_exists($CFG->dirroot.'/'.$path)) { + $hascount++; + } + } + + return (count($paths) == $hascount); + } + + /** + * Gets the Font Awesome markup for the given icon. + * + * @param string $theicon + * @param array $classes - Optional extra classes to add. + * @param array $attributes - Optional attributes to add. + * @param string $content - Optional content. + * + * @return string markup or empty string if no icon specified. + */ + static public function getfontawesomemarkup($theicon, $classes = array(), $attributes = array(), $content = '') { + $icon = ''; + if (!empty($theicon)) { + $classes[] = 'fa fa-'.$theicon; + $attributes['aria-hidden'] = 'true'; + $attributes['class'] = implode(' ', $classes); + $icon = \html_writer::tag('i', $content, $attributes); + } + return $icon; + } + + /** + * Returns the RGB for the given hex. + * + * @param string $hex + * @return array + */ + static public function hex2rgb($hex) { + // From: http://bavotasan.com/2011/convert-hex-color-to-rgb-using-php/. + $hex = str_replace("#", "", $hex); + + if (strlen($hex) == 3) { + $r = hexdec(substr($hex, 0, 1).substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1).substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1).substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + $rgb = array('r' => $r, 'g' => $g, 'b' => $b); + return $rgb; // Returns the rgb as an array. + } + + /** + * Returns the RGBA for the given hex and alpha. + * + * @param string $hex + * @param string $alpha + * @return string + */ + static public function hex2rgba($hex, $alpha) { + $rgba = self::hex2rgb($hex); + $rgba[] = $alpha; + return 'rgba('.implode(", ", $rgba).')'; // Returns the rgba values separated by commas. + } + + /** + * Gets the overridden template if the setting for that template has been enabled and set. + * + * @param string $templatename + * @return string or false if not overridden. + */ + static public function get_template_override($templatename) { + $template = false; + + $overridetemplates = get_config('theme_adaptable', 'templatessel'); + if ($overridetemplates) { + $overridetemplates = explode(',', $overridetemplates); + + if (in_array($templatename, $overridetemplates)) { + global $PAGE; + + $overridetemplatesetting = str_replace('/', '_', $templatename); + $setting = 'activatetemplateoverride_'.$overridetemplatesetting; + + if (!empty($PAGE->theme->settings->$setting)) { + $setting = 'overriddentemplate_'.$overridetemplatesetting; + + if (!empty($PAGE->theme->settings->$setting)) { + $template = $PAGE->theme->settings->$setting; + } + } + } + } + + return $template; + } + + + /** + * Renderers the overridden template if the setting for that template has been enabled and set. + * + * @param string $templatename + * @param array|stdClass $data Context containing data for the template. + * @return string or false if not overridden. + */ + static public function apply_template_override($templatename, $data) { + $output = false; + + $template = self::get_template_override($templatename); + if (!empty($template)) { + global $PAGE; + $renderer = $PAGE->get_renderer('theme_adaptable', 'mustache'); + + /* Pass in the setting value as our Mustache engine uses the Mustache_Loader_StringLoader + instead of effectively the Mustache_Loader_FilesystemLoader and that just returns the + 'name' as passed in. The engine then calls 'loadSource' from 'loadTemplate' which can + have 'Mustache_Source' as an input, being the mustache template source itself. */ + $output = $renderer->render_from_template($template, $data); + } + + return $output; + } + + /** + * Admin setting layout builder to build the setting layout and reduce code duplication. + * + * @param admin_settingpage $settingpage + * @param string $adminsettingname + * @param array $admindefaults + * @param array $adminchoices + * + * @return array of the imgblder and totalblocks. + */ + static public function admin_settings_layout_builder($settingpage, $adminsettingname, $admindefaults, $adminchoices) { + global $CFG, $PAGE; + + $totalblocks = 0; + $imgpath = $CFG->wwwroot.'/theme/adaptable/pix/layout-builder/'; + $imgblder = ''; + for ($i = 1; $i <= 5; $i++) { + $name = 'theme_adaptable/'.$adminsettingname.$i; + $title = get_string($adminsettingname, 'theme_adaptable'); + $description = get_string($adminsettingname.'desc', 'theme_adaptable'); + $default = $admindefaults[$i - 1]; + $setting = new \admin_setting_configselect($name, $title, $description, $default, $adminchoices); + $settingpage->add($setting); + + $settingname = $adminsettingname.$i; + + if (!isset($PAGE->theme->settings->$settingname)) { + $PAGE->theme->settings->$settingname = '0-0-0-0'; + } + + if ($PAGE->theme->settings->$settingname != '0-0-0-0') { + $imgblder .= '<img src="'.$imgpath.$PAGE->theme->settings->$settingname.'.png'.'" style="padding-top: 5px">'; + } + + $vals = explode('-', $PAGE->theme->settings->$settingname); + foreach ($vals as $val) { + if ($val > 0) { + $totalblocks++; + } + } + } + + return array('imgblder' => $imgblder, 'totalblocks' => $totalblocks); + } +} diff --git a/theme/adaptable/classes/traits/null_object.php b/theme/adaptable/classes/traits/null_object.php new file mode 100644 index 0000000..627e8ac --- /dev/null +++ b/theme/adaptable/classes/traits/null_object.php @@ -0,0 +1,84 @@ +<?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/>. + +/** + * For the null object pattern - https://www.wikiwand.com/en/Null_Object_pattern. + * + * @package theme_adaptable + * @author gthomas2 + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable\traits; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Facilitates the null object pattern - https://www.wikiwand.com/en/Null_Object_pattern. + * + * @package theme_adaptable + * @author gthomas2 + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait null_object { + + protected $_defaults = []; + + /** + * Has this class been set. + * + * @package theme_adaptable + * @param bool $ignoreinitialstate - if true, will consider an object with default values set by set_default as + * not set. + * @return bool + */ + public function is_set($ignoreinitialstate = false) { + $reflect = new \ReflectionClass($this); + $props = $reflect->getDefaultProperties(); + foreach ($props as $prop => $default) { + if ($prop === '_defaults') { + continue; + } + if (isset($this->$prop) && $this->$prop != $default) { + if ($ignoreinitialstate) { + if (!isset($this->_defaults[$prop]) || $this->_defaults[$prop] !== $this->$prop) { + return true; + } + } + } + } + return false; + } + + /** + * Set and track default value + * + * @package theme_adaptable + * + * @param string $prop + * @param string $val + */ + public function set_default($prop, $val) { + + if (isset($this->_defaults[$prop])) { + throw new \coding_exception('Default value already set for '.$prop.' - '.$this->_defaults[$prop]); + } + $this->$prop = $val; + $this->_defaults[$prop] = $this->$prop; + } +} diff --git a/theme/adaptable/classes/traits/single_section_page.php b/theme/adaptable/classes/traits/single_section_page.php new file mode 100644 index 0000000..1881fd4 --- /dev/null +++ b/theme/adaptable/classes/traits/single_section_page.php @@ -0,0 +1,353 @@ +<?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/>. + + +/** + * Trait for single section page functions. + * + * @package theme_adaptable + * @copyright 2018 Manoj Solanki (Coventry University) + * @copyright Copyright (c) 2016 Moodlerooms Inc. (http://www.moodlerooms.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_adaptable\traits; + +defined('MOODLE_INTERNAL') || die(); + +use html_writer; +use context_course; +use completion_info; +use core_text; +use url_select; + +// Load libraries. +require_once($CFG->dirroot.'/course/renderer.php'); +require_once($CFG->dirroot.'/message/lib.php'); +require_once($CFG->dirroot.'/course/format/topics/renderer.php'); +require_once($CFG->dirroot.'/course/format/weeks/renderer.php'); + +trait single_section_page { + /** + * Output the html for a single section page. + * + * @package theme_adaptable + * + * @param stdClass $course The course entry from DB. + * @param array $sections (argument not used). + * @param array $mods (argument not used). + * @param array $modnames (argument not used). + * @param array $modnamesused (argument not used). + * @param int $displaysection The section number in the course which is being displayed. + */ + public function print_single_section_page($course, $sections, $mods, $modnames, $modnamesused, $displaysection) { + $this->print_single_section_page_content($course, $sections, $mods, $modnames, $modnamesused, $displaysection); + } + + /** + * Return the number of sections for a given course. + * + * The parameter numsections is removed from course formats as of Moodle version + * 3.3. This function assists with retrieving this information. See Moodle Tracker issue + * MDL-57769 for further information. In this theme, references to $course->numsections + * have been replaced by calls calls to this function to get the information. + * Also see https://bitbucket.org/covuni/moodle-theme_adaptable/pull-requests/43/fixes-renderersphp-for-the-missing/diff. + * + * @package theme_adaptable + * + * @param stdClass $course Course + * + * @return int Number of sections. + */ + public function get_num_sections($course) { + + global $DB; + $numsections = $DB->count_records('course_sections', array('course' => $course->id)) - 1; + return $numsections; + } + + /** + * Output the html for a single section page. + * + * @package theme_adaptable + * + * @param stdClass $course The course entry from DB. + * @param array $sections (argument not used). + * @param array $mods (argument not used). + * @param array $modnames (argument not used). + * @param array $modnamesused (argument not used). + * @param int $displaysection The section number in the course which is being displayed. + * @param boolean $showsectionzero states if section zero is to be shown at the top of the section. + */ + protected function print_single_section_page_content($course, $sections, $mods, $modnames, $modnamesused, $displaysection, + $showsectionzero = 1) { + global $PAGE, $OUTPUT; + + // Build, on the fly, 'numsections' property (see Moodle's Tracker issue MDL-57769 for details). + global $DB; + $course->numsections = $DB->count_records('course_sections', array('course' => $course->id)) - 1; + + $modinfo = get_fast_modinfo($course); + $course = course_get_format($course)->get_course(); + + // Can we view the section in question? + if (!($sectioninfo = $modinfo->get_section_info($displaysection))) { + // This section doesn't exist. + print_error('unknowncoursesection', 'error', null, $course->fullname); + return; + } + + if (!$sectioninfo->uservisible) { + if (!$course->hiddensections) { + echo $this->start_section_list(); + echo $this->section_hidden($displaysection, $course->id); + echo $this->end_section_list(); + } + // Can't view this section. + return; + } + + // Copy activity clipboard.. + echo $this->course_activity_clipboard($course, $displaysection); + if ($showsectionzero) { + $thissection = $modinfo->get_section_info(0); + if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) { + echo $this->start_section_list(); + echo $this->section_header($thissection, $course, true, $displaysection); + echo $this->courserenderer->course_section_cm_list($course, $thissection, $displaysection); + echo $this->courserenderer->course_section_add_cm_control($course, 0, $displaysection); + echo $this->section_footer(); + echo $this->end_section_list(); + } + } + + // Start single-section div. + echo html_writer::start_tag('div', array('class' => 'single-section')); + + // The requested section page. + $thissection = $modinfo->get_section_info($displaysection); + + // Title with section navigation links. + $sectionnavlinks = $this->get_nav_links($course, $modinfo->get_section_info_all(), $displaysection); + $sectiontitle = ''; + $sectiontitle .= html_writer::start_tag('div', array('class' => 'section-navigation navigationtitle')); + + // Title attributes. + $classes = 'sectionname'; + if (!$thissection->visible) { + $classes .= ' dimmed_text'; + } + $sectionname = html_writer::tag('span', $this->section_title_without_link($thissection, $course)); + $sectiontitle .= $this->output->heading($sectionname, 2, $classes); + + $sectiontitle .= html_writer::end_tag('div'); + echo $sectiontitle; + + // Now the list of sections. + echo $this->start_section_list(); + + if (!$showsectionzero) { + echo $this->section_header_onsectionpage_topic0notattop($thissection, $course); + } else { + echo $this->section_header($thissection, $course, true, $displaysection); + } + // Show completion help icon. + $completioninfo = new completion_info($course); + echo $completioninfo->display_help_icon(); + + echo $this->courserenderer->course_section_cm_list($course, $thissection, $displaysection); + echo $this->courserenderer->course_section_add_cm_control($course, $displaysection, $displaysection); + + // Display course page block activity bottom region if this is a course section. + if (!empty($PAGE->theme->settings->coursepageblockactivitybottomenabled)) { + echo $OUTPUT->get_block_regions('customrowsetting', 'course-section-', '12-0-0-0'); + } + + echo $this->section_footer(); + echo $this->end_section_list(); + + // Display section bottom navigation. + $sectionbottomnav = ''; + $sectionbottomnav .= html_writer::start_tag('nav', array('class' => 'section_footer')); + $sectionbottomnav .= $sectionnavlinks['previous']; + $sectionbottomnav .= $sectionnavlinks['next']; + $sectionbottomnav .= html_writer::tag('div', '', array('class' => 'clearfix')); + $sectionbottomnav .= html_writer::end_tag('nav'); + $sectionbottomnav .= html_writer::tag('div', $this->section_nav_selection($course, $sections, $displaysection), + array('class' => 'jumpnav')); + echo $sectionbottomnav; + + // Close single-section div. + echo html_writer::end_tag('div'); + } + + /** + * Generate the html for the 'Jump to' menu on a single section page. + * + * @package theme_adaptable + * + * @param stdClass $course The course entry from DB + * @param array $sections The course_sections entries from the DB + * @param int $displaysection the current displayed section number. + * + * @return string HTML to output. + */ + protected function section_nav_selection($course, $sections, $displaysection) { + return $this->section_nav_selection_content($course, $sections, $displaysection); + } + + /** + * Generate the html for the 'Jump to' menu on a single section page. + * + * @package theme_adaptable + * + * @param stdClass $course The course entry from DB + * @param array $sections The course_sections entries from the DB + * @param int $displaysection the current displayed section number. + * @param int $section Section number to start on. + * + * @return string HTML to output. + */ + protected function section_nav_selection_content($course, $sections, $displaysection, $section = 1) { + $o = ''; + $sectionmenu = array(); + $sectionmenu[course_get_url($course)->out(false)] = get_string('maincoursepage', 'theme_adaptable'); + $modinfo = get_fast_modinfo($course); + + // Get 'numsections' property (see Moodle's Tracker issue MDL-57769 for details). + // Also see https://bitbucket.org/covuni/moodle-theme_adaptable/issues/728/fixes-renderersphp-for-the-missing. + $numsections = $this->get_num_sections($course); + while ($section <= $numsections) { + $thissection = $modinfo->get_section_info($section); + $showsection = $thissection->uservisible or !$course->hiddensections; + if (($showsection) && ($section != $displaysection) && ($url = course_get_url($course, $section))) { + $sectionmenu[$url->out(false)] = $this->shorten_string(get_section_name($course, $section)); + } + $section++; + } + + $select = new url_select($sectionmenu, '', array('' => get_string('jumpto', 'theme_adaptable'))); + $select->class = 'jumpmenu'; + $select->formid = 'sectionmenu'; + $o .= $this->output->render($select); + + return $o; + } + + /** + * String shortening method. + * + * @package theme_adaptable + * + * @param string $string + * @param string $ellipsis + * + * + * @return string + */ + private function shorten_string($string, $ellipsis = '..') { + $maxlen = 50; + $string = strip_tags($string); + $boundary = $maxlen - strlen($ellipsis); + if ((core_text::strlen($string) > $maxlen)) { + $shortstring = core_text::substr($string, 0, $boundary) . $ellipsis; + } else { + $shortstring = $string; + } + return $shortstring; + } + + /** + * Generate next/previous section links for naviation. + * + * @package theme_adaptable + * + * @param stdClass $course The course entry from DB. + * @param array $sections The course_sections entries from the DB. + * @param int $sectionno The section number in the coruse which is being displayed. + * @return array associative array with previous and next section link. + */ + protected function get_nav_links($course, $sections, $sectionno) { + return $this->get_nav_links_content($course, $sections, $sectionno); + } + + /** + * Generate next/previous section links for naviation. + * + * @package theme_adaptable + * + * @param stdClass $course The course entry from DB. + * @param array $sections The course_sections entries from the DB. + * @param int $sectionno The section number in the coruse which is being displayed. + * @param int $buffer Control the navigation items for when section 0 is in the grid in the Grid format. + * @return array associative array with previous and next section link. + */ + protected function get_nav_links_content($course, $sections, $sectionno, $buffer = 0) { + // FIXME: This is really evil and should by using the navigation API. + $course = course_get_format($course)->get_course(); + $canviewhidden = has_capability('moodle/course:viewhiddensections', context_course::instance($course->id)) + or !$course->hiddensections; + + $links = array('previous' => html_writer::tag('div', '', array('class' => 'previous_section prevnext')), + 'next' => html_writer::tag('div', '', array('class' => 'next_section prevnext'))); + $back = $sectionno - 1; + + $hasprev = $hasnext = false; + while ($back > $buffer && !$hasprev) { + if ($canviewhidden || $sections[$back]->uservisible) { + $params = array(); + $params['class'] = 'previous_section prevnext'; + if (!$sections[$back]->visible) { + $params['class'] = 'previous_section prevnext dimmed_text'; + } + $previouslink = html_writer::tag('span', '<i class="fa fa-angle-double-left"></i>', array('class' => 'nav_icon')); + $sectionname = html_writer::tag('span', get_string('previoussection', 'theme_adaptable'), + array('class' => 'nav_guide')) . '<br>'; + $sectionname .= get_section_name($course, $sections[$back]); + $previouslink .= html_writer::tag('span', $sectionname, array('class' => 'text')); + $links['previous'] = html_writer::link(course_get_url($course, $back), $previouslink, $params); + $hasprev = true; + } + $back--; + } + + $forward = $sectionno + 1; + + // Get 'numsections' property (see Moodle's Tracker issue MDL-57769 for details). + // Also see https://bitbucket.org/covuni/moodle-theme_adaptable/issues/728/fixes-renderersphp-for-the-missing. + $numsections = $this->get_num_sections($course); + while ($forward <= $numsections && !$hasnext) { + if ($canviewhidden || $sections[$forward]->uservisible) { + $params = array(); + $params['class'] = 'next_section prevnext'; + if (!$sections[$forward]->visible) { + $params['class'] = 'next_section prevnext dimmed_text'; + } + + $sectionname = html_writer::tag('span', get_string('nextsection', 'theme_adaptable'), + array('class' => 'nav_guide')) . '<br>'; + $sectionname .= get_section_name($course, $sections[$forward]); + $nextlink = html_writer::tag('span', $sectionname, array('class' => 'text')); + $nextlink .= html_writer::tag('span', '<i class="fa fa-angle-double-right"></i>', array('class' => 'nav_icon')); + $links['next'] = html_writer::link(course_get_url($course, $forward), $nextlink, $params); + $hasnext = true; + } + $forward++; + } + + return $links; + } +} diff --git a/theme/adaptable/config.php b/theme/adaptable/config.php new file mode 100644 index 0000000..b71e91f --- /dev/null +++ b/theme/adaptable/config.php @@ -0,0 +1,286 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die(); + +global $PAGE; + +// Set up regions for individual pages. This is done +// to avoid being able to move regions (when configuring) +// to non-existent block regions for the page . This is because +// Moodle shows all regions available even if they aren't used +// on that specific page. Please note that frontpage and dashboard +// page use $frontlayoutregions to avoid losing existing regions that +// are renamed. + +// The plugin internal name. +$THEME->name = 'adaptable'; + +// Print sheet. +$THEME->sheets = array('print'); + +// The frontpage regions. +$frontlayoutregions = array('side-post', + 'middle', + 'frnt-footer', + 'frnt-market-a', + 'frnt-market-b', + 'frnt-market-c', + 'frnt-market-d', + 'frnt-market-e', + 'frnt-market-f', + 'frnt-market-g', + 'frnt-market-h', + 'frnt-market-i', + 'frnt-market-j', + 'frnt-market-k', + 'frnt-market-l', + 'frnt-market-m', + 'frnt-market-n', + 'frnt-market-o', + 'frnt-market-p', + 'frnt-market-q', + 'frnt-market-r', + 'frnt-market-s', + 'frnt-market-t', + 'news-slider-a', + 'course-tab-one-a', + 'course-tab-two-a', + 'my-tab-one-a', + 'my-tab-two-a', + 'course-section-a' +); + +// The course page regions. +$courselayoutregions = array('side-post', + 'middle', + 'frnt-footer', + 'course-top-a', + 'course-top-b', + 'course-top-c', + 'course-top-d', + 'news-slider-a', + 'course-tab-one-a', + 'course-tab-two-a', + 'my-tab-one-a', + 'my-tab-two-a', + 'course-bottom-a', + 'course-bottom-b', + 'course-bottom-c', + 'course-bottom-d', + 'course-section-a' +); + +$standardregions = array('side-post'); +$regions = $standardregions; + +if ( (is_object($PAGE)) && ($PAGE->pagelayout) ) { + switch ($PAGE->pagelayout) { + case "frontpage": + $regions = $frontlayoutregions; + break; + case "mydashboard": + $regions = $frontlayoutregions; + break; + case "course": + $regions = $courselayoutregions; + break; + } +} + +// The theme HTML DOCTYPE. +$THEME->doctype = 'html5'; + +// Theme parent. +$THEME->parents = ['boost']; + +// Styles. +$THEME->sheets = array( + 'adaptable', + 'backup-restore', + 'blocks', + 'bootstrap', + 'button', + 'cardblocks', + 'core', + 'course', + 'extras', + 'form', + 'header', + 'menu', + 'messages', + 'navigation', + 'notifications', + 'responsive', + 'tabs', + 'user', + 'print', + 'categorycustom', + 'browser', + 'custom' +); + +$THEME->supportscssoptimisation = false; +$THEME->yuicssmodules = array(); +$THEME->editor_sheets = array(); + +$THEME->plugins_exclude_sheets = array( + 'block' => array( + 'html', + ) +); + +// Dashboard regions. +$usedashboard = true; + +// Disabling block docking. +$THEME->enable_dock = false; + +// Call the renderer. +$THEME->rendererfactory = 'theme_overridden_renderer_factory'; + +// Load the theme layouts. +$THEME->layouts = array( + // Most backwards compatible layout without the blocks - this is the layout used by default. + 'base' => array( + 'file' => 'columns2.php', + 'regions' => array(), + ), + // Standard layout with blocks, this is recommended for most pages with general information. + 'standard' => array( + 'file' => 'columns2.php', + 'regions' => array('side-post'), + 'defaultregion' => 'side-post', + ), + // Main course page. + 'course' => array( + 'file' => 'course.php', + 'regions' => $regions, + 'defaultregion' => 'side-post', + 'options' => array('langmenu' => true), + ), + 'coursecategory' => array( + 'file' => 'columns2.php', + 'regions' => array('side-post'), + 'defaultregion' => 'side-post', + ), + // Part of course, typical for modules - default page layout if $cm specified in require_login(). + 'incourse' => array( + 'file' => 'columns2.php', + 'regions' => array('side-post', 'course-section-a'), + 'defaultregion' => 'side-post', + ), + // The site home page. + 'frontpage' => array( + 'file' => 'frontpage.php', + 'regions' => $regions, + 'defaultregion' => 'side-post' + ), + // Server administration scripts. + 'admin' => array( + 'file' => 'columns2.php', + 'regions' => array('side-post'), + 'defaultregion' => 'side-post' + + ), + // My dashboard page. + 'mydashboard' => array( + 'file' => 'dashboard.php', + 'regions' => $regions, + 'defaultregion' => 'side-post', + 'options' => array('langmenu' => true), + ), + // My public page. + 'mypublic' => array( + 'file' => 'columns2.php', + 'regions' => array('side-post'), + 'defaultregion' => 'side-post' + ), + // Login page. + 'login' => array( + 'file' => 'login.php', + 'regions' => array(), + 'options' => array('langmenu' => true, 'nonavbar' => true), + ), + // Pages that appear in pop-up windows - no navigation, no blocks, no header. + 'popup' => array( + 'file' => 'columns1.php', + 'regions' => array(), + 'options' => array('nofooter' => true, 'nonavbar' => true), + ), + // No blocks and minimal footer - used for legacy frame layouts only! + 'frametop' => array( + 'file' => 'columns1.php', + 'regions' => array(), + 'options' => array('nofooter' => true, 'nocoursefooter' => true), + ), + // Embeded pages, like iframe/object embeded in moodleform - it needs as much space as possible. + 'embedded' => array( + 'file' => 'embedded.php', + 'regions' => array() + ), + // Used during upgrade and install, and for the 'This site is undergoing maintenance' message. + // This must not have any blocks, and it is good idea if it does not have links to + // other places - for example there should not be a home link in the footer... + 'maintenance' => array( + 'file' => 'maintenance.php', + 'regions' => array(), + 'options' => array('nofooter' => true, 'nonavbar' => true, 'nocoursefooter' => true, 'nocourseheader' => true), + ), + // Should display the content and basic headers only. + 'print' => array( + 'file' => 'columns1.php', + 'regions' => array(), + 'options' => array('nofooter' => true, 'nonavbar' => false), + ), + // The pagelayout used when a redirection is occuring. + 'redirect' => array( + 'file' => 'embedded.php', + 'regions' => array(), + ), + // The pagelayout used for reports. + 'report' => array( + 'file' => 'columns2.php', + 'regions' => array('side-post'), + 'defaultregion' => 'side-post', + ), + // The pagelayout used for safebrowser and securewindow. + 'secure' => array( + 'file' => 'secure.php', + 'regions' => array('side-post', 'course-section-a'), + 'options' => array('nofooter' => true, 'nonavbar' => true), + 'defaultregion' => 'side-post', + ), +); + +// Select the opposite sidebar when switch to RTL. +$THEME->blockrtlmanipulations = array( + 'side-pre' => 'side-post', + 'side-post' => 'side-pre' +); + +$THEME->csspostprocess = 'theme_adaptable_process_css'; diff --git a/theme/adaptable/db/caches.php b/theme/adaptable/db/caches.php new file mode 100644 index 0000000..0be7be6 --- /dev/null +++ b/theme/adaptable/db/caches.php @@ -0,0 +1,33 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = array( + 'userdata' => array( + 'mode' => cache_store::MODE_SESSION + ) +); diff --git a/theme/adaptable/db/upgrade.php b/theme/adaptable/db/upgrade.php new file mode 100644 index 0000000..ec94652 --- /dev/null +++ b/theme/adaptable/db/upgrade.php @@ -0,0 +1,55 @@ +<?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/>. + +/** + * Database upgrade. + * + * @package theme_adaptable + * @copyright 2019 G Barnard + * @author G Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Upgrade. + * + * @param int $oldversion Is this an old version + * @return bool Success. + */ +function xmldb_theme_adaptable_upgrade($oldversion = 0) { + + if ($oldversion < 2020073101) { + if (get_config('theme_adaptable', 'fontname') == 'default') { + set_config('fontname', 'sans-serif', 'theme_adaptable'); + } + if (get_config('theme_adaptable', 'fontheadername') == 'default') { + set_config('fontheadername', 'sans-serif', 'theme_adaptable'); + } + if (get_config('theme_adaptable', 'fonttitlename') == 'default') { + set_config('fonttitlename', 'sans-serif', 'theme_adaptable'); + } + + upgrade_plugin_savepoint(true, 2020073101, 'theme', 'adaptable'); + } + + // Automatic 'Purge all caches'.... + purge_all_caches(); + + return true; +} diff --git a/theme/adaptable/jquery/adaptable_v2_1_1_2.js b/theme/adaptable/jquery/adaptable_v2_1_1_2.js new file mode 100644 index 0000000..c5ad7ec --- /dev/null +++ b/theme/adaptable/jquery/adaptable_v2_1_1_2.js @@ -0,0 +1,88 @@ +// +// This file is part of Adaptable theme for moodle +// +// 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/>. +// +// +// Adaptable main JS file +// +// @package theme_adaptable +// @copyright 2015-2019 Jeremy Hopkins (Coventry University) +// @copyright 2015-2019 Fernando Acedo (3-bits.com) +// @copyright 2018-2019 Manoj Solanki (Coventry University) +// +// @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + +jQuery(document).ready(function($) { + // Ticker. + $('#ticker').tickerme(); + // New for every three. + if($('header').css("position") == "fixed") { + $('.outercont').css('padding-top', $('header').height()); + } + + // Slider. + if ($('#main-slider').length) { + $('#main-slider').flexslider({ + namespace : "flex-", // New: {NEW} String: Prefix string attached to the class of every element generated by the plugin + selector : ".slides > li", // New: {NEW} Selector: Must match a simple pattern. '{container} > {slide}' -- Ignore pattern at your own peril + animation : "slide", // String: Select your animation type, "fade" or "slide" + easing : "swing", // {NEW} String: Determines the easing method used in jQuery transitions. jQuery easing plugin is supported! + direction : "horizontal", // String: Select the sliding direction, "horizontal" or "vertical" + reverse : false, // {NEW} Boolean: Reverse the animation direction + animationLoop : true, // Boolean: Should the animation loop? If false, directionNav will received "disable" classes at either end + smoothHeight : false, // {NEW} Boolean: Allow height of the slider to animate smoothly in horizontal mode + startAt : 0, // Integer: The slide that the slider should start on. Array notation (0 = first slide) + slideshow : true, // Boolean: Animate slider automatically + slideshowSpeed : 7000, // Integer: Set the speed of the slideshow cycling, in milliseconds + animationSpeed : 600, // Integer: Set the speed of animations, in milliseconds + initDelay : 0, // {NEW} Integer: Set an initialization delay, in milliseconds + randomize : false, // Boolean: Randomize slide order. + + // Usability features. + pauseOnAction : true, // Boolean: Pause the slideshow when interacting with control elements, highly recommended. + pauseOnHover : false, // Boolean: Pause the slideshow when hovering over slider, then resume when no longer hovering + useCSS : true, // {NEW} Boolean: Slider will use CSS3 transitions if available + touch : true, // {NEW} Boolean: Allow touch swipe navigation of the slider on touch-enabled devices + video : false, // {NEW} Boolean: If using video in the slider, will prevent CSS3 3D Transforms to avoid graphical glitches + + // Primary Controls. + controlNav : true, // Boolean: Create navigation for paging control of each slide? Note! Leave true for manualControls usage + directionNav : true, // Boolean: Create navigation for previous / next navigation? (true/false) + prevText : "Previous", // String: Set the text for the "previous" directionNav item + nextText : "Next", // String: Set the text for the "next" directionNav item + + // Secondary Navigation. + keyboard : true, // Boolean: Allow slider navigating via keyboard left/right keys + multipleKeyboard : false, // {NEW} Boolean: Allow keyboard navigation to affect multiple sliders. Default behavior cuts out keyboard navigation with more than one slider present. + mousewheel : false, // {UPDATED} Boolean: Requires jquery.mousewheel.js (https://github.com/brandonaaron/jquery-mousewheel) - Allows slider navigating via mousewheel + pausePlay : false, // Boolean: Create pause/play dynamic element + pauseText : 'Pause', // String: Set the text for the "pause" pausePlay item + playText : 'Play', // String: Set the text for the "play" pausePlay item + + // Special properties. + controlsContainer : "", // {UPDATED} Selector: USE CLASS SELECTOR. Declare which container the navigation elements should be appended too. Default container is the FlexSlider element. Example use would be ".flexslider-container". Property is ignored if given element is not found. + manualControls : "", // Selector: Declare custom control navigation. Examples would be ".flex-control-nav li" or "#tabs-nav li img", etc. The number of elements in your controlNav should match the number of slides/tabs. + sync : "", // {NEW} Selector: Mirror the actions performed on this slider with another slider. Use with care. + asNavFor : "", // {NEW} Selector: Internal property exposed for turning the slider into a thumbnail navigation for another slider + }); + + if ($('.container.slidewrap').length) { + $(".container.slidewrap").on('transitionend', function() { + var slider1 = $('#main-slider').data('flexslider'); + slider1.resize(); + }); + } + } +}); diff --git a/theme/adaptable/jquery/jquery-easing-min.js b/theme/adaptable/jquery/jquery-easing-min.js new file mode 100644 index 0000000..4b31c20 --- /dev/null +++ b/theme/adaptable/jquery/jquery-easing-min.js @@ -0,0 +1,8 @@ +/* + * jQuery Easing v1.4.1 - http://gsgd.co.uk/sandbox/jquery/easing/ + * Open source under the BSD License. + * Copyright 2008 George McGinley Smith + * All rights reserved. + * https://raw.github.com/gdsmith/jquery-easing/master/LICENSE +*/ +(function(factory){if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory($)})}else if(typeof module==="object"&&typeof module.exports==="object"){exports=factory(require("jquery"))}else{factory(jQuery)}})(function($){$.easing.jswing=$.easing.swing;var pow=Math.pow,sqrt=Math.sqrt,sin=Math.sin,cos=Math.cos,PI=Math.PI,c1=1.70158,c2=c1*1.525,c3=c1+1,c4=2*PI/3,c5=2*PI/4.5;function bounceOut(x){var n1=7.5625,d1=2.75;if(x<1/d1){return n1*x*x}else if(x<2/d1){return n1*(x-=1.5/d1)*x+.75}else if(x<2.5/d1){return n1*(x-=2.25/d1)*x+.9375}else{return n1*(x-=2.625/d1)*x+.984375}}$.extend($.easing,{def:"easeOutQuad",swing:function(x){return $.easing[$.easing.def](x)},easeInQuad:function(x){return x*x},easeOutQuad:function(x){return 1-(1-x)*(1-x)},easeInOutQuad:function(x){return x<.5?2*x*x:1-pow(-2*x+2,2)/2},easeInCubic:function(x){return x*x*x},easeOutCubic:function(x){return 1-pow(1-x,3)},easeInOutCubic:function(x){return x<.5?4*x*x*x:1-pow(-2*x+2,3)/2},easeInQuart:function(x){return x*x*x*x},easeOutQuart:function(x){return 1-pow(1-x,4)},easeInOutQuart:function(x){return x<.5?8*x*x*x*x:1-pow(-2*x+2,4)/2},easeInQuint:function(x){return x*x*x*x*x},easeOutQuint:function(x){return 1-pow(1-x,5)},easeInOutQuint:function(x){return x<.5?16*x*x*x*x*x:1-pow(-2*x+2,5)/2},easeInSine:function(x){return 1-cos(x*PI/2)},easeOutSine:function(x){return sin(x*PI/2)},easeInOutSine:function(x){return-(cos(PI*x)-1)/2},easeInExpo:function(x){return x===0?0:pow(2,10*x-10)},easeOutExpo:function(x){return x===1?1:1-pow(2,-10*x)},easeInOutExpo:function(x){return x===0?0:x===1?1:x<.5?pow(2,20*x-10)/2:(2-pow(2,-20*x+10))/2},easeInCirc:function(x){return 1-sqrt(1-pow(x,2))},easeOutCirc:function(x){return sqrt(1-pow(x-1,2))},easeInOutCirc:function(x){return x<.5?(1-sqrt(1-pow(2*x,2)))/2:(sqrt(1-pow(-2*x+2,2))+1)/2},easeInElastic:function(x){return x===0?0:x===1?1:-pow(2,10*x-10)*sin((x*10-10.75)*c4)},easeOutElastic:function(x){return x===0?0:x===1?1:pow(2,-10*x)*sin((x*10-.75)*c4)+1},easeInOutElastic:function(x){return x===0?0:x===1?1:x<.5?-(pow(2,20*x-10)*sin((20*x-11.125)*c5))/2:pow(2,-20*x+10)*sin((20*x-11.125)*c5)/2+1},easeInBack:function(x){return c3*x*x*x-c1*x*x},easeOutBack:function(x){return 1+c3*pow(x-1,3)+c1*pow(x-1,2)},easeInOutBack:function(x){return x<.5?pow(2*x,2)*((c2+1)*2*x-c2)/2:(pow(2*x-2,2)*((c2+1)*(x*2-2)+c2)+2)/2},easeInBounce:function(x){return 1-bounceOut(1-x)},easeOutBounce:bounceOut,easeInOutBounce:function(x){return x<.5?(1-bounceOut(1-2*x))/2:(1+bounceOut(2*x-1))/2}})}); diff --git a/theme/adaptable/jquery/jquery-flexslider-min.js b/theme/adaptable/jquery/jquery-flexslider-min.js new file mode 100644 index 0000000..62b7142 --- /dev/null +++ b/theme/adaptable/jquery/jquery-flexslider-min.js @@ -0,0 +1,5 @@ +/* + * jQuery FlexSlider v2.4.0 + * Copyright 2012 WooThemes + * Contributing Author: Tyler Smith + */!function($){$.flexslider=function(e,t){var a=$(e);a.vars=$.extend({},$.flexslider.defaults,t);var n=a.vars.namespace,i=window.navigator&&window.navigator.msPointerEnabled&&window.MSGesture,s=("ontouchstart"in window||i||window.DocumentTouch&&document instanceof DocumentTouch)&&a.vars.touch,r="click touchend MSPointerUp keyup",o="",l,c="vertical"===a.vars.direction,d=a.vars.reverse,u=a.vars.itemWidth>0,v="fade"===a.vars.animation,p=""!==a.vars.asNavFor,m={},f=!0;$.data(e,"flexslider",a),m={init:function(){a.animating=!1,a.currentSlide=parseInt(a.vars.startAt?a.vars.startAt:0,10),isNaN(a.currentSlide)&&(a.currentSlide=0),a.animatingTo=a.currentSlide,a.atEnd=0===a.currentSlide||a.currentSlide===a.last,a.containerSelector=a.vars.selector.substr(0,a.vars.selector.search(" ")),a.slides=$(a.vars.selector,a),a.container=$(a.containerSelector,a),a.count=a.slides.length,a.syncExists=$(a.vars.sync).length>0,"slide"===a.vars.animation&&(a.vars.animation="swing"),a.prop=c?"top":"marginLeft",a.args={},a.manualPause=!1,a.stopped=!1,a.started=!1,a.startTimeout=null,a.transitions=!a.vars.video&&!v&&a.vars.useCSS&&function(){var e=document.createElement("div"),t=["perspectiveProperty","WebkitPerspective","MozPerspective","OPerspective","msPerspective"];for(var n in t)if(void 0!==e.style[t[n]])return a.pfx=t[n].replace("Perspective","").toLowerCase(),a.prop="-"+a.pfx+"-transform",!0;return!1}(),a.ensureAnimationEnd="",""!==a.vars.controlsContainer&&(a.controlsContainer=$(a.vars.controlsContainer).length>0&&$(a.vars.controlsContainer)),""!==a.vars.manualControls&&(a.manualControls=$(a.vars.manualControls).length>0&&$(a.vars.manualControls)),a.vars.randomize&&(a.slides.sort(function(){return Math.round(Math.random())-.5}),a.container.empty().append(a.slides)),a.doMath(),a.setup("init"),a.vars.controlNav&&m.controlNav.setup(),a.vars.directionNav&&m.directionNav.setup(),a.vars.keyboard&&(1===$(a.containerSelector).length||a.vars.multipleKeyboard)&&$(document).bind("keyup",function(e){var t=e.keyCode;if(!a.animating&&(39===t||37===t)){var n=39===t?a.getTarget("next"):37===t?a.getTarget("prev"):!1;a.flexAnimate(n,a.vars.pauseOnAction)}}),a.vars.mousewheel&&a.bind("mousewheel",function(e,t,n,i){e.preventDefault();var s=a.getTarget(0>t?"next":"prev");a.flexAnimate(s,a.vars.pauseOnAction)}),a.vars.pausePlay&&m.pausePlay.setup(),a.vars.slideshow&&a.vars.pauseInvisible&&m.pauseInvisible.init(),a.vars.slideshow&&(a.vars.pauseOnHover&&a.hover(function(){a.manualPlay||a.manualPause||a.pause()},function(){a.manualPause||a.manualPlay||a.stopped||a.play()}),a.vars.pauseInvisible&&m.pauseInvisible.isHidden()||(a.vars.initDelay>0?a.startTimeout=setTimeout(a.play,a.vars.initDelay):a.play())),p&&m.asNav.setup(),s&&a.vars.touch&&m.touch(),(!v||v&&a.vars.smoothHeight)&&$(window).bind("resize orientationchange focus",m.resize),a.find("img").attr("draggable","false"),setTimeout(function(){a.vars.start(a)},200)},asNav:{setup:function(){a.asNav=!0,a.animatingTo=Math.floor(a.currentSlide/a.move),a.currentItem=a.currentSlide,a.slides.removeClass(n+"active-slide").eq(a.currentItem).addClass(n+"active-slide"),i?(e._slider=a,a.slides.each(function(){var e=this;e._gesture=new MSGesture,e._gesture.target=e,e.addEventListener("MSPointerDown",function(e){e.preventDefault(),e.currentTarget._gesture&&e.currentTarget._gesture.addPointer(e.pointerId)},!1),e.addEventListener("MSGestureTap",function(e){e.preventDefault();var t=$(this),n=t.index();$(a.vars.asNavFor).data("flexslider").animating||t.hasClass("active")||(a.direction=a.currentItem<n?"next":"prev",a.flexAnimate(n,a.vars.pauseOnAction,!1,!0,!0))})})):a.slides.on(r,function(e){e.preventDefault();var t=$(this),i=t.index(),s=t.offset().left-$(a).scrollLeft();0>=s&&t.hasClass(n+"active-slide")?a.flexAnimate(a.getTarget("prev"),!0):$(a.vars.asNavFor).data("flexslider").animating||t.hasClass(n+"active-slide")||(a.direction=a.currentItem<i?"next":"prev",a.flexAnimate(i,a.vars.pauseOnAction,!1,!0,!0))})}},controlNav:{setup:function(){a.manualControls?m.controlNav.setupManual():m.controlNav.setupPaging()},setupPaging:function(){var e="thumbnails"===a.vars.controlNav?"control-thumbs":"control-paging",t=1,i,s;if(a.controlNavScaffold=$('<ol class="'+n+"control-nav "+n+e+'"></ol>'),a.pagingCount>1)for(var l=0;l<a.pagingCount;l++){if(s=a.slides.eq(l),i="thumbnails"===a.vars.controlNav?'<img src="'+s.attr("data-thumb")+'"/>':"<a>"+t+"</a>","thumbnails"===a.vars.controlNav&&!0===a.vars.thumbCaptions){var c=s.attr("data-thumbcaption");""!=c&&void 0!=c&&(i+='<span class="'+n+'caption">'+c+"</span>")}a.controlNavScaffold.append("<li>"+i+"</li>"),t++}a.controlsContainer?$(a.controlsContainer).append(a.controlNavScaffold):a.append(a.controlNavScaffold),m.controlNav.set(),m.controlNav.active(),a.controlNavScaffold.delegate("a, img",r,function(e){if(e.preventDefault(),""===o||o===e.type){var t=$(this),i=a.controlNav.index(t);t.hasClass(n+"active")||(a.direction=i>a.currentSlide?"next":"prev",a.flexAnimate(i,a.vars.pauseOnAction))}""===o&&(o=e.type),m.setToClearWatchedEvent()})},setupManual:function(){a.controlNav=a.manualControls,m.controlNav.active(),a.controlNav.bind(r,function(e){if(e.preventDefault(),""===o||o===e.type){var t=$(this),i=a.controlNav.index(t);t.hasClass(n+"active")||(a.direction=i>a.currentSlide?"next":"prev",a.flexAnimate(i,a.vars.pauseOnAction))}""===o&&(o=e.type),m.setToClearWatchedEvent()})},set:function(){var e="thumbnails"===a.vars.controlNav?"img":"a";a.controlNav=$("."+n+"control-nav li "+e,a.controlsContainer?a.controlsContainer:a)},active:function(){a.controlNav.removeClass(n+"active").eq(a.animatingTo).addClass(n+"active")},update:function(e,t){a.pagingCount>1&&"add"===e?a.controlNavScaffold.append($("<li><a>"+a.count+"</a></li>")):1===a.pagingCount?a.controlNavScaffold.find("li").remove():a.controlNav.eq(t).closest("li").remove(),m.controlNav.set(),a.pagingCount>1&&a.pagingCount!==a.controlNav.length?a.update(t,e):m.controlNav.active()}},directionNav:{setup:function(){var e=$('<ul class="'+n+'direction-nav"><li class="'+n+'nav-prev"><a class="'+n+'prev" href="#">'+a.vars.prevText+'</a></li><li class="'+n+'nav-next"><a class="'+n+'next" href="#">'+a.vars.nextText+"</a></li></ul>");a.controlsContainer?($(a.controlsContainer).append(e),a.directionNav=$("."+n+"direction-nav li a",a.controlsContainer)):(a.append(e),a.directionNav=$("."+n+"direction-nav li a",a)),m.directionNav.update(),a.directionNav.bind(r,function(e){e.preventDefault();var t;(""===o||o===e.type)&&(t=a.getTarget($(this).hasClass(n+"next")?"next":"prev"),a.flexAnimate(t,a.vars.pauseOnAction)),""===o&&(o=e.type),m.setToClearWatchedEvent()})},update:function(){var e=n+"disabled";1===a.pagingCount?a.directionNav.addClass(e).attr("tabindex","-1"):a.vars.animationLoop?a.directionNav.removeClass(e).removeAttr("tabindex"):0===a.animatingTo?a.directionNav.removeClass(e).filter("."+n+"prev").addClass(e).attr("tabindex","-1"):a.animatingTo===a.last?a.directionNav.removeClass(e).filter("."+n+"next").addClass(e).attr("tabindex","-1"):a.directionNav.removeClass(e).removeAttr("tabindex")}},pausePlay:{setup:function(){var e=$('<div class="'+n+'pauseplay"><a></a></div>');a.controlsContainer?(a.controlsContainer.append(e),a.pausePlay=$("."+n+"pauseplay a",a.controlsContainer)):(a.append(e),a.pausePlay=$("."+n+"pauseplay a",a)),m.pausePlay.update(a.vars.slideshow?n+"pause":n+"play"),a.pausePlay.bind(r,function(e){e.preventDefault(),(""===o||o===e.type)&&($(this).hasClass(n+"pause")?(a.manualPause=!0,a.manualPlay=!1,a.pause()):(a.manualPause=!1,a.manualPlay=!0,a.play())),""===o&&(o=e.type),m.setToClearWatchedEvent()})},update:function(e){"play"===e?a.pausePlay.removeClass(n+"pause").addClass(n+"play").html(a.vars.playText):a.pausePlay.removeClass(n+"play").addClass(n+"pause").html(a.vars.pauseText)}},touch:function(){function t(t){a.animating?t.preventDefault():(window.navigator.msPointerEnabled||1===t.touches.length)&&(a.pause(),g=c?a.h:a.w,S=Number(new Date),x=t.touches[0].pageX,b=t.touches[0].pageY,f=u&&d&&a.animatingTo===a.last?0:u&&d?a.limit-(a.itemW+a.vars.itemMargin)*a.move*a.animatingTo:u&&a.currentSlide===a.last?a.limit:u?(a.itemW+a.vars.itemMargin)*a.move*a.currentSlide:d?(a.last-a.currentSlide+a.cloneOffset)*g:(a.currentSlide+a.cloneOffset)*g,p=c?b:x,m=c?x:b,e.addEventListener("touchmove",n,!1),e.addEventListener("touchend",s,!1))}function n(e){x=e.touches[0].pageX,b=e.touches[0].pageY,h=c?p-b:p-x,y=c?Math.abs(h)<Math.abs(x-m):Math.abs(h)<Math.abs(b-m);var t=500;(!y||Number(new Date)-S>t)&&(e.preventDefault(),!v&&a.transitions&&(a.vars.animationLoop||(h/=0===a.currentSlide&&0>h||a.currentSlide===a.last&&h>0?Math.abs(h)/g+2:1),a.setProps(f+h,"setTouch")))}function s(t){if(e.removeEventListener("touchmove",n,!1),a.animatingTo===a.currentSlide&&!y&&null!==h){var i=d?-h:h,r=a.getTarget(i>0?"next":"prev");a.canAdvance(r)&&(Number(new Date)-S<550&&Math.abs(i)>50||Math.abs(i)>g/2)?a.flexAnimate(r,a.vars.pauseOnAction):v||a.flexAnimate(a.currentSlide,a.vars.pauseOnAction,!0)}e.removeEventListener("touchend",s,!1),p=null,m=null,h=null,f=null}function r(t){t.stopPropagation(),a.animating?t.preventDefault():(a.pause(),e._gesture.addPointer(t.pointerId),w=0,g=c?a.h:a.w,S=Number(new Date),f=u&&d&&a.animatingTo===a.last?0:u&&d?a.limit-(a.itemW+a.vars.itemMargin)*a.move*a.animatingTo:u&&a.currentSlide===a.last?a.limit:u?(a.itemW+a.vars.itemMargin)*a.move*a.currentSlide:d?(a.last-a.currentSlide+a.cloneOffset)*g:(a.currentSlide+a.cloneOffset)*g)}function o(t){t.stopPropagation();var a=t.target._slider;if(a){var n=-t.translationX,i=-t.translationY;return w+=c?i:n,h=w,y=c?Math.abs(w)<Math.abs(-n):Math.abs(w)<Math.abs(-i),t.detail===t.MSGESTURE_FLAG_INERTIA?void setImmediate(function(){e._gesture.stop()}):void((!y||Number(new Date)-S>500)&&(t.preventDefault(),!v&&a.transitions&&(a.vars.animationLoop||(h=w/(0===a.currentSlide&&0>w||a.currentSlide===a.last&&w>0?Math.abs(w)/g+2:1)),a.setProps(f+h,"setTouch"))))}}function l(e){e.stopPropagation();var t=e.target._slider;if(t){if(t.animatingTo===t.currentSlide&&!y&&null!==h){var a=d?-h:h,n=t.getTarget(a>0?"next":"prev");t.canAdvance(n)&&(Number(new Date)-S<550&&Math.abs(a)>50||Math.abs(a)>g/2)?t.flexAnimate(n,t.vars.pauseOnAction):v||t.flexAnimate(t.currentSlide,t.vars.pauseOnAction,!0)}p=null,m=null,h=null,f=null,w=0}}var p,m,f,g,h,S,y=!1,x=0,b=0,w=0;i?(e.style.msTouchAction="none",e._gesture=new MSGesture,e._gesture.target=e,e.addEventListener("MSPointerDown",r,!1),e._slider=a,e.addEventListener("MSGestureChange",o,!1),e.addEventListener("MSGestureEnd",l,!1)):e.addEventListener("touchstart",t,!1)},resize:function(){!a.animating&&a.is(":visible")&&(u||a.doMath(),v?m.smoothHeight():u?(a.slides.width(a.computedW),a.update(a.pagingCount),a.setProps()):c?(a.viewport.height(a.h),a.setProps(a.h,"setTotal")):(a.vars.smoothHeight&&m.smoothHeight(),a.newSlides.width(a.computedW),a.setProps(a.computedW,"setTotal")))},smoothHeight:function(e){if(!c||v){var t=v?a:a.viewport;e?t.animate({height:a.slides.eq(a.animatingTo).height()},e):t.height(a.slides.eq(a.animatingTo).height())}},sync:function(e){var t=$(a.vars.sync).data("flexslider"),n=a.animatingTo;switch(e){case"animate":t.flexAnimate(n,a.vars.pauseOnAction,!1,!0);break;case"play":t.playing||t.asNav||t.play();break;case"pause":t.pause()}},uniqueID:function(e){return e.filter("[id]").add(e.find("[id]")).each(function(){var e=$(this);e.attr("id",e.attr("id")+"_clone")}),e},pauseInvisible:{visProp:null,init:function(){var e=m.pauseInvisible.getHiddenProp();if(e){var t=e.replace(/[H|h]idden/,"")+"visibilitychange";document.addEventListener(t,function(){m.pauseInvisible.isHidden()?a.startTimeout?clearTimeout(a.startTimeout):a.pause():a.started?a.play():a.vars.initDelay>0?setTimeout(a.play,a.vars.initDelay):a.play()})}},isHidden:function(){var e=m.pauseInvisible.getHiddenProp();return e?document[e]:!1},getHiddenProp:function(){var e=["webkit","moz","ms","o"];if("hidden"in document)return"hidden";for(var t=0;t<e.length;t++)if(e[t]+"Hidden"in document)return e[t]+"Hidden";return null}},setToClearWatchedEvent:function(){clearTimeout(l),l=setTimeout(function(){o=""},3e3)}},a.flexAnimate=function(e,t,i,r,o){if(a.vars.animationLoop||e===a.currentSlide||(a.direction=e>a.currentSlide?"next":"prev"),p&&1===a.pagingCount&&(a.direction=a.currentItem<e?"next":"prev"),!a.animating&&(a.canAdvance(e,o)||i)&&a.is(":visible")){if(p&&r){var l=$(a.vars.asNavFor).data("flexslider");if(a.atEnd=0===e||e===a.count-1,l.flexAnimate(e,!0,!1,!0,o),a.direction=a.currentItem<e?"next":"prev",l.direction=a.direction,Math.ceil((e+1)/a.visible)-1===a.currentSlide||0===e)return a.currentItem=e,a.slides.removeClass(n+"active-slide").eq(e).addClass(n+"active-slide"),!1;a.currentItem=e,a.slides.removeClass(n+"active-slide").eq(e).addClass(n+"active-slide"),e=Math.floor(e/a.visible)}if(a.animating=!0,a.animatingTo=e,t&&a.pause(),a.vars.before(a),a.syncExists&&!o&&m.sync("animate"),a.vars.controlNav&&m.controlNav.active(),u||a.slides.removeClass(n+"active-slide").eq(e).addClass(n+"active-slide"),a.atEnd=0===e||e===a.last,a.vars.directionNav&&m.directionNav.update(),e===a.last&&(a.vars.end(a),a.vars.animationLoop||a.pause()),v)s?(a.slides.eq(a.currentSlide).css({opacity:0,zIndex:1}),a.slides.eq(e).css({opacity:1,zIndex:2}),a.wrapup(f)):(a.slides.eq(a.currentSlide).css({zIndex:1}).animate({opacity:0},a.vars.animationSpeed,a.vars.easing),a.slides.eq(e).css({zIndex:2}).animate({opacity:1},a.vars.animationSpeed,a.vars.easing,a.wrapup));else{var f=c?a.slides.filter(":first").height():a.computedW,g,h,S;u?(g=a.vars.itemMargin,S=(a.itemW+g)*a.move*a.animatingTo,h=S>a.limit&&1!==a.visible?a.limit:S):h=0===a.currentSlide&&e===a.count-1&&a.vars.animationLoop&&"next"!==a.direction?d?(a.count+a.cloneOffset)*f:0:a.currentSlide===a.last&&0===e&&a.vars.animationLoop&&"prev"!==a.direction?d?0:(a.count+1)*f:d?(a.count-1-e+a.cloneOffset)*f:(e+a.cloneOffset)*f,a.setProps(h,"",a.vars.animationSpeed),a.transitions?(a.vars.animationLoop&&a.atEnd||(a.animating=!1,a.currentSlide=a.animatingTo),a.container.unbind("webkitTransitionEnd transitionend"),a.container.bind("webkitTransitionEnd transitionend",function(){clearTimeout(a.ensureAnimationEnd),a.wrapup(f)}),clearTimeout(a.ensureAnimationEnd),a.ensureAnimationEnd=setTimeout(function(){a.wrapup(f)},a.vars.animationSpeed+100)):a.container.animate(a.args,a.vars.animationSpeed,a.vars.easing,function(){a.wrapup(f)})}a.vars.smoothHeight&&m.smoothHeight(a.vars.animationSpeed)}},a.wrapup=function(e){v||u||(0===a.currentSlide&&a.animatingTo===a.last&&a.vars.animationLoop?a.setProps(e,"jumpEnd"):a.currentSlide===a.last&&0===a.animatingTo&&a.vars.animationLoop&&a.setProps(e,"jumpStart")),a.animating=!1,a.currentSlide=a.animatingTo,a.vars.after(a)},a.animateSlides=function(){!a.animating&&f&&a.flexAnimate(a.getTarget("next"))},a.pause=function(){clearInterval(a.animatedSlides),a.animatedSlides=null,a.playing=!1,a.vars.pausePlay&&m.pausePlay.update("play"),a.syncExists&&m.sync("pause")},a.play=function(){a.playing&&clearInterval(a.animatedSlides),a.animatedSlides=a.animatedSlides||setInterval(a.animateSlides,a.vars.slideshowSpeed),a.started=a.playing=!0,a.vars.pausePlay&&m.pausePlay.update("pause"),a.syncExists&&m.sync("play")},a.stop=function(){a.pause(),a.stopped=!0},a.canAdvance=function(e,t){var n=p?a.pagingCount-1:a.last;return t?!0:p&&a.currentItem===a.count-1&&0===e&&"prev"===a.direction?!0:p&&0===a.currentItem&&e===a.pagingCount-1&&"next"!==a.direction?!1:e!==a.currentSlide||p?a.vars.animationLoop?!0:a.atEnd&&0===a.currentSlide&&e===n&&"next"!==a.direction?!1:a.atEnd&&a.currentSlide===n&&0===e&&"next"===a.direction?!1:!0:!1},a.getTarget=function(e){return a.direction=e,"next"===e?a.currentSlide===a.last?0:a.currentSlide+1:0===a.currentSlide?a.last:a.currentSlide-1},a.setProps=function(e,t,n){var i=function(){var n=e?e:(a.itemW+a.vars.itemMargin)*a.move*a.animatingTo,i=function(){if(u)return"setTouch"===t?e:d&&a.animatingTo===a.last?0:d?a.limit-(a.itemW+a.vars.itemMargin)*a.move*a.animatingTo:a.animatingTo===a.last?a.limit:n;switch(t){case"setTotal":return d?(a.count-1-a.currentSlide+a.cloneOffset)*e:(a.currentSlide+a.cloneOffset)*e;case"setTouch":return d?e:e;case"jumpEnd":return d?e:a.count*e;case"jumpStart":return d?a.count*e:e;default:return e}}();return-1*i+"px"}();a.transitions&&(i=c?"translate3d(0,"+i+",0)":"translate3d("+i+",0,0)",n=void 0!==n?n/1e3+"s":"0s",a.container.css("-"+a.pfx+"-transition-duration",n),a.container.css("transition-duration",n)),a.args[a.prop]=i,(a.transitions||void 0===n)&&a.container.css(a.args),a.container.css("transform",i)},a.setup=function(e){if(v)a.slides.css({width:"100%","float":"left",marginRight:"-100%",position:"relative"}),"init"===e&&(s?a.slides.css({opacity:0,display:"block",webkitTransition:"opacity "+a.vars.animationSpeed/1e3+"s ease",zIndex:1}).eq(a.currentSlide).css({opacity:1,zIndex:2}):0==a.vars.fadeFirstSlide?a.slides.css({opacity:0,display:"block",zIndex:1}).eq(a.currentSlide).css({zIndex:2}).css({opacity:1}):a.slides.css({opacity:0,display:"block",zIndex:1}).eq(a.currentSlide).css({zIndex:2}).animate({opacity:1},a.vars.animationSpeed,a.vars.easing)),a.vars.smoothHeight&&m.smoothHeight();else{var t,i;"init"===e&&(a.viewport=$('<div class="'+n+'viewport"></div>').css({overflow:"hidden",position:"relative"}).appendTo(a).append(a.container),a.cloneCount=0,a.cloneOffset=0,d&&(i=$.makeArray(a.slides).reverse(),a.slides=$(i),a.container.empty().append(a.slides))),a.vars.animationLoop&&!u&&(a.cloneCount=2,a.cloneOffset=1,"init"!==e&&a.container.find(".clone").remove(),a.container.append(m.uniqueID(a.slides.first().clone().addClass("clone")).attr("aria-hidden","true")).prepend(m.uniqueID(a.slides.last().clone().addClass("clone")).attr("aria-hidden","true"))),a.newSlides=$(a.vars.selector,a),t=d?a.count-1-a.currentSlide+a.cloneOffset:a.currentSlide+a.cloneOffset,c&&!u?(a.container.height(200*(a.count+a.cloneCount)+"%").css("position","absolute").width("100%"),setTimeout(function(){a.newSlides.css({display:"block"}),a.doMath(),a.viewport.height(a.h),a.setProps(t*a.h,"init")},"init"===e?100:0)):(a.container.width(200*(a.count+a.cloneCount)+"%"),a.setProps(t*a.computedW,"init"),setTimeout(function(){a.doMath(),a.newSlides.css({width:a.computedW,"float":"left",display:"block"}),a.vars.smoothHeight&&m.smoothHeight()},"init"===e?100:0))}u||a.slides.removeClass(n+"active-slide").eq(a.currentSlide).addClass(n+"active-slide"),a.vars.init(a)},a.doMath=function(){var e=a.slides.first(),t=a.vars.itemMargin,n=a.vars.minItems,i=a.vars.maxItems;a.w=void 0===a.viewport?a.width():a.viewport.width(),a.h=e.height(),a.boxPadding=e.outerWidth()-e.width(),u?(a.itemT=a.vars.itemWidth+t,a.minW=n?n*a.itemT:a.w,a.maxW=i?i*a.itemT-t:a.w,a.itemW=a.minW>a.w?(a.w-t*(n-1))/n:a.maxW<a.w?(a.w-t*(i-1))/i:a.vars.itemWidth>a.w?a.w:a.vars.itemWidth,a.visible=Math.floor(a.w/a.itemW),a.move=a.vars.move>0&&a.vars.move<a.visible?a.vars.move:a.visible,a.pagingCount=Math.ceil((a.count-a.visible)/a.move+1),a.last=a.pagingCount-1,a.limit=1===a.pagingCount?0:a.vars.itemWidth>a.w?a.itemW*(a.count-1)+t*(a.count-1):(a.itemW+t)*a.count-a.w-t):(a.itemW=a.w,a.pagingCount=a.count,a.last=a.count-1),a.computedW=a.itemW-a.boxPadding},a.update=function(e,t){a.doMath(),u||(e<a.currentSlide?a.currentSlide+=1:e<=a.currentSlide&&0!==e&&(a.currentSlide-=1),a.animatingTo=a.currentSlide),a.vars.controlNav&&!a.manualControls&&("add"===t&&!u||a.pagingCount>a.controlNav.length?m.controlNav.update("add"):("remove"===t&&!u||a.pagingCount<a.controlNav.length)&&(u&&a.currentSlide>a.last&&(a.currentSlide-=1,a.animatingTo-=1),m.controlNav.update("remove",a.last))),a.vars.directionNav&&m.directionNav.update()},a.addSlide=function(e,t){var n=$(e);a.count+=1,a.last=a.count-1,c&&d?void 0!==t?a.slides.eq(a.count-t).after(n):a.container.prepend(n):void 0!==t?a.slides.eq(t).before(n):a.container.append(n),a.update(t,"add"),a.slides=$(a.vars.selector+":not(.clone)",a),a.setup(),a.vars.added(a)},a.removeSlide=function(e){var t=isNaN(e)?a.slides.index($(e)):e;a.count-=1,a.last=a.count-1,isNaN(e)?$(e,a.slides).remove():c&&d?a.slides.eq(a.last).remove():a.slides.eq(e).remove(),a.doMath(),a.update(t,"remove"),a.slides=$(a.vars.selector+":not(.clone)",a),a.setup(),a.vars.removed(a)},m.init()},$(window).blur(function(e){focused=!1}).focus(function(e){focused=!0}),$.flexslider.defaults={namespace:"flex-",selector:".slides > li",animation:"fade",easing:"swing",direction:"horizontal",reverse:!1,animationLoop:!0,smoothHeight:!1,startAt:0,slideshow:!0,slideshowSpeed:7e3,animationSpeed:600,initDelay:0,randomize:!1,fadeFirstSlide:!0,thumbCaptions:!1,pauseOnAction:!0,pauseOnHover:!1,pauseInvisible:!0,useCSS:!0,touch:!0,video:!1,controlNav:!0,directionNav:!0,prevText:"Previous",nextText:"Next",keyboard:!0,multipleKeyboard:!1,mousewheel:!1,pausePlay:!1,pauseText:"Pause",playText:"Play",controlsContainer:"",manualControls:"",sync:"",asNavFor:"",itemWidth:0,itemMargin:0,minItems:1,maxItems:0,move:0,allowOneSlide:!0,start:function(){},before:function(){},after:function(){},end:function(){},added:function(){},removed:function(){},init:function(){}},$.fn.flexslider=function(e){if(void 0===e&&(e={}),"object"==typeof e)return this.each(function(){var t=$(this),a=e.selector?e.selector:".slides > li",n=t.find(a);1===n.length&&e.allowOneSlide===!0||0===n.length?(n.fadeIn(400),e.start&&e.start(t)):void 0===t.data("flexslider")&&new $.flexslider(this,e)});var t=$(this).data("flexslider");switch(e){case"play":t.play();break;case"pause":t.pause();break;case"stop":t.stop();break;case"next":t.flexAnimate(t.getTarget("next"),!0);break;case"prev":case"previous":t.flexAnimate(t.getTarget("prev"),!0);break;default:"number"==typeof e&&t.flexAnimate(e,!0)}}}(jQuery); \ No newline at end of file diff --git a/theme/adaptable/jquery/pace-min.js b/theme/adaptable/jquery/pace-min.js new file mode 100644 index 0000000..bead178 --- /dev/null +++ b/theme/adaptable/jquery/pace-min.js @@ -0,0 +1,2 @@ +/*! pace 1.0.2 */ +(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];c<this.bindings[a].length;)this.bindings[a][c].handler===b?e.push(this.bindings[a].splice(c,1)):e.push(c++);return e}},a.prototype.trigger=function(){var a,b,c,d,e,f,g,h,i;if(c=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],null!=(g=this.bindings)?g[c]:void 0){for(e=0,i=[];e<this.bindings[c].length;)h=this.bindings[c][e],d=h.handler,b=h.ctx,f=h.once,d.apply(null!=b?b:this,a),f?i.push(this.bindings[c].splice(e,1)):i.push(e++);return i}},a}(),j=window.Pace||{},window.Pace=j,v(j,g.prototype),D=j.options=v({},u,window.paceOptions,x()),U=["ajax","document","eventLag","elements"],Q=0,S=U.length;S>Q;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='<div class="pace-progress">\n <div class="pace-progress-inner"></div>\n</div>\n<div class="pace-activity"></div>',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e;e=[];for(d in b.prototype)try{null==a[d]&&"function"!=typeof b[d]?"function"==typeof Object.defineProperty?e.push(Object.defineProperty(a,d,{get:function(){return b.prototype[d]},configurable:!0,enumerable:!0})):e.push(a[d]=b.prototype[d]):e.push(void 0)}catch(f){c=f}return e},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e,f){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return a.lengthComputable?h.progress=100*a.loaded/a.total:h.progress=h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a<D.eventLag.lagThreshold?(f.progress=100,clearInterval(b)):f.progress=100*(3/(a+3))},50)}return a}(),m=function(){function a(a){this.source=a,this.last=this.sinceLastUpdate=0,this.rate=D.initialRate,this.catchup=0,this.progress=this.lastProgress=0,null!=this.source&&(this.progress=F(this.source,"progress"))}return a.prototype.tick=function(a,b){var c;return null==b&&(b=F(this.source,"progress")),b>=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(["pace"],function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); \ No newline at end of file diff --git a/theme/adaptable/jquery/plugins.php b/theme/adaptable/jquery/plugins.php new file mode 100644 index 0000000..e83f1b0 --- /dev/null +++ b/theme/adaptable/jquery/plugins.php @@ -0,0 +1,54 @@ +<?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/>. + +/** + * This is built using the bootstrap template to allow for new theme's using Moodle's new Bootstrap theme engine + * + * @package theme_adaptable + * @copyright 2013 Julian Ridden + * @copyright 2014 Gareth J Barnard, David Bezemer + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * This file describes jQuery plugins available in the moodle + * core component. These can be included in page using: + * $PAGE->requires->jquery(); + * $PAGE->requires->jquery_plugin('migrate', 'core'); + * $PAGE->requires->jquery_plugin('ui', 'core'); + * $PAGE->requires->jquery_plugin('ui-css', 'core'); + * + * Please note that other moodle plugins can not use the sample + * jquery plugin names, only one is loaded if collision detected. + * + * Any Moodle plugin may add jquery/plugins.php and include extra + * jQuery plugins. + * + * Themes or other plugin may blacklist any jquery plugin, + * for example to override default jQueryUI theme. + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugins = array( + 'adaptable' => array('files' => array('adaptable_v2_1_1_2.js')), + 'easing' => array('files' => array('jquery-easing-min.js')), + 'flexslider' => array('files' => array('jquery-flexslider-min.js')), + 'pace' => array('files' => array('pace-min.js')), + 'ticker' => array('files' => array('tickerme.js')) +); diff --git a/theme/adaptable/jquery/tickerme.js b/theme/adaptable/jquery/tickerme.js new file mode 100644 index 0000000..28e71f3 --- /dev/null +++ b/theme/adaptable/jquery/tickerme.js @@ -0,0 +1,124 @@ +/*! + * jQuery Tickerme Plugin v1.0 + */ + +(function($){ + + $.fn.tickerme = function(options) { + + var opts = $.extend( {}, $.fn.tickerme.defaults, options ); + + return this.each(function(){ + + var ticker = $(this); + + // SVG definitions for the play/pause/previous/next controls. + var control_definitions = '<svg display="none" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" width="224" height="32" viewBox="0 0 224 32"><defs><g id="icon-play"><path class="path1" d="M6 4l20 12-20 12z"></path></g><g id="icon-pause"><path class="path1" d="M4 4h10v24h-10zM18 4h10v24h-10z"></path></g><g id="icon-prev"><path class="path1" d="M18 5v10l10-10v22l-10-10v10l-11-11z"></path></g><g id="icon-next"><path class="path1" d="M16 27v-10l-10 10v-22l10 10v-10l11 11z"></path></g></defs></svg>'; + var control_styles = '<style type="text/css">#ticker_container{width:100%}#newscontent{float:left}#news{display:none}#controls{float:right;height:16px}.icon{display:inline-block;width:16px;height:16px;fill:' + + opts.control_color + '}.icon:hover{fill:' + opts.control_rollover + '}</style>'; + + // Array to contain news contents. + var contents = []; + var position = -1; + var timer; + + init(); + + /* Initialise */ + + function init() { + + // Hide all. + $(ticker).hide(); + + // Create the buttons. + $('body').prepend(control_definitions).prepend(control_styles); + + var controls = '<div id="ticker_container">'; + controls += '<div id="newscontent"><div id="news" aria-live="polite"></div></div>'; + controls += '<div id="controls">'; + controls += '<span id="pause_trigger"><i class="fa fa-pause"></i></span>'; + controls += '<span id="play_trigger" style="display:none"><i class="fa fa-play"></i></span>'; + controls += '</div>'; + controls += '</div>'; + $(controls).insertAfter(ticker); + + // Load up the array. + $(ticker).children().each(function(i){ + contents[i] = ($(this).html()); + }); + + load_container(); + } + + /* load_container */ + + function load_container() { + + if (position == (contents.length - 1)) { + position = 0; + } else { + position++; + } + // Fade out the current item, replace it with the next one, and fade it in. + if (opts.type == 'fade') { + $('#news').fadeOut(opts.fade_speed,function(){ + $('#newscontent').html('<div id="news">' + contents[position] + '</div>'); + $('#news').fadeIn(opts.fade_speed); + }); + } + timer = setTimeout(load_container,opts.duration); + } + + /* Control functions */ + + $('#pause_trigger').click(function() { + clearTimeout(timer); + $(this).hide(); + $('#play_trigger').show(); + return false; + }); + + $('#play_trigger').click(function(){ + load_container(); + $(this).hide(); + $('#pause_trigger').show(); + return false; + }); + + $('a#prev_trigger').click(function(){ + if (position == 0) { + position = (contents.length - 1); + } else { + position--; + } + $('#newscontent').html('<div id="news" style="display:block">'+contents[position]+'</div>'); + if (opts.auto_stop) $('#pause_trigger').trigger('click'); + return false; + }); + + $('a#next_trigger').click(function(){ + if (position == (contents.length - 1)) { + position = 0; + } else { + position++; + } + $('#newscontent').html('<div id="news" style="display:block">' + contents[position] + '</div>'); + if (opts.auto_stop) $('#pause_trigger').trigger('click'); + return false; + }); + + }); + + }; + + $.fn.tickerme.defaults = { + fade_speed: 500, + duration: 4500, + auto_stop: true, + type: 'fade', + control_color: '#333333', + control_rollover: '#666666' + }; + +}(jQuery)); diff --git a/theme/adaptable/lang/en/theme_adaptable.php b/theme/adaptable/lang/en/theme_adaptable.php new file mode 100644 index 0000000..933b20d --- /dev/null +++ b/theme/adaptable/lang/en/theme_adaptable.php @@ -0,0 +1,1944 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +// General. +$string['choosereadme'] = ' +<div class="clearfix"> +<div class="well"> +<h2>Adaptable</h2> +<p><img class="img-polaroid" src="adaptable/pix/screenshot.png" /></p> +</div> +<div class="well"> +<h3>About</h3> +<p>Adaptable is a Moodle 2-columns responsive theme highly customizable and based in the popular BCU theme. Version 2 is using + Boost as a base theme and therefore Bootstrap 4.</p> +<p>The theme is licensed under the GPL (GNU General Public License). You can find a complete licence copy <a href="http://www.gnu.org/licenses/" target="_HERE">blank</a></p> +<br> +<h4>Modify it! - Improve it! - Share it!</h4> +<br> +<h3>Credits</h3> +<p>This theme has been developed by:<br> +Jeremy Hopkins (Coventry University)<br> +Fernando Acedo (<a href="https://3-bits.com" target="_blank">3-bits.com</a>)</p> +Manoj Solanki (Coventry University)<br> +<br> +<h2>Other Credits</h2> +<p>In the development of version 2 they have also collaborated:<br> +Gareth J. Barnard (http://moodle.org/user/profile.php?id=442195) +Stuart Lamour +Alistair Spark (University College London) +<br> +<p>among many other developers, testers, translators and volunteers (See <a href="adaptable/README.txt" target="_blank">README.txt</a>)</p> +<br> +<h3>Bugs Report</h3> +<p>You can report bugs (and please, <b>ONLY</b> bugs) in our <a href="https://gitlab.com/3bits/moodle-theme_adaptable2" target="_blank">repository</a></p> +<br> +<h3>Technical Support</h3> +<p>You can ask your questions in the moodle forum:</p> +<ul> +<li>English: <a href="https://moodle.org/mod/forum/discuss.php?d=340404" target="_blank">https://moodle.org/mod/forum/discuss.php?d=340404</a> +<li>Español: <a href="https://moodle.org/mod/forum/discuss.php?d=326804" target="_blank">https://moodle.org/mod/forum/discuss.php?d=326804</a> +<li>Català: <a href="https://moodle.org/mod/forum/discuss.php?d=340406" target="_blank">https://moodle.org/mod/forum/discuss.php?d=340406</a> +</ul> +<br> +<h3>Demo</h3> +<p>You can see a demo version <a href="https://adaptable.ws/demo" target="_blank">HERE</a></p> +</div> +</div>'; + +$string['pluginname'] = 'Adaptable'; +$string['configtitle'] = 'Adaptable'; +$string['configtabtitle'] = 'Adaptable settings'; + +$string['region-middle'] = 'Middle'; +$string['region-frnt-footer'] = 'Footer'; +$string['region-side-post'] = 'Right'; +$string['region-side-pre'] = 'Left'; +$string['frnt-footer'] = 'Blocks in this area will only be visible to admin users'; +$string['side-post1'] = 'side bar in footer'; + +$string['region-frnt-market-a'] = 'Page region 1'; +$string['region-frnt-market-b'] = 'Page region 2'; +$string['region-frnt-market-c'] = 'Page region 3'; +$string['region-frnt-market-d'] = 'Page region 4'; +$string['region-frnt-market-e'] = 'Page region 5'; +$string['region-frnt-market-f'] = 'Page region 6'; +$string['region-frnt-market-g'] = 'Page region 7'; +$string['region-frnt-market-h'] = 'Page region 8'; +$string['region-frnt-market-i'] = 'Page region 9'; +$string['region-frnt-market-j'] = 'Page region 10'; +$string['region-frnt-market-k'] = 'Page region 11'; +$string['region-frnt-market-l'] = 'Page region 12'; +$string['region-frnt-market-m'] = 'Page region 13'; +$string['region-frnt-market-n'] = 'Page region 14'; +$string['region-frnt-market-o'] = 'Page region 15'; +$string['region-frnt-market-p'] = 'Page region 16'; +$string['region-frnt-market-q'] = 'Page region 17'; +$string['region-frnt-market-r'] = 'Page region 18'; +$string['region-frnt-market-s'] = 'Page region 19'; +$string['region-frnt-market-t'] = 'Page region 20'; + +// Course page block regions. +$string['region-course-top-a'] = 'Course page top region 1'; +$string['region-course-top-b'] = 'Course page top region 2'; +$string['region-course-top-c'] = 'Course page top region 3'; +$string['region-course-top-d'] = 'Course page top region 4'; + +$string['region-news-slider-a'] = 'Course page slider region'; + +$string['region-course-section-a'] = 'Course page activity end bottom region'; + +$string['region-course-bottom-a'] = 'Course page bottom region 5'; +$string['region-course-bottom-b'] = 'Course page bottom region 6'; +$string['region-course-bottom-c'] = 'Course page bottom region 7'; +$string['region-course-bottom-d'] = 'Course page bottom region 8'; + +// Settings page headings ******************************************. +$string['settingsmaincolors'] = 'Main Colours'; +$string['settingsheadercolors'] = 'Header Colours'; +$string['settingsmobilecolors'] = 'Mobile Colours'; +$string['settingsinfoboxcolors'] = 'Info Box Colours'; +$string['settingssecondinfoboxcolors'] = 'Second Info Box Colours'; +$string['settingsmarketingcolors'] = 'Marketing Block Colours'; +$string['settingsoverlaycolors'] = 'Overlay Tiles Colours'; +$string['settingsnavbarcolors'] = 'Navigation Bar (navbar) Colours'; +$string['settingsalertbox'] = 'Alert Box'; +$string['settingsbreadcrumbcolors'] = 'Breadcrumb Colours'; +$string['settingsmessagescolors'] = 'Messages Pop-Up Colours'; +$string['settingsfootercolors'] = 'Footer Colours'; +$string['settingsfonts'] = 'Fonts'; +$string['settingsanalytics'] = 'Analytics'; +$string['settingsblocksgeneral'] = 'General'; +$string['settingscolors'] = 'Colours'; +$string['settingsborders'] = 'Borders'; +$string['settingscourses'] = 'Courses'; +$string['settingstopicsweeks'] = 'Topics / Weeks'; +$string['settingsblockicons'] = 'Icons'; + +// Admin Menu Strings. +$string['blocksettings'] = 'Block Settings'; +$string['frontpagealertsettings'] = 'Alert Box'; +$string['frontpageblockregionsettings'] = 'Block Region Builder'; +$string['dashboardblockregionsettings'] = 'Dashboard Block Region Builder'; +$string['coursepageblockregionsettings'] = 'Course page Block Region Builder'; +$string['frontpageblocksettings'] = 'Marketing Blocks'; +$string['frontpagetickersettings'] = 'Frontpage Ticker'; +$string['frontpageslidersettings'] = 'Frontpage Slider'; +$string['frontpagecoursesettings'] = 'Frontpage Courses List'; +$string['frontpagesettingsheading'] = 'Frontpage rendering'; +$string['frontpagedesc'] = 'Configure the way that the course boxes are rendered on the frontpage.'; +$string['frontpagerenderer'] = 'Frontpage Course Boxes'; +$string['frontpagerendererdesc'] = 'Control the way that the coure boxes on the front page are rendered.'; +$string['frontpagerendereroption1'] = 'Tiles'; +$string['frontpagerendereroption2'] = 'Tiles w/ overlay'; +$string['frontpagerendereroption3'] = 'Moodle default'; +$string['frontpagerendereroption4'] = 'Coventry Tiles'; + + +// Ticker **********************************************************. +$string['tickersettings'] = 'News Ticker'; +$string['tickersettingsheading'] = 'Setup News Ticker on Front Page. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a>'; +$string['tickerdesc'] = 'Here you can set a news ticker to run across the front page of your Moodle site. + It is intended as a less intrusive alternative to the slider taking up very little space. + To setup your ticker simply enter a paragraph list and include any hyperlinks you need to in that text. + <strong>Note:</strong> If your ticker does not load properly switch to HTML view and ensure you have only p tags: + <pre> + <p>News item one.....</p> + <p>News item two.....</p> + </pre>'; + + +$string['tickerdefault'] = 'No news items to display'; + +$string['enableticker'] = 'Enable the news ticker on the homepage?'; +$string['enabletickerdesc'] = 'Check to enable the homepage ticker.'; + +$string['enabletickermy'] = 'Enable the news ticker on the My Home / Dashboard page?'; +$string['enabletickermydesc'] = 'Check to enable the ticker on My Home / Dashboard.'; + +$string['enabletickerc'] = 'Enable the news ticker on internal pages?'; +$string['enabletickercdesc'] = 'Check to enable the ticker on internal pages.'; + +$string['tickerwidth'] = 'News Ticker Width'; +$string['tickerwidthdesc'] = 'You can use this setting to fix the news ticker at 100% width.'; + +$string['tickertextprofilefield'] = 'Custom Profile Field Name=Value (optional)'; +$string['tickertextprofilefielddesc'] = 'Add access rule using for custom profile field eg: usertype=student'; + +$string['ticker'] = 'Announcements'; + +$string['tickerwidth'] = 'Fixed Width'; +$string['tickerfullscreen'] = 'Full Screen width'; + +// Slideshow *******************************************************. +$string['slideshowsettings'] = 'Slideshow'; +$string['slideshowsettingsheading'] = 'Customize the carousel on the front page. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a>'; +$string['slideshowdesc'] = 'Upload the images, add the links and description for the carousel on the front page.'; + +$string['sliderimage'] = 'Slider Picture'; +$string['sliderimagedesc'] = 'Add an image for your slide. Recommended size is 1600px x 400px or higher.'; + +$string['slidercaption'] = 'Slider Caption'; +$string['slidercaptiondesc'] = 'Add a caption for your slide'; + +$string['sliderurl'] = 'Slide Link URL'; +$string['sliderurldesc'] = 'Add a URL to which your slide links to when clicked.'; + +$string['slidermargintop'] = 'Margin above slider'; +$string['slidermargintopdesc'] = 'Set the size of the margin above the slider.'; + +$string['slidermarginbottom'] = 'Margin below slider'; +$string['slidermarginbottomdesc'] = 'Set the size of the margin below the slider.'; + +$string['sliderenabled'] = 'Enable Slider'; +$string['sliderenableddesc'] = 'Enable a slider at the top of your home page'; + +$string['sliderfullscreen'] = 'Slider full screen'; +$string['sliderfullscreendesc'] = 'Check this box to make the slider full screen (100% width)'; + +$string['slideroption2'] = 'Choose Slider Type'; +$string['slideroption2desc'] = 'Choose Slider Type <strong>and then click SAVE</strong> to see colour settings for your chosen slider'; + +$string['slideroption2snippet'] = '<p>Sample HTML for Slider Captions:</p> +<pre> +<div class="span6 col-sm-6"> +<h3>Hand-crafted</h3> <h4>pixels and code for the Moodle community</h4> +<a href="#" class="submit">Please favorite our theme!</a> +</pre>'; + +$string['slidercount'] = 'Slider Count'; +$string['slidercountdesc'] = 'Select how many slides you want to add <strong>then click SAVE</strong> to load the input fields'; + +$string['sliderh3color'] = 'Slider 1 H3 Colour'; +$string['sliderh3colordesc'] = 'Choose the colour you want for the slider 1 H3 tag'; + +$string['sliderh4color'] = 'Slider 1 H4 Colour'; +$string['sliderh4colordesc'] = 'Choose the colour you want for the slider 1 H4 tag'; + +$string['slidersubmitcolor'] = 'Slider 1 Submit Text'; +$string['slidersubmitcolordesc'] = 'Choose the text colour of the Slider 1 submit button'; + +$string['slidersubmitbgcolor'] = 'Slider 1 Submit bg'; +$string['slidersubmitbgcolordesc'] = 'Choose the background colour of the Slider 1 submit button'; + +$string['slider2h3color'] = 'Slider 2 H3 Text Colour'; +$string['slider2h3colordesc'] = 'Choose the text colour you want for the slider 2 H3 tag'; + +$string['slider2h4color'] = 'Slider 2 H4 Text Colour'; +$string['slider2h4colordesc'] = 'Choose the text colour you want for the slider 2 H4 tag'; + +$string['slider2h3bgcolor'] = 'Slider 2 H3 bg Colour'; +$string['slider2h3bgcolordesc'] = 'Choose the background colour you want for the slider 2 H3 tag'; + +$string['slider2h4bgcolor'] = 'Slider 2 H4 bg Colour'; +$string['slider2h4bgcolordesc'] = 'Choose the background colour you want for the slider 2 H4 tag'; + +$string['slideroption2submitcolor'] = 'Slider 2 Submit Text'; +$string['slideroption2submitcolordesc'] = 'Set a background colour for the submit text in slider style option 2 colour'; + +$string['slideroption2color'] = 'Slider 2 Submit bg'; +$string['slideroption2colordesc'] = 'Set a background colour for the submit text in slider style option'; + +$string['slideroption2a'] = 'Slider style option 2 arrow background colour'; +$string['slideroption2adesc'] = 'Set the slider style option 2 arrow background colour'; + +$string['sliderstyle1'] = 'Slider style 1'; +$string['sliderstyle2'] = 'Slider style 2'; + + +// Block Regions ***************************************************. +$string['blocklayoutbuilder'] = 'Frontpage Block Regions'; +$string['blocklayoutbuilderdesc'] = 'Below you can build your own layout for block regions on the front page. +To add content these regions you will need to <strong> turn editing on on the front page of Moodle</strong>. +Then you can begin to drag/drop blocks into the regions you create!'; + +$string['dashblocklayoutbuilder'] = 'Dashboard Block Regions'; +$string['dashblocklayoutbuilderdesc'] = 'Below you can build your own layout for block regions on the Dashboard page. +To add content these regions you will need to <strong> turn editing on, on the dashboard page of Moodle</strong>. +Then you can begin to drag/drop blocks into the regions you create!'; + +$string['coursepagesidebarinfooterenabledsection'] = 'Common settings'; +$string['coursepagesidebarinfooterenabledsectiondesc'] = 'Common settings for most of the course formats.'; + +$string['coursepageblocklayoutbuilder'] = 'Course Page Block Regions'; +$string['coursepageblocklayoutbuilderdesc'] = 'Below you can build your own layout for block regions on the course page. +To add content these regions you will need to <strong> turn editing on, on the course page of Moodle</strong>. +Then you can begin to drag/drop blocks into the regions you create!'; + +$string['blocklayoutlayoutcheck'] = 'Check your layout'; +$string['blocklayoutlayoutcheckdesc'] = 'Use the tool below to check the number of blocks you have used and see a representation of your new layout.'; +$string['blocklayoutlayoutcount1'] = 'You can set a maximum of '; +$string['blocklayoutlayoutcount2'] = ' block regions. You are currently using: '; + +$string['blocklayoutlayoutrow'] = 'Block Region Row '; +$string['blocklayoutlayoutrowdesc'] = 'Add / set layout for block region row on front page.'; + +$string['dashblocklayoutlayoutrow'] = 'Dashboard Block Region Row '; +$string['dashblocklayoutlayoutrowdesc'] = 'Add / set layout for block region row on Dashboard page.'; + +$string['coursepageblocklayoutlayouttoprow'] = 'Course page Block Top Region Row '; +$string['coursepageblocklayoutlayouttoprowdesc'] = 'Add / set layout for block region row on Course page.'; + +$string['coursepageblocklayoutlayoutbottomrow'] = 'Course page Block Bottom Region Row '; +$string['coursepageblocklayoutlayoutbottomrowdesc'] = 'Add / set layout for block region row on Course page.'; + +$string['frontpageblocksenabled'] = 'Enable custom block region on front page'; +$string['frontpageblocksenableddesc'] = 'You can enable / disable custom block regions on the front page. +You can then drag and drop blocks into the regions you created'; + +$string['dashblocksenabled'] = 'Enable custom block region on Dashboard page'; +$string['dashblocksenableddesc'] = 'You can enable / disable custom block regions on the Dashboard page. +You can then drag and drop blocks into the regions you created'; + +$string['dashblocksposition'] = 'Custom block region position'; +$string['dashblockspositiondesc'] = 'When custom block regions are enabled for the dashboard page, choose the position.'; +$string['dashblocksabovecontent'] = 'Show above main content'; +$string['dashblocksbelowcontent'] = 'Show below main content'; + +$string['coursepageblocksenabled'] = 'Enable custom block regions on Course page'; +$string['coursepageblocksenableddesc'] = 'You can enable / disable custom block regions (top and bottom) on the Course page. +You can then drag and drop blocks into the regions you created'; + +$string['coursepagenewssliderblockregionheading'] = 'Custom news slider block region'; +$string['coursepagenewssliderblockregionheadingdesc'] = 'A custom block region designed for use with the news slider that is part of Adaptable UI. This region appears above course activities on a Course page. To add news slider block, first install the <strong><a href="https://moodle.org/plugins/block_news_slider">Adaptable UI news slider</a></strong> and configure it to appear in the region "course page slider region" on all course pages.'; + +$string['coursepageblocksliderenabled'] = 'Enable custom slider block region on Course page'; +$string['coursepageblocksliderenableddesc'] = 'Enable this region on all course pages.'; + +$string['coursepageactivitybottomblockregionheading'] = 'Custom course activity bottom block region'; +$string['coursepageactivitybottomblockregionheadingdesc'] = 'A custom block region that appears after the end of activities.'; + +$string['coursepageblockactivitybottomenabled'] = 'Enable course activity bottom block region on course page'; +$string['coursepageblockactivitybottomenableddesc'] = 'Enable this region on all course pages.'; + +$string['coursepagesidebarinfooterenabled'] = 'Move sidebar to footer on Course page'; +$string['coursepagesidebarinfooterenableddesc'] = 'Wide course page layout by moving sidebar to footer.'; + +$string['layoutcheck'] = 'Check your layout'; +$string['layoutcheckdesc'] = 'Use the tool below to check the number of blocks you have used and see a representation of your new layout.'; +$string['layoutcount1'] = 'You can set a maximum of '; +$string['layoutcount2'] = ' block regions. You are currently using: '; + +$string['sidebaricon'] = 'Show / hide the sidebar'; + +// Marketing Blocks & Info Box *************************************. +$string['marketingsettings'] = 'Marketing Blocks'; +$string['marketingsettingsheading'] = 'Customize the marketing blocks that appear on the front page. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a>'; +$string['marketingdesc'] = 'There are two full width info boxes with differing styles you can use. +In addition to this there is a layout builder allowing you to decide how many blocks you need and define your own layout, please see the <a href="./../theme/adaptable/README.txt" target="_blank">README</a> file that comes with this theme.'; + +$string['marketingbuilderheading'] = 'Marketing Block Layout Builder'; +$string['marketingbuilderdesc'] = 'Use the tool below to setup your marketing blocks. Once defined the block settings will appear further down the page.'; + +$string['marketlayoutrow'] = 'Marketing Block Row'; +$string['marketlayoutrowdesc'] = 'Add / set layout for marketing block row on front page'; + +$string['market'] = 'Marketing Block '; +$string['marketdesc'] = 'Add html for marketing block (see the <a href="./../theme/adaptable/README.txt" target="_blank">README</a> file for additional info and hints).'; + +$string['layoutaddcontent'] = 'Happy With Your Layout? Now Add Content to Your Blocks:'; +$string['layoutaddcontentdesc1'] = 'You have configured '; +$string['layoutaddcontentdesc2'] = ' marketing blocks. If you are happy with this layout add content to the blocks below. +If you are not happy use the layout builder above to make changes<br />'; + +$string['infobox'] = 'Frontpage Info Box'; +$string['infoboxdesc'] = 'Frontpage info block HTML (see the <a href="./../theme/adaptable/README.txt" target="_blank">README</a> file for additional info and hints).<br><br><strong>Note: </strong><i>This element is only used for compatibility with BCU and will be removed in coming versions.</i>'; + +$string['infoboxfullscreen'] = 'Make infobox full screen'; +$string['infoboxfullscreendesc'] = 'Turning this option on will make the infobox full screen.'; + +$string['infobox2'] = 'Frontpage Secondary Info Box'; +$string['infobox2desc'] = 'Frontpage Secondary Info Box (see the <a href="./../theme/adaptable/README.txt" target="_blank">README</a> file for additional info and hints).<br><br><strong>Note: </strong><i>This element is only used for compatibility with BCU and will be removed in coming versions.</i>'; + +$string['frontpagemarketenabled'] = 'Enable Marketing Blocks'; +$string['frontpagemarketenableddesc'] = 'Set the marketing blocks in the frontpage.'; + +$string['frontpagemarketoption'] = 'Choose style for marketing blocks'; +$string['frontpagemarketoptiondesc'] = 'You can apply different styles to marketing blocks. +Note: BCU style is designed to work with images at top of block.'; + +$string['bcustyle'] = 'BCU style'; +$string['coventrystyle'] = 'Coventry style'; +$string['nostyle'] = 'No style'; +$string['disabled'] = 'Disabled'; +$string['expandable'] = 'Expandable'; +$string['static'] = 'Static'; + + +// Footer **********************************************************. +$string['footersettings'] = 'Footer'; +$string['footersettingsheading'] = 'Set the content that should appear in the footer. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a> '; + +$string['footerdesc'] = 'Control the content that appears in the 4 footer sections of the page.'; + +$string['showfooterblocks'] = 'Show Footnote Block'; +$string['showfooterblocksdesc'] = 'Show / hide the lower footerblock used for footnote / Moodle docs region'; + +$string['footerblocksplacement'] = 'Footer Blocks Placement '; +$string['footerblocksplacementdesc'] = 'Control where the upper footer blocks are displayed. Sitewide is default.'; +$string['footerblocksplacement1'] = 'Sitewide'; +$string['footerblocksplacement2'] = 'Homepage Only'; +$string['footerblocksplacement3'] = 'Never'; + +$string['footerlayoutrow'] = 'Footer Layout Builder'; +$string['footerlayoutrowdesc'] = 'Design your layout for footer block regions.'; + +$string['footnote'] = 'Footnote'; +$string['footnotedesc'] = 'Add text to the footer.'; + +$string['footerheader'] = 'Footer Title Section '; +$string['footerdesc'] = 'Add a title for footer section '; + +$string['footercontent'] = 'Footer Content Section '; +$string['footercontentdesc'] = 'Add content to footer section '; + +$string['hidefootersocial'] = 'Show social icons'; +$string['hidefootersocialdesc'] = 'Show social icons in the footer below the blocks.'; + +// Data Retention Summary Button. +$string['gdprbutton'] = 'Data Retention Summary button'; +$string['gdprbuttondesc'] = 'Display the Data Retention Summary button in the footer.'; + +// Moodle Docs link. +$string['moodledocs'] = 'Moodle Docs link'; +$string['moodledocsdesc'] = 'Display the Moodle Docs link in the footer.'; + + +// NavBar **********************************************************. +$string['stickynavbar'] = 'Sticky Navbar at the top'; +$string['stickynavbardesc'] = 'Stick the Navbar at the top of the screen when scrolling down.'; + +$string['navbarcachetime'] = 'Navbar Cache Time'; +$string['navbarcachetimedesc'] = 'The number of minutes the navigation bar is cached for.'; + +$string['navbarmenusettings'] = 'Navbar Custom Menu'; +$string['navbarmenusettingsheading'] = 'Customize the menu in the navigation bar. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a>.'; +$string['navbarmenusettingsdesc'] = 'Allows you to add a menu to the navigation bar.'; + +$string['navbarsettings'] = 'Navbar Settings'; +$string['navbarsettingsheading'] = 'Customize the navigation bar'; +$string['navbardesc'] = 'Allows you to control all of the elements that appear on the navigation bar.'; + +$string['navbarstyles'] = 'Navbar Styles'; +$string['navbarstylesheading'] = 'Customize the navigation bar styles'; +$string['navbarstylesdesc'] = 'Allows you to control the styles of the elements that appear on the navigation bar.'; + +$string['navbarlinkssettings'] = 'Navbar Links'; +$string['navbarlinksettingsheading'] = 'Customize the links on the navigation bar'; +$string['navbarlinksettingsdesc'] = 'Allows you to control the links that appear on the navigation bar.'; + +$string['navbardisplayicons'] = 'Display icons'; +$string['navbardisplayiconsdesc'] = 'Display icons next to main menu headings'; + +$string['navbardisplaysubmenuarrow'] = 'Display sub-menu arrow'; +$string['navbardisplaysubmenuarrowdesc'] = 'Display sub-menu arrow (downward facing) when a menu heading has sub-menu options.'; + +$string['home'] = 'Home'; +$string['enablemy'] = 'Dashboard'; +$string['enablemydesc'] = 'Display a link to the Dashboard page'; + +$string['enableprofile'] = 'User Profile'; +$string['enableprofiledesc'] = 'Display a link to the users profile'; + +$string['enableeditprofile'] = 'Edit Profile'; +$string['enableeditprofiledesc'] = 'Display a link to edit the users profile'; + +$string['enablebadges'] = 'Badges'; +$string['enablebadgesdesc'] = 'Display a link to the users badges'; + +$string['enablegrades'] = 'Grades'; +$string['enablegradesdesc'] = 'Display a link to the users grades'; + +$string['enablecalendar'] = 'User Calendar'; +$string['enablecalendardesc'] = 'Display a link to the users calendar'; + +$string['enableprivatefiles'] = 'Private Files'; +$string['enableprivatefilesdesc'] = 'Display a link to the users private files'; + +$string['enablesearchbox'] = 'Enable Search Box'; +$string['enablesearchboxdesc'] = 'Display a search box in the header'; + +$string['searchcourses'] = 'Search Courses'; + +$string['enablepref'] = 'My Preferences'; +$string['enableprefdesc'] = 'Display a link to the user preferences page'; + +$string['enablenote'] = 'My Notifications'; +$string['enablenotedesc'] = 'Display a link to the user notifications page'; + +$string['enableblog'] = 'Enable My blogs'; +$string['enableblogdesc'] = 'Display a link to the users blogs page'; + +$string['enableposts'] = 'My Posts'; +$string['enablepostsdesc'] = 'Display a link to the my posts page'; + +$string['enablefeed'] = 'My Feedback'; +$string['enablefeeddesc'] = 'Display a link to the users "My Feedback" page - Note: this requires the <a href="https://moodle.org/plugins/report_myfeedback" target="blank">My Feedback Plugin</a>'; + +$string['enableaccesstool'] = 'Accessibility Tool'; +$string['enableaccesstooldesc'] = 'Display a link to the users "Accessibility Tool" Preferences page - Note: this requires the <a href="https://github.com/sharpchi/moodle-local_accessibilitytool" target="blank">Accessibility Tool Plugin</a>'; + +$string['myblogs'] = 'My Blogs'; + +$string['noenrolments'] = 'No enrolments found.'; + +$string['enablemyhomedesc'] = 'Display a link to {$a}'; +$string['enableeventsdesc'] = 'Display a link to the calendar'; + +$string['enablethiscoursedesc'] = 'Display a dropdown with activities from the current course.'; +$string['enablecoursesectionsdesc'] = 'Display a sub-menu on the \'This course\' menu containing links to each shown section.'; +$string['sections'] = 'Sections'; + +$string['enablecompetencieslink'] = 'Competencies link'; +$string['enablecompetencieslinkdesc'] = 'Display competencies link in the \'This course\' menu. Note: \'core_competency|enabled\' needs to be ticked.'; + +$string['search'] = 'Search'; +$string['togglenavigation'] = 'Toggle navigation'; + +// Navbar styling *********************************************************. +$string['navbardropdownborderradius'] = 'Dropdown menu border radius'; +$string['navbardropdownborderradiusdesc'] = 'Controls the border radius for dropdown menus (e.g. rounded corners).'; +$string['navbardropdownhovercolor'] = 'Dropdown menu background hover colour'; +$string['navbardropdownhovercolordesc'] = 'Dropdown menu background colour when hovering over menu items.'; +$string['navbardropdowntextcolor'] = 'Dropdown menu text colour'; +$string['navbardropdowntextcolordesc'] = 'Dropdown menu item text colour.'; +$string['navbardropdowntexthovercolor'] = 'Dropdown menu text hover colour'; +$string['navbardropdowntexthovercolordesc'] = 'Dropdown menu text colour when hovering over menu items.'; +$string['navbardropdowntransitiontime'] = 'Navbar transition time'; +$string['navbardropdowntransitiontimedesc'] = 'Navbar transition effect time in seconds. Provides a fade-in animation effect when hovering over a menu that has sub-menus.'; + +// This Course menu *********************************************************. +$string['enablemysitesdesc'] = 'Display a dropdown with the course activities and other options'; +$string['headernavbarthiscourseheading'] = 'This Course Menu'; +$string['headernavbarthiscourseheadingdesc'] = 'In this menu the student can access directly all the course activities and the participants list and his grades.'; + +$string['displayparticipants'] = 'Display Participants'; +$string['displayparticipantsdesc'] = 'Display the Participants item in the menu'; +$string['displaygrades'] = 'Display Grades'; +$string['displaygradesdesc'] = 'Display the Grades item in the menu'; + +// My courses menu *********************************************************. +$string['enablemysitesdesc'] = 'Display a dropdown with a users courses'; +$string['headernavbarmycoursesheading'] = 'My Courses Menu'; +$string['headernavbarmycoursesheadingdesc'] = 'All options for the My Courses (My Sites) menu option that displays list of current user's courses'; + +$string['enablemysitesrestriction'] = 'Restrict user's courses dropdown to a custom profile field'; +$string['enablemysitesrestrictiondesc'] = 'Restrict dropdown with a user's courses by custom profile field. E.g. usertype=staff'; + +$string['mysitessortoverride'] = 'Enable My Courses Custom Sort'; +$string['mysitessortoverridedesc'] = 'Use custom profile fields or generic strings (year etc) to collapse past courses in sub menu'; +$string['mysitessortoverridefield'] = 'My Courses Custom Profile Field(s) or Strings'; +$string['mysitessortoverridefielddesc'] = 'Comma delimited list of profile fields or strings to check for in course short codes'; + +$string['mysitessortoverrideoff'] = 'Display all enrolled courses in single flat list'; +$string['mysitessortoverridestrings'] = 'Display enrolled containing strings in first list, others in sub menu'; +$string['mysitessortoverrideprofilefields'] = 'Display courses found in profile fields in first list, others in sub menu'; +$string['mysitessortoverrideprofilefieldscohort'] = 'Display courses found in profile fields + cohorts in first list, others in sub menu'; +$string['mysitessortoverridemyoverview'] = 'Use list from my overview'; +$string['mysitessortoverridelast'] = 'Last accessed time or enrolment start time if never accessed'; + +$string['mysitesmaxlength'] = 'My Courses Max Length'; +$string['mysitesmaxlengthdesc'] = 'Adjust the max length of coursenames in the My Courses dropdown to optimise for your font.'; + +$string['mycoursesmenulimit'] = 'My Courses Menu Limit'; +$string['mycoursesmenulimitdesc'] = 'Adjust the max number of courses that appear in the My Courses dropdown. 0 will show all courses.'; + +$string['mysitesmenudisplay'] = 'My Courses Menu Display'; +$string['mysitesmenudisplaydesc'] = 'Choose what text to display for a menu item and on hover.'; +$string['mysitesmenudisplayshortcodenohover'] = 'Show short code and no text on hover.'; +$string['mysitesmenudisplayshortcodefullnameonhover'] = 'Show short code and full course name on hover.'; +$string['mysitesmenudisplayfullnamenohover'] = 'Show full course title and no text on hover.'; +$string['mysitesmenudisplayfullnamefullnameonhover'] = 'Show full course title and full title on hover.'; + +$string['enablehomedesc'] = 'Display a link to the frontpage.'; + +$string['enablehomeredirect'] = 'Enable Home redirect=0'; +$string['enablehomeredirectdesc'] = 'Enable redirect=0 on home. This is for use on sites where where My Home is default homepage. It prevents users clicking the Home link from being redirected back to My Home / Dashboard'; + +$string['chiddenicon'] = 'My courses hidden icon'; +$string['chiddenicondesc'] = 'The Font Awesome 4 icon without the prefixing \'fa-\' to be used for hidden courses. If empty, the default will be used.'; +$string['cfrozenicon'] = 'My courses frozen icon'; +$string['cfrozenicondesc'] = 'The Font Awesome 4 icon without the prefixing \'fa-\' to be used for frozen courses. If empty, the default will be used.'; +$string['cneveraccessedicon'] = 'My courses never accessed icon'; +$string['cneveraccessedicondesc'] = 'The Font Awesome 4 icon without the prefixing \'fa-\' to be used for courses that the user is enrolled on but not accessed. If empty, the default will be used.'; +$string['cdefaulticon'] = 'My courses default icon'; +$string['cdefaulticondesc'] = 'The Font Awesome 4 icon without the prefixing \'fa-\' to be used for courses when they don\'t already have an icon. If empty, the default will be used.'; + + +// Colours *********************************************************. +$string['colorsettings'] = 'Colours'; +$string['colorsettingsheading'] = 'Modify the main colours used throughout the theme.'; +$string['colordesc'] = 'You can select the colours that you would like to use throughout the theme. Use Hex or any other standard notation. As an alternate option you can use transparent and inherited as a value'; +$string['linkcolor'] = 'Link Colour'; +$string['linkcolordesc'] = 'Set the colour of links in the theme, use html hex code.'; + +$string['linkhover'] = 'Link Hover colour'; +$string['linkhoverdesc'] = 'Set the colour of links (on hover) in the theme, use html hex code.'; + +$string['backcolor'] = 'Background colour'; +$string['backcolordesc'] = 'Set the background colour.'; + +$string['regionmaincolor'] = 'Main region colour'; +$string['regionmaincolordesc'] = 'Set the background colour for main content area.'; + +$string['maincolor'] = 'Main colour'; +$string['maincolordesc'] = 'Main colour for blocks and footer.'; + +$string['footertextcolor'] = 'Footer text colour'; +$string['footertextcolordesc'] = 'The colour of the text in the footer, use html hex code.'; + +$string['footerbkcolor'] = 'Footer background colour'; +$string['footerbkcolordesc'] = 'Set the footer background colour.'; + +$string['footertextcolor2'] = 'Lower footer text colour'; +$string['footertextcolor2desc'] = 'Lower footer text colour.'; + +$string['footerlinkcolor'] = 'Footer blocks link colour'; +$string['footerlinkcolordesc'] = 'Footer blocks link colour.'; + +$string['headerbkcolor'] = 'Top header background colour'; +$string['headerbkcolordesc'] = 'Set the top header background colour.'; + +$string['msgbadgecolor'] = 'Message badge background colour'; +$string['msgbadgecolordesc'] = 'Set the background colour for the messages badge / bubble in the header (displays number of unread messages)'; + +$string['messagingbackgroundcolor'] = 'Messages main window background colour'; +$string['messagingbackgroundcolordesc'] = 'Set the background colour for the messages main chat window.'; + +$string['headerbkcolor2'] = 'Lower header background colour'; +$string['headerbkcolor2desc'] = 'Set the lower header background colour. Note that this also sets the colour for the background in Header style 2.'; + +$string['headertextcolor'] = 'Top Header blocks text and link colour'; +$string['headertextcolordesc'] = 'Set the top header blocks text and link colour.'; + +$string['headertextcolor2'] = 'Lower Header blocks text and link colour'; +$string['headertextcolor2desc'] = 'Set the lower header blocks text and link colour.'; + +$string['blockheadercolor'] = 'Block header font colour'; +$string['blockheadercolordesc'] = 'Set the block header font colour.'; + +$string['blockbackgroundcolor'] = 'Block background colour'; +$string['blockbackgroundcolordesc'] = 'Set the background colour for all blocks.'; + +$string['blockheaderbackgroundcolor'] = 'Block heading background colour'; +$string['blockheaderbackgroundcolordesc'] = 'Set the heading background colour for all blocks.'; + +$string['blockbordercolor'] = 'Block border colour'; +$string['blockbordercolordesc'] = 'Set the block border colour.'; + +$string['blockregionbackground'] = 'Block Region Backround Colour'; +$string['blockregionbackgrounddesc'] = 'Background colour of container holding custom block layouts on the front page'; + +$string['blockheaderbordertop'] = 'Block header top border thickness'; +$string['blockheaderbordertopdesc'] = 'Set the thickness of the top border of block headers'; + +$string['blockheaderborderleft'] = 'Block header left border thickness'; +$string['blockheaderborderleftdesc'] = 'Set the thickness of the left hand border of block headers'; + +$string['blockheaderborderright'] = 'Block header right border thickness'; +$string['blockheaderborderrightdesc'] = 'Set the thickness of the right hand border of block headers'; + +$string['blockheaderborderbottom'] = 'Block header bottom border thickness'; +$string['blockheaderborderbottomdesc'] = 'Set the thickness of the bottom border of block headers'; + +$string['blockmainbordertop'] = 'Block main top border thickness'; +$string['blockmainbordertopdesc'] = 'Set the thickness of the top border of the main block area'; + +$string['blockmainborderleft'] = 'Block main left border thickness'; +$string['blockmainborderleftdesc'] = 'Set the thickness of the left hand border of the main block area'; + +$string['blockmainborderright'] = 'Block main right border thickness'; +$string['blockmainborderrightdesc'] = 'Set the thickness of the right hand border of the main block area'; + +$string['blockmainborderbottom'] = 'Block main bottom border thickness'; +$string['blockmainborderbottomdesc'] = 'Set the thickness of the bottom border of the main block area'; + +$string['blockheaderbordertopstyle'] = 'Block header border style'; +$string['blockheaderbordertopstyledesc'] = 'Set the style of the border of block headers'; + +$string['blockmainbordertopstyle'] = 'Block Main border style'; +$string['blockmainbordertopstyledesc'] = 'Set the style of the border of block content area'; + +$string['blockheadertopradius'] = 'Block header top radius'; +$string['blockheadertopradiusdesc'] = 'Set the radius of top header block to achieve a curved / rounded effect'; + +$string['blockheaderbottomradius'] = 'Block header bottom radius'; +$string['blockheaderbottomradiusdesc'] = 'Set the radius of bottom header block to achieve a curved / rounded effect'; + +$string['blockmaintopradius'] = 'Block main top radius'; +$string['blockmaintopradiusdesc'] = 'Set the top radius of main block area to achieve a curved / rounded effect'; + +$string['blockmainbottomradius'] = 'Block main bottom radius'; +$string['blockmainbottomradiusdesc'] = 'Set the bottom radius of main block area to achieve a curved / rounded effect'; + +$string['marketblockbordercolor'] = 'Marketing block border line colour'; +$string['marketblockbordercolordesc'] = 'Set the marketing block border line colour'; + +$string['marketblocksbackgroundcolor'] = 'Marketing blocks region background colour'; +$string['marketblocksbackgroundcolordesc'] = 'Set the Marketing blocks region background colour.'; + +$string['sectionheadingcolor'] = 'Section Heading Text Colour'; +$string['sectionheadingcolordesc'] = 'Set the colour for section headings text'; + +$string['collapsedtopicscoloursenabled'] = 'Collapsed Topics toggle fore and backgound colour settings'; +$string['collapsedtopicscoloursenableddesc'] = 'Use Collapsed Topics fore and bacground colour settings instead of Adaptable\'s \'sectionheadingcolor\' and \'coursesectionheaderbg\' settings.'; + +$string['homebk'] = 'Frontpage Background Image'; +$string['homebkdesc'] = 'Upload an image that will be a background image on the homepage.'; + +$string['editonbk'] = 'Editing and Customize this page button background'; +$string['editonbkdesc'] = 'Set the background colour for the editing and customize this page button'; + +$string['editoffbk'] = 'Editing and Customize this page OFF button background'; +$string['editoffbkdesc'] = 'Set the background colour for the editing and customize this page button in OFF state.'; + +$string['dividingline'] = 'Dividing line in header'; +$string['dividinglinedesc'] = 'The colour for the dividing line found in the header'; + +$string['dividingline2'] = 'Dividing line in footer'; +$string['dividingline2desc'] = 'The colour for the dividing line found in the footer'; + +$string['breadcrumbbackgroundcolor'] = 'Breadcrumb background colour'; +$string['breadcrumbbackgroundcolordesc'] = 'Set the background colour of the breadcrumb.'; + +$string['breadcrumbtextcolor'] = 'Breadcrumb text colour'; +$string['breadcrumbtextcolordesc'] = 'Set the text colour of the breadcrumb.'; + +$string['activebreadcrumb'] = 'Active breadcrumb background colour'; +$string['activebreadcrumbdesc'] = 'Set the background colour of the active breadcrumb colour, and remainder of the breadcrumb bar.'; + +$string['messagepopupbackground'] = 'Messages pop-up background colour'; +$string['messagepopupbackgrounddesc'] = 'Set the background colour of messages pop-up header.'; + +$string['messagepopupcolor'] = 'Messages pop-up text colour'; +$string['messagepopupcolordesc'] = 'Set the text colour of messages pop-up header.'; + +$string['menubkcolor'] = 'Main Menu background colour'; +$string['menubkcolordesc'] = 'Set a Main Menu background colour'; + +$string['menubordercolor'] = 'Main Menu bottom border colour'; +$string['menubordercolordesc'] = 'Set a Main Menu border bottom colour'; + +$string['menufontcolor'] = 'Main Menu font colour'; +$string['menufontcolordesc'] = 'Set a Main Menu font colour'; + +$string['menuhovercolor'] = 'Main Menu hover colour'; +$string['menuhovercolordesc'] = 'Set a Main Menu hover colour'; + +$string['mobilemenubkcolor'] = 'Mobile Menu background colour'; +$string['mobilemenubkcolordesc'] = 'Set the Main Menu background colour on mobile devices (collapsed)'; + +$string['mobileslidebartabbkcolor'] = 'Sidebar tab background colour'; +$string['mobileslidebartabbkcolordesc'] = 'The sidebar background colour on mobile devices (collapsed)'; + +$string['mobileslidebartabiconcolor'] = 'Sidebar tab icon colour'; +$string['mobileslidebartabiconcolordesc'] = 'The sidebar icon colour on mobile devices (collapsed)'; + +$string['selectiontext'] = 'Selection text colour'; +$string['selectiontextdesc'] = 'Set the text colour when a text in the screen is selected.'; + +$string['selectionbackground'] = 'Selection background colour'; +$string['selectionbackgrounddesc'] = 'Set the background colour when a text in the screen is selected.'; + +// Course Formats *********************************************************. +$string['coursesettings'] = 'Course Formats'; +$string['coursesettingsheading'] = 'Course Formats Settings'; +$string['coursesettingsdesc'] = 'Customize some of the most used Moodle course formats to fit the main design.'; + +// Common settings. +$string['showyourprogress'] = 'Show 'Your Progress' label '; +$string['showyourprogressdesc'] = 'Show / hide the 'Your Progress' label in the top of the course content. This label is only for information purposes and can be hidden. '; + +// Course Section background color. +$string['coursesectionbgcolor'] = 'Course Section Background'; +$string['coursesectionbgcolordesc'] = 'Set the background colour of the course section.'; + +// Topics / Weeks Settings. +$string['topicsweeks'] = 'Topics/Weeks course format'; +$string['topicsweeksdesc'] = 'Set styles for the Topics/Weeks course format'; + +$string['coursesectionheaderbg'] = 'Course Section Header Background'; +$string['coursesectionheaderbgdesc'] = 'Set the background colour of the course section headers'; + +$string['currentcolor'] = 'Current Course Section Highlight Colour'; +$string['currentcolordesc'] = 'Set the colour for the current course section highlight'; + +$string['coursesectionheaderborderstyle'] = 'Course Section Header Border Style'; +$string['coursesectionheaderborderstyledesc'] = 'Set the style of the course section header border (only bottom border is used as outer container also has border)'; + +$string['coursesectionheaderbordercolor'] = 'Course Section Header Border Colour'; +$string['coursesectionheaderbordercolordesc'] = 'Set the colour of the course section header border (only bottom border is used as outer container also has border)'; + +$string['coursesectionheaderborderwidth'] = 'Course Section Header Border Width'; +$string['coursesectionheaderborderwidthdesc'] = 'Set the width of the course section header border (only bottom border is used as outer container also has border)'; + +$string['coursesectionheaderborderradiustop'] = 'Course Header Section Border Radius Top'; +$string['coursesectionheaderborderradiustopdesc'] = 'Set the top radius of course section header borders (rounded corners)'; + +$string['coursesectionheaderborderradiusbottom'] = 'Course Header Section Border Radius Bottom'; +$string['coursesectionheaderborderradiusbottomdesc'] = 'Set the bottom radius of course section header borders (rounded corners)'; + +$string['coursesectionborderstyle'] = 'Course section border style'; +$string['coursesectionborderstyledesc'] = 'Set the border style of course sections'; + +$string['coursesectionborderwidth'] = 'Course Section Border Width'; +$string['coursesectionborderwidthdesc'] = 'Set the width of course section borders'; + +$string['coursesectionbordercolor'] = 'Course Section Border Colour'; +$string['coursesectionbordercolordesc'] = 'Set the border colour of course sections'; + +$string['coursesectionborderradius'] = 'Course Section Border Radius'; +$string['coursesectionborderradiusdesc'] = 'Set the radius of course section borders (rounded corners)'; + +// Course section activity styling. +$string['coursesectionactivityuseadaptableicons'] = 'Use Adaptable Icon Set'; +$string['coursesectionactivityuseadaptableiconsdesc'] = 'Turn this on to use Adaptable icons. If turned off, please also ensure you remove the directories adaptable/pix_plugins and adaptable/pix_core/f to use default Moodle icons.'; + +$string['coursesectionactivityiconsize'] = 'Course Section Activity Icon Size'; +$string['coursesectionactivityiconsizedesc'] = 'Set the icon size for activities / recursos (e.g. a value of 16px will set it at 16px x 16px).'; + +$string['coursesectionactivityheadingcolour'] = 'Course Section Activity Heading Colour'; +$string['coursesectionactivityheadingcolourdesc'] = 'The colour for clickable activities displayed on the course homepage.'; + +// These four settings actually refer to bottom border (it was originally all around border, but naming kept as it was originally). +$string['coursesectionactivityborderwidth'] = 'Course Section Activity Bottom Border Width'; +$string['coursesectionactivityborderwidthdesc'] = 'Set width of the border that appears at the bottom of a course section activity.'; +$string['coursesectionactivityborderstyle'] = 'Course Section Activity Bottom Border Style'; +$string['coursesectionactivityborderstyledesc'] = 'Set the style of the course section activity bottom border.'; +$string['coursesectionactivitybordercolor'] = 'Course Section Activity Bottom Border Colour '; +$string['coursesectionactivitybordercolordesc'] = 'Set the colour of the course section activity bottom border.'; +$string['coursesectionactivityleftborderwidth'] = 'Course Section Activity Left Border Width'; +$string['coursesectionactivityleftborderwidthdesc'] = 'Set width of the border that appears on the left of a course section activity.'; + +$string['coursesectionactivitycolors'] = 'Course Section Activity Options'; + +$string['coursesectionactivityassignleftbordercolor'] = 'Assignment activity left border display colour'; +$string['coursesectionactivityassignleftbordercolordesc'] = 'Set the colour of the left border.'; +$string['coursesectionactivityassignbgcolor'] = 'Assignment activity background colour'; +$string['coursesectionactivityassignbgcolordesc'] = 'Set the Assignment activity background colour. Type <strong>transparent</strong> in the box for transparency.'; + +$string['coursesectionactivityforumleftbordercolor'] = 'Forum activity left border display colour'; +$string['coursesectionactivityforumleftbordercolordesc'] = 'Set the colour of the left border.'; +$string['coursesectionactivityforumbgcolor'] = 'Forum activity background colour'; +$string['coursesectionactivityforumbgcolordesc'] = 'Set the Forum activity background colour. Type <strong>transparent</strong> in the box for transparency.'; + +$string['coursesectionactivityquizleftbordercolor'] = 'Quiz activity left border display colour'; +$string['coursesectionactivityquizleftbordercolordesc'] = 'Set the colour of the left border.'; +$string['coursesectionactivityquizbgcolor'] = 'Quiz activity background colour'; +$string['coursesectionactivityquizbgcolordesc'] = 'Set the Quiz activity background colour. Type <strong>transparent</strong> in the box for transparency.'; + +// Social Wall Settings. +$string['socialwall'] = 'Social Wall'; +$string['socialwallheading'] = 'Social Wall Settings'; +$string['socialwalldesc'] = 'Customise the appearance of the <a href="https://moodle.org/plugins/format_socialwall">Social Wall Course Format</a> (if in use on your site)'; + +$string['socialwallbackgroundcolor'] = 'Background colour'; +$string['socialwallbackgroundcolordesc'] = 'The background colour of a Social Wall course.'; + +$string['socialwallsectionradius'] = 'Border radius'; +$string['socialwallsectionradiusdesc'] = 'The border radius of Social Wall sections.'; + +$string['socialwallbordertopstyle'] = 'Border style'; +$string['socialwallbordertopstyledesc'] = 'The border style of Social Wall sections.'; + +$string['socialwallborderwidth'] = 'Border width'; +$string['socialwallborderwidthdesc'] = 'The border width of Social Wall sections.'; + +$string['socialwallbordercolor'] = 'Border colour'; +$string['socialwallbordercolordesc'] = 'The border colour of Social Wall sections.'; + +$string['socialwallactionlinkcolor'] = 'Action link colour'; +$string['socialwallactionlinkcolordesc'] = 'The colour of action links in Social Wall.'; + +$string['socialwallactionlinkhovercolor'] = 'Action link hover colour'; +$string['socialwallactionlinkhovercolordesc'] = 'The colour of action links when hovered in Social Wall.'; + +// Blocks General **************************************************. +$string['shownavigationblockoncoursepage'] = 'Show Navigation Block on course page'; +$string['shownavigationblockoncoursepagedesc'] = 'Set this to show the navigation block on the course page.'; + +// Fonts ***********************************************************. +$string['fontsettings'] = 'Fonts'; +$string['fontsettingsheading'] = 'Modify the fonts used throughout the theme.'; +$string['fontdesc'] = 'You can select the <a href="https://www.google.com/fonts" target="_blank">Google Fonts</a> that you would like to use throughout the theme. Select the subset needed (latin is always included) and enter the right font weight or the font will not displayed.'; + +$string['fontname'] = 'Main font'; +$string['fontnamedesc'] = 'Select the default font, \'sans-serif\', or <a href="https://www.google.com/fonts" target="_blank">Google Fonts</a> used in the site.'; + +$string['customfontname'] = 'Custom Main font'; +$string['customfontnamedesc'] = 'Enter the name of the custom Main Font only if you selected 'Custom' in the Main Font dropdown.'; + +$string['fontsize'] = 'Main font size'; +$string['fontsizedesc'] = 'Select the default font size (in percentage) used in the whole site.'; + +$string['fontheadername'] = 'Headers font'; +$string['fontheadernamedesc'] = 'Select the default font, \'sans-serif\', or <a href="https://www.google.com/fonts" target="_blank">Google Fonts</a> used in the text and blocks headers.'; + +$string['customfontheadername'] = 'Custom Header font'; +$string['customfontheadernamedesc'] = 'Enter the name of the custom Header font only if you selected 'Custom' in the Header Font dropdown.'; + +$string['fontcolor'] = 'Main font colour'; +$string['fontcolordesc'] = 'Set the colour of the font in the theme, use html hex code.'; + +$string['fontheadercolor'] = 'Headers font colour'; +$string['fontheadercolordesc'] = 'Set the colour of the headers font in the theme, use html hex code.'; + +$string['fontweight'] = 'Main font weight'; +$string['fontweightdesc'] = 'Font weight used in site. Select a value from 100 to 900 depending the font selected.'; + +$string['fontheaderweight'] = 'Headers font weight'; +$string['fontheaderweightdesc'] = 'Headers font weight used in the site. Select a value from 100 to 900 depending the font selected.'; + +$string['fonttitlename'] = 'Site / Course title font'; +$string['fonttitlenamedesc'] = 'Select the default font, \'sans-serif\', or <a href="https://www.google.com/fonts" target="_blank">Google Fonts</a> used in title site and course titles.'; + +$string['customfonttitlename'] = 'Custom Title font'; +$string['customfonttitlenamedesc'] = 'Enter the name of the custom Title Font only if you selected 'Custom' in the Title Font dropdown.'; + +$string['fonttitlecolor'] = 'Site / Course title font colour'; +$string['fonttitlecolordesc'] = 'Set the colour of the site title and course title font in the theme, use html hex code.'; + +$string['fonttitleweight'] = 'Site / Course title font weight'; +$string['fonttitleweightdesc'] = 'Set the font weight used in the site title and course titles. Select a value from 100 to 900 depending the font selected.'; + +$string['fonttitlesize'] = 'Site / Course title font size'; +$string['fonttitlesizedesc'] = 'Site title and course title font size used in site. Select a value fron the list.'; + +$string['fonttitlecolorcourse'] = 'Course title font colour'; +$string['fonttitlecolorcoursedesc'] = 'Set the colour of the course title font in the theme, use html hex code.'; + +$string['fontsubset'] = 'Google Fonts subset'; +$string['fontsubsetdesc'] = 'Select other character subset than latin to be applied to all the fonts. Latin subset is already included by default.'; + +$string['menufontsize'] = 'Font size for navigation bar'; +$string['menufontsizedesc'] = 'Set the size of the font used in the main navigation bar.'; + +$string['menufontpadding'] = 'Padding for Navigation Items'; +$string['menufontpaddingdesc'] = 'Set the padding of the items in the main navigation bar.'; + +$string['fontblockheadercolor'] = 'Blocks header font colour'; +$string['fontblockheadercolordesc'] = 'Set the colour of the header font moodle blocks, use html hex code. Note that this affects icon colour too.'; + +$string['fontblockheaderweight'] = 'Blocks header font weight'; +$string['fontblockheaderweightdesc'] = 'Set the font weight used in the moodle blocks header. Select a value from 100 to 900 depending the font selected.'; + +$string['fontblockheadersize'] = 'Blocks header font size'; +$string['fontblockheadersizedesc'] = 'Set the font size used in the moodle blocks header. Select a value fron the list.'; + + +// Icons ***********************************************************. +$string['blockicons'] = 'Block Icons'; +$string['blockiconsdesc'] = 'Set this to show block icons in the block header area.'; + +$string['blockiconsheadersize'] = 'Blocks header icon size'; +$string['blockiconsheadersizedesc'] = 'Set the font icon size used in the moodle blocks header. Select a value fron the list.'; + + +// Buttons *********************************************************. +$string['buttonsettings'] = 'Buttons'; +$string['buttonsettingsheading'] = 'Customize the buttons of this theme.'; +$string['buttondesc'] = 'Alter the appearance of buttons used in this theme.'; + +$string['buttonradius'] = 'Set Button Radius'; +$string['buttonradiusdesc'] = 'Higher radius = curved buttons, lower radius = square buttons'; + +$string['buttoncolor'] = 'Button colour'; +$string['buttoncolordesc'] = 'The colour of the main buttons used throughout the site.'; + +$string['buttonhovercolor'] = 'Button colour (When hovering)'; +$string['buttonhovercolordesc'] = 'The colour that the button changes to when hovering over the button.'; + +$string['buttontextcolor'] = 'Button text colour'; +$string['buttontextcolordesc'] = 'The colour of text used on buttons'; + +$string['buttoncolorscnd'] = 'Secondary Button colour'; +$string['buttoncolordescscnd'] = 'The colour of the secondary buttons used throughout the site.'; + +$string['buttonhovercolorscnd'] = 'Secondary Button colour (When hovering)'; +$string['buttonhovercolordescscnd'] = 'The colour that the secondary button changes to when hovering over the button.'; + +$string['buttontextcolorscnd'] = 'Secondary Button text colour'; +$string['buttontextcolordescscnd'] = 'The colour of text used on secondary buttons.'; + +$string['buttoncolorcancel'] = 'Cancel Button colour'; +$string['buttoncolordesccancel'] = 'Background colour for Cancel button.<br />Type transparent in the box for transparency.'; + +$string['buttonhovercolorcancel'] = 'Cancel Button colour (When hovering)'; +$string['buttonhovercolordesccancel'] = 'The colour that the cancel button changes to when hovering over the button. <br />Type transparent in the box for transparency.'; + +$string['buttontextcolorcancel'] = 'Cancel Button text colour'; +$string['buttontextcolordesccancel'] = 'The colour of text used on cancel buttons.'; + +$string['editfont'] = 'Editing and Customize this page button font colour'; +$string['editfontdesc'] = 'Set the Editing and Customize this page button font colour'; + +$string['editverticalpadding'] = 'Set vertical padding of editing buttons'; +$string['edithorizontalpadding'] = 'Set Horizontal padding of editing buttons'; + +$string['buttondropshadow'] = 'Drop shadow decoration on bottom of button'; +$string['buttondropshadowdesc'] = 'Show a drop shadow (shading) on bottom of button.'; +$string['none'] = 'None'; +$string['slight'] = 'Slight'; +$string['standard'] = 'Standard'; + +// Login button. +$string['logintextbutton'] = 'Log In'; + +$string['buttonlogincolor'] = 'Login button colour'; +$string['buttonlogincolordesc'] = 'The colour of the login button.'; + +$string['buttonloginhovercolor'] = 'Login button hover colour'; +$string['buttonloginhovercolordesc'] = 'The hover colour of the login button.'; + +$string['buttonlogintextcolor'] = 'Login button text colour'; +$string['buttonlogintextcolordesc'] = 'The colour of text used on the login button.'; + +$string['buttonloginpadding'] = 'Set Padding for Log In Button'; +$string['buttonloginpaddingdesc'] = 'Higher number = bigger button'; + +$string['buttonloginheight'] = 'Set Height for Login Button'; +$string['buttonloginheightdesc'] = 'Only effective if using a login form in the upper header'; + +$string['buttonloginmargintop'] = 'Set Top Margin for Login Button'; +$string['buttonloginmargintopdesc'] = 'Allows spacing / position of login button to be altered'; + +$string['loginplaceholder'] = 'Username'; +$string['passwordplaceholder'] = 'Password'; + + +// Header ***********************************************************. +$string['headersettings'] = 'Header'; +$string['headersettingsheading'] = 'Customize the header of this theme. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a>'; +$string['headerdesc'] = 'Upload your favicon, logo, set login form in header, adjust titles in header.<br /> +You can set font size and styles for titles in the <a href="./../admin/settings.php?section=theme_adaptable_font">fonts</a> settings page.'; + +$string['headerbgimage'] = 'Background image'; +$string['headerbgimagedesc'] = 'Set a background image in the header. Minimum size is 1600x180px (1900x180px recommended). The image cover the full header. You can add a colour in 'Top header background colour' or use <i>transparent</i> to show the background image. In that case, modify the text colour to get displayed correctly over the image.'; + +$string['enableheading'] = 'Header course title'; +$string['enableheadingdesc'] = 'Set the mode to display the course title in the header when the default Moodle site title is enabled.'; + +$string['breadcrumbdisplay'] = 'Breadcrumb display'; +$string['breadcrumbdisplaydesc'] = 'Set the display of what should be in the breadcrumb area in a course.'; + +$string['sitetitlecoursesdisabled'] = 'Disabled - only show course titles in course pages'; +$string['sitetitlecoursesenabled'] = 'Enabled - show site title and course titles in course pages'; + +$string['coursetitlemaxwidth'] = 'Course Title Maximum Length'; +$string['coursetitlemaxwidthdesc'] = 'Set the maximum number of characters of the course title area'; + +$string['pageheaderheight'] = 'Page Header Height'; +$string['pageheaderheightdesc'] = 'Set the height of the main header area (containing logo and titles)'; + +$string['coursepageheaderhidesitetitle'] = 'Hide site title on course pages'; +$string['coursepageheaderhidesitetitledesc'] = 'Hide site title, logo and search bar on course pages. Use this along with page header height setting to show a smaller header on course-related pages.'; + +$string['breadcrumb'] = 'Breadcrumb'; +$string['breadcrumbtitle'] = 'Breadcrumb course name'; +$string['breadcrumbtitledesc'] = 'Set the mode to display the course title in the breadcrumb.'; + +$string['coursetitlefullname'] = 'Course Full Name'; +$string['coursetitleshortname'] = 'Course Short Name / Code'; + +$string['headerstyleheading'] = 'Header Style Settings'; +$string['headerstyleheadingdesc'] = 'Adaptable supports two header styles, the original three row header and a newer simplified two row header. +Be aware that if you switch to the newer two row header you will <strong>NOT</strong> be able to: <br /> +<ol><li>Show social icons in the header</li><li>Display the site logo on mobile devices</li></ol>'; + +$string['headerstyle'] = 'Header style'; +$string['headerstyledesc'] = 'Choose the style of header. Header 1 refers to the original 3 row Adaptable header. Header 2 is a minimal 2 row header. Note that when using Header 2, for the setting "Use Search Box or Social Icons", this will always use a search box.'; +$string['headerstyle1'] = 'Header 1 (original 3 row header)'; +$string['headerstyle2'] = 'Header 2 (2 row header)'; + +$string['header2searchbox'] = 'Header 2 search box expandable'; +$string['header2searchboxdesc'] = 'Disabled, static or expand and collapse functionality on the search box when using header 2.'; + +$string['socialorsearch'] = 'Use Search Box or Social Icons'; +$string['socialorsearchdesc'] = 'You can set the theme to either display social icons or a search box in the header. <br /> +You can set social icons under the <a href="./../admin/settings.php?section=theme_adaptable_social">Header Social</a> settings page</strong>.'; + +$string['socialorsearchnone'] = 'None'; + +$string['socialorsearchsocial'] = 'Display social icons in header'; +$string['socialorsearchsearch'] = 'Display search box in header'; + +$string['searchboxpadding'] = 'Search box padding'; +$string['searchboxpaddingdesc'] = 'Set padding above search box (if being used instead of social icons) <br />E.g. 5px 10px 5px 10px (top, right, bottom, left).<br> You can set social icons under the <a href="./../admin/settings.php?section=theme_adaptable_social">Header Social</a> settings page</strong>.'; + +$string['enablesavecanceloverlay'] = 'Enable Save / Cancel overlay on settings pages'; +$string['enablesavecanceloverlaydesc'] = 'Display a Save / Cancel button overlay at the top of a settings page to make it easier to save settings.'; + +$string['usernavheading'] = 'Customize the user navigation dropdown'; +$string['usernav'] = 'Header User'; +$string['usernavdesc'] = 'Allows you to control all of the elements that appear in the user navigation dropdown.'; + +$string['showusername'] = 'Show username'; +$string['showusernamedesc'] = 'Show the username on the user menu on the navbar.'; + +$string['usernameposition'] = 'User name position'; +$string['usernamepositiondesc'] = 'Set the postion of the userame, \'Left\' or \'Right\'.'; + +$string['menusettings'] = 'Header Menus'; +$string['menusettingsheading'] = 'Customize menus in the upper header the header of this theme.'; + +$string['logo'] = 'Logo'; +$string['logodesc'] = 'Upload a logo for use on your site. Recommended size is 200px by 80px.'; + +$string['favicon'] = 'Favicon'; +$string['favicondesc'] = 'Upload a favicon for use on your site,'; + +$string['enableavailablecourses'] = 'Display "Available Courses"'; +$string['enableavailablecoursesdesc'] = 'Display "Available Courses" text in the frontpage'; + +$string['thiscourse'] = 'This course'; +$string['coursesections'] = 'Course sections'; + +$string['loadingcolor'] = 'Loading colour'; +$string['loadingcolordesc'] = 'The loading bar colour in the top of the page'; + +$string['sitetitle'] = 'Display site title'; +$string['sitetitledesc'] = 'Display the default Moodle site title from <a href="./../admin/settings.php?section=frontpagesettings" target="_blank">Front Page Settings</a> or enter a custom site title in the text box below.'; + +$string['sitetitleoff'] = 'Disable site title'; +$string['sitetitledefault'] = 'Use Moodle site title (site name)'; +$string['sitetitlecustom'] = 'Use custom site title (enter in the \'sitetitletext\' box below)'; + +$string['pageheaderlayout'] = 'Page header layout'; +$string['pageheaderlayoutdesc'] = 'The page header layout. Only affects header style one. Header style two unchanged. Note: \'Alternative\' is subject to change without notice and is still in development.'; +$string['pageheaderoriginal'] = 'Original'; +$string['pageheaderalternative'] = 'Alternative'; + +$string['sitetitletext'] = 'Site title'; +$string['sitetitletextdesc'] = 'Site title displayed in the header. You can use any HTML tag and apply inline styles. It is recommended to use an h1 HTML tag around the title, instead of the p tag that gets added as default when you type something in this field.'; + +$string['displaylogin'] = 'Display login'; +$string['displaylogindesc'] = 'Select how to display the login box in header.'; + +$string['displayloginbutton'] = 'Button'; +$string['displayloginbox'] = 'Login Box'; +$string['displayloginno'] = 'No Login Box'; + +$string['hideblocks'] = 'Hide blocks'; +$string['showblocks'] = 'Show blocks'; +$string['fullscreen'] = 'Full screen'; +$string['standardview'] = 'Standard view'; +$string['sitelinkslabel'] = 'Site links'; + +$string['enablezoom'] = 'Enable Zoom'; +$string['enablezoomdesc'] = 'Allow users to toggle between fullscreen and fixed width.'; +$string['enablezoomshowtext'] = 'Show text for Enable Zoom'; +$string['enablezoomshowtextdesc'] = 'Shows the text beside the button.'; + +$string['defaultzoom'] = 'Default Zoom'; +$string['defaultzoomdesc'] = 'Default screen size when enable zoom is disabled or the user has not made a preference. Choose between fullscreen and fixed width.'; +$string['normal'] = 'Fixed width'; +$string['wide'] = 'Fullscreen'; + +$string['enableshowhideblocks'] = 'Enable Show Hide Blocks'; +$string['enableshowhideblocksdesc'] = 'Allows users to show / hide all blocks.'; +$string['enableshowhideblockstext'] = 'Show text for Show Hide Blocks'; +$string['enableshowhideblockstextdesc'] = 'Shows the text beside the button.'; + +$string['enablenavbarwhenloggedout'] = 'Enable Navbar when logged out'; +$string['enablenavbarwhenloggedoutdesc'] = 'Shows the navbar even when logged out. Limited to Help and Tools menu only.'; + +$string['fullscreenwidth'] = 'Full screen width.'; +$string['fullscreenwidthdesc'] = 'Set the max width of the theme when it is in full screen mode / zoom.'; + +$string['standardscreenwidth'] = 'Standard screen width.'; +$string['standardscreenwidthdesc'] = 'Set the width of the screen when toggled to "standard" view (also see related enablezoom and defaultzoom settings).'; + +// Help Links ******************************************************. +$string['headernavbarhelpheading'] = 'Help Links and Options'; +$string['headernavbarhelpheadingdesc'] = 'Options for help menu links.'; + +$string['helplinkscount'] = 'Number of help links'; +$string['helplinkscountdesc'] = 'Set the number of help links you want to add to the main navigation bar.'; + +$string['enablehelp'] = 'Help Link \'{$a->number}\''; +$string['enablehelpdesc'] = 'Add a help link \'{$a->number}\' in the navbar.'; + +$string['helplinktitle'] = 'Help Link \'{$a->number}\' title'; +$string['helplinktitledesc'] = 'The title for help link \'{$a->number}\' in the navbar. If empty then defaults to \'Help {$a->number}\'.'; + +$string['helptitle'] = 'Help {$a->number}'; + +$string['helpprofilefield'] = 'Help link \'{$a->number}\' custom profile field (optional)'; +$string['helpprofilefielddesc'] = 'Add help link \'{$a->number}\' access rule using for custom profile field eg: usertype=student'; + + +// Courses Overlay *************************************************. +$string['rendereroverlaycolor'] = 'Overlay colour'; +$string['rendereroverlaycolordesc'] = 'The colour of the overlay, when the "Tiles w/ overlay" renderer is selected.'; + +$string['rendereroverlayfontcolor'] = 'Overlay font colour'; +$string['rendereroverlayfontcolordesc'] = 'The colour of the font, when hovering over a coursebox with "Tiles w/ overlay" renderer enabled.'; + +$string['covbkcolor'] = 'Coventry tiles title background colour'; +$string['covbkcolordesc'] = 'Set the title background colour with the Coventry Tiles renderer is selected.'; + +$string['covfontcolor'] = 'Coventry tiles title font colour'; +$string['covfontcolordesc'] = 'Set the title font colour with the Coventry Tiles renderer is selected.'; + +$string['covhidebutton'] = 'Coventry tiles course button'; +$string['covhidebuttondesc'] = 'Set this to hide the course button when Coventry Tiles is selected.'; + +$string['frontpagerendererdefaultimage'] = 'Default course image'; +$string['frontpagerendererdefaultimagedesc'] = 'The default image to use when no course image is found, (only applies for Tiles w/ overlay)'; + +$string['tilesshowcontacts'] = 'Show course contacts'; +$string['tilesshowcontactsdesc'] = 'Show / hide course contacts'; + +$string['tilesbordercolor'] = 'Coursebox tiles border colour'; +$string['tilesbordercolordesc'] = 'Set a colour for the coursebox tiles border'; + +$string['tilescontactstitle'] = 'Show course contacts role'; +$string['tilescontactstitledesc'] = 'Show / hide the role of a course contact. If not displayed, an image will be displayed before each contact'; + +$string['tilesshowallcontacts'] = 'Show all course contacts'; +$string['tilesshowallcontactsdesc'] = 'Show all course contacts or just one'; + +$string['course'] = 'Course'; + + +// Alerts **********************************************************. +// Alert message if acting as another role. +$string['actingasrole'] = 'You are currently acting as a different role'; + +// Alert Hidden Course. +$string['alerthiddencourse'] = 'Enable course alerts'; +$string['alerthiddencoursedesc'] = 'Display alerts in on course page.'; + +$string['alerthiddencoursetext-1'] = 'This course is hidden and cannot be accessed by students. '; +$string['alerthiddencoursetext-2'] = 'Click here to update settings'; + +// Alert Box Enable. +$string['enablealert'] = 'Enable Alert Box {$a}'; +$string['enablealertdesc'] = 'Enable Alert Box {$a}'; + +// Alert Box Generic Strings. +$string['alerttype'] = 'Alert Box Type'; +$string['alerttypedesc'] = 'Select the type of alert: info (blue), warning (yellow) or announcement (green)'; + +$string['alerttext'] = 'Alert text'; +$string['alerttextdesc'] = 'Enter the text to display in the Alert box'; + +$string['enablealerts'] = 'Enable / Disable Alerts'; +$string['enablealertsdesc'] = 'Enable / disable site alerts, not course, see the \'alerthiddencourse\' setting.'; + +$string['enablealertstriptags'] = 'Automatically strip html tags from alerts'; +$string['enablealertstriptagsdesc'] = 'Enable will clean up the alert messages automatically and disable to allow you to use html / links in messages but you will have to clean up manually in html view.'; + +$string['alertkeyvalue'] = 'Alert Key'; +$string['alertkeyvalue_details'] = 'The key that identifies this alert, from previous alerts. If you change this, all users who have dismissed the alert previously will see it again. If you change the alert, you will likely want to change this to ensure all users see it.'; + +$string['alertsettingscourse'] = 'Course Alert Settings'; + +$string['alertsettingsgeneral'] = 'General Alert Settings'; +$string['alertsettings'] = 'Alert Box {$a}'; + +$string['alertcount'] = 'Alert count'; +$string['alertcountdesc'] = 'The number of alerts to show in the edit area below.'; + +$string['alertsettingsheading'] = 'Customize top Alert Box. See the layout <a href="./../theme/adaptable/pix/layout.png" target="_blank"> here</a>'; +$string['alertdesc'] = 'Enter and customize a text to be displayed in the top of the site as an alert. It is possible to set more than one alert to target different user types. You also have the option of showing alerts sitewide or on homepages only. <br /><br /><strong>Note:</strong> it is now also possible to display alerts within course pages to warn teachers that courses are hidden.'; + +// Alerts Types. +$string['alertdisabled'] = 'Disabled'; +$string['alertdisabledesc'] = 'Disable this alert.'; + +$string['alertinfo'] = 'Info'; +$string['alertinfodesc'] = 'Display information in the Alert Box.'; + +$string['alertwarning'] = 'Warning'; +$string['alertwarningdesc'] = 'Display a warning in the Alert Box.'; + +$string['alertannounce'] = 'Announcement'; +$string['alertannouncedesc'] = 'Display an announcement in the Alert Box.'; + +$string['alertprofilefield'] = 'Custom Profile Field Name=Value (optional)'; +$string['alertprofilefielddesc'] = 'Add access rule using for custom profile field eg: usertype=student'; + +// Alert Access - Visibility. +$string['alertaccessglobal'] = 'Visible to everyone'; +$string['alertaccessusers'] = 'Visible to logged in users'; +$string['alertaccessadmins'] = 'Visible to administrators'; +$string['alertaccessprofile'] = 'Add custom profile field restriction'; + +$string['alertaccess'] = 'Alert Visibility'; +$string['alertaccessdesc'] = 'Set access restriction type for alert box visibility. Note: if using "Add custom profile field restriction" you will need to add values for profile fields below.'; + +// Moodle/Adaptable version alert messages. +$string['beta'] = 'DEVELOPMENT VERSION. DO NOT USE IN PRODUCTION SITES'; +$string['deprecated'] = 'MOODLE DEPRECATED VERSION. DO NOT USE ADAPTABLE IN THIS SITE'; + +// Alerts Colors****************************************************. +$string['alertcolorsheading'] = 'Customize top Alert Boxes'; +$string['alertcolorsheadingdesc'] = 'Set colours and icon.'; + +$string['alertcolorinfo'] = 'Info Colour'; +$string['alertcolorinfodesc'] = 'Icon colour of the Info type alert boxes'; +$string['alertbackgroundcolorinfo'] = 'Info Background Colour'; +$string['alertbackgroundcolorinfodesc'] = 'Background colour of the Info type alert boxes'; +$string['alertbordercolorinfo'] = 'Info Border Colour'; +$string['alertbordercolorinfodesc'] = 'Border colour of the Info type alert boxes'; +$string['alerticoninfo'] = 'Info Icon'; +$string['alerticoninfodesc'] = 'Set the <a href="http://fortawesome.github.io/Font-Awesome/icons/">Font Awesome Icon</a> to be used in Info type alert boxes. Enter the icon name without the fa- prefix.'; + +$string['alertcolorwarning'] = 'Warning Colour'; +$string['alertcolorwarningdesc'] = 'Icon colour of the Warning type alert boxes'; +$string['alertbackgroundcolorwarning'] = 'Warning Background Colour'; +$string['alertbackgroundcolorwarningdesc'] = 'Background colour of the Warning type alert boxes'; +$string['alertbordercolorwarning'] = 'Warning Border Colour'; +$string['alertbordercolorwarningdesc'] = 'Border colour of the Warning type alert boxes'; +$string['alerticonwarning'] = 'Warning Icon'; +$string['alerticonwarningdesc'] = 'Set the <a href="http://fortawesome.github.io/Font-Awesome/icons/">Font Awesome Icon</a> to be usedin in Warning type alert boxes. Enter the icon name without the fa- prefix.'; + +$string['alertcolorsuccess'] = 'Announcement Colour'; +$string['alertcolorsuccessdesc'] = 'Icon colour of the Announcement type alert boxes'; +$string['alertbackgroundcolorsuccess'] = 'Announcement Background Colour'; +$string['alertbackgroundcolorsuccessdesc'] = 'Background colour of the Announcement type alert boxes'; +$string['alertbordercolorsuccess'] = 'Announcement Border Colour'; +$string['alertbordercolorsuccessdesc'] = 'Border colour of the Announcement type alert boxes'; +$string['alerticonsuccess'] = 'Announcement Icon'; +$string['alerticonsuccessdesc'] = 'Set the <a href="http://fortawesome.github.io/Font-Awesome/icons/">Font Awesome Icon</a> to be usedin in Announcement type alert boxes. Enter the icon name without the fa- prefix.'; + +// Mobile **********************************************************. +$string['responsivesettings'] = 'Layout Responsive'; +$string['responsivesettingsheading'] = 'Control how your site behaves at different screen sizes'; +$string['responsivesettingsdesc'] = 'Here you can control the responsive behaviour of your site (which elements appear on screens of different sizes).<br/> + By default most non essential items are hidden on mobile devices, being set to appear only on larger screens.<br/> + You can make elements appears on smaller screens by choosing to display them on "Extra Small" or "Small screens".<br/> + To gain a better understanding of how these settings work please refer to the Bootstrap 4 documentation:<br/><br/> + https://getbootstrap.com/docs/4.0/utilities/display/ <br/><br/>'; + +$string['responsivesocial'] = 'Social Icons'; +$string['responsivesocialdesc'] = 'What sized screens would you like social icons to be displayed on?'; + +$string['responsivecoursetitle'] = 'Course / Site Title'; +$string['responsivecoursetitledesc'] = 'What sized screens would you like the Site / Course titles to be displayed on?'; + +$string['responsivesectionnav'] = 'Activity / Section Nagivation'; +$string['responsivesectionnavdesc'] = 'Show / Hide the the <strong>text</strong> for "prev" "next" activty / section navigation on small screens. + By default we hide this text on smaller screens so only the icons for <prev> <next> links display and not the full section / activity name.'; + +$string['responsivelogo'] = 'Logo'; +$string['responsivelogodesc'] = 'What sized screens would you like the logo to be displayed on?'; + +$string['responsiveheader'] = 'Main Header'; +$string['responsiveheaderdesc'] = 'What sized screens would you like the main header to be displayed on?<br/> + This setting only applies if you are using the default / original 3 row header.<br/> + Removing the header entirely will also remove elements contained within it:<br/> + <ul><li>Social Icons</li><li>Logo</li><li>Site / Course Title</li></ul>'; + +$string['responsiveticker'] = 'News Ticker'; +$string['responsivetickerdesc'] = 'What sized screens would you like the News Ticker to be displayed on?'; + +$string['responsivepagefooter'] = 'Footer'; +$string['responsivepagefooterdesc'] = 'What sized screens would you like the Footer to be displayed on?'; + +$string['responsiveslider'] = 'Frontpage Slider'; +$string['responsivesliderdesc'] = 'What sized screens would you like the Frontpage Slider to be displayed on?'; + +$string['responsivesearchicon'] = 'Show search icon'; +$string['responsivesearchicondesc'] = 'Show search icon on small screen devices.'; +$string['responsivebreadcrumb'] = 'Beadcrumb Navigation'; +$string['responsivebreadcrumbdesc'] = 'What sized screens would you like Breadcrumb Navigation to be displayed on?'; + +// Bootstrap class descriptions used in array definitions. +$string['bs4all'] = 'Extra Small - Extra Large'; +$string['bs4small'] = 'Small - Extra Large'; +$string['bs4medium'] = 'Medium - Extra Large'; +$string['bs4large'] = 'Large - Extra Large'; +$string['bs4extralarge'] = 'Extra Large Only'; +$string['bs4none'] = 'None'; + +// Layout **********************************************************. +$string['layoutsettings'] = 'Layout'; +$string['layoutdesc'] = 'Set the default layout that users see.'; +$string['layoutsettingsheading'] = 'Control aspects of the site\'s layout'; + +$string['blockside'] = 'Location of the blocks'; +$string['blocksidedesc'] = 'Control whether blocks appear on the left or right of the page'; + +$string['rightblocks'] = 'Right side'; +$string['leftblocks'] = 'Left side'; + +$string['sidebarnotlogged'] = 'Show sidebar when not logged'; +$string['sidebarnotloggeddesc'] = 'Show the blocks sidebar when the user is not logged'; + +$string['emoticonsize'] = 'Emoticons size'; +$string['emoticonsizedesc'] = 'Set the height and width of the moodle emoticons.'; + +$string['infoiconcolor'] = 'Help Icon colour'; +$string['infoiconcolordesc'] = 'Set the colour of the info/help icon used by tooltips.'; + +$string['dangericoncolor'] = 'Warning Icon colour'; +$string['dangericoncolordesc'] = 'Set the colour of the warning/danger icon mainly used in mandatory fields.'; + +$string['helptarget'] = 'Help target'; +$string['helptargetdesc'] = 'Do you want the help link to open in a new window?'; + +$string['hideinforum'] = 'Hide Help and Tools in Forums'; +$string['hideinforumdesc'] = 'When using fixed width and all menu options the forum search box spills onto the line below. Turning this option on will hide tools and help menu items in forums allowing it to display properly.'; + +$string['targetnewwindow'] = 'New window'; +$string['targetsamewindow'] = 'Same window'; + +$string['toolsmenu2'] = '2nd Tools menu'; +$string['toolsmenu2desc'] = 'You can configure links to be shown under a tools menu. Each line consists of some menu text, a link URL (optional), a tooltip title (optional) and a language code or comma-separated list of codes (optional, for displaying the line to users of the specified language only), separated by pipe characters. You can specify a structure using hyphens. For example: +<pre> +Moodle community|https://moodle.org +-Moodle free support|https://moodle.org/support +-Moodle development|https://moodle.org/development +--Moodle Docs|http://docs.moodle.org|Moodle Docs +--German Moodle Docs|http://docs.moodle.org/de|Documentation in German|de +Moodle.com|http://moodle.com/ +</pre>'; + +$string['toolsmenulabel'] = 'Tools'; +$string['toolsmenulabel2'] = 'Tools 2'; +$string['events'] = 'Events'; +$string['future'] = 'Future'; +$string['hiddencourses'] = 'Hidden Courses'; +$string['hiddenfromview'] = 'Hidden From View'; +$string['inprogress'] = 'In Progress'; +$string['mysites'] = 'My Courses'; +$string['past'] = 'Past'; +$string['pastcourses'] = 'Past Courses'; +$string['people'] = 'Participants'; +$string['help'] = 'Help'; + +$string['showfooterblocks'] = 'Show footer blocks'; +$string['showfooterblocksdesc'] = 'Show / hide the four configurable footer blocks'; + +$string['breadcrumbseparator'] = 'Breadcrumb separator'; +$string['breadcrumbseparatordesc'] = 'Set the <a href="https://fortawesome.github.io/Font-Awesome/icons/" target="_blank">Font Awesome Icon</a> to be used as item separator in the breadcrumb. enter the icon name without the fa- prefix.'; + +$string['breadcrumbhome'] = 'Breadcrumb home'; +$string['breadcrumbhomedesc'] = 'Display home breadcrumb as an icon or as a text.'; + +$string['breadcrumbhometext'] = 'Text'; +$string['breadcrumbhomeicon'] = 'Icon'; + +$string['mysitesexclude'] = 'Enable excluding hidden courses'; +$string['mysitesinclude'] = 'Enable including hidden courses'; +$string['mysitesdisabled'] = 'Disable'; + +$string['newstickercount'] = 'Number of News Ticker Sections'; +$string['newstickercountdesc'] = 'Define multiple news ticker sections with access rules to target different audiences'; + +$string['tickertext'] = 'News Ticker Text'; +$string['tickertextdesc'] = 'Add news ticker text in list format. See the read me for more info.'; + +$string['newmenu1trigger'] = 'Top Menu Dropdown 1 trigger word'; +$string['newmenu1triggerdesc'] = 'Set a Top Menu Dropdown 1 trigger word. Font awesome icons can be used'; + +$string['menusheading'] = 'Configure Navigation for link Menus in the upper header'; +$string['menustitledesc'] = 'Tools Menus (in navbar) and Top Menus (upper header) can be restricted based on custom profile fields (optional). To add a restriction enter the name of the profile field and expected value. Menu Structure follows the common Moodle format: +<pre> +Moodle community|https://moodle.org +-Moodle free support|https://moodle.org/support +-Moodle development|https://moodle.org/development +--Moodle Docs|http://docs.moodle.org|Moodle Docs +--German Moodle Docs|http://docs.moodle.org/de|Documentation in German|de +Moodle.com|http://moodle.com/ +</pre> +'; + +$string['menusession'] = 'Store access details in session'; +$string['menusessiondesc'] = 'For performance reasons it is suggested this is enabled. You may want to disable when testing'; + +$string['disablecustommenu'] = 'Disable Moodle Custom Menu'; +$string['disablecustommenudesc'] = 'Disable Moodle Custom Menus in the navigation bar (will still render in other themes you may have installed)'; + +$string['menusessionttl'] = 'Minutes to store access rules in session'; +$string['menusessionttldesc'] = 'Number of minutes after which menu access rules are refreshed in the users session.'; + + +// Tool menus ******************************************************. +$string['newmenudesc'] = 'Configure links to be shown under a top header menu.'; +$string['newmenufield'] = 'Custom Profile Field Name=Value (optional)'; +$string['newmenufielddesc'] = 'Add access rule using for custom profile field eg: usertype=student'; +$string['newmenurequirelogin'] = 'Require login'; +$string['newmenurequirelogindesc'] = 'If enabled this menu will only be visible to logged in users'; + +$string['menusdesc'] = ''; + +$string['newmenu2trigger'] = 'Top Menu Dropdown 2 trigger word'; +$string['newmenu2triggerdesc'] = 'Set a Top Menu Dropdown 2 trigger word. Font awesome icons can be used'; + +$string['enablemenus'] = 'Enable Menus'; +$string['enablemenusdesc'] = 'It is recommended you leave this off if menus are not in use for preformance reasons'; + +$string['menuslinkright'] = 'Show menus link in topright'; +$string['menuslinkrightdesc'] = 'If checked, show the link to the menus on the top right next to the messages menu'; + +$string['menuslinkicon'] = "Links menu icon"; +$string['menuslinkicondesc'] = "Choose a custom font awesome icon."; + +$string['disablemenuscoursepages'] = 'Disable Menus on Course Pages'; +$string['disablemenuscoursepagesdesc'] = 'Turning this option on will limit the display of top menus to site pages, the homepage, and dashboard (My Home) etc. and will not show in any course pages'; + +$string['topmenufontsize'] = 'Top Menu Font Size'; +$string['topmenufontsizedesc'] = 'Adjust the font size of the top menus'; + +$string['menuuseroverride'] = 'Allow user override'; +$string['menuuseroverridedesc'] = 'These settings can be used to give users control over where menus appear via a custom profile field. To use this option you will need to create a custom profile field in the "list" format with values in the corresponding order: +<pre>1. Sitewide (the first item in the list will be for sitewide menu visibility) +2. Homepages Only (the second item in the list will set visibility to only site / homepages) +3. Hidden (the third value in the list will hide menus entirely)</pre> +You should NOT use the "Disable Menus on Course Pages" option in conjuction with profile field settings, instead use set the default value to your "Homepages Only" entry when setting up your profile field list. + +Note: Users will have to log out of Moodle and back in again for this change to take effect, you may want to add a note explaining this in the custom profile field.'; + +$string['menuoverrideprofilefield'] = 'Custom profile field name'; +$string['menuoverrideprofilefielddesc'] = 'The name of the custom profile "list" field used for user override'; +$string['menuoverrideprofilefielddefault'] = 'topmenusettings'; + +$string['topmenuscount'] = 'Number of top Menus'; +$string['topmenuscountdesc'] = 'Set the number of top menus you want to add to the theme header'; + +$string['menusheadingvisibility'] = 'General settings for Top Menu visibility'; +$string['menusheadingvisibilitydesc'] = 'The following settings allow you to control where menus appear and optionally allow users to customise their settings'; + +$string['newmenuheading'] = 'Top Menu'; +$string['newmenu'] = 'Top Menu Dropdown'; +$string['newmenutitle'] = 'Top Menu Title'; +$string['newmenutitledesc'] = 'The title of the dropdown list that will appear in the header of your site'; +$string['newmenutitledefault'] = 'Menu'; + +$string['enabletoolsmenus'] = 'Enable Tools Menus'; +$string['enabletoolsmenusdesc'] = 'It is recommended you leave this off if menus are not in use for preformance reasons'; + +$string['toolsmenuheading'] = 'Tools Menus (in main nagivation)'; +$string['toolsmenuheadingdesc'] = 'You can configure links to be shown under a tools menu (in main navigation bar). + The format is similar to that used for Moodle custom menus but allows you to add fa icons to menu items: +<pre> +<span class="fa fa-video-camera"></span> Record Screen|http://google.co.uk|Record Screen +<span class="fa fa-picture-o"></span> ThinkStock|http://google.co.uk|ThinkStock +<span class="fa fa-clock-o"></span> Exam Clock|http://google.co.uk|Exam Clock +</pre><br />'; + +$string['toolsmenuscount'] = 'Number of tools Menus'; +$string['toolsmenuscountdesc'] = 'Set the number of tools menus you want to add to the main navigation bar'; + +$string['toolsmenuheading'] = 'Tools Menu '; +$string['toolsmenu'] = 'Tools Menu Dropdown'; +$string['toolsmenudesc'] = 'Add a drop down menu to the main navigation bar'; +$string['toolsmenutitle'] = 'Tools Menu Title'; +$string['toolsmenutitledefault'] = 'Tools'; +$string['toolsmenutitledesc'] = 'Add the title of the menu you would like to display in the main navigation bar'; + +$string['toolsmenulabel'] = 'Tools Menu'; + +$string['toolsmenufield'] = 'Custom Profile Field Name=Value (optional)'; +$string['toolsmenufielddesc'] = 'Add access rule using for custom profile field eg: usertype=student'; + + +// Social settings *************************************************. +$string['socialsettings'] = 'Header Social'; +$string['socialheading'] = 'Social Icon Settings'; +$string['socialtitledesc'] = 'You can disable the sitewide search box and enable social icons / links in its place. +To setup icons enter a delimited list into the "Social Icon List" field below. +This should be in the format: + +url|title|icon + +For example: +<pre> +https://example.com/course/search.php|Search Moodle|fa-search +https://facebook.com/|Facebook|fa-facebook-square +https://twitter.com/|Twitter|fa-twitter-square +https://instagram.com|Instagram|fa-instagram +https://example.com|My Web|fa-globe +</pre> +For reference you can find the full list of <a href="https://fortawesome.github.io/Font-Awesome/icons/">Font Awesome Icons Here</a>'; + +$string['socialsize'] = 'Set the font size of the social icons'; +$string['socialsizedesc'] = 'For a better view, the size needs to be 5px greater than the desired actual size.'; +$string['responsivesocialsize'] = 'Set the font size of the social icons on Mobile'; +$string['responsivesocialsizedesc'] = 'For a better view, the size needs to be 5px greater than the desired actual size.'; +$string['socialpaddingside'] = 'Adjust padding for the side of the social icon'; +$string['socialpaddingsidedesc'] = 'This will be space between the icon and another element or the gap between two icons. i.e. the padding on the icon will be this value divided by two. Default value follows Instagram branding guidelines.'; +$string['socialpaddingtop'] = 'Adjust padding above social icons (alters vertical position)'; +$string['socialpaddingtopdesc'] = 'This will be the minimum of 15px (set using the margin) plus this value.'; + +$string['socialtarget'] = 'Social Links Open Target'; +$string['socialtargetdesc'] = 'How would you like social links to open (same or new window)'; + +$string['socialsearchicon'] = 'Search Moodle'; +$string['socialsearchicondesc'] = 'Enable to put a search link alongside social icons (as using social disables sitewide search box)'; + +$string['socialicondesc'] = 'Set Font Awesome icon for example: fa-facebook'; + +$string['socialiconlist'] = 'Social Icon List'; +$string['socialiconlistdesc'] = 'Enter a delimited list to setup the social icons / links you need using the format: url|title|icon'; + +// Templates. +$string['templatessettings'] = 'Templates'; +$string['templatesheading'] = 'Templates Settings'; +$string['templatesheadingdesc'] = 'Override templates'; +$string['templatessel'] = 'Templates to override'; +$string['templatesseldesc'] = 'Select the templates to override, then each will be shown on their own page. If not selected here, then the template will not be overridden even if it is enabled. To select none, use the \'Ctrl\' key.'; +$string['overridetemplate'] = 'Override template: {$a}'; +$string['activatetemplateoverride'] = 'Activate template override for \'{$a}\''; +$string['activatetemplateoverridedesc'] = 'When ticked then the value in the \'{$a->setting}\' setting will be used as the \'{$a->template}\' template if it contains text.'; +$string['overriddentemplate'] = 'Overridden template: {$a}'; +$string['overriddentemplatedesc'] = 'If set then the text here will be used as the template \'{$a}\'. To ensure that the preview works, there needs to be the standard example context in JSON format.'; +$string['overriddentemplatepreview'] = 'Setting overridden preview'; +$string['overriddentemplatenopreview'] = 'No preview because of missing JSON example'; +$string['originaltemplatepreview'] = 'Original / overridden theme file preview'; +$string['originaltemplatesource'] = 'Original / overridden theme file source'; + +// Analytics *********************************. +$string['analyticssettings'] = 'Analytics'; +$string['analyticssettingsheading'] = 'Setup Google Analytics and/or Matomo'; +$string['analyticssettingsdesc'] = 'You can setup multiple codes for Google Analytics and targed them to user profile fields. Or you can use Matomo, the open source analytics.'; + +// GA. +$string['googleanalyticssettings'] = 'Google Analytics'; +$string['googleanalyticssettingsheading'] = 'Setup Google Analytics for your site'; +$string['googleanalyticssettingsdesc'] = 'You can setup multiple codes for Google Analytics and targed them to user profile fields.'; + +$string ['enableanalytics'] = 'Enable Google Analytics'; +$string ['enableanalyticsdesc'] = 'Enable Google Analytics settings on your Moodle site'; + +$string ['analyticstext'] = 'Analytics ID'; +$string ['analyticstextdesc'] = 'Enter Google Analytics ID'; + +$string['analyticscount'] = 'Analytics count'; +$string['analyticscountdesc'] = 'The number of analytics fields to show in the edit area below.'; + +$string ['analyticsprofilefield'] = 'Custom Profile Field Name=Value (optional)'; +$string ['analyticsprofilefielddesc'] = 'Add access rule using for custom profile field eg: usertype=student'; + +$string ['anonymizega'] = 'Anonymize the user IP'; +$string ['anonymizegadesc'] = 'Anonymize the user IP send to Google Analytics'; + +// Matomo (formerly Piwik). +$string['piwiksettings'] = 'Matomo (formely Piwik) Analytics'; +$string['piwiksettingsheading'] = 'Setup Matomo'; +$string['piwiksettingsdesc'] = 'Generate clean URL for in advanced tracking.'; + +$string['piwikenabled'] = 'Enabled'; +$string['piwikenableddesc'] = 'Enable Matomo tracking for Moodle.'; + +$string['piwiksiteid'] = 'Site ID'; +$string['piwiksiteiddesc'] = 'Enter your Site ID.'; + +$string['piwikimagetrack'] = 'Image Tracking'; +$string['piwikimagetrackdesc'] = 'Enable Image Tracking for Moodle for browsers with JavaScript disabled.'; + +$string['piwiksiteurl'] = 'Matomo URL'; +$string['piwiksiteurldesc'] = 'Enter your Matomo Analytics URL without http(s) or a trailing slash'; + +$string['piwiktrackadmin'] = 'Tracking Admins'; +$string['piwiktrackadmindesc'] = 'Enable tracking of Admin users (not recommended)'; + + +// Custom CSS and Javascript ********************************. +$string['customcss'] = 'Custom CSS'; +$string['customcssdesc'] = 'Whatever CSS rules you add to this textarea will be reflected in every page, making for easier customization of this theme.'; + +$string['customcssjssettings'] = 'Custom CSS & JS'; +$string['genericsettingsheading'] = 'Apply your own modifications'; +$string['genericsettingsdescription'] = 'Here you can find various settings to add your own CSS and JavaScript code to the theme.'; + +$string['jssection'] = 'Javascript Section'; +$string['jssectiondesc'] = 'Add javascript code to the site.'; + +$string['jssectionrestricted'] = 'Conditional Javascript Section'; +$string['jssectionrestricteddesc'] = 'Add javascript code to the site conditionally based on a custom profile field. This javascript will only appear if this condition is met.'; + +$string['jssectionrestrictedprofilefield'] = 'Javascript Section custom profile field'; +$string['jssectionrestrictedprofilefielddesc'] = 'Show the javascript above ONLY when user matches this custom profile field value, eg: faculty=fbl.'; + +$string['jssectionrestricteddashboardonly'] = 'Include Javascript only on dashboard page'; +$string['jssectionrestricteddashboardonlydesc'] = 'Show the javascript only on the dashboard page. Otherwise this displays sitewide.'; + +// Cache definitions. +$string['cachedef_userdata'] = 'A session cache used to store user specific data.'; + +// Activity and section navigation *******************. +$string['nextactivity'] = 'Next Activity'; +$string['previousactivity'] = 'Previous Activity'; + +$string['nextsection'] = 'Next Section'; +$string['previoussection'] = 'Previous Section'; + +$string['maincoursepage'] = 'Main Course Page'; +$string['jumpto'] = 'Jump to...'; + +// General *******************************************. +$string['hide'] = 'Hide'; +$string['show'] = 'Show'; +$string['versioninfo'] = 'Release {$a->release}, version {$a->version} on Moodle {$a->moodle}'; + +// Grade editing *************************************. +$string['turngradereditingoff'] = 'Turn grader editing off'; +$string['turngradereditingon'] = 'Turn grader editing on'; + +// Navbar Links menu *********************************. +$string['linksmenu'] = 'Links Menu'; + +// Navbar user menu **********************************. +$string['usermenu'] = 'User menu'; + +// Save / Discard button text ************************. +$string['savebuttontext'] = 'Save changes'; +$string['discardbuttontext'] = 'Cancel'; + +// Activity settings. +$string['activitiesheading'] = 'Activities'; +$string['introboxbackgroundcolor'] = 'Intro Box background color'; +$string['introboxbackgroundcolordesc'] = 'Background colour for the intro box (highlights activity description) used in forums and other activities'; + +// Forum settings. +$string['settingsforumheading'] = 'Forum'; +$string['forumheaderbackgroundcolor'] = 'Forum post header background'; +$string['forumheaderbackgroundcolordesc'] = 'Background colour for a Forum post'; +$string['forumbodybackgroundcolor'] = 'Forum post body background'; +$string['forumbodybackgroundcolordesc'] = 'Background colour for a Forum post'; + +// Course page further information *******************. +// Activity display **********************************. +$string['answered'] = 'Answered'; +$string['attempted'] = 'Attempted'; +$string['contributed'] = 'Contributed'; +$string['draft'] = 'Not published to students'; +$string['due'] = 'Due {$a}'; +$string['expired'] = 'Expired'; +$string['feedbackavailable'] = 'Feedback available'; +$string['notanswered'] = 'Not answered'; +$string['notattempted'] = 'Not attempted'; +$string['notcontributed'] = 'Not contributed'; +$string['notsubmitted'] = 'Not Submitted'; +$string['overdue'] = 'Overdue'; +$string['reopened'] = 'Reopened'; +$string['submitted'] = 'Submitted'; + +$string['xofyanswered'] = '{$a->completed} of {$a->participants} Answered'; +$string['xofyattempted'] = '{$a->completed} of {$a->participants} Attempted'; +$string['xofycontributed'] = '{$a->completed} of {$a->participants} Contributed'; +$string['xofysubmitted'] = '{$a->completed} of {$a->participants} Submitted'; +$string['xungraded'] = '{$a} Ungraded'; + +$string['checked'] = 'Checked'; +$string['warning'] = 'Warning'; + +$string['coursesectionactivityfurtherinformation'] = 'Course page further information'; +$string['coursesectionactivityfurtherinformationassign'] = 'Show Assignment information'; +$string['coursesectionactivityfurtherinformationassigndesc'] = 'Show assignment information, such as due date, submission status. For teachers / admins, show number of submissions.'; +$string['coursesectionactivityfurtherinformationquiz'] = 'Show quiz information'; +$string['coursesectionactivityfurtherinformationquizdesc'] = 'Show quiz information, such as submission status. For teachers / admins, show number of submissions.'; +$string['coursesectionactivityfurtherinformationchoice'] = 'Show choice information'; +$string['coursesectionactivityfurtherinformationchoicedesc'] = 'Show choice information, such as submission status. For teachers / admins, show number of submissions.'; +$string['coursesectionactivityfurtherinformationfeedback'] = 'Show feedback information'; +$string['coursesectionactivityfurtherinformationfeedbackdesc'] = 'Show feedback information, such as submission status. For teachers / admins, show number of submissions.'; +$string['coursesectionactivityfurtherinformationlesson'] = 'Show lesson information'; +$string['coursesectionactivityfurtherinformationlessondesc'] = 'Show lesson information, such as submission status. For teachers / admins, show number of submissions.'; +$string['coursesectionactivityfurtherinformationdata'] = 'Show database information'; +$string['coursesectionactivityfurtherinformationdatadesc'] = 'Show data information, such as submission status. For teachers / admins, show number of submissions.'; + +// Activity display margins. +$string['coursesectionactivitymargintop'] = 'Top margin activity spacing'; +$string['coursesectionactivitymargintopdesc'] = 'Top margin spacing between activities'; +$string['coursesectionactivitymarginbottom'] = 'Bottom margin activity spacing.'; +$string['coursesectionactivitymarginbottomdesc'] = 'Bottom margin spacing between activities.'; + +// Properties. +$string['properties'] = 'Import / Export Adaptable settings'; +$string['propertiessub'] = 'Current theme settings'; +$string['propertiesdesc'] = 'In this section you can import / export current Adaptable theme settings (properties) in JSON format. You can also view all current settings on this Moodle installation.'; +$string['propertiesproperty'] = 'Property'; +$string['propertiesvalue'] = 'Value'; +$string['propertiesexport'] = 'Export properties as a JSON string'; +$string['propertiesreturn'] = 'Return'; +$string['putpropertiesheading'] = 'Import theme settings'; +$string['putpropertiesname'] = 'Import properties'; +$string['putpropertiesdesc'] = 'Paste the JSON string and \'Save changes\'. Warning! Does not validate setting values and performs a \'Purge all caches\'.'; +$string['putpropertyreport'] = 'Report:'; +$string['putpropertyversion'] = 'version:'; +$string['putpropertyproperties'] = 'Properties'; +$string['putpropertyour'] = 'Our'; +$string['putpropertiesignorecti'] = 'Ignoring all course title image settings.'; +$string['putpropertiesreportfiles'] = 'Remember to upload the following files to their settings:'; +$string['putpropertiessettingsreport'] = 'Settings report:'; +$string['putpropertiesvalue'] = '->'; +$string['putpropertiesfrom'] = 'from'; +$string['putpropertieschanged'] = 'Changed:'; +$string['putpropertiesunchanged'] = 'Unchanged:'; +$string['putpropertiesadded'] = 'Added:'; +$string['putpropertiesignored'] = 'Ignored:'; + +// Privacy. +$string['privacy:metadata'] = 'Adaptable do not store any individual user data.'; + +// Adaptable Tabbed layout changes. +$string['tabbedlayoutheading'] = 'Adaptable Tabbed Layout'; +$string['tabbedlayoutcoursepage'] = 'Course page Tabbed Layout'; +$string['tabbedlayoutcoursepagedesc'] = 'Use a tabbed layout for the course page. This displays all content in tabs, with course content in one tab and allowing you to tailor the content in other tabs. Use this setting to configure the order of tabs.'; +$string['tabbedlayoutcoursepagelink'] = 'Course page Tabbed Layout course link'; +$string['tabbedlayoutcoursepagelinkdesc'] = 'Have a tab link back to the course page in the course tabs.'; +$string['tabbedlayoutcoursepagetabcolorselected'] = 'Selected tab colour for course page'; +$string['tabbedlayoutcoursepagetabcolorselecteddesc'] = 'Choose the colour for the currently selected tab.'; +$string['tabbedlayoutcoursepagetabcolorunselected'] = 'Unselected tab colour for course page'; +$string['tabbedlayoutcoursepagetabcolorunselecteddesc'] = 'Choose the colour for any other tab.'; +$string['tabbedlayoutcoursepagetabpersistencetime'] = 'Course homepage Tab persistence time'; +$string['tabbedlayoutcoursepagetabpersistencetimedesc'] = 'Course homepage Tab persists on the selected tab when refreshing for a period of inactivity. Set the inactivity period here. For example, set to 30 so that the first tab is selected after 30 minutes of inactivity'; +$string['tabbedlayoutdashboard'] = 'Dashboard page Tabbed Layout'; +$string['tabbedlayoutdashboarddesc'] = 'Use a tabbed layout for the Dashboard page. This displays all content in tabs, with course content in one tab and allowing you to tailor the content in other tabs. Use this setting to configure the order of tabs.'; +$string['tabbedlayoutdashboardtabcolorselected'] = 'Selected tab colour for dashboard'; +$string['tabbedlayoutdashboardtabcolorselecteddesc'] = 'Choose the colour for the currently selected tab.'; +$string['tabbedlayoutdashboardtabcolorunselected'] = 'Unselected tab colour for dashboard'; +$string['tabbedlayoutdashboardtabcolorunselecteddesc'] = 'Choose the colour for any other tab.'; +$string['tabbedlayoutdashboardtab1condition'] = 'Tab 1 Custom Profile Field Restriction (optional)'; +$string['tabbedlayoutdashboardtab1conditiondesc'] = 'Add access rule for displaying Tab 1 using custom profile field eg: showtab1=true'; +$string['tabbedlayoutdashboardtab2condition'] = 'Tab 2 Custom Profile Field Restriction (optional)'; +$string['tabbedlayoutdashboardtab2conditiondesc'] = 'Add access rule for displaying Tab 2 using custom profile field eg: showtab2=true'; + +$string['tabbedlayouttablabelcourse'] = 'Course Content'; +$string['tabbedlayouttablabelcourse1'] = 'Tab 1'; +$string['tabbedlayouttablabelcourse2'] = 'Tab 2'; +$string['tabbedlayouttablabeldashboard'] = 'Dashboard Content'; +$string['tabbedlayouttablabeldashboard1'] = 'Tab 1'; +$string['tabbedlayouttablabeldashboard2'] = 'Tab 2'; + +$string['region-course-tab-one-a'] = 'Course page tab region 1'; +$string['region-course-tab-two-a'] = 'Course page tab region 2'; +$string['region-my-tab-one-a'] = 'Dashboard page tab region 1'; +$string['region-my-tab-two-a'] = 'Dashboard page tab region 2'; + +// Number of course tiles in front page. +$string['frontpagenumbertiles'] = 'Number of course tiles per row'; +$string['frontpagenumbertilesdesc'] = 'Number of course tiles to display per row in the front page courses list'; +$string['frontpagetiles1'] = '1 tile'; +$string['frontpagetiles2'] = '2 tiles'; +$string['frontpagetiles3'] = '3 tiles'; +$string['frontpagetiles4'] = '4 tiles'; +$string['frontpagetiles6'] = '6 tiles'; + +// Edit settings. +$string['editsettingsbutton'] = 'Edit settings and Button display'; +$string['editsettingsbuttondesc'] = 'Configure here what should appear in the Navbar for editing settings. Note that these options do not apply to pages for which there are no related settings, such as the dashboard page.'; +$string['editsettingsbuttonshowcog'] = 'Show edit settings icon (cog / gear wheel icon) only.'; +$string['editsettingsbuttonshowbutton'] = 'Show edit button only. E.g. "Turn Editing on" button on course page.'; +$string['editsettingsbuttonshowcogandbutton'] = 'Show edit settings icon and cog. Note that this takes up more space in navigation.'; +$string['displayeditingbuttontext'] = 'Editing button text'; +$string['displayeditingbuttontextdesc'] = 'Show or hide the text on the editing button. Note: Only applies to the course editing button.'; + +// Login *******************************************************. +$string['loginsettings'] = 'Login Page'; +$string['loginsettingsheading'] = 'Customize the login page'; +$string['logindesc'] = 'Customize the login page with adding an image background and texts above and below the login box.'; + + +$string['loginsettingsheading'] = 'Customize the login page.'; +$string['loginbgimage'] = 'Background image'; +$string['loginbgimagedesc'] = 'Add a background image to the full size page.'; +$string['loginbgstyle'] = 'Login background style'; +$string['loginbgstyledesc'] = 'Select the style for the uploaded image.'; +$string['loginbgopacity'] = 'Login page header, navbar, login box and footer background opacity when there is a background image'; +$string['loginbgopacitydesc'] = 'Login background opacity for the header, navbar, login box and footer when there is a background image.'; +$string['loginheader'] = 'Login page header'; +$string['loginheaderdesc'] = 'Show the login page header.'; +$string['loginfooter'] = 'Login page footer'; +$string['loginfooterdesc'] = 'Show the login page footer.'; +$string['logintextboxtop'] = 'Top text box'; +$string['logintextboxtopdesc'] = 'Add a custom text above the login box.'; +$string['logintextboxbottom'] = 'Bottom text box'; +$string['logintextboxbottomdesc'] = 'Add a custom text below the login box.'; + +$string['stylecover'] = 'Cover'; +$string['stylestretch'] = 'Stretch'; + +// User profile. +$string['aboutme'] = 'About me'; +$string['course'] = 'Course'; +$string['courses'] = 'Courses'; +$string['more'] = 'More'; + +// User & user profile settings. +$string['usersettings'] = 'User Profile'; +$string['usersettingsdesc'] = 'Set settings for the user profile.'; +$string['usersettingsheading'] = 'Control aspects of the user profile'; +$string['customcoursetitle'] = 'Custom course title'; +$string['customcoursetitledesc'] = 'Name of the user profile custom field for the course title.'; +$string['customcoursesubtitle'] = 'Custom course title'; +$string['customcoursesubtitledesc'] = 'Name of the user profile custom field for the course title.'; +$string['enabletabbedprofile'] = 'Enable tabbed profile'; +$string['enabletabbedprofiledesc'] = 'Enable the tabbed profile functionality.'; +$string['enabledtabbedprofileeditprofilelink'] = 'Enable edit profile link'; +$string['enabledtabbedprofileeditprofilelinkdesc'] = 'Enable the tabbed profile edit profile link. Note: When enabled will only show if the viewing user has permission to edit the profile.'; +$string['enabledtabbedprofileuserpreferenceslink'] = 'Enable preferences link'; +$string['enabledtabbedprofileuserpreferenceslinkdesc'] = 'Enable the tabbed profile user preferences link.'; + +$string['usernodescription'] = 'User has not updated their description yet.'; +$string['usernointerests'] = 'User has not updated their interests yet.'; + +// Category headers settings. +$string['categoryheaderssettings'] = 'Category headers'; +$string['categoryheaderssettingsdesc'] = 'Set settings for the headers within a category.'; +$string['categoryheaderssettingsheading'] = 'Change the header for one or more top level categories and below.'; + +$string['categoryhavecustomheader'] = 'Category custom header'; +$string['categoryhavecustomheaderdesc'] = 'Select the top level categories that will have a custom header. To select more than one or deselect a category, use the \'Ctrl\' key. Save and refesh the page to update Note: Sub-categories of the selected will inherit the setting values.'; +$string['categoryheaderheader'] = 'Settings for the top level category \'{$a->name}\' with id \'{$a->id}\'.'; +$string['categoryheaderheaderdesc'] = 'Set the settings for the top level category \'{$a->name}\' with id \'{$a->id}\'.'; +$string['categoryheaderheaderdescchildren'] = 'Set the settings for the top level category \'{$a->name}\' with id \'{$a->id}\' and its children \'{$a->children}\'.'; +$string['categoryheaderbgimage'] = 'Category \'{$a->name}\' with id \'{$a->id}\' background image'; +$string['categoryheaderbgimagedesc'] = 'Set a background image for the top level category \'{$a->name}\' with id \'{$a->id}\' in the header. Minimum size is 1600x180px (1900x180px recommended). The image will cover the full header.'; +$string['categoryheaderbgimagedescchildren'] = 'Set a background image for the top level category \'{$a->name}\' with id \'{$a->id}\' and its children \'{$a->children}\' in the header. Minimum size is 1600x180px (1900x180px recommended). The image will cover the full header.'; +$string['categoryheaderlogo'] = 'Category \'{$a->name}\' with id \'{$a->id}\' logo'; +$string['categoryheaderlogodesc'] = 'Set a logo for the top level category \'{$a->name}\' with id \'{$a->id}\' in the header. Recommended size is 200px by 80px.'; +$string['categoryheaderlogodescchildren'] = 'Set a logo for the top level category \'{$a->name}\' with id \'{$a->id}\' and its children \'{$a->children}\' in the header. Recommended size is 200px by 80px.'; +$string['categoryheadercustomtitle'] = 'Category \'{$a->name}\' with id \'{$a->id}\' custom title'; +$string['categoryheadercustomtitledesc'] = 'Set the custom title for the top level category \'{$a->name}\' with id \'{$a->id}\' in the header. If blank then you will be presented with what has been configured by the \'sitetitle\' and \'sitetitletext\' settings on non-course pages or on course pages when \'enableheading\' is \'off\'. When populated on non-course category linked pages then this will replace the site title. When populated on course pages then will appear above the title as established by the \'enableheading\' setting. But on mobiles any \'title\' will only appear depending on the \'responsivecoursetitle\' setting.'; +$string['categoryheadercustomtitledescchildren'] = 'Set the custom title for the top level category \'{$a->name}\' with id \'{$a->id}\' and its children \'{$a->children}\' in the header. If blank then you will be presented with what has been configured by the \'sitetitle\' and \'sitetitletext\' settings on non-course pages or on course pages when \'enableheading\' is \'off\'. When populated on non-course category linked pages then this will replace the site title. When populated on course pages then will appear above the title as established by the \'enableheading\' setting. But on mobiles any \'title\' will only appear depending on the \'responsivecoursetitle\' setting.'; +$string['categoryheadercustomcss'] = 'Category \'{$a->name}\' with id \'{$a->id}\' custom CSS'; +$string['categoryheadercustomcssdesc'] = 'Set custom CSS for the top level category \'{$a->name}\' with id \'{$a->id}\'. This will generate CSS for the theme selector prefixed with \'.category-{$a->id}\'. If the CSS does not appear to be applied, then check the PHP log file.'; +$string['categoryheadercustomcssdescchildren'] = 'Set custom CSS for the top level category \'{$a->name}\' with id \'{$a->id}\' and its children \'{$a->children}\'. This will generate CSS for the theme selector prefixed with \'.category-{$a->id}\' and for all child ids. If the CSS does not appear to be applied, then check the PHP log file.'; +$string['invalidcategorycss'] = 'Invalid category custom CSS for category \'{$a->topcatname}\' with id \'{$a->topcatid}\': \'{$a->css}\'.'; +$string['invalidcategorygeneratedcss'] = 'Invalid category custom generated CSS: \'{$a->css}\'.'; + +// Print settings. +$string['printsettings'] = 'Print'; +$string['printsettingsdesc'] = 'Set the settings for printing.'; +$string['printsettingsheading'] = 'Print settings'; + +$string['printpageorientation'] = 'Page orientation'; +$string['printpageorientationdesc'] = 'Set orientation of the page to \'Portrait\' or \'Landscape\'.'; +$string['landscape'] = 'Landscape'; +$string['portrait'] = 'Portrait'; + +$string['printbodyfontsize'] = 'Body font size'; +$string['printbodyfontsizedesc'] = 'Set the size of the body font.'; +$string['printmargin'] = 'Margin'; +$string['printmargindesc'] = 'Set the margin.'; +$string['printlineheight'] = 'Line height'; +$string['printlineheightdesc'] = 'Set the line height.'; diff --git a/theme/adaptable/layout/columns1.php b/theme/adaptable/layout/columns1.php new file mode 100644 index 0000000..f392500 --- /dev/null +++ b/theme/adaptable/layout/columns1.php @@ -0,0 +1,52 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Include header. +require_once(dirname(__FILE__) . '/includes/header.php'); + +?> + +<div class="container outercont"> + <?php + echo $OUTPUT->page_navbar(); + ?> + <div id="page-content" class="row"> + <section id="region-main" class="col-12"> + <?php + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->activity_navigation(); + echo $OUTPUT->course_content_footer(); + ?> + </section> + </div> +</div> + +<?php +// Include footer. +require_once(dirname(__FILE__) . '/includes/footer.php'); diff --git a/theme/adaptable/layout/columns2.php b/theme/adaptable/layout/columns2.php new file mode 100644 index 0000000..43d424d --- /dev/null +++ b/theme/adaptable/layout/columns2.php @@ -0,0 +1,86 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Include header. +require_once(dirname(__FILE__) . '/includes/header.php'); + + +// If page is Grader report don't show side post. +if (($PAGE->pagetype == "grade-report-grader-index") || + ($PAGE->bodyid == "page-grade-report-grader-index")) { + $left = true; + $hassidepost = false; +} else { + $left = $PAGE->theme->settings->blockside; + $hassidepost = $PAGE->blocks->region_has_content('side-post', $OUTPUT); +} +$regions = theme_adaptable_grid($left, $hassidepost); +?> + +<div class="container outercont"> + <?php + echo $OUTPUT->page_navbar(); + ?> + <div id="page-content" class="row<?php echo $regions['direction'];?>"> + <section id="region-main" class="<?php echo $regions['content'];?>"> + <?php + echo $OUTPUT->get_course_alerts(); + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + + if ($PAGE->has_set_url()) { + $currenturl = $PAGE->url; + } else { + $currenturl = $_SERVER["REQUEST_URI"]; + } + + // Display course page block activity bottom region if this is a mod page of type where you're viewing + // a section, page or book (chapter). + if (!empty($PAGE->theme->settings->coursepageblockactivitybottomenabled)) { + if ( stristr ($currenturl, "mod/page/view") || + stristr ($currenturl, "mod/book/view") ) { + echo $OUTPUT->get_block_regions('customrowsetting', 'course-section-', '12-0-0-0'); + } + } + + echo $OUTPUT->activity_navigation(); + echo $OUTPUT->course_content_footer(); + ?> + </section> + + <?php + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', $regions['blocks'].' d-print-none '); + } + ?> + </div> +</div> + +<?php +// Include footer. +require_once(dirname(__FILE__) . '/includes/footer.php'); diff --git a/theme/adaptable/layout/course.php b/theme/adaptable/layout/course.php new file mode 100644 index 0000000..b48a77c --- /dev/null +++ b/theme/adaptable/layout/course.php @@ -0,0 +1,233 @@ +<?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/>. + +/** + * Course-specific layout page + * + * Includes course block region checking and formatting. + * + * @package theme_adaptable + * @copyright 2017 Manoj Solanki (Coventry University) + * @author 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Include header. +require_once(dirname(__FILE__) . '/includes/header.php'); + +$left = $PAGE->theme->settings->blockside; + +// If page is Grader report, override blockside setting to align left. +if (($PAGE->pagetype == "grade-report-grader-index") || + ($PAGE->bodyid == "page-grade-report-grader-index")) { + $left = true; +} + +$movesidebartofooter = !empty(($PAGE->theme->settings->coursepagesidebarinfooterenabled)) ? true : false; +$hassidepost = $PAGE->blocks->region_has_content('side-post', $OUTPUT); + +// Definition of block regions for top and bottom. These are used in potentially retrieving +// any missing block regions (due to layout changes that may hide blocks). +$blocksarray = array ( + array('settingsname' => 'coursepageblocklayoutlayouttoprow', + 'classnamebeginswith' => 'course-top-'), + array('settingsname' => 'coursepageblocklayoutlayoutbottomrow', + 'classnamebeginswith' => 'course-bottom-') +); + +if ($movesidebartofooter) { + // Side post in the footer so don't adjust the layout but pretend it is not there. + $regions = theme_adaptable_grid($left, false); +} else { + $regions = theme_adaptable_grid($left, $hassidepost); +} +?> + +<div class="container outercont"> + <?php + echo $OUTPUT->page_navbar(); + ?> + <div id="page-content" class="row<?php echo $regions['direction'];?>"> + <?php + // If course page, display course top block region. + if (!empty($PAGE->theme->settings->coursepageblocksenabled)) { + echo '<div id="frontblockregion" class="container">'; + echo '<div class="row">'; + echo $OUTPUT->get_block_regions('coursepageblocklayoutlayouttoprow', 'course-top-'); + echo '</div>'; + echo '</div>'; + } + ?> + + <section id="region-main" class="<?php echo $regions['content'];?>"> + + <?php + if (!empty($PAGE->theme->settings->tabbedlayoutcoursepage)) { + // Use Adaptable tabbed layout. + $currentpage = theme_adaptable_get_current_page(); + + $taborder = explode ('-', $PAGE->theme->settings->tabbedlayoutcoursepage); + $count = 0; + + echo '<main id="coursetabcontainer" class="tabcontentcontainer">'; + + $sectionid = optional_param('sectionid', 0, PARAM_INT); + $section = optional_param('section', 0, PARAM_INT); + if ((!empty($PAGE->theme->settings->tabbedlayoutcoursepagelink)) && + (($sectionid) || ($section))) { + global $COURSE; + $courseurl = new moodle_url('/course/view.php', array('id' => $COURSE->id)); + echo '<div class="linktab"><a href="'.$courseurl->out(true).'"><i class="fa fa-th-large"></i></a></div>'; + } + + foreach ($taborder as $tabnumber) { + if ($tabnumber == 0) { + $tabname = 'tab-content'; + $tablabel = get_string('tabbedlayouttablabelcourse', 'theme_adaptable'); + } else { + $tabname = 'tab' . $tabnumber; + $tablabel = get_string('tabbedlayouttablabelcourse' . $tabnumber, 'theme_adaptable'); + } + + $checkedstatus = ''; + + if (($count == 0 && $currentpage == 'coursepage') || + ($currentpage != 'coursepage' && $tabnumber == 0)) { + $checkedstatus = 'checked'; + } + + $extrastyles = ''; + + if ($currentpage == 'coursepage') { + $extrastyles = ' style="display: none"'; + } + + echo '<input id="' . $tabname . '" type="radio" name="tabs" class="coursetab" ' . + $checkedstatus . ' >' . + '<label for="' . $tabname . '" class="coursetab" ' . $extrastyles . '>' . $tablabel .'</label>'; + + $count++; + } + + // Basic array used by appropriately named blocks below (e.g. course-tab-one). All this is to re-use existing + // functionality and the non-use of numbers in block region names. + $wordtonumber = array (1 => 'one', 2 => 'two'); + + foreach ($taborder as $tabnumber) { + if ($tabnumber == 0) { + echo '<section id="adaptable-course-tab-content" class="adaptable-tab-section tab-panel">'; + + echo $OUTPUT->get_course_alerts(); + if (!empty($PAGE->theme->settings->coursepageblocksliderenabled) ) { + echo $OUTPUT->get_block_regions('customrowsetting', 'news-slider-', '12-0-0-0'); + } + + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->course_content_footer(); + + echo '</section>'; + } else { + echo '<section id="adaptable-course-tab-' . $tabnumber . '" class="adaptable-tab-section tab-panel">'; + + echo $OUTPUT->get_block_regions('customrowsetting', 'course-tab-' . $wordtonumber[$tabnumber] . '-', + '12-0-0-0'); + echo '</section>'; + } + } + echo '</main>'; + } else { + echo $OUTPUT->get_course_alerts(); + if (!empty($PAGE->theme->settings->coursepageblocksliderenabled) ) { + echo $OUTPUT->get_block_regions('customrowsetting', 'news-slider-', '12-0-0-0'); + } + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->course_content_footer(); + } + ?> + +<?php +// Check here if sidebar is configured to be in footer as we want to include +// the sidebar information in the main content. + +if ($movesidebartofooter == false) { ?> + </section> +<?php } + +// Check if the block regions are disabled in settings. If it is and there were any blocks +// assigned to those regions, they would obviously not display. This will allow to override +// the call to get_missing_block_regions to just display them all. + +$displayall = false; + +if (empty($PAGE->theme->settings->coursepageblocksenabled)) { + $displayall = true; +} + +if ($movesidebartofooter == false) { + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', $regions['blocks'].' d-print-none '); + } + + // Get any missing blocks from changing layout settings. E.g. From 4-4-4-4 to 6-6-0-0, to recover + // what was in the last 2 spans that are now 0. + echo $OUTPUT->get_missing_block_regions($blocksarray, 'col-12', $displayall); +} + +// If course page, display course bottom block region. +if (!empty($PAGE->theme->settings->coursepageblocksenabled)) { + echo '<div id="frontblockregion" class="container">'; + echo '<div class="row">'; + echo $OUTPUT->get_block_regions('coursepageblocklayoutlayoutbottomrow', 'course-bottom-'); + echo '</div>'; + echo '</div>'; +} + +if ($movesidebartofooter) { + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', ' col-12 d-print-none '); + } + + // Get any missing blocks from changing layout settings. E.g. From 4-4-4-4 to 6-6-0-0, to recover + // what was in the last 2 spans that are now 0. + echo $OUTPUT->get_missing_block_regions($blocksarray, array(), $displayall); +} + +if ($movesidebartofooter) { ?> + </section> +<?php } ?> + </div> +</div> + +<?php +// Include footer. +require_once(dirname(__FILE__) . '/includes/footer.php'); + +if (!empty($PAGE->theme->settings->tabbedlayoutcoursepagetabpersistencetime)) { + $tabbedlayoutcoursepagetabpersistencetime = $PAGE->theme->settings->tabbedlayoutcoursepagetabpersistencetime; +} else { + $tabbedlayoutcoursepagetabpersistencetime = 30; +} +if (!empty($PAGE->theme->settings->tabbedlayoutcoursepage)) { + $PAGE->requires->js_call_amd('theme_adaptable/utils', 'init', array('currentpage' => $currentpage, + 'tabpersistencetime' => $tabbedlayoutcoursepagetabpersistencetime)); + + echo '<noscript><style>label.coursetab { display: block !important; }</style><noscript>'; +} diff --git a/theme/adaptable/layout/dashboard.php b/theme/adaptable/layout/dashboard.php new file mode 100644 index 0000000..f6ba480 --- /dev/null +++ b/theme/adaptable/layout/dashboard.php @@ -0,0 +1,193 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2017 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Include header. +require_once(dirname(__FILE__) . '/includes/header.php'); + +// Set layout. +$left = $PAGE->theme->settings->blockside; +$hassidepost = $PAGE->blocks->region_has_content('side-post', $OUTPUT); +$regions = theme_adaptable_grid($left, $hassidepost); + +$dashblocksposition = (!empty($PAGE->theme->settings->dashblocksposition)) ? $PAGE->theme->settings->dashblocksposition : 'abovecontent'; + +$dashblocklayoutlayoutrow = ''; +if (!empty($PAGE->theme->settings->dashblocksenabled)) { + $dashblocklayoutlayoutrow = '<div id="frontblockregion" class="row">'; + $dashblocklayoutlayoutrow .= $OUTPUT->get_block_regions('dashblocklayoutlayoutrow'); + $dashblocklayoutlayoutrow .= '</div>'; +} +?> + +<div class="container outercont"> + <?php + if ( (!empty($PAGE->theme->settings->dashblocksenabled)) && + (empty($PAGE->theme->settings->tabbedlayoutdashboard)) && ($dashblocksposition == 'abovecontent') ) { + echo $dashblocklayoutlayoutrow; + } ?> + <div id="page-content" class="row<?php echo $regions['direction'];?>"> + <?php + if (!empty($PAGE->theme->settings->tabbedlayoutdashboard)) { + + $showtabs = array (0 => true, 1 => true, 2 => true); + // Get any custom user profile field restriction for tab 1 and 2. (e.g. showtab1=false). + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + profile_load_data($USER); + + if (!empty($PAGE->theme->settings->tabbedlayoutdashboardtab1condition)) { + $fields = explode('=', $PAGE->theme->settings->tabbedlayoutdashboardtab1condition); + $ftype = $fields[0]; + $setvalue = $fields[1]; + + // Get user profile field (if it exists). + $ftype = "profile_field_$ftype"; + if (isset($USER->$ftype)) { + if ($USER->$ftype != $setvalue) { + // Condition is true, so don't show this tab. + $showtabs[1] = false; + } + } + } + + if (!empty($PAGE->theme->settings->tabbedlayoutdashboardtab2condition)) { + $fields = explode('=', $PAGE->theme->settings->tabbedlayoutdashboardtab2condition); + $ftype = $fields[0]; + $setvalue = $fields[1]; + + // Get user profile field (if it exists). + $ftype = "profile_field_$ftype"; + if (isset($USER->$ftype)) { + if ($USER->$ftype != $setvalue) { + // Condition is true, so don't show this tab. + $showtabs[2] = false; + } + } + } + + $taborder = explode ('-', $PAGE->theme->settings->tabbedlayoutdashboard); + $count = 0; + echo '<section id="region-main" class="' . $regions['content'] . '">'; + + echo '<main id="dashboardtabcontainer" class="tabcontentcontainer">'; + + foreach ($taborder as $tabnumber) { + if ((!empty($showtabs[$tabnumber])) && ($showtabs[$tabnumber] == true)) { + // Tab 0 is the original content tab. + if ($tabnumber == 0) { + $tabname = 'dashboard-tab-content'; + $tablabel = get_string('tabbedlayouttablabeldashboard', 'theme_adaptable'); + } else { + $tabname = 'dashboard-tab' . $tabnumber; + $tablabel = get_string('tabbedlayouttablabeldashboard' . $tabnumber, 'theme_adaptable'); + } + + echo '<input id="' . $tabname . '" type="radio" name="tabs" class="dashboardtab" ' . + ($count == 0 ? ' checked ' : '') . '>' . + '<label for="' . $tabname . '" class="dashboardtab">' . $tablabel .'</label>'; + $count++; + } + } + + // Basic array used by appropriately named blocks below (e.g. course-tab-one). All this is due to the re-use of + // existing functionality and non-use of numbers in block region names. + $wordtonumber = array (1 => 'one', 2 => 'two'); + foreach ($taborder as $tabnumber) { + if ($tabnumber == 0) { + echo '<section id="adaptable-dashboard-tab-content" class="adaptable-tab-section tab-panel">'; + + if ( (!empty($PAGE->theme->settings->dashblocksenabled)) && ($dashblocksposition == 'abovecontent') ) { + echo $dashblocklayoutlayoutrow; + } + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->course_content_footer(); + if ( (!empty($PAGE->theme->settings->dashblocksenabled)) && ($dashblocksposition == 'belowcontent') ) { + echo $dashblocklayoutlayoutrow; + } + + echo '</section>'; + } else { + if ($showtabs[$tabnumber] == true) { + echo '<section id="adaptable-dashboard-tab-' . $tabnumber . '" class="adaptable-tab-section tab-panel">'; + echo $OUTPUT->get_block_regions('customrowsetting', 'my-tab-' . $wordtonumber[$tabnumber] . + '-', '12-0-0-0'); + echo '</section>'; + } + } + } + + echo '</main>'; + echo '</section>'; + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', $regions['blocks'].' d-print-none '); + } + } else { ?> + <section id="region-main" class="<?php echo $regions['content'];?>"> + <?php + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->course_content_footer(); + ?> + </section> + + <?php + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', $regions['blocks'].' d-print-none '); + } + } + ?> + +</div> + +<?php +if ( (!empty($PAGE->theme->settings->dashblocksenabled)) && (empty($PAGE->theme->settings->tabbedlayoutdashboard)) + && ($dashblocksposition == 'belowcontent') ) { + echo $dashblocklayoutlayoutrow; +} +?> + +<?php +if (is_siteadmin()) { +?> + <div class="hidden-blocks"> + <div class="row"> + <h3><?php echo get_string('frnt-footer', 'theme_adaptable') ?></h3> + <?php + echo $OUTPUT->blocks('frnt-footer', 'col-10'); + ?> + </div> + </div> + <?php +} +?> +</div> + +<?php +// Include footer. +require_once(dirname(__FILE__) . '/includes/footer.php'); diff --git a/theme/adaptable/layout/embedded.php b/theme/adaptable/layout/embedded.php new file mode 100644 index 0000000..d331034 --- /dev/null +++ b/theme/adaptable/layout/embedded.php @@ -0,0 +1,53 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +echo $OUTPUT->doctype() ?> +<html <?php echo $OUTPUT->htmlattributes(); ?>> +<head> + <title><?php echo $OUTPUT->page_title(); ?></title> + <link rel="shortcut icon" href="<?php echo $OUTPUT->favicon(); ?>" /> + <?php echo $OUTPUT->standard_head_html() ?> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> + +<body <?php echo $OUTPUT->body_attributes(); ?>> +<?php echo $OUTPUT->standard_top_of_body_html() ?> +<div id="page"> + <div id="page-content" class="clearfix"> + <?php echo $OUTPUT->main_content(); ?> + </div> +</div> +<?php echo $OUTPUT->standard_end_of_body_html() ?> +<script type="text/javascript"> + require(['theme_boost/loader']); +</script> +<script type="text/javascript"> + <?php echo $PAGE->theme->settings->jssection;?> +</script> +</body> +</html> diff --git a/theme/adaptable/layout/frontpage.php b/theme/adaptable/layout/frontpage.php new file mode 100644 index 0000000..b18cda7 --- /dev/null +++ b/theme/adaptable/layout/frontpage.php @@ -0,0 +1,147 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Let's go to include first the common header file. +require_once(dirname(__FILE__) . '/includes/header.php'); + +// And now we go to create the main layout. +$left = $PAGE->theme->settings->blockside; +$hassidepost = $PAGE->blocks->region_has_content('side-post', $OUTPUT); +$regions = theme_adaptable_grid($left, $hassidepost); + +// Let's include the images slider if enabled. +if (!empty($PAGE->theme->settings->sliderenabled)) { + echo $OUTPUT->get_frontpage_slider(); +} + +// And let's show Infobox 1 if enabled. +if (!empty($PAGE->theme->settings->infobox)) { + if (!empty($PAGE->theme->settings->infoboxfullscreen)) { + echo '<div id="theinfo">'; + } else { + echo '<div id="theinfo" class="container">'; + } + echo '<div class="row">'; + echo $OUTPUT->get_setting('infobox', 'format_html'); + echo '</div>'; + echo '</div>'; +} + +// If Marketing Blocks are enabled then let's show them. +if (!empty($PAGE->theme->settings->frontpagemarketenabled)) { + echo $OUTPUT->get_marketing_blocks(); +} + +if (!empty($PAGE->theme->settings->frontpageblocksenabled)) { ?> + <div id="frontblockregion" class="container"> + <div class="row"> + <?php echo $OUTPUT->get_block_regions(); ?> + </div> + </div> + <?php +} + +// And finally let's show the Infobox 2 if enabled. +if (!empty($PAGE->theme->settings->infobox2)) { + if (!empty($PAGE->theme->settings->infoboxfullscreen)) { + echo '<div id="theinfo2">'; + } else { + echo '<div id="theinfo2" class="container">'; + } + echo '<div class="row">'; + echo $OUTPUT->get_setting('infobox2', 'format_html'); + echo '</div>'; + echo '</div>'; +} + +// The main content goes here. +?> +<div class="container outercont"> + <div id="page-content" class="row<?php echo $regions['direction'];?>"> + <div id="page-navbar" class="col-12"> + <nav class="breadcrumb-button"><?php echo $OUTPUT->page_heading_button(); ?></nav> + </div> + + <section id="region-main" class="<?php echo $regions['content'];?>"> + <?php + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->course_content_footer(); + ?> + </section> + <?php + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', $regions['blocks'].' d-print-none '); + } + ?> + </div> + +<?php + +// Let's show the hidden blocks region ONLY for administrators. +if (is_siteadmin()) { +?> + <div class="hidden-blocks"> + <div class="row"> + + <?php + if (!empty($PAGE->theme->settings->coursepageblocksliderenabled) ) { + echo $OUTPUT->get_block_regions('customrowsetting', 'news-slider-', '12-0-0-0'); + } + + if (!empty($PAGE->theme->settings->coursepageblockactivitybottomenabled)) { + echo $OUTPUT->get_block_regions('customrowsetting', 'course-section-', '12-0-0-0'); + } + + if (!empty($PAGE->theme->settings->tabbedlayoutcoursepage)) { + echo $OUTPUT->get_block_regions('customrowsetting', 'course-tab-one-', '12-0-0-0'); + echo $OUTPUT->get_block_regions('customrowsetting', 'course-tab-two-', '12-0-0-0'); + } + + if (!empty($PAGE->theme->settings->tabbedlayoutdashboard)) { + echo $OUTPUT->get_block_regions('customrowsetting', 'my-tab-one-', '12-0-0-0'); + echo $OUTPUT->get_block_regions('customrowsetting', 'my-tab-two-', '12-0-0-0'); + } + + ?> + + <h3><?php echo get_string('frnt-footer', 'theme_adaptable') ?></h3> + <?php + echo $OUTPUT->blocks('frnt-footer', 'col-10'); + ?> + </div> + </div> + <?php +} +?> +</div> + +<?php +// And to finish, we include the common footer file. +require_once(dirname(__FILE__) . '/includes/footer.php'); diff --git a/theme/adaptable/layout/includes/footer.php b/theme/adaptable/layout/includes/footer.php new file mode 100644 index 0000000..f50f18b --- /dev/null +++ b/theme/adaptable/layout/includes/footer.php @@ -0,0 +1,157 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015-2017 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Load messages / notifications. +echo $OUTPUT->standard_after_main_region_html(); +?> + +<footer id="page-footer" class="<?php echo $PAGE->theme->settings->responsivepagefooter?>"> + +<?php +echo $OUTPUT->get_footer_blocks(); + +if ($PAGE->theme->settings->hidefootersocial == 1) { ?> + <div class="container"> + <div class="row"> + <div class="col-12 pagination-centered socialicons"> + <?php + echo $OUTPUT->socialicons(); + ?> + </div> + </div> + </div> +<?php } + +if ($PAGE->theme->settings->moodledocs) { + $footnoteclass = 'col-md-4 my-md-0 my-2'; +} else { + $footnoteclass = 'col-md-8 my-md-0 my-2'; +} + +if ($PAGE->theme->settings->showfooterblocks) { +?> + <div class="info container2 clearfix"> + <div class="container"> + <div class="row"> + <div class="<?php echo $footnoteclass; ?>"> + <div class="tool_usertours-resettourcontainer"></div> + <?php echo $OUTPUT->get_setting('footnote', 'format_html');?> + </div> + + <?php + if ($PAGE->theme->settings->moodledocs) { + ?> + <div class="col-md-4 my-md-0 my-2 helplink"> + <?php + echo $OUTPUT->page_doc_link(); ?> + </div> + <?php + } + ?> + <div class="col-md-4 my-md-0 my-2"> + <?php echo $OUTPUT->standard_footer_html(); ?> + </div> + </div> + </div> + </div> + <?php +} +?> +</footer> + +<div id="back-to-top"><i class="fa fa-angle-up "></i></div> + +<?php + // If admin settings page, show template for floating save / discard buttons. + $templatecontext = [ + 'topmargin' => ($PAGE->theme->settings->stickynavbar ? '35px' : '0px'), + 'savetext' => get_string('savebuttontext', 'theme_adaptable'), + 'discardtext' => get_string('discardbuttontext', 'theme_adaptable') + ]; + if (strstr($PAGE->pagetype, 'admin-setting')) { + if ($PAGE->theme->settings->enablesavecanceloverlay) { + echo $OUTPUT->render_from_template('theme_adaptable/savediscard', $templatecontext); + } + } +?> + +<?php echo $OUTPUT->standard_end_of_body_html() ?> + +</div> +<?php echo $PAGE->theme->settings->jssection; ?> + +<?php + + +// Conditional javascript based on a user profile field. +if (!empty($PAGE->theme->settings->jssectionrestrictedprofilefield)) { + // Get custom profile field setting. (e.g. faculty=fbl). + $fields = explode('=', $PAGE->theme->settings->jssectionrestrictedprofilefield); + $ftype = $fields[0]; + $setvalue = $fields[1]; + + // Get user profile field (if it exists). + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + profile_load_data($USER); + $ftype = "profile_field_$ftype"; + if (isset($USER->$ftype)) { + if ($USER->$ftype == $setvalue) { + // Match between user profile field value and value in setting. + + if (!empty($PAGE->theme->settings->jssectionrestricteddashboardonly)) { + + // If this is set to restrict to dashboard only, check if we are on dashboard page. + if ($PAGE->has_set_url()) { + $url = $PAGE->url; + } else if ($ME !== null) { + $url = new moodle_url(str_ireplace('/my/', '/', $ME)); + } + + // In practice, $url should always be valid. + if ($url !== null) { + // Check if this is the dashboard page. + if (strstr ($url->raw_out(), '/my/')) { + echo $PAGE->theme->settings->jssectionrestricted; + } + } + } else { + echo $PAGE->theme->settings->jssectionrestricted; + } + } + } +} + +?> + +<?php echo $OUTPUT->get_all_tracking_methods(); ?> +<script type="text/javascript"> + require(['theme_boost/loader']); +</script> +</body> +</html> diff --git a/theme/adaptable/layout/includes/head.php b/theme/adaptable/layout/includes/head.php new file mode 100644 index 0000000..24b7208 --- /dev/null +++ b/theme/adaptable/layout/includes/head.php @@ -0,0 +1,131 @@ +<?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/>. + +/** + * Head + * + * @package theme_adaptable + * @copyright 2020 G J Barnard (http://moodle.org/user/profile.php?id=442195) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die(); + +// Select fonts used. +$fontname = ''; +$fontheadername = ''; +$fonttitlename = ''; +$fontweight = ''; +$fontheaderweight = ''; +$fonttitleweight = ''; +$fontssubset = ''; + +switch ($PAGE->theme->settings->fontname) { + case 'sans-serif': + // Use 'sans-serif'. + break; + + default: + // Get the Google font. + $fontname = str_replace(" ", "+", $PAGE->theme->settings->fontname); + break; +} + +switch ($PAGE->theme->settings->fontheadername) { + case 'sans-serif': + // Use 'sans-serif'. + break; + + default: + // Get the Google font. + $fontheadername = str_replace(" ", "+", $PAGE->theme->settings->fontheadername); + break; +} + +switch ($PAGE->theme->settings->fonttitlename) { + case 'sans-serif': + // Use 'sans-serif'. + break; + + default: + // Get the Google font. + $fonttitlename = str_replace(" ", "+", $PAGE->theme->settings->fonttitlename); + break; +} + +if ((!empty($fontname)) || (!empty($fontheadername)) || (!empty($fonttitlename))) { + // Get the Google Font weights. + $fontweight = ':'.$PAGE->theme->settings->fontweight.','.$PAGE->theme->settings->fontweight.'i'; + $fontheaderweight = ':'.$PAGE->theme->settings->fontheaderweight.','.$PAGE->theme->settings->fontheaderweight.'i'; + $fonttitleweight = ':'.$PAGE->theme->settings->fonttitleweight.','.$PAGE->theme->settings->fonttitleweight.'i'; + + // Get the Google fonts subset. + if (!empty($PAGE->theme->settings->fontsubset)) { + $fontssubset = '&subset='.$PAGE->theme->settings->fontsubset; + } +} + +// HTML head. +echo $OUTPUT->standard_head_html(); +$siteurl = new moodle_url(''); +?> + <!-- CSS print media --> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <!-- Twitter Card data --> + <meta name="twitter:card" value="summary"> + <meta name="twitter:site" value="<?php echo $SITE->fullname; ?>" /> + <meta name="twitter:title" value="<?php echo $OUTPUT->page_title(); ?>" /> + + <!-- Open Graph data --> + <meta property="og:title" content="<?php echo $OUTPUT->page_title(); ?>" /> + <meta property="og:type" content="website" /> + <meta property="og:url" content="<?php echo $siteurl->out(); ?>" /> + <meta name="og:site_name" value="<?php echo $SITE->fullname; ?>" /> + + <!-- Chrome, Firefox OS and Opera on Android topbar color --> + <meta name="theme-color" content="<?php echo $PAGE->theme->settings->maincolor; ?>" /> + + <!-- Windows Phone topbar color --> + <meta name="msapplication-navbutton-color" content="<?php echo $PAGE->theme->settings->maincolor; ?>" /> + + <!-- iOS Safari topbar color --> + <meta name="apple-mobile-web-app-status-bar-style" content="<?php echo $PAGE->theme->settings->maincolor; ?>" /> + + <?php + // Load fonts. + if ((!empty($fontname)) && ($fontname != 'default')) { + echo '<!-- Load Google Fonts -->'; + echo '<link href="https://fonts.googleapis.com/css?family='; + echo $fontname.$fontweight.$fontssubset; + echo '" rel="stylesheet" type="text/css">'; + } + + if ((!empty($fontheadername)) && ($fontheadername != 'default')) { + echo '<link href="https://fonts.googleapis.com/css?family='; + echo $fontheadername.$fontheaderweight.$fontssubset; + echo '" rel="stylesheet" type="text/css">'; + } + + if ((!empty($fonttitlename)) && ($fonttitlename != 'default')) { + echo '<link href="https://fonts.googleapis.com/css?family='; + echo $fonttitlename.$fonttitleweight.$fontssubset; + echo '" rel="stylesheet" type="text/css">'; + } + ?> +</head> +<?php diff --git a/theme/adaptable/layout/includes/header.php b/theme/adaptable/layout/includes/header.php new file mode 100644 index 0000000..f6cdaa1 --- /dev/null +++ b/theme/adaptable/layout/includes/header.php @@ -0,0 +1,344 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die(); + +/* Check if this is a course or module page and check setting to hide site title. + If not one of these pages, by default show it (set $hidesitetitle to false). */ +if ( (strstr($PAGE->pagetype, 'course')) || + (strstr($PAGE->pagetype, 'mod')) && ($this->page->course->id > 1) ) { + $hidesitetitle = !empty(($PAGE->theme->settings->coursepageheaderhidesitetitle)) ? true : false; +} else { + $hidesitetitle = false; +} + +// Screen size. +theme_adaptable_initialise_zoom(); +$setzoom = theme_adaptable_get_zoom(); + +theme_adaptable_initialise_full(); +$setfull = theme_adaptable_get_full(); + +$bsoptionsdata = array('data' => array()); + +// Main navbar. +if (isset($PAGE->theme->settings->stickynavbar) && $PAGE->theme->settings->stickynavbar == 1 + && $PAGE->pagetype != "grade-report-grader-index" && $PAGE->bodyid != "page-grade-report-grader-index") { + $fixedheader = true; + $bsoptionsdata['data']['stickynavbar'] = true; +} else { + $bsoptionsdata['data']['stickynavbar'] = false; +} + +// JS calls. +$PAGE->requires->js_call_amd('theme_adaptable/adaptable', 'init'); +$PAGE->requires->js_call_amd('theme_adaptable/bsoptions', 'init', $bsoptionsdata); +$PAGE->requires->js_call_amd('theme_adaptable/drawer', 'init'); + +// Layout. +$left = (!right_to_left()); // To know if to add 'pull-right' and 'desktop-first-column' classes in the layout for LTR. + +// Navbar Menu. +$shownavbar = false; +if ( + (isloggedin() && !isguestuser()) || + (!empty($PAGE->theme->settings->enablenavbarwhenloggedout)) ) { + + // Show navbar unless disabled by config. + if (empty($PAGE->layout_options['nonavbar'])) { + $shownavbar = true; + } +} +// Load header background image if it exists. +$headerbg = ''; +if (!empty($PAGE->theme->settings->categoryhavecustomheader)) { + $currenttopcat = \theme_adaptable\toolbox::get_current_top_level_catetgory(); + if (!empty($currenttopcat)) { + $categoryheaderbgimageset = 'categoryheaderbgimage'.$currenttopcat; + if (!empty($PAGE->theme->settings->$categoryheaderbgimageset)) { + $headerbg = ' class="headerbgimage" style="background-image: ' . + 'url(\''.$PAGE->theme->setting_file_url($categoryheaderbgimageset, $categoryheaderbgimageset).'\');"'; + } + } +} else { + $currenttopcat = false; +} +if ((empty($headerbg)) && (!empty($PAGE->theme->settings->headerbgimage))) { + $headerbg = ' class="headerbgimage" style="background-image: ' . + 'url(\''.$PAGE->theme->setting_file_url('headerbgimage', 'headerbgimage').'\');"'; +} + +/* Choose the header style. There styles available are: + "style1" (original header) + "style2" (2 row header). +*/ + +if (!empty($PAGE->theme->settings->headerstyle)) { + $adaptableheaderstyle = $PAGE->theme->settings->headerstyle; +} else { + $adaptableheaderstyle = "style1"; +} + +// Social icons class. +$showicons = ""; +$showicons = $PAGE->theme->settings->blockicons; +if ($showicons == 1) { + $showiconsclass = "showblockicons"; +} else { + $showiconsclass = " "; +} + +$standardscreenwidthclass = 'standard'; +if (!empty($PAGE->theme->settings->standardscreenwidth)) { + $standardscreenwidthclass = $PAGE->theme->settings->standardscreenwidth; +} + +// HTML header. +echo $OUTPUT->doctype(); +?> +<html <?php echo $OUTPUT->htmlattributes(); ?>> +<head> + <title><?php echo $OUTPUT->page_title(); ?></title> + <link rel="icon" href="<?php echo $OUTPUT->favicon(); ?>" /> + +<?php +// Include header. +require_once(dirname(__FILE__) . '/head.php'); + +// If it is a mobile and the header is not hidden or it is a desktop there will be a page header. +$pageheader = 'has-page-header'; + +$hasheaderbg = ''; +if (!empty($headerbg)) { + $hasheaderbg = 'has-header-bg'; +} + +$nomobilenavigation = ''; +if (!empty($PAGE->theme->settings->responsivesectionnav)) { + $nomobilenavigation = 'nomobilenavigation'; +} +?> +<body <?php echo $OUTPUT->body_attributes(array('theme_adaptable', 'two-column', $setzoom, 'header-'.$adaptableheaderstyle, + $pageheader, $hasheaderbg, $nomobilenavigation)); ?>> + +<?php +echo $OUTPUT->standard_top_of_body_html(); + +// Development or wrong moodle version alert. +// echo $OUTPUT->get_dev_alert();. +?> + +<div id="page" class="<?php echo "$setfull $showiconsclass $standardscreenwidthclass"; ?>"> + +<?php +echo $OUTPUT->get_alert_messages(); + +$headercontext = [ + 'output' => $OUTPUT +]; + +if ((!isloggedin() || isguestuser()) && ($PAGE->pagetype != "login-index")) { + if ($PAGE->theme->settings->displaylogin != 'no') { + $loginformcontext = [ + 'displayloginbox' => ($PAGE->theme->settings->displaylogin == 'box') ? true : false, + 'output' => $OUTPUT, + 'token' => s(\core\session\manager::get_login_token()), + 'url' => new moodle_url('/login/index.php') + ]; + + $headercontext['loginoruser'] = $OUTPUT->render_from_template('theme_adaptable/headerloginform', $loginformcontext); + } else { + $headercontext['loginoruser'] = ''; + } +} else { + // Display user profile menu. + // Only used when user is logged in and not on the secure layout. + if ((isloggedin()) && ($PAGE->pagelayout != 'secure')) { + // User icon. + $userpic = $OUTPUT->user_picture($USER, array('link' => false, 'visibletoscreenreaders' => false, + 'size' => 50, 'class' => 'userpicture')); + // User name. + $username = format_string(fullname($USER)); + + // User menu dropdown. + if (!empty($PAGE->theme->settings->usernameposition)) { + $usernameposition = $PAGE->theme->settings->usernameposition; + if ($usernameposition == 'right') { + $usernamepositionleft = false; + } else { + $usernamepositionleft = true; + } + } else { + $usernamepositionleft = true; + } + + // Set template context. + $usermenucontext = [ + 'username' => $username, + 'userpic' => $userpic, + 'showusername' => $PAGE->theme->settings->showusername, + 'usernamepositionleft' => $usernamepositionleft, + 'userprofilemenu' => $OUTPUT->user_profile_menu(), + ]; + $usermenu = $OUTPUT->render_from_template('theme_adaptable/usermenu', $usermenucontext); + $headercontext['loginoruser'] = '<li class="nav-item dropdown ml-3 ml-md-4 mr-2 mr-md-0">'.$usermenu.'</li>'; + } else { + $headercontext['loginoruser'] = ''; + } +} + +if (!$hidesitetitle) { + $headercontext['sitelogo'] = $OUTPUT->get_logo($currenttopcat); + $headercontext['sitetitle'] = $OUTPUT->get_title($currenttopcat); +} + +$headercontext['headerbg'] = $headerbg; +$headercontext['nonavbar'] = (!empty($PAGE->layout_options['nonavbar'])); +$headercontext['responsivesearchicon'] = (!empty($PAGE->theme->settings->responsivesearchicon)) ? ' d-xs-block d-sm-block d-md-none my-auto' : ' d-none'; +$headercontext['shownavbar'] = $shownavbar; +if (!empty($PAGE->theme->settings->pageheaderlayout)) { + $headercontext['pageheaderoriginal'] = ($PAGE->theme->settings->pageheaderlayout == 'original'); +} else { + $headercontext['pageheaderoriginal'] = true; +} + +// Navbar Menu. +if ($shownavbar) { + $headercontext['shownavbar'] = [ + 'disablecustommenu' => (!empty($PAGE->theme->settings->disablecustommenu)), + 'navigationmenu' => $OUTPUT->navigation_menu('main-navigation'), + 'navigationmenudrawer' => $OUTPUT->navigation_menu('main-navigation-drawer'), + 'output' => $OUTPUT, + 'searchurl' => new moodle_url('/admin/search.php'), + 'toolsmenu' => ($PAGE->theme->settings->enabletoolsmenus) + ]; + + if ($PAGE->theme->settings->enabletoolsmenus) { + $headercontext['shownavbar']['toolsmenudrawer'] = $OUTPUT->tools_menu('tools-menu-drawer'); + } + + $navbareditsettings = $PAGE->theme->settings->editsettingsbutton; + $headercontext['shownavbar']['showcog'] = true; + $showeditbuttons = false; + + if ($navbareditsettings == 'button') { + $showeditbuttons = true; + $headercontext['shownavbar']['showcog'] = false; + } else if ($navbareditsettings == 'cogandbutton') { + $showeditbuttons = true; + } + + if ($headercontext['shownavbar']['showcog']) { + $headercontext['shownavbar']['coursemenucontent'] = $OUTPUT->context_header_settings_menu(); + $headercontext['shownavbar']['othermenucontent'] = $OUTPUT->region_main_settings_menu(); + } + + /* Ensure to only hide the button on relevant pages. Some pages will need the button, such as the + dashboard page. Checking if the cog is being displayed above to figure out if it still needs to + show (when there is no cog). Also show mod pages (e.g. Forum, Lesson) as these sometimes have + a button for a specific purpose. */ + if (($showeditbuttons) || + (($headercontext['shownavbar']['showcog']) && ((empty($headercontext['shownavbar']['coursemenucontent'])) && (empty($headercontext['shownavbar']['othermenucontent'])))) || + (strstr($PAGE->pagetype, 'mod-'))) { + $headercontext['shownavbar']['pageheadingbutton'] = $OUTPUT->page_heading_button(); + } + + if (isloggedin()) { + if (!empty($this->page->theme->settings->enableshowhideblocks)) { + $zoomside = ((!empty($this->page->theme->settings->blockside)) && ($this->page->theme->settings->blockside == 1)) ? 'left' : 'right'; + $hidetitle = get_string('hideblocks', 'theme_adaptable'); + $showtitle = get_string('showblocks', 'theme_adaptable'); + if ($setzoom == 'zoomin') { // Blocks not shown. + $zoominicontitle = $showtitle; + if ($zoomside == 'right') { + $icontype = 'outdent'; + } else { + $icontype = 'indent'; + } + } else { + $zoominicontitle = $hidetitle; + if ($zoomside == 'right') { + $icontype = 'indent'; + } else { + $icontype = 'outdent'; + } + } + $headercontext['shownavbar']['showhideblocks'] = true; + $headercontext['shownavbar']['showhideblockszoomside'] = $zoomside; + $headercontext['shownavbar']['showhideblockszoominicontitle'] = $zoominicontitle; + $headercontext['shownavbar']['showhideblockshidetitle'] = $hidetitle; + $headercontext['shownavbar']['showhideblocksshowtitle'] = $showtitle; + $headercontext['shownavbar']['showhideblocksicontype'] = $icontype; + $headercontext['shownavbar']['showhideblockstext'] = ($PAGE->theme->settings->enableshowhideblockstext); + + $PAGE->requires->js_call_amd('theme_adaptable/zoomin', 'init'); + } + if ($PAGE->theme->settings->enablezoom) { + $headercontext['shownavbar']['enablezoom'] = true; + $headercontext['shownavbar']['enablezoomshowtext'] = ($PAGE->theme->settings->enablezoomshowtext); + } + } +} + +if ($adaptableheaderstyle == "style1") { + $headercontext['menuslinkright'] = (!empty($PAGE->theme->settings->menuslinkright)); + $headercontext['coursesearch'] = new moodle_url('/course/search.php'); + $headercontext['langmenu'] = (empty($PAGE->layout_options['langmenu']) || $PAGE->layout_options['langmenu']); + $headercontext['responsiveheader'] = $PAGE->theme->settings->responsiveheader; + + if (!$headercontext['nonavbar']) { + // Social icons. + if ($PAGE->theme->settings->socialorsearch == 'social') { + $headersocialcontext = [ + 'classes' => $PAGE->theme->settings->responsivesocial, + 'pageheaderoriginal' => $headercontext['pageheaderoriginal'], + 'output' => $OUTPUT + ]; + $headercontext['socialorsearch'] = $OUTPUT->render_from_template('theme_adaptable/headersocial', $headersocialcontext); + } + // Search box. + if ((!$hidesitetitle) && ($PAGE->theme->settings->socialorsearch == 'search') ) { + $headersearchcontext = [ + 'pagelayout' => ($headercontext['pageheaderoriginal']) ? 'pagelayoutoriginal' : 'pagelayoutalternative', + 'url' => new moodle_url('/course/search.php') + ]; + $headercontext['socialorsearch'] = $OUTPUT->render_from_template('theme_adaptable/headersearch', $headersearchcontext); + } + } + + echo $OUTPUT->render_from_template('theme_adaptable/headerstyleone', $headercontext); +} else if ($adaptableheaderstyle == "style2") { + $headercontext['topmenus'] = $OUTPUT->get_top_menus(false); + if (empty($PAGE->layout_options['langmenu']) || $PAGE->layout_options['langmenu']) { + $headercontext['langmenu'] = '<div class="my-auto">'.$OUTPUT->lang_menu(false).'</div>'; + } + + echo $OUTPUT->render_from_template('theme_adaptable/headerstyletwo', $headercontext); +} + +// Display News Ticker. +echo $OUTPUT->get_news_ticker(); diff --git a/theme/adaptable/layout/includes/loginnofooter.php b/theme/adaptable/layout/includes/loginnofooter.php new file mode 100644 index 0000000..df610a0 --- /dev/null +++ b/theme/adaptable/layout/includes/loginnofooter.php @@ -0,0 +1,37 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2021 G J Barnard (http://moodle.org/user/profile.php?id=442195) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +echo $OUTPUT->standard_after_main_region_html(); +echo $OUTPUT->standard_end_of_body_html(); + +echo $OUTPUT->get_all_tracking_methods(); ?> +<script type="text/javascript"> + require(['theme_boost/loader']); +</script> +</body> +</html> +<?php diff --git a/theme/adaptable/layout/includes/loginnoheader.php b/theme/adaptable/layout/includes/loginnoheader.php new file mode 100644 index 0000000..6643935 --- /dev/null +++ b/theme/adaptable/layout/includes/loginnoheader.php @@ -0,0 +1,70 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2019 G J Barnard (http://moodle.org/user/profile.php?id=442195) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die(); + +// Set HTTPS if needed. +if (empty($CFG->loginhttps)) { + $wwwroot = $CFG->wwwroot; +} else { + $wwwroot = str_replace("http://", "https://", $CFG->wwwroot); +} + +$standardscreenwidthclass = 'standard'; +if (!empty($PAGE->theme->settings->standardscreenwidth)) { + $standardscreenwidthclass = $PAGE->theme->settings->standardscreenwidth; +} + +// HTML header. +echo $OUTPUT->doctype(); +?> +<html <?php echo $OUTPUT->htmlattributes(); ?>> +<head> + <title><?php echo $OUTPUT->page_title(); ?></title> + <link rel="icon" href="<?php echo $OUTPUT->favicon(); ?>" /> + +<?php + +theme_adaptable_initialise_full(); +$setfull = theme_adaptable_get_full(); + +// Include header. +require_once(dirname(__FILE__) . '/head.php'); +?> + +<body <?php echo $OUTPUT->body_attributes(array('two-column')); ?>> + +<?php +echo $OUTPUT->standard_top_of_body_html(); + +// Development or wrong moodle version alert. +// echo $OUTPUT->get_dev_alert();. +?> + +<div id="page" class="container-fluid <?php echo "$setfull $standardscreenwidthclass"; ?>"> + +<?php + // Display alerts. + echo $OUTPUT->get_alert_messages(); diff --git a/theme/adaptable/layout/login.php b/theme/adaptable/layout/login.php new file mode 100644 index 0000000..24ade93 --- /dev/null +++ b/theme/adaptable/layout/login.php @@ -0,0 +1,82 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @copyright 2019 G J Barnard (http://moodle.org/user/profile.php?id=442195) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Include header. +global $PAGE, $OUTPUT; + +if (!empty($PAGE->theme->settings->loginheader)) { + require_once(dirname(__FILE__) . '/includes/header.php'); +} else { + require_once(dirname(__FILE__) . '/includes/loginnoheader.php'); +} + +echo '<div class="container outercont">'; + echo $OUTPUT->page_navbar(); + ?> + <div id="page-content" class="row"> + <section id="region-main" class="col-12"> + <?php + + $logintextboxtop = $OUTPUT->get_setting('logintextboxtop', 'format_html'); + $logintextboxbottom = $OUTPUT->get_setting('logintextboxbottom', 'format_html'); + $logintextstartwrapper = ''; + $logintextendwrapper = ''; + if ((!empty($logintextboxtop)) || (!empty($logintextboxbottom))) { + $logintextstartwrapper = '<div class="row justify-content-center"><div class="col-xl-6 col-sm-8 ">' . + '<div class="card"><div class="card-block">'; + $logintextendwrapper = '</div></div></div></div>'; + } + + if (!empty($logintextboxtop)) { + echo $logintextstartwrapper; + echo $logintextboxtop; + echo $logintextendwrapper; + } + + echo $OUTPUT->main_content(); + + if (!empty($logintextboxbottom)) { + echo '<div class="my-1 my-sm-5"></div>'; + echo $logintextstartwrapper; + echo $logintextboxbottom; + echo $logintextendwrapper; + } + + ?> + </section> + </div> +</div> + +<?php +// Include footer. +if (!empty($PAGE->theme->settings->loginfooter)) { + require_once(dirname(__FILE__) . '/includes/footer.php'); +} else { + require_once(dirname(__FILE__) . '/includes/loginnofooter.php'); +} diff --git a/theme/adaptable/layout/maintenance.php b/theme/adaptable/layout/maintenance.php new file mode 100644 index 0000000..b4a1bda --- /dev/null +++ b/theme/adaptable/layout/maintenance.php @@ -0,0 +1,49 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +echo $OUTPUT->doctype(); +?> +<html <?php echo $OUTPUT->htmlattributes(); ?>> + <head> + <title><?php echo $OUTPUT->page_title(); ?></title> + <link rel="shortcut icon" href="<?php echo $OUTPUT->favicon(); ?>" /> + <?php echo $OUTPUT->standard_head_html() ?> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + </head> + <body> + <div class="container outercont"> + <div id="page-content" class="row"> + <section id="region-main" class="col-12"> + <?php + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + echo $OUTPUT->course_content_footer(); + ?> + </section> + </div> + </div> diff --git a/theme/adaptable/layout/secure.php b/theme/adaptable/layout/secure.php new file mode 100644 index 0000000..964cf2b --- /dev/null +++ b/theme/adaptable/layout/secure.php @@ -0,0 +1,71 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2018 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Include header. +require_once(dirname(__FILE__) . '/includes/header.php'); + +$left = $PAGE->theme->settings->blockside; + +$hassidepost = $PAGE->blocks->region_has_content('side-post', $OUTPUT); +$regions = theme_adaptable_grid($left, $hassidepost); +?> + +<div id="page" class="container-outercont"> + <?php + echo $OUTPUT->page_navbar(); + ?> + <div id="page-content" class="row<?php echo $regions['direction'];?>"> + <section id="region-main" class="<?php echo $regions['content']; ?>"> + <?php + echo $OUTPUT->get_course_alerts(); + echo $OUTPUT->course_content_header(); + echo $OUTPUT->main_content(); + if ($PAGE->has_set_url()) { + $currenturl = $PAGE->url; + } else { + $currenturl = $_SERVER["REQUEST_URI"]; + } ?> + </section> + <?php + if ($hassidepost) { + echo $OUTPUT->blocks('side-post', $regions['blocks'].' d-print-none '); + } + ?> + </div> +</div> + +<script type="text/javascript"> + <?php echo $PAGE->theme->settings->jssection;?> +</script> + +<?php echo $OUTPUT->standard_end_of_body_html(); ?> +<script type="text/javascript"> + require(['theme_boost/loader']); +</script> +</body> +</html> diff --git a/theme/adaptable/lib.php b/theme/adaptable/lib.php new file mode 100644 index 0000000..d01018c --- /dev/null +++ b/theme/adaptable/lib.php @@ -0,0 +1,833 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +define('THEME_ADAPTABLE_DEFAULT_ALERTCOUNT', '1'); +define('THEME_ADAPTABLE_DEFAULT_ANALYTICSCOUNT', '1'); +define('THEME_ADAPTABLE_DEFAULT_TOPMENUSCOUNT', '1'); +define('THEME_ADAPTABLE_DEFAULT_TOOLSMENUSCOUNT', '1'); +define('THEME_ADAPTABLE_DEFAULT_NEWSTICKERCOUNT', '1'); +define('THEME_ADAPTABLE_DEFAULT_SLIDERCOUNT', '3'); + + + +/** + * Parses CSS before it is cached. + * + * This function can make alterations and replace patterns within the CSS. + * + * @param string $css The CSS + * @param theme_config $theme The theme config object. + * @return string The parsed CSS The parsed CSS. + */ +function theme_adaptable_process_css($css, $theme) { + + // Set category custom CSS. + $css = theme_adaptable_set_categorycustomcss($css, $theme->settings); + + // Collapsed Topics colours. + if (empty($theme->settings->collapsedtopicscoloursenabled)) { + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span.the_toggle h3.sectionname,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span.the_toggle h3.sectionname a,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span.the_toggle h3.sectionname a:hover,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span.the_toggle h3.sectionname a:focus,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden h3.sectionname'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden h3.sectionname a,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden h3.sectionname a:hover,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden h3.sectionname a:focus {'.PHP_EOL; + $css .= ' color: [[setting:sectionheadingcolor]];'.PHP_EOL; + $css .= '}'.PHP_EOL;; + + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content div.toggle,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content div.toggle:hover,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content div.toggle:focus {'.PHP_EOL; + $css .= ' background-color: [[setting:coursesectionheaderbg]];'.PHP_EOL; + $css .= '}'.PHP_EOL; + + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span:hover,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content .toggle span:focus,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden:hover,'.PHP_EOL; + $css .= '.theme_adaptable .course-content ul.ctopics li.section .content.sectionhidden:focus {'.PHP_EOL; + $css .= ' color: inherit;'.PHP_EOL; + $css .= '}'.PHP_EOL; + } + + // Set custom CSS. + if (!empty($theme->settings->customcss)) { + $customcss = $theme->settings->customcss; + } else { + $customcss = null; + } + $css = theme_adaptable_set_customcss($css, $customcss); + + // Define the default settings for the theme in case they've not been set. + $defaults = array( + '[[setting:linkcolor]]' => '#51666C', + '[[setting:linkhover]]' => '#009688', + '[[setting:maincolor]]' => '#3A454b', + '[[setting:backcolor]]' => '#FFFFFF', + '[[setting:regionmaincolor]]' => '#FFFFFF', + '[[setting:rendereroverlaycolor]]' => '#3A454b', + '[[setting:rendereroverlayfontcolor]]' => '#FFFFFF', + '[[setting:buttoncolor]]' => '#51666C', + '[[setting:buttontextcolor]]' => '#ffffff', + '[[setting:buttonhovercolor]]' => '#009688', + '[[setting:buttoncolorscnd]]' => '#51666C', + '[[setting:buttontextcolorscnd]]' => '#ffffff', + '[[setting:buttonhovercolorscnd]]' => '#009688', + '[[setting:buttoncolorcancel]]' => '#ef5350', + '[[setting:buttontextcolorcancel]]' => '#ffffff', + '[[setting:buttonhovercolorcancel]]' => '#e53935', + '[[setting:buttonlogincolor]]' => '#ef5350', + '[[setting:buttonloginhovercolor]]' => '#e53935', + '[[setting:buttonlogintextcolor]]' => '#0084c2', + '[[setting:buttonloginpadding]]' => '0px', + '[[setting:buttonloginheight]]' => '24px', + '[[setting:buttonloginmargintop]]' => '2px', + '[[setting:buttonradius]]' => '5px', + '[[setting:buttondropshadow]]' => '0px', + '[[setting:dividingline]]' => '#ffffff', + '[[setting:dividingline2]]' => '#ffffff', + '[[setting:breadcrumb]]' => '#b4bbbf', + '[[setting:breadcrumbtextcolor]]' => '#444444', + '[[setting:breadcrumbseparator]]' => 'angle-right', + '[[setting:loadingcolor]]' => '#00B3A1', + '[[setting:messagepopupbackground]]' => '#fff000', + '[[setting:messagepopupcolor]]' => '#333333', + '[[setting:messagingbackgroundcolor]]' => '#FFFFFF', + '[[setting:footerbkcolor]]' => '#424242', + '[[setting:footertextcolor]]' => '#ffffff', + '[[setting:footertextcolor2]]' => '#ffffff', + '[[setting:footerlinkcolor]]' => '#ffffff', + '[[setting:headerbkcolor]]' => '#00796B', + '[[setting:headerbkcolor2]]' => '#009688', + '[[setting:headertextcolor]]' => '#ffffff', + '[[setting:headertextcolor2]]' => '#ffffff', + '[[setting:msgbadgecolor]]' => '#E53935', + '[[setting:blockbackgroundcolor]]' => '#FFFFFF', + '[[setting:blockheaderbackgroundcolor]]' => '#FFFFFF', + '[[setting:blockbordercolor]]' => '#59585D', + '[[setting:blockregionbackgroundcolor]]' => 'transparent', + '[[setting:selectiontext]]' => '#000000', + '[[setting:selectionbackground]]' => '#00B3A1', + '[[setting:marketblockbordercolor]]' => '#e8eaeb', + '[[setting:marketblocksbackgroundcolor]]' => 'transparent', + '[[setting:blockheaderbordertop]]' => '1px', + '[[setting:blockheaderborderleft]]' => '0px', + '[[setting:blockheaderborderright]]' => '0px', + '[[setting:blockheaderborderbottom]]' => '0px', + '[[setting:blockmainbordertop]]' => '0px', + '[[setting:blockmainborderleft]]' => '0px', + '[[setting:blockmainborderright]]' => '0px', + '[[setting:blockmainborderbottom]]' => '0px', + '[[setting:blockheaderbordertopstyle]]' => 'dashed', + '[[setting:blockmainbordertopstyle]]' => 'solid', + '[[setting:blockheadertopradius]]' => '0px', + '[[setting:blockheaderbottomradius]]' => '0px', + '[[setting:blockmaintopradius]]' => '0px', + '[[setting:blockmainbottomradius]]' => '0px', + '[[setting:coursesectionbgcolor]]' => '#FFFFFF', + '[[setting:coursesectionheaderbg]]' => '#FFFFFF', + '[[setting:coursesectionheaderbordercolor]]' => '#F3F3F3', + '[[setting:coursesectionheaderborderstyle]]' => '', + '[[setting:coursesectionheaderborderwidth]]' => '', + '[[setting:coursesectionheaderborderradiustop]]' => '', + '[[setting:coursesectionheaderborderradiusbottom]]' => '', + '[[setting:coursesectionborderstyle]]' => '1px', + '[[setting:coursesectionborderwidth]]' => '', + '[[setting:coursesectionbordercolor]]' => '#e8eaeb', + '[[setting:coursesectionborderradius]]' => '', + '[[setting:coursesectionactivityiconsize]]' => '24px', + '[[setting:coursesectionactivityheadingcolour]]' => '#0066cc', + '[[setting:coursesectionactivityborderwidth]]' => '2px', + '[[setting:coursesectionactivityborderstyle]]' => 'dashed', + '[[setting:coursesectionactivitybordercolor]]' => '#eeeeee', + '[[setting:coursesectionactivityleftborderwidth]]' => '3px', + '[[setting:coursesectionactivityassignleftbordercolor]]' => '#0066cc', + '[[setting:coursesectionactivityassignbgcolor]]' => '#FFFFFF', + '[[setting:coursesectionactivityforumleftbordercolor]]' => '#990099', + '[[setting:coursesectionactivityforumbgcolor]]' => '#FFFFFF', + '[[setting:coursesectionactivityquizleftbordercolor]]' => '#FF3333', + '[[setting:coursesectionactivityquizbgcolor]]' => '#FFFFFF', + '[[setting:coursesectionactivitymargintop]]' => '2px', + '[[setting:coursesectionactivitymarginbottom]]' => '2px', + '[[setting:tilesbordercolor]]' => '#3A454b', + '[[setting:slidermargintop]]' => '20px', + '[[setting:slidermarginbottom]]' => '20px', + '[[setting:currentcolor]]' => '#d9edf7', + '[[setting:sectionheadingcolor]]' => '#3A454b', + '[[setting:menufontsize]]' => '14px', + '[[setting:menufontpadding]]' => '20px', + '[[setting:topmenufontsize]]' => '14px', + '[[setting:menubkcolor]]' => '#ffffff', + '[[setting:menufontcolor]]' => '#444444', + '[[setting:menuhovercolor]]' => '#00B3A1', + '[[setting:menubordercolor]]' => '#00B3A1', + '[[setting:mobilemenubkcolor]]' => '#F9F9F9', + '[[setting:mobileslidebartabbkcolor]]' => '#F9F9F9', + '[[setting:mobileslidebartabiconcolor]]' => '#000000', + '[[setting:navbardropdownborderradius]]' => '0px', + '[[setting:navbardropdownhovercolor]]' => '#EEE', + '[[setting:navbardropdowntextcolor]]' => '#007', + '[[setting:navbardropdowntexthovercolor]]' => '#000', + '[[setting:navbardropdowntransitiontime]]' => '0.0s', + '[[setting:covbkcolor]]' => '#3A454b', + '[[setting:covfontcolor]]' => '#ffffff', + '[[setting:editonbk]]' => '#4caf50', + '[[setting:editoffbk]]' => '#f44336', + '[[setting:editverticalpadding]]' => '', + '[[setting:edithorizontalpadding]]' => '', + '[[setting:edittopmargin]]' => '', + '[[setting:editfont]]' => '#ffffff', + '[[setting:sliderh3color]]' => '#ffffff', + '[[setting:sliderh4color]]' => '#ffffff', + '[[setting:slidersubmitbgcolor]]' => '#51666C', + '[[setting:slidersubmitcolor]]' => '#ffffff', + '[[setting:slider2h3color]]' => '#000000', + '[[setting:slider2h4color]]' => '#000000', + '[[setting:slider2h3bgcolor]]' => '#000000', + '[[setting:slider2h4bgcolor]]' => '#ffffff', + '[[setting:slideroption2color]]' => '#51666C', + '[[setting:slideroption2submitcolor]]' => '#ffffff', + '[[setting:slideroption2a]]' => '#51666C', + '[[setting:socialsize]]' => '37px', + '[[setting:mobile]]' => '22', + '[[setting:socialpaddingside]]' => 16, + '[[setting:socialpaddingtop]]' => '0%', + '[[setting:fontname]]' => 'Open Sans', + '[[setting:fontsize]]' => '95%', + '[[setting:fontheadername]]' => 'Roboto', + '[[setting:fontcolor]]' => '#333333', + '[[setting:fontheadercolor]]' => '#333333', + '[[setting:fontweight]]' => '400', + '[[setting:fontheaderweight]]' => '400', + '[[setting:fonttitlename]]' => 'Roboto Condensed', + '[[setting:fonttitleweight]]' => '400', + '[[setting:fonttitlesize]]' => '48px', + '[[setting:fonttitlecolor]]' => '#ffffff', + '[[setting:fonttitlecolorcourse]]' => '#ffffff', + '[[setting:customfontname]]' => '', + '[[setting:customfontheadername]]' => '', + '[[setting:customfonttitlename]]' => '', + '[[setting:searchboxpadding]]' => '0px 0px 10px 0px', + '[[setting:enablesavecanceloverlay]]' => true, + '[[setting:pageheaderheight]]' => '72px', + '[[setting:emoticonsize]]' => '16px', + '[[setting:fullscreenwidth]]' => '98%', + '[[setting:coursetitlemaxwidth]]' => '20', + '[[setting:responsiveheader]]' => 'd-none d-lg-block', + '[[setting:responsivesocial]]' => 'd-none d-lg-block', + '[[setting:responsivesocialsize]]' => '34px', + '[[setting:responsivelogo]]' => 'd-none d-lg-block', + '[[setting:responsivecoursetitle]]' => 'd-none d-lg-block', + '[[setting:responsivesectionnav]]' => '1', + '[[setting:responsivesearchicon]]' => true, + '[[setting:responsiveticker]]' => 'd-none d-lg-block', + '[[setting:responsivebreadcrumb]]' => 'd-none d-md-flex', + '[[setting:responsiveslider]]' => 'd-none d-lg-block', + '[[setting:responsivepagefooter]]' => 'd-none d-lg-block', + '[[setting:hidefootersocial]]' => 1, + '[[setting:enableavailablecourses]]' => 'display', + '[[setting:enableticker]]' => true, + '[[setting:enabletickermy]]' => true, + '[[setting:tickerwidth]]' => '', + '[[setting:socialwallbackgroundcolor]]' => '#FFFFFF', + '[[setting:socialwallsectionradius]]' => '6px', + '[[setting:socialwallbordertopstyle]]' => 'solid', + '[[setting:socialwallborderwidth]]' => '2px', + '[[setting:socialwallbordercolor]]' => '#B9B9B9', + '[[setting:socialwallactionlinkcolor]]' => '#51666C', + '[[setting:socialwallactionlinkhovercolor]]' => '#009688', + '[[setting:fontblockheaderweight]]' => '400', + '[[setting:fontblockheadersize]]' => '22px', + '[[setting:fontblockheadercolor]]' => '#3A454b', + '[[setting:blockiconsheadersize]]' => '20px', + '[[setting:alertcolorinfo]]' => '#3a87ad', + '[[setting:alertbackgroundcolorinfo]]' => '#d9edf7', + '[[setting:alertbordercolorinfo]]' => '#bce8f1', + '[[setting:alertcolorsuccess]]' => '#468847', + '[[setting:alertbackgroundcolorsuccess]]' => '#dff0d8', + '[[setting:alertbordercolorsuccess]]' => '#d6e9c6', + '[[setting:alertcolorwarning]]' => '#8a6d3b', + '[[setting:alertbackgroundcolorwarning]]' => '#fcf8e3', + '[[setting:alertbordercolorwarning]]' => '#fbeed5', + '[[setting:forumheaderbackgroundcolor]]' => '#ffffff', + '[[setting:forumbodybackgroundcolor]]' => '#ffffff', + '[[setting:introboxbackgroundcolor]]' => '#ffffff', + '[[setting:showyourprogress]]' => '', + '[[setting:tabbedlayoutdashboardcolorselected]]' => '#06c', + '[[setting:tabbedlayoutdashboardcolorunselected]]' => '#eee', + '[[setting:tabbedlayoutcoursepagetabcolorselected]]' => '#06c', + '[[setting:tabbedlayoutcoursepagetabcolorunselected]]' => '#eee', + '[[setting:frontpagenumbertiles]]' => '4', + '[[setting:sidebarnotlogged]]' => 'true', + '[[setting:gdprbutton]]' => 1, + '[[setting:infoiconcolor]]' => '#5bc0de', + '[[setting:dangericoncolor]]' => '#d9534f', + '[[setting:loginheader]]' => 1, + '[[setting:loginfooter]]' => 1, + '[[setting:printpageorientation]]' => 'landscape', + '[[setting:printbodyfontsize]]' => '11pt', + '[[setting:printmargin]]' => '2cm 1cm 2cm 2cm', + '[[setting:printlineheight]]' => '1.2' + ); + + // Get all the defined settings for the theme and replace defaults. + foreach ($theme->settings as $key => $val) { + if (array_key_exists('[[setting:'.$key.']]', $defaults) && !empty($val)) { + $defaults['[[setting:'.$key.']]'] = $val; + } + } + + $homebkg = ''; + if (!empty($theme->settings->homebk)) { + $homebkg = $theme->setting_file_url('homebk', 'homebk'); + $homebkg = 'background-image: url("' . $homebkg . '");'; + } + $defaults['[[setting:homebkg]]'] = $homebkg; + + $loginbgimage = ''; + if (!empty($theme->settings->loginbgimage)) { + $loginbgimage = $theme->setting_file_url('loginbgimage', 'loginbgimage'); + $loginbgimage = 'background-image: url("' . $loginbgimage . '");'; + } + $defaults['[[setting:loginbgimage]]'] = $loginbgimage; + + $loginbgstyle = ''; + if (!empty($theme->settings->loginbgstyle)) { + $replacementstyle = 'cover'; + if ($theme->settings->loginbgstyle === 'stretch') { + $replacementstyle = '100% 100%'; + } + $loginbgstyle = 'background-size: ' . $replacementstyle . ';'; + } + $defaults['[[setting:loginbgstyle]]'] = $loginbgstyle; + + $loginbgopacity = ''; + if (!empty($theme->settings->loginbgopacity)) { + $loginbgopacity = '#page-login-index header {'.PHP_EOL; + $loginbgopacity .= 'background-color: '.\theme_adaptable\toolbox::hex2rgba($theme->settings->headerbkcolor2, + $theme->settings->loginbgopacity).') !important;'.PHP_EOL; + $loginbgopacity .= '}'.PHP_EOL; + $loginbgopacity .= '#page-login-index #page-navbar,'.PHP_EOL. + '#page-login-index .card {'; + $loginbgopacity .= 'background-color: rgba(255, 255, 255, '.$theme->settings->loginbgopacity.') !important;'.PHP_EOL; + $loginbgopacity .= '}'.PHP_EOL; + $loginbgopacity .= '#page-login-index #page-footer {'.PHP_EOL; + $loginbgopacity .= 'background-color: '.\theme_adaptable\toolbox::hex2rgba($theme->settings->footerbkcolor, + $theme->settings->loginbgopacity).') !important;'.PHP_EOL; + $loginbgopacity .= '}'.PHP_EOL; + } + $defaults['[[setting:loginbgopacity]]'] = $loginbgopacity; + + $socialpaddingsidehalf = '16'; + if (!empty($theme->settings->socialpaddingside)) { + $socialpaddingsidehalf = ''.$theme->settings->socialpaddingside / 2; + } + $defaults['[[setting:socialpaddingsidehalf]]'] = $socialpaddingsidehalf; + + // Replace the CSS with values from the $defaults array. + $css = strtr($css, $defaults); + if (empty($theme->settings->tilesshowallcontacts) || $theme->settings->tilesshowallcontacts == false) { + $css = theme_adaptable_set_tilesshowallcontacts($css, false); + } else { + $css = theme_adaptable_set_tilesshowallcontacts($css, true); + } + return $css; +} + +/** + * Adds any category custom CSS to the CSS before it is cached. + * + * @param string $css The original CSS. + * @param array $settings Theme settings. + * @return string The CSS which now contains our custom CSS. + */ +function theme_adaptable_set_categorycustomcss($css, $settings) { + $tohavecustomheader = $settings->categoryhavecustomheader; + $replacement = ''; + if (!empty($tohavecustomheader)) { + $customheaderids = explode(',', $tohavecustomheader); + $topcats = \theme_adaptable\toolbox::get_top_categories_with_children(); + $scss = new core_scss(); + $categoryscss = ''; + foreach ($customheaderids as $customheaderid) { + $categoryheadercustomcssset = 'categoryheadercustomcss'.$customheaderid; + if (!empty($settings->$categoryheadercustomcssset)) { + // Validate and add if ok. + try { + $scss->compile($settings->$categoryheadercustomcssset); + + $catids = array($customheaderid); + $catinfo = $topcats[$customheaderid]; + if (!empty($catinfo['children'])) { + // Child categories. + $catids = array_merge($catids, array_keys($catinfo['children'])); + } + $categoryids = array(); + foreach ($catids as $catid) { + $categoryids[] = '.category-'.$catid; + } + $categoryselector = implode(', ', $categoryids); + $categoryscss .= $categoryselector.'{'.PHP_EOL; + $categoryscss .= $settings->$categoryheadercustomcssset; + $categoryscss .= PHP_EOL.'}'.PHP_EOL; + } catch (Leafo\ScssPhp\Exception\ParserException $e) { + debugging(get_string('invalidcategorycss', 'theme_adaptable', + array('css' => $settings->$categoryheadercustomcssset, + 'topcatname' => $catinfo['name'], 'topcatid' => $customheaderid)), DEBUG_NONE); + } catch (Leafo\ScssPhp\Exception\CompilerException $e) { + debugging(get_string('invalidcategorycss', 'theme_adaptable', + array('css' => $settings->$categoryheadercustomcssset, + 'topcatname' => $catinfo['name'], 'topcatid' => $customheaderid)), DEBUG_NONE); + } + } + } + + if (!empty($categoryscss)) { + try { + $replacement = $scss->compile($categoryscss); + } catch (Leafo\ScssPhp\Exception\ParserException $e) { + debugging(get_string('invalidcategorygeneratedcss', 'theme_adaptable', array('css' => $categoryscss)), DEBUG_NONE); + } catch (Leafo\ScssPhp\Exception\CompilerException $e) { + debugging(get_string('invalidcategorygeneratedcss', 'theme_adaptable', array('css' => $categoryscss)), DEBUG_NONE); + } + } + } + + $tag = '[[setting:catgorycustomcss]]'; + + $css = str_replace($tag, $replacement, $css); + + return $css; +} + +/** + * Adds any custom CSS to the CSS before it is cached. + * + * @param string $css The original CSS. + * @param string $customcss The custom CSS to add. + * @return string The CSS which now contains our custom CSS. + */ +function theme_adaptable_set_customcss($css, $customcss) { + $tag = '[[setting:customcss]]'; + $replacement = $customcss; + if (is_null($replacement)) { + $replacement = ''; + } + + $css = str_replace($tag, $replacement, $css); + + return $css; +} + +/** + * Set display of course contacts on front page tiles + * @param string $css + * @param string $display + * @return $string + */ +function theme_adaptable_set_tilesshowallcontacts($css, $display) { + $tag = '[[setting:tilesshowallcontacts]]'; + if ($display) { + $replacement = 'block'; + } else { + $replacement = 'none'; + } + $css = str_replace($tag, $replacement, $css); + return $css; +} + +/** + * Get the user preference for the zoom (show / hide block) function. + */ +function theme_adaptable_get_zoom() { + return get_user_preferences('theme_adaptable_zoom', ''); +} + +/** + * Set user preferences for zoom (show / hide block) function + * @return void + */ +function theme_adaptable_initialise_zoom() { + user_preference_allow_ajax_update('theme_adaptable_zoom', PARAM_TEXT); +} + +/** + * Set the user preference for full screen + * @return void + */ +function theme_adaptable_initialise_full() { + if (theme_adaptable_get_setting('enablezoom')) { + user_preference_allow_ajax_update('theme_adaptable_full', PARAM_TEXT); + } +} + +/** + * Get the user preference for the zoom function. + */ +function theme_adaptable_get_full() { + $fullpref = ''; + if ((isloggedin()) && (theme_adaptable_get_setting('enablezoom'))) { + $fullpref = get_user_preferences('theme_adaptable_full', ''); + } + + if (empty($fullpref)) { // Zoom disabled, not logged in or user not chosen preference. + $defaultzoom = theme_adaptable_get_setting('defaultzoom'); + if (empty($defaultzoom)) { + $defaultzoom = 'normal'; + } + if ($defaultzoom == 'normal') { + $fullpref = 'nofull'; + } else { + $fullpref = 'fullin'; + } + } + + return $fullpref; +} + +/** + * Get the key of the last closed alert for a specific alert index. + * This will be used in the renderer to decide whether to include the alert or not + * @param int $alertindex + */ +function theme_adaptable_get_alertkey($alertindex) { + user_preference_allow_ajax_update('theme_adaptable_alertkey' . $alertindex, PARAM_TEXT); + return get_user_preferences('theme_adaptable_alertkey' . $alertindex, ''); +} + +/** + * Get theme setting + * @param string $setting + * @param string $format = false + */ +function theme_adaptable_get_setting($setting, $format = false) { + static $theme; + if (empty($theme)) { + $theme = theme_config::load('adaptable'); + } + + if (empty($theme->settings->$setting)) { + return false; + } else if (!$format) { + return $theme->settings->$setting; + } else if ($format === 'format_text') { + return format_text($theme->settings->$setting, FORMAT_PLAIN); + } else if ($format === 'format_html') { + return format_text($theme->settings->$setting, FORMAT_HTML, array('trusted' => true)); + } else { + return format_string($theme->settings->$setting); + } +} + +/** + * Serves any files associated with the theme settings. + * + * @param stdClass $course + * @param stdClass $cm + * @param context $context + * @param string $filearea + * @param array $args + * @param bool $forcedownload + * @param array $options + * @return bool + */ +function theme_adaptable_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + static $theme; + if (empty($theme)) { + $theme = theme_config::load('adaptable'); + } + if ($context->contextlevel == CONTEXT_SYSTEM) { + // By default, theme files must be cache-able by both browsers and proxies. From 'More' theme. + if (!array_key_exists('cacheability', $options)) { + $options['cacheability'] = 'public'; + } + if ($filearea === 'logo') { + return $theme->setting_file_serve('logo', $args, $forcedownload, $options); + } else if ($filearea === 'favicon') { + return $theme->setting_file_serve('favicon', $args, $forcedownload, $options); + } else if ($filearea === 'homebk') { + return $theme->setting_file_serve('homebk', $args, $forcedownload, $options); + } else if ($filearea === 'pagebackground') { + return $theme->setting_file_serve('pagebackground', $args, $forcedownload, $options); + } else if ($filearea === 'frontpagerendererdefaultimage') { + return $theme->setting_file_serve('frontpagerendererdefaultimage', $args, $forcedownload, $options); + } else if ($filearea === 'headerbgimage') { + return $theme->setting_file_serve('headerbgimage', $args, $forcedownload, $options); + } else if ($filearea === 'loginbgimage') { + return $theme->setting_file_serve('loginbgimage', $args, $forcedownload, $options); + } else if (preg_match("/^p[1-9][0-9]?$/", $filearea)) { + return $theme->setting_file_serve($filearea, $args, $forcedownload, $options); + } else if ((substr($filearea, 0, 9) === 'marketing') && (substr($filearea, 10, 5) === 'image')) { + return $theme->setting_file_serve($filearea, $args, $forcedownload, $options); + } else if (preg_match("/^categoryheaderbgimage[1-9][0-9]*$/", $filearea)) { // Link: http://regexpal.com/ useful. + return $theme->setting_file_serve($filearea, $args, $forcedownload, $options); + } else if (preg_match("/^categoryheaderlogo[1-9][0-9]*$/", $filearea)) { // Link: http://regexpal.com/ useful. + return $theme->setting_file_serve($filearea, $args, $forcedownload, $options); + } else if ($filearea === 'iphoneicon') { + return $theme->setting_file_serve('iphoneicon', $args, $forcedownload, $options); + } else if ($filearea === 'iphoneretinaicon') { + return $theme->setting_file_serve('iphoneretinaicon', $args, $forcedownload, $options); + } else if ($filearea === 'ipadicon') { + return $theme->setting_file_serve('ipadicon', $args, $forcedownload, $options); + } else if ($filearea === 'ipadretinaicon') { + return $theme->setting_file_serve('ipadretinaicon', $args, $forcedownload, $options); + } else if ($filearea === 'fontfilettfheading') { + return $theme->setting_file_serve('fontfilettfheading', $args, $forcedownload, $options); + } else if ($filearea === 'fontfilettfbody') { + return $theme->setting_file_serve('fontfilettfbody', $args, $forcedownload, $options); + } else if ($filearea === 'adaptablemarkettingimages') { + return $theme->setting_file_serve('adaptablemarkettingimages', $args, $forcedownload, $options); + } else { + send_file_not_found(); + } + } else { + send_file_not_found(); + } +} + +/** + * Get course activities for this course menu + */ +function theme_adaptable_get_course_activities() { + global $PAGE; + // A copy of block_activity_modules. + $course = $PAGE->course; + $modinfo = get_fast_modinfo($course); + $modfullnames = array(); + + $archetypes = array(); + + foreach ($modinfo->cms as $cm) { + // Exclude activities which are not visible or have no link (=label). + if (!$cm->uservisible or !$cm->has_view()) { + continue; + } + if (array_key_exists($cm->modname, $modfullnames)) { + continue; + } + if (!array_key_exists($cm->modname, $archetypes)) { + $archetypes[$cm->modname] = plugin_supports('mod', $cm->modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); + } + if ($archetypes[$cm->modname] == MOD_ARCHETYPE_RESOURCE) { + if (!array_key_exists('resources', $modfullnames)) { + $modfullnames['resources'] = get_string('resources'); + } + } else { + $modfullnames[$cm->modname] = $cm->modplural; + } + } + core_collator::asort($modfullnames); + + return $modfullnames; +} + +/** + * Initialize page + * @param moodle_page $page + */ +function theme_adaptable_page_init(moodle_page $page) { + global $CFG; + + $page->requires->jquery_plugin('pace', 'theme_adaptable'); + $page->requires->jquery_plugin('flexslider', 'theme_adaptable'); + $page->requires->jquery_plugin('ticker', 'theme_adaptable'); + $page->requires->jquery_plugin('easing', 'theme_adaptable'); + $page->requires->jquery_plugin('adaptable', 'theme_adaptable'); + + if ((isloggedin()) && (theme_adaptable_get_setting('enableaccesstool')) && + (file_exists($CFG->dirroot . "/local/accessibilitytool/lib.php"))) { + require_once($CFG->dirroot . "/local/accessibilitytool/lib.php"); + local_accessibilitytool_page_init($page); + } +} + +/** + * Strip full site title from header + * @param string $heading + */ +function theme_adaptable_remove_site_fullname($heading) { + global $SITE, $PAGE; + if (strpos($PAGE->pagetype, 'course-view-') === 0) { + return $heading; + } + + $header = preg_replace("/^".$SITE->fullname."/", "", $heading); + + return $header; +} + +/** + * Generate theme grid. + * @param bool $left + * @param bool $hassidepost + */ +function theme_adaptable_grid($left, $hassidepost) { + if ($hassidepost) { + if ('rtl' === get_string('thisdirection', 'langconfig')) { + $left = !$left; // Invert. + } + $regions = array('content' => 'col-9'); + $regions['blocks'] = 'col-3'; + if ($left) { + $regions['direction'] = ' flex-row-reverse'; + } else { + $regions['direction'] = ''; + } + } else { + $regions = array('content' => 'col-12'); + $regions['direction'] = ''; + return $regions; + } + + return $regions; +} + +/** + * + * Get the current page to allow us to check if the block is allowed to display. + * + * @return string The page name, which is either "frontpage", "dashboard", "coursepage", "coursesectionpage" or empty string. + * + */ +function theme_adaptable_get_current_page() { + global $PAGE; + + // This will store the kind of activity page type we find. E.g. It will get populated with 'section' or similar. + $currentpage = ''; + + // We expect $PAGE->url to exist. It should! + $url = $PAGE->url; + + if ($PAGE->pagetype == 'site-index') { + $currentpage = 'frontpage'; + } else if ($PAGE->pagetype == 'my-index') { + $currentpage = 'dashboard'; + } + // Check if course home page. + if (empty ($currentpage)) { + if ($url !== null) { + // Check if this is the course view page. + if (strstr ($url->raw_out(), 'course/view.php')) { + + $currentpage = 'coursepage'; + + // Check url paramaters. Count should be 1 if course home page. Use this to check if section page. + $urlparams = $url->params(); + + // Allow the block to display on course sections too if the relevant setting is on. + if ((count ($urlparams) > 1) && (array_key_exists('section', $urlparams))) { + $currentpage = 'coursesectionpage'; + } + + } + } + } + + return $currentpage; +} + +/** + * Extend the course navigation. + * + * Ref: MDL-69249. + * + * @param navigation_node $coursenode The navigation node. + * @param stdClass $course The course. + * @param context_course $coursecontext The course context. + */ +function theme_adaptable_extend_navigation_course($coursenode, $course, $coursecontext) { + global $PAGE; + + if (($PAGE->theme->name == 'adaptable') && ($PAGE->user_allowed_editing())) { + // Add the turn on/off settings. + if ($PAGE->pagetype == 'grade-report-grader-index') { + $editurl = clone($PAGE->url); + $editurl->param('plugin', 'grader'); + + // From /grade/report/grader/index.php. + if (has_capability('moodle/grade:edit', $coursecontext)) { + global $USER; + $editing = $USER->gradeediting[$course->id]; + } else { + $editing = 0; + } + /* Note: The 'single_button' will still use the Moodle core strings because of the + way /grade/report/grader/index.php is written. */ + if ($editing) { + $editstring = get_string('turngradereditingoff', 'theme_adaptable'); + } else { + $editstring = get_string('turngradereditingon', 'theme_adaptable'); + } + } else { + if ($PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { + // We are on the course page, retain the current page params e.g. section. + $editurl = clone($PAGE->url); + } else { + // Edit on the main course page. + $editurl = new moodle_url( + '/course/view.php', + array('id' => $course->id, 'return' => $PAGE->url->out_as_local_url(false)) + ); + } + $editing = $PAGE->user_is_editing(); + if ($editing) { + $editstring = get_string('turneditingoff'); + } else { + $editstring = get_string('turneditingon'); + } + } + $editurl->param('sesskey', sesskey()); + + if ($editing) { + $editurl->param('edit', '0'); + } else { + $editurl->param('edit', '1'); + } + + $childnode = navigation_node::create( + $editstring, + $editurl, + navigation_node::TYPE_SETTING, null, 'turneditingonoff', new pix_icon('i/edit', '') + ); + $keylist = $coursenode->get_children_key_list(); + if (!empty($keylist)) { + if (count($keylist) > 1) { + $beforekey = $keylist[1]; + } else { + $beforekey = $keylist[0]; + } + } else { + $beforekey = null; + } + $coursenode->add_node($childnode, $beforekey); + } +} diff --git a/theme/adaptable/libs/admin_confightmleditor.php b/theme/adaptable/libs/admin_confightmleditor.php new file mode 100644 index 0000000..34f6664 --- /dev/null +++ b/theme/adaptable/libs/admin_confightmleditor.php @@ -0,0 +1,266 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * Class to configure html editor for admin settings allowing use of repositories. + * + * TODO: Does not remove old files when no longer in use! No separate file area for each setting. + * + * Special thanks to Iban Cardona i Subiela (http://icsbcn.blogspot.com.es/2015/03/use-image-repository-in-theme-settings.html) + * This post laid the ground work for most of the code featured in this file. + * + */ +class adaptable_setting_confightmleditor extends admin_setting_configtext { + + /** @var int number of rows */ + private $rows; + + /** @var int number of columns */ + private $cols; + + /** @var string filearea - filearea within Moodle repository API */ + private $filearea; + + /** + * Constructor + * + * @param string $name + * @param string $visiblename + * @param string $description + * @param mixed $defaultsetting string or array + * @param mixed $paramtype + * @param int $cols + * @param int $rows + * @param string $filearea + */ + public function __construct($name, $visiblename, $description, $defaultsetting, + $paramtype = PARAM_RAW, $cols = '60', $rows= '8', + $filearea = 'adaptablemarkettingimages') { + $this->rows = $rows; + $this->cols = $cols; + $this->filearea = $filearea; + $this->nosave = (during_initial_install() or CLI_SCRIPT); + parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype); + editors_head_setup(); + } + + /** + * Gets the file area options. + * + * @param context_user $ctx + * @return array + */ + private function get_options(context_user $ctx) { + $default = array(); + $default['noclean'] = false; + $default['context'] = $ctx; + $default['maxbytes'] = 0; + $default['maxfiles'] = -1; + $default['forcehttps'] = false; + $default['subdirs'] = false; + $default['changeformat'] = 0; + $default['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED; + $default['return_types'] = (FILE_INTERNAL | FILE_EXTERNAL); + + return $default; + } + + /** + * Returns an XHTML string for the editor + * + * @param string $data + * @param string $query + * @return string XHTML string for the editor + */ + public function output_html($data, $query='') { + if (PHPUNIT_TEST) { + $userid = 2; // Admin user. + } else { + global $USER; + $userid = $USER->id; + } + + $default = $this->get_defaultsetting(); + + $defaultinfo = $default; + if (!is_null($default) and $default !== '') { + $defaultinfo = "\n".$default; + } + + $ctx = context_user::instance($userid); + $editor = editors_get_preferred_editor(FORMAT_HTML); + $options = $this->get_options($ctx); + $draftitemid = file_get_unused_draft_itemid(); + $component = is_null($this->plugin) ? 'core' : $this->plugin; + $data = file_prepare_draft_area($draftitemid, $options['context']->id, + $component, $this->get_full_name().'_draftitemid', + $draftitemid, $options, $data); + + $fpoptions = array(); + $args = new stdClass(); + + // Need these three to filter repositories list. + $args->accepted_types = array('web_image'); + $args->return_types = $options['return_types']; + $args->context = $ctx; + $args->env = 'filepicker'; + + // Advimage plugin. + $imageoptions = initialise_filepicker($args); + $imageoptions->context = $ctx; + $imageoptions->client_id = uniqid(); + $imageoptions->maxbytes = $options['maxbytes']; + $imageoptions->areamaxbytes = $options['areamaxbytes']; + $imageoptions->env = 'editor'; + $imageoptions->itemid = $draftitemid; + + // Moodlemedia plugin. + $args->accepted_types = array('video', 'audio'); + $mediaoptions = initialise_filepicker($args); + $mediaoptions->context = $ctx; + $mediaoptions->client_id = uniqid(); + $mediaoptions->maxbytes = $options['maxbytes']; + $mediaoptions->areamaxbytes = $options['areamaxbytes']; + $mediaoptions->env = 'editor'; + $mediaoptions->itemid = $draftitemid; + + // Advlink plugin. + $args->accepted_types = '*'; + $linkoptions = initialise_filepicker($args); + $linkoptions->context = $ctx; + $linkoptions->client_id = uniqid(); + $linkoptions->maxbytes = $options['maxbytes']; + $linkoptions->areamaxbytes = $options['areamaxbytes']; + $linkoptions->env = 'editor'; + $linkoptions->itemid = $draftitemid; + + $fpoptions['image'] = $imageoptions; + $fpoptions['media'] = $mediaoptions; + $fpoptions['link'] = $linkoptions; + + $editor->use_editor($this->get_id(), $options, $fpoptions); + + return format_admin_setting($this, $this->visiblename, + '<div class="form-textarea"> + <textarea rows="'. $this->rows .'" cols="'. $this->cols .'" id="'. $this->get_id() .'" name="'.$this->get_full_name() + .'"spellcheck="true">'. s($data) .' + </textarea> + </div> + <input value="'.$draftitemid.'" name="'.$this->get_full_name().'_draftitemid" type="hidden" />', + $this->description, true, '', $defaultinfo, $query); + } + + /** + * Writes the setting to the database. + * + * @param mixed $data + * @return string + * @throws coding_exception + * @throws dml_exception + * @throws file_exception + * @throws stored_file_creation_exception + */ + public function write_setting($data) { + global $CFG, $USER; + + if ($this->nosave) { + return ''; + } + + if ($this->paramtype === PARAM_INT and $data === '') { + // ... do not complain if '' used instead of 0 ! + $data = 0; + } + // ... $data is a string. + $validated = $this->validate($data); + if ($validated !== true) { + return $validated; + } + + $options = $this->get_options(context_user::instance($USER->id)); + $fs = get_file_storage(); + $component = is_null($this->plugin) ? 'core' : $this->plugin; + $wwwroot = $CFG->wwwroot; + if ($options['forcehttps']) { + $wwwroot = str_replace('http://', 'https://', $wwwroot); + } + + $draftitemidname = sprintf('%s_draftitemid', $this->get_full_name()); + if (PHPUNIT_TEST or !isset($_REQUEST[$draftitemidname])) { + $draftitemid = 0; + } else { + $draftitemid = $_REQUEST[$draftitemidname]; + } + + $hasfiles = false; + $draftfiles = $fs->get_area_files($options['context']->id, 'user', 'draft', $draftitemid, 'id'); + foreach ($draftfiles as $file) { + if (!$file->is_directory()) { + $strtosearch = "$wwwroot/draftfile.php/".$options['context']->id."/user/draft/$draftitemid/".$file->get_filename(); + if (stripos($data, $strtosearch) !== false) { + $filerecord = array( + 'contextid' => context_system::instance()->id, + 'component' => $component, + 'filearea' => $this->filearea, + 'filename' => $file->get_filename(), + 'filepath' => '/', + 'itemid' => 0, + 'timemodified' => time() + ); + if (!$filerec = $fs->get_file($filerecord['contextid'], + $filerecord['component'], + $filerecord['filearea'], + $filerecord['itemid'], + $filerecord['filepath'], + $filerecord['filename'])) { + $filerec = $fs->create_file_from_storedfile($filerecord, $file); + } + $url = moodle_url::make_pluginfile_url($filerec->get_contextid(), + $filerec->get_component(), + $filerec->get_filearea(), + $filerec->get_itemid(), + $filerec->get_filepath(), + $filerec->get_filename()); + $data = str_ireplace($strtosearch, $url, $data); + $hasfiles = true; + } + } + } + if (!$hasfiles) { + if (trim(html_to_text($data)) === '') { + $data = ''; + } + } + + return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin')); + } +} diff --git a/theme/adaptable/package.json b/theme/adaptable/package.json new file mode 100644 index 0000000..54f420f --- /dev/null +++ b/theme/adaptable/package.json @@ -0,0 +1,31 @@ +{ + "name": "theme-adaptable-tasks", + "version": "0.1.1", + "description": "Grunt tasks for Moodle Adaptable theme.", + "main": "Gruntfile.js", + "author": { + "name": "Gareth J Barnard", + "url": "http://about.me/gjbarnard" + }, + "repository": { + "type": "git", + "url": "https://gitlab.com/3bits/moodle-theme_adaptable2.git" + }, + "bugs": { + "url": "https://gitlab.com/3bits/moodle-theme_adaptable2/issues" + }, + "devDependencies": { + "grunt": "1.0.1", + "grunt-exec": "~0.4.6", + "grunt-contrib-jshint": "2.0.0", + "grunt-contrib-uglify": "1.0.1", + "grunt-sass": "2.1.0", + "grunt-stylelint": "0.6.0", + "semver": "5.3.0", + "stylelint": "8.0.0" + }, + "engines": { + "node": ">=8.9" + }, + "license": "GPL-3.0" +} diff --git a/theme/adaptable/pix/2xlogo.png b/theme/adaptable/pix/2xlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..a888d8fa2438d9368de6523a9c82fdf750ee75dc GIT binary patch literal 9122 zcmeAS@N?(olHy`uVBq!ia0y~yVA#pPz+le7#=yW3ApZIb0|S?Trn7TEKt_H^esM;A zfr6*AvqC{pep+TuDg#5st+~PJA;B-jY`@?8;^f`YFvUARNR&e%$f>(QWTKjuBBy{? zhvLGUqJakmU0qqEPb}zQ?(1vl5e?Mg>EgP<{zP$6U-ROQu2a7sRxi%Jw&(k^v;VeU zw|l<k^W4qn*$;5=O!F`fG;QEn=_Dukp!xWbV-KI`IW%&q6fg*IY)(-u{;SN$@T1Pg zMqB9s`#uK7ip$&_3<rE?A25(<|Ih!*X=V&#Lkr`9l8F}H91S824pn_l%NQKo7#rr! zPzz>Qz`~GlIyvbvLx3(rf_nY4%M1;3f6T0AWQd-kD9X5CB7=f(YKIF$hBd>Q^I=`C z3=swlB5B@7EE#U-G8lM<r+F}J*v@d^oB;PG1`ZwugTRhfO$L@~hJ+K!$`uSPGZ{qO zK84$S)Ltj>T#JFBV&*2Bohp{DjnN#sh3w(sdU6`)JjJy|%o3RrXF2E{s+=il)M� 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 Kb6Mw<&;$Uxv54jX literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/bkg.png b/theme/adaptable/pix/bkg.png new file mode 100644 index 0000000000000000000000000000000000000000..807bd6a6f0ce023dfc5624c7067b2a3a6e54006f GIT binary patch literal 129467 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�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 z
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ϙooNh&C#I!9lWDQTs|6P7I<@@0mU-ni_e$Zqt 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/favicon.ico b/theme/adaptable/pix/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..511f2041513ee5e2ad994586784b9b840fa8c3cf GIT binary patch literal 7406 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/icon.png b/theme/adaptable/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6fc142f42628466efb456c64961da36f77ee2b41 GIT binary patch literal 1369 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIjKx9jP7LeL$-HD>U|>mi z^mSxl*x1kgCy|wbfq}EYBeIx*f$s<iGfvg!lV)IGdFSck7?R=qHZsyDCRIc(`)bs5 ztxl=MVUH(C^x8>IKl0X5y0?GUuDw4JV&Y5;H>&NMG2_;)pd9T~$!(iNxNliGtlhMQ zwP{M!HZh3_OGUiAxtV#GS<@0`oG<?O`#jU$J2Q6{FX!19|H<~Q<^TG9_5X_BSw5e& z;ZULPfuhI%RTrEr<J8POYY^R=oR=>B?~Q$PeH&ZhzfUU9o;`n>w@^`s(cVO^T{BZV zY-x^h&voH^kGp-=MrDs)9@5zpAK3Hv?%N$EwUO^u^=@Fv$x~pg-gaQhpX3sc_GGVH zO|#E@taz8eS7a4!Xlkxq9`2sQ$)VD@Q&pEyKK!3+c(zkjfof8C!xODX)~RAVI*Llq zSPj-I*98PJ?Jjn!dUU)?;J}`VOtsfvR=w<$*LGt#e&q1)po>OUI(q{JmNi7p5Hj!f zNz~E&y~1*DN6pl2X4gFPMOsuBtk}-|eyaFm=Ux47AKwOX&$FD+d;3y7mtM=f{ZU`I zk1-`F>M-W@v+HfY9dK^P^A_h;TiJ@F`l<lI!kN1R>}vw6M3yyVEdG%r{!WG6_oR1s zMDo2x=UUrKO2=4ycEsE$SP{_4E&6=sYE9|XvN_Yl61hGIq_4d3X#bx4LyWIy_@=am z-QALyk`@&?$G|`Q%jQ=>XMK)0?pgb?WeZ1O!`+uhE`F~4v1}bz{-2N9^XzAMe>Y|3 zf2R8Q-yM~^G5+6f{@->YR;1?V_lXNu{`&nZ>gQ_(re&H3xOQ28;Bx(EwMxvj;p3xw zSHu-}`gr7IPT8$#;N!49S!RC7)JJ6#?`;vk;;py*`xZAxhwDEi%%&6=T=dLLY4MO0 zWo!<fTrv6G&PRKGF}YnVV7zm#wt7+29bZ%D#hE)API3R;QT_gW_|q>l+qep<pMBJ1 z%{_7M^LwQwlUk*guyd7gKR8g^aw>G=j;@LQU*<@E+_C0vg@(TCje?o0+xwr*J{qDV zu!~isWP9*sf%RDeCyr-ao_i(Y3J)L0#dMi(9WM$jmx2tr5fZ+#_Scu3+B~CGdUGa= zU2N22jJdxjI&Mj>2h%>gI8Oen+80~&82bcPIp`(tT+?Az!d39->m(*$#_Y>N2@Csl zW<BsZQLLcL7}J}V9~aWQ>)G8K5zA^@UF>gd*&uH!Wu~<@e#@K7J+ANAH(mMFcz*Ti zreg<!TW4CB8mZ{^uzqJsd#!nA&Nk=Tde3>+WEWn#$334RPw2;#t36CV-!#fRDUZD9 ztU1BIM|zUS3FkX$w<pY~xHEh1ippPqdM;appNjjzGDk-0NNkK7%WAvKiCas&_yfFZ zHz_##Oi_OL_{xd<o6QARzkIslrOo7|DeJBHnf83(oH3{L;o8IdS<f=qeM)l+%I$ZJ zN$wM0{N3RPi@}eFYhQA%I(FJm=%*0tj|*R>d<i?q#Aws~|HX}m6a1g7n)v4-zrwV8 zelJ(bbF&y&y-{A2*{>@2hc#U|l+kZ{bk)n_qD&HVM3bCY-kv{K9~j*D(baXn_$nre z+{I=&OMZ1Qt4I0nRXbUEuIv8XHIbJpk4<8dh)G%!HT&bC_m!u(+x`oRC4aG3zV>zf z+0G}XTDhmreP#QbwNmo{_w6%NI27ulKd+w09#uGVw`=W@_bhv#I6qwZrPk6jDd5{C zRr&a<a;`rQMD|?r))Q$_Il!&Aq;k!TOnK45L-)&+QxAV|-O}H?p#G5JHNNdJ{N4^a gk7hMIW&I%kDs_*gUd+tt3=9kmp00i_>zopr0E19}C;$Ke literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/12-0-0-0.png b/theme/adaptable/pix/layout-builder/12-0-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..b4068660772dab1e3c011bf1190e18b141b187f7 GIT binary patch literal 1357 zcmeAS@N?(olHy`uVBq!ia0y~yV41?ez+l3`#=yXE=V#Im1_lPUByV>YhW{YAVDIwD z3=9mM1s;*b3=G`DAk4@xYmNj10|R@Br>`sf14d~f28(cy_8tZX2HDJzh!W@g+}zZ> z5(W@(E=o--Nlj5G&n(GMaQE~LNYP7WXJBC2=;`7ZQgQ3;-N5|I5pu^KUUx4vxFxhQ z(WJy}#r?$Rzw4(I1y7fq!S|B&?X4Ysd&OHjm?pbfU(I4YUtGQ>KIwJLztTq`>-F`X zB}#l+x@%qWp19>xHtja!ab9OO`^@J(+jq;h<?y}W`@8S>zW4usf42QrTWU4eZ~5hy zB@pVL-}m>kXWzMV=gIW=`m;ql&pdiN+r0AszrW$kOPBl4Usn0+%RTY(*y-Q?*jzRN zX?j<GW$yg>>ECMpf32<G^1S-Z?>hbS>G3e*;>s&4XP!QNdd+<M^F=#-z8=55{qx(m zZ^N0Fu9{-vTm1f(iSN$~-ywEG^_1+AUs3(}_xqiD_MEY=|E+s|p6zaFjo#aD!{eDi zLFc(oN@MD_+~SMdzV{_FGcYh5h}ZmVBB{p6z`#(jA&DEL=Em|VlY&Bbe?2q}#M^NG zW7XWNSzD{wzc0$1)pI*{)%j&UAc+s2vDYK@rfWWZcRa6q`ts$H$1gf?GB7YSG*4Nz zs_e2VNKHjYxdzDS4cBeX&wO6Fz03rxV(&V$*?X+4z3-QLPWrU){pIg_=Ymu{nBA7> zalLe=-Mk>J+}qC<<r@1=z6+LcOr588SOsJv-!oY+5d9|9%X8(bySMi50V&vU9qh=d z%Q7dabT7*^%G<uuZnhCf=74%>Z2HcaWm9&`@79f*H!n@bof+&jzMxgBZuw3Jne{;N z-a-~oSj2s{NuT%p?yZYptG?~JmGkM<8=czgs-CCX_xa!dUIt3f4f#@Sim}&Ie^q#` z+>$%Hd)rwP-!gWP$JwV=&f)>3YzB)NP=AMOsxDnsmi_+|NaumL<F8A@R)@}7<~d1c z*0Pg3?k@Y4a|&cL!~83ybDtK?40>1hW?MyN)vQ7pUQiBVu<%&9D$6_;6n72n741SG z=T@KnJg0JAdA2*)svEDfHlJL*dGnXp=_;D?zwB!7-<AZa+LtNORuVh?<zvmIm$v2V z-7>pua#I#0As6y2)5r&8;)4X3zn4y#vg+2_zo(@^3aVw-UyocHmN`q!b5q7FwZ~<% zUv1VD2FcU}T`!$>I@N3E?~S(~J$;#Ztk5I?Y>>jzRjclnfZ~=x?h!PCOwSiTtGu^X z))%BAVg2f}YU|IPyHp-KX-UnC-(PC;(?KTwSe7VpsWf(3|MDqbx3}%gxb1T}r5q$t z5ioQ4Ndu6H4b7EMe@_hxT9vi^n|?k>!JE=Oap_y5mS)aUiRYPe?Dp+D_wL>MePGeL z;Ok%aW<R!gRwg00-ulbF{=YT-wfEmY{Rzt<ko>amOSt-!dwZ*=+uz%__xe5y%aiBz z<L>PK_U^pB{B}Ir0=!PXy1DsE>D*WG>*LSgertBV{+Fit_3JzL?b|0VFDv_0-rfEC zI#A|DDj4eK=y|RRDk>_Pr0zd&$*kv<d>|jlys+P%E~PIbk<$+1db;|#taD0e0s!!X BWUv4L literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/3-3-3-3.png b/theme/adaptable/pix/layout-builder/3-3-3-3.png new file mode 100644 index 0000000000000000000000000000000000000000..9248eb997491a626e4c624d5def0fed46ef9de65 GIT binary patch literal 1954 zcmeAS@N?(olHy`uVBq!ia0y~yVBN{Uz~IEe#=yW(wf^=I1_lPUByV>YhW{YAVDIwD z3=9mM1s;*b3=G`DAk4@xYmNj10|R@Br>`sf14b!PQTFT%TUQ1K2HDJzh!W@g+}zZ> z5(W@(E=o--Nlj5G&n(GMaQE~LNYP7WXJBAw^K@|xskrs_PGG)pxZJUa|0~qws%5q> zP&)eZ=q2qPt>1pvPnxD%EtwuFY`jf){iDs|D;H+?GB<DecE+aviu9!EtB$`|e$jKL z&eAE@UYGt!-`#tB%Hlu~*TN6kyYK3m&GvQi+O}=mw!OFbKG~c*e_r|YCJWVDdD+?7 ztKZtiM@L6T=WgG+b?aN_+ua=<+xP#MyZ-s-V#nHlm;FEfovhBjf7{KRM`zdXKbE(B zGf3IhvR`-0Kfb!U`oyO{OXQ|)y?gE2wQth5|NZ)UGPWgS_u92<*KYe=lbf5H`+xge z`E^Gxr{|sj|0;jK`cjYjf63X|*|(SH{_>g8|1STZbnQOg?ePWo&Yk-9|JU<X`O&%m zKW+P6oC<at$jP=@)zgB!{_oq@_qFP8M!@mMg(B(c?EAOv-rs*U3#9D!?6-ejX}#Lj z_p(H4ef>X6HuZ1yMcE)nyfxqUcY1u1fdte^5W&vvw%(dcuV#tf*01<4%pm{w_1oJO z+au<@QQf|cxhDCx{|6<P+l*Ue8<=y;RX}mZuw|-u9+(kzTGbrPSR3kMvu8#1$K&#W z(^AjW{rULho<Hk>>zB(Ia@iejGyYo8(Q#wV^^Gxl$uXgREDMyk$U59+yinF)_u;W) zL))oMJttE>eK44n%Ez)G8>}P4^)|OV(}gofbHv0qM?R^Po9UegG0Vd1c5D%|$(DMD zZwq)9yk*F+W;mX``}$O{J1#`&?PLV8GS(_7Et2U?GMedPDfRQUHG}NJw+y%V8n!Wi zahw+9RT_J|Y<KMsR!7z!T9xyTmsr{Etjn@)*v7ozEkkWa^~U}c6PQZ6w=ri}Gu-04 zz}~9NkdanbD!O`CUzSm)xPE+<0qcb_hApxU%3JQLnmahpKC3qEbn22RN<l>m46)fD zT@Ko}><S2k*Tx8!$Z3YV_w*b*aM#@7b|%aP0WK~%*Iq~5yK`j6syQ4E?inzN2gjQk zc&DaHPClt}>~W#XG)sYobz5Z_a@iM%=IYCEIv9&@j*K~zHt~67ir!AfEg+c(8oA{s zc`sb}$9S{Z2;xJT^>5?CnLxqhkiGK>C}T4$*xJ?6um4KVUAexNeSvb@Ek1D2I`F4x zE)BU}nzQ|Od%QqM!}>|jD}9z<p8R?_ENt4hiU024(sbAkcVmDBQ$yOi=bu-+wpz8S z3ldl_8h+NuY3H(EC}W7aS~mR-1Mk}FApg1CdJ7IR*$qFlK?#B(>f*OXWpE5GSQ}Hs z400n%oF>(kigq7WI<=|i=F?Ojws-p&Z~1`|Cqpj#i)N)qkGppr4}I#wVC4i8WVV-M z$ylLtdWG6#iQeOak<T0$c3lHUUD%er1u6_@W6q=%8qRi|_k7~cCv1>NT?~oT-|Q*; zryz-@V9&PdSWb{@7}h?WBn@ITT+>ogYWpo-+VA%77avj_O;|c5DE9isyLs|;jscAO zyyiVOoa^VmZayqX<!{BkQPx_*aCZ?rMLy(Un6-1>`Q*^+fxc>m{~oon2SXFP6C|<6 zt$+ONnOfQISQjtkEW+N$0(Q!_qLaKJ21<0ci<jz)b!)UA-mwECff;mkM63@h{QNF< z&-!e2rVD~_$Nl)I;NX1tp~0$ET^B24CSvBuH=m4>%)0kzC>5XN1*Z`P_sreic@1rw zbh=L%!2|3FmH_+pq*OI?d-vHY*_#OhPYWa}3xDLQ?Y^t`E-zoX_L=!sP*Hlg?ERZJ zI?FD<JlPg^8&qO$z55(o_JK?1+fTREf7*TL-o0b?_xE&mbky6_%iXuHEGnoeua?fQ z`>h*f^=JJ?s5v!Oz3KLU1S03ne{y)!w%fOE-7?Ghy*XX|_rEZ3>3+3r{(4Z!T%S{2 z_+pxklH6b3ATRlS-!ELx$<58(z3=Y_+y0urBGAGTTz+qj1-ohYt#3L@r%XGYdgt!l z+E2Wpp+^fWY%KQe-{1f1@9*>f-ap#5eE-~#&^b4MZGQOq@3OaXfg&Hjyu3WQ+QIG$ z#L~53_hjt`Q)O%4MpnH!B?F2=2EQu@1i_38VgFf|{0f-*?cDD&kbtMFpUXO@geCw5 C=ZDMy literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/3-3-6-0.png b/theme/adaptable/pix/layout-builder/3-3-6-0.png new file mode 100644 index 0000000000000000000000000000000000000000..b3bd1fa4fcdab38d5a573bc4703721bfae8b5910 GIT binary patch literal 1909 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 CwT>YG literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/3-6-3-0.png b/theme/adaptable/pix/layout-builder/3-6-3-0.png new file mode 100644 index 0000000000000000000000000000000000000000..c7258201735dda2a622d3a8c2b5c08e6c99e1eb2 GIT binary patch literal 1896 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/3-9-0-0.png b/theme/adaptable/pix/layout-builder/3-9-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..487e8dafa7b9fda41a0ed345821dc43cb11af98b GIT binary patch literal 1771 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/4-4-4-0.png b/theme/adaptable/pix/layout-builder/4-4-4-0.png new file mode 100644 index 0000000000000000000000000000000000000000..66f04fffd5b3deb972c03adceed0ea192f44de7e GIT binary patch literal 1853 zcmeAS@N?(olHy`uVBq!ia0y~yVBO8Yz~IQi#=yYv<6LDp0|NtFlDE4H!+#K5uy^@n z1_lPs0*}aI1_o|n5N2eUHAjMhfq}im)7O>#0i%?tu+GH3<Yop22HDJzh!W@g+}zZ> z5(W@(E=o--Nlj5G&n(GMaQE~LNYP7WXJBAk=;`7ZQgQ3;-TQuz)0mDuyk37m<w2mQ zh0Z0u@(pkQ#`|o|itvpn)ePG1xnVDRM{^{v>g9DNWq)jze^~tD@q@QDo8}}*v?+qD zU@%B5os?7e&(h!D|9#!(U(eJ|KmF7lfBSve&Aja|f5)x9nzi-T<ed0e9kHEz_xeUo zTP#&tz3yt()~L0%-_5UQZH;<6=ldSNN%j9fdE3?g`tokZy!rE&uUe&F_tt*@-)X<k zK06!l05)Xh_Vu^6oqhZLetOQEonauM_?^$UZ~qnpV%`$I_1Vhv+`03`g@qsA&6t+| z=lJ9$8mCQsCvU&kwFqS0+bg&K*8P8Xq0SqkBdYpcKFA%nEZ<ie@*Mu~<;#@2dEfud zn3meLsN(bM*z1{Fqt=F1Z+dHA_e-<Czd!S~S^fWq?e;Ry@(<3-2Fd-NTK(^F|KjT2 zPFs*a)_yzxzVh#{=GFfX+u!(q>ih+eAIsk48g8oRsef570(JDaJNLmZF24J@aJ>yU z2xShYnu8gW!_`hE?mz$T`~A)L-_PY>@R?%E&QQ(AP#mUy^6%eTxox*~rk$?Yvs?Nu zdkF)>`_0c863#OwoI9l=`O7Z$`rn7@F-#1P)6O#<n8$oz&ec8!hQ7^l>x)ZEr!LF9 z_vcAELxNLvoCqHagI~t^*fQn=6W84`3(uXsYLyHd!;NBwgp}%c8%@9tQ9BQF(eq8x zAd!ZXt0$+F7Ek`X%ro@5X6*wp2A@fAw|U;)mO1NKCc}Z2o5c(po--JvZ&hY!cr3%$ z{v~$$uQz8B85(Ta8Qiu#*W1nbz~lRD-@4iwpWE9aW8;^v&}wI52)_6Es+Mb@g$RSi zMBDF71`HOL40vAd(u=)r$pbR)z=>~nFx;a(dAfMj`?B3{zE`U=F$kx^e0}mX_g;Be zKzT=2J&<Ntc5EJVgDrc5rS{~M7uE6?7#W1qVamaQ6a@{Wpo&vm49oUEcYWx<@Sp{3 zsm#jU+e>nTL#G~Q0L4U4&UxEupv=l};K|N6U`E2Hm@;Ni7#UQSPFj<fS9H&0<tnYE z{CAidJfqfL|0cum#**Po*xWsPF6_>fTF%HIe68-sglCTqIf8<vA##;0BZJKI0*fW@ zf6ux4Oc<ovV4LmscMJtfexFs_Z)dkG_jXMDAPTC=%9+m|DYY(N!;tU`H9g(h=6ieF z%2lV}5iaKq4jH5%(%a1l3ac4=tST!Pe!uy%^3^WCm(1H45+=Q|1SP^5o2@)=@ylO| ziLu~im|;-O$M8*t;n|up7KVa3haXPZ_rAES?3n?>fqBdf%D0QB-(@IRQlDp5Sp!N? zJO91gEfV@PfFa>jbzSY;MH(_(40BG*t7ep7kXb3;RuVgX-D^Zrhb8Rdo4g<w9e6So zlyu5>$MXH&E6mu?xe1!PUa9Q7pYv(M-L#!C&+^O5HNE+3m=9dJ0Zr+tlXz<9uY3LJ z4YL(P!X}s>ZV56RC=6R2s;94i*~FL2Ka%mlm+yNtWH=b+T$s1}4O@fqR(ZQ6zCpw# ztND93|K4!_dEMs9&$Gi{mx^^C72m(@{`=4C-<th3$N&{Z)sx<?KAJRf_3G6Z>sCyK z6i3(Q{hkdfr^@=O|J_Xe{`qsU+_u~1k58XIZC>B_?x!U`KR;6Wyft_B9^)PNCqeXn zJ7Ftt2q~uDUVmRXSLM0y=V$!?-poFKE@)bO-Dl6x>pLTKjP`uw23v3a^;_s=lan0t znKnO<S{t_dYTeU${S%-@od=h%7s~JM-u}chXv)*s`wzVC-3>0lm*4vLUZcMkq7d%h cbJhRiEj}f!*){o~C@5w;UHx3vIVCg!01{7R3IG5A literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/4-8-0-0.png b/theme/adaptable/pix/layout-builder/4-8-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..1272f57776bdaf6a8d16166557d4df5f4730e477 GIT binary patch literal 1894 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 II;Vst03zya6#xJL literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/5-7-0-0.png b/theme/adaptable/pix/layout-builder/5-7-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..2b8b07656f7ce399d1fd88065b39a5d1cc767903 GIT binary patch literal 6920 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/6-3-3-0.png b/theme/adaptable/pix/layout-builder/6-3-3-0.png new file mode 100644 index 0000000000000000000000000000000000000000..192253be8fd220dc0ac1c598fa284dc8bb92e51f GIT binary patch literal 1907 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/6-6-0-0.png b/theme/adaptable/pix/layout-builder/6-6-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..038a8eaba4961c1f13874f7e13f6b6825322d9c2 GIT binary patch literal 1644 zcmeAS@N?(olHy`uVBq!ia0y~yVBN{Uz+lh8#=yXER`}>`1_lPUByV>YhW{YAVDIwD z3=9mM1s;*b3=G`DAk4@xYmNj10|R@Br>`sf14b!P0nU%hpRhA9Fvw<xM3gw^=jNv7 zl`w#Sb5UwyNotBhd1gt5g1e`0K#E=}I|Bpj1y2{pkcwMx@1D<-zAk#;p>e%O;H;h* zC%P;nmuF2%o%p@JX+gihDOVrnMT{4d&Q4U)nv<6$x~~1|x|!WS?Hr_o?N98xBO@PB zzG?sePu}AC8T)?gz54w5-gP-SxkqpF_q&!`WNeLE8@Bpt)>hB$>7k<ae;>A=_-xbm zq-tl+*|%@I??3;0H4CDmuP{%4`|Y>ghd-H%ix>X-a^?4}e;*D1?Yw*J;feSFkO?4z ze3!o6C2s%!<MJK(w+*&uL$qdX{aajZ`Ty5z@%E=DORBzXU8M1-#?JniJQKv~sI_G; za(CZ8TDQM{q1D<|2z_VY{w=O9tFE>_)4!U5;lTfs_V@O5^3V3YQYu^j`{(n6vEiF; zzkT%a<3(v!hK8Mc)AP>D@`A#XVdbVLpqOG%h@5r@ETFP_&+gsbi!>IkT6JR6nN9|V z0|&P3j(v0M+8sUy2HQ{Or&IStukEwDDa63=qj+!p`okdczcJxV3<qAMMt=Ll%D}M1 zbo=x^Him|c(I&nzadF2|thT;jV`w<?tjs%)fx+$llP4;Vo~U#!Dv)4cm~i9HyIn$H zn<u571iQ&$QxX3SMuvvO&j~&Z3>NGDoW2_Rq*AW(o%3A=1_PJwWu2dY-T~QadusEl zA}$67g9P=>ktx$|Gca6OUBbe^&=Y%m+s6CvAE#LL?pi0y!N8zmvOT(pnZcmy{q@&B zHr#ZHy}mI+syv>V!NI9`_x(y1koAZFI6X-kB+!sD_4e-FC%TnZtt$Giz`(#0aC_Uu zcV*AY7#LnmjMtz3F>h<vy4Q2q7#Mgi{GL7AU^_E|gZ=fX3=9TUk<)DJ85tNd&%Rx{ zn2{mjjaauUws1%>>+a}Sq#?t`z@TEbefK)BI~r0Vi<m+7Pgrw@xr~7!;f9TbFatxM z_Wu35x@^uXpRZjZ&A`z1qV%$h?flud85tJ1M}|%fW?*PwoP9P86bJ<_FF6<(BzFJa z*88_k{_;zcSG&p-7#Jp8xRZC1m*K#RwIl_?)7!gu->Cf~5qsT|mw`cI!M0pTjDLFl z)F^EA)I>=J2DVp!&-%^)Ic5H9F9rsQy*f&pbgJvDtQi<4oVyceCB?vS>|N>QBdhcG z_r6R?y2#7G(0TSPD82RE%ONTZ9{jW24D#y(@2B6x!zVtke6o|jl$qgxfbJ}{bI*&5 ztr-};Xsm`NhRrw66iG8M^l8X4GB6~U*VoJc{%g1Fa^{uNw+;*p3JZ7Mog~fBa54J+ zog?jM)n4uDJDT+HT{Z)QM@HUv>wZwsGpNAR06a2kcHcdgx4nBEG{$Rhx9yZ=VEFa) z`u=}X@9*t1)nR5h;IKA7?+_?u{{7U!k!-}kkUY)RWm@Wa|8#jRkQ<+U(|`W$(nSUa zgR=Fr)#747Swr^EB54MOGjHdg#~LEf`@q?=Aq4J_=dZnXt?OR*I&M$%hkDzyvG4+A zqi^R<7%w~Te~hHy$KQvYWAvvB3SKnvtqHxp_x_*Xhn?4it+tTi+q)iK<Y;c29}+5R z|L@D?1bMs6yz9_nY}W1lb8esR=s5gPV0XW~veKiMFJET-dHK8VuK=ttGCTTha?OI% zZ~y-*uc(KWV9&n&*U*pK`|AzA-}!S%Ml-+spKw!W-_9Ktza|zUYutSKvc#iVzgO?- nTW<gF!#}94S!dtc7XRMQ&=#Q3W@#xJ0g4V!S3j3^P6<r_Gr!(O literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/7-5-0-0.png b/theme/adaptable/pix/layout-builder/7-5-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..c85f9b1bb58cf3ce76154ee48cd058af77f77aaf GIT binary patch literal 7060 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 NOHWrnmvv4FO#m&7GCKeO literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/8-4-0-0.png b/theme/adaptable/pix/layout-builder/8-4-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..bac0520a1979fa2d06889b37f41c3b966f5c3639 GIT binary patch literal 1813 zcmeAS@N?(olHy`uVBq!ia0y~yVBNvMz~IQi#=yWJ{PR!<0|NtFlDE4H!+#K5uy^@n z1_lPs0*}aI1_o|n5N2eUHAjMhfq}im)7O>#0i%?tyo5~u4J!r)2HDJzh!W@g+}zZ> z5(W@(E=o--Nlj5G&n(GMaQE~LNYP7WXJBBf^mK6yskrs_u4DeK7h(+$KPMg+P=DvL zz1w2;%*eB4A!q;p=U)7q`PP(oS{+=O2AsJEH}CMe>ama~HfF<bo1m}{+b$k|bfP`s z(%ZMW($+lfhfi#JbMyGEZN2Tke`josS{t_dY8IHXz4YyJmT3I7*C*JTHO`+?S}Jn< z$-jSgcJ=@Ee6Ec11}Xe^=I!GA=av8N@Be?#(xLwE-|sti?5Oy*@oACd_4V<`(>7m( z7zH-*o5t<($Ax#!Jr4{G{qytH)r-12j$glSIM*+Bem2O$-*dlR-2U0-{?{iR9c7z4 zx82=jo`3sBj#>2oV31ufllbp$v!3N#{pW8CMBDD_xcc7}Rn@h7KZ~h8y%{-Y&K#TH zH>&pb9ZeG4_;mUun7*x1YrkE1yZfWXv(1qqp`z>m{nBP@J{YLGWBFuH6W`59UV6Oe zPuaZX{PK2hVhWfU7#ya{{%+cxIqS!lmy^Z07{1MU&U~B+6hjW;ksx^nh6|ozcR&IR zB|&y|zs=eXC&pa+dhu>E4+8^39oyxTDruW%zAk3Carr@+UH|Ug*6NI)G_WaWRzWQv z!|%`23>X*~_J)hNPDzzqd^xzCk->J>^PS0z3=9k2zgoRI<F(Z%oBk=2m>C=9ohjzd zV_;yY(qH=JD?7uB^ETz(LSUP%PBuw{7!7`>PH*aYnDHbzih+TlK;dY?kNx^*i)#58 zsy)rk&2wFjBs}Qf$;iO)V!O`iMXOdVj#kna>yB6(_VAYs!?)SbcOK+qU|?vU8no#K zTZ8-rYmi5O<yp;ixoi?REp^WSkL|Y`7##ec?PN6vIeL4H-s3X6{<BqY4Hy=<f4=j4 zKFARaFI2$JWMDvY*75xN#|kWZPVf5Gdluw020Dj})SPLlJ#NbKmzfwC8kjZP4ub>V zo032|vMF<sjdl0u&&Asrks>F5XUwr0yZ*041j_y1<AEZsG1s={=dgjCHN#KMaQ^wl z8eJ=1TTOah%<v<e?ek5YckkZKVPRlkkU6m_^U=p&drD;(KCG|wmSA9LxToEv<agT0 zZSlmz3=MK;Ki}ENz`$^!{Oi@wm}{j^D&;g)*_jw@XFdlf&@XXbFJJRBT$o?^PD~XX zQ6-bW@yNh1*Q8>P=Jx(fMh1ohi6Vy+ci6Ev{9QVG_G~kyB!dsfLCO0|^t9B#(5n}9 zCyIBwt_fQW4!^UX?>t~(U}!k(r5TySe1QLiEGTW)mK)D?gQS@G^LO)dFnmdw7Y)iQ z7pi?PAE`0x{yJTskC~y$<b3TvP<mr%@Z0nNRE#n#n5?q{WCFvBC4cT#$Q*uXpr@~| z{PS2E0|Ud4{j>d+8_qu)GoSguJW#mU)%}rJyP1K3p`kfKPYj%bpX(-q^2d6OE(=@R z-iH}a!0FPaT!(>y;Y)07sp#}l*~n?GUMCxoQt|Ylt|>u0cWz5DRGFUlEdz%PBIVwy z>Yj8qO?dUytn3I8*ObU*;L;3IOuoIiz2DEz50sJidgPnWuGq1^f6LP_t+--Pku~eN z|MQ>MSiiq|sJQO_p3bvrpWi&*|L<G$pQ6HrXojp>)iqr|Uhb{9ubqv}k=fVRZ?}7A zdL2}DRsJiPSO48x&3yLRYxQe#Z(jfjLTq2Vjr;BP^Oj}zf6MND1}eEt&(A-1>dU{s zzs*i>s`&Wm=w$V^*I)1W{ljqE-}3bI^gxlLoBK0CWi7Oful9Mnc(P6Z|2;L5$De)s zW+o>mS7vuUdmYGepZ~nH@qfE+swFr$WER#l6x#J2OwBc~0ttA!`njxgN@xNAG5;z? literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout-builder/9-3-0-0.png b/theme/adaptable/pix/layout-builder/9-3-0-0.png new file mode 100644 index 0000000000000000000000000000000000000000..6df7dfd3c4efd657a6a31547a8f5e4b4f97f70e5 GIT binary patch literal 1708 zcmeAS@N?(olHy`uVBq!ia0y~yVBN{Uz~IEe#=yW(wf^=I1_lPUByV>YhW{YAVDIwD z3=9mM1s;*b3=G`DAk4@xYmNj10|R@Br>`sf14b!PMTuCi!|n_W46>Oa5hc#~xw)x% zB@7_oT$GwvlA5AWo>`Ki;O^-gkfN8$&cMJX;OXKRQgQ3;-LrYO)5IDc9+vO?^_$1# z)3pOf$^*Xqd%yJHf=ddY6sFGIBAPjAvzNGR^&W%8f}PKAYURt!IT|TrF@Mv(_rF0l zFf<5iec%62%HQ8V|KGv8Z*$j#troXmeg3(HT>r(=xnZlXW^IjH3!&bIZtZ?*^zPj| zrSqRxTb<f*GGlAhTZ7$u|GYgt{l}l5pJ#KI+Rb+l6w$BycKrSR<B9w0zsj#b(!VzB z_y4S5ufp<j@$1KY-Bw@CYQOpRX^vh0(WH;hoX*DoWE2#PoaY`I`tjCjb(p0PyQk;9 z-c|Q?MiK9wlr2$f%Vyl2XKQEo?fK`QZ(C<QpO|`c_5AI*$WC1wR&Dy$-oCc$@=FoT zr9WOipTGXy&PmIcFE5yUF>C9s@9)38d3}9-ZR~YM1_lNL)%kTlRqK9#dYaXL=l;H0 zZga_o@9)3ei2w(XgtmJgnBk_K|H|rI{lAy{kAwIz6K3-qFgSJip#cB!WS#KUKPu|$ zOWy~B4D^`@lKQmo^`hL{|9_sH22##*+LtXsVyPgA9lYiC+oa7mT|z@Arp|0+@G04D zUBt}5z%Y4>r>2>vYoJIQqrt<w?>4G~6f?M;o+u4sG+Zot0!n5K30qFReVcn`wN}@r z9-H#dAh8`R=8_GGQ#6;ZSanJTq*(6+l66;1d`ohrpXQ`VFbLm!+job7f#HCP?6=<) zHglDhirju{wn@iWq=EBY8NVIK`IE1FuicU>oqJn~?LbTUZe??jfeeeEp5O&B4rDy_ zy?o?nP2a9{z92_GaJrMi&=x57?RSNRT=LGSqsM>$K2f~?4oLBb#XC<j@F>6EeQvq| zNN>yxP{4R8gV@V5cHcglwz+fFDy7p#%#1T$<duWHr>417WMYtrYa-aOa@9;A#R*$v zc7mM7AfXG3jn#MV-8;5t)gq0q6UFBF3=9kn-Z$Qr<ry(BYXo}*hEDYa>5B>2wX-v8 zqX9=lxmGVoV8)_lnTuZUnlbg9JVWrkx7%z$@$cFG*G|rVxrgS__5lows=a+#8#{DP z^ROf=+I<%s=QEZ}30g8`kp|ach8Z97zTXfAc>yUVEWyc!!652s+4k<6HFBwuZu53m zf?_462^7+&N{WTACM61iw241aV>aly!~qH^&8%5!yViBzL`@NYv-kHy^2>*Z&MA>; z8VtgB-pbX0B0z2B>)#o-&2DWo<!Ru2Uj~Z<c*;CAds*k_pEAC&vq7oxK<kc^3_Oc% z-hBU9U~?>MYghf9`|k68KLzEZ2Klp~lv#Pd^y0Q$o8Lkp^$p#HlNlx4GFd<gQzUo$ z?TGc)14BboBGVKYgzv%<?9$n@Jp=<qS`!(3Dz;a@;RJaKDNn!>$lCq)p5(o<;`?3e z;*wLoJ3?=|K3i$*@%{bBU;O%a^gO&Y0~ajo7yixL8FMUc^UhoP^GhIQ!`pBB`|g}S zfBs{EjhoAns=akz+qaxQ|M9~If!OPNf9!;mg<#!Vqu!csxxaMEqggw_W#R9#YRmPj z{{P}WeN*StpQqD(x;FJ3Et34JZSR6oIDM14^?uc=U8f#zo81$(`YPY%+nc}r-1+}^ zsLkom;q@oJXaCCHf>QpiU-<X8<I`Iuxzc$?QES6~|GPWy>tE^q*`Od`I571WzhXz? UZQJv6H-ZE_UHx3vIVCg!0D185H~;_u literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/layout.png b/theme/adaptable/pix/layout.png new file mode 100644 index 0000000000000000000000000000000000000000..b96e6c5f0a6b6092dad3ace3294a8248c56d8daf GIT binary patch literal 54724 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�_b-XxUpE8N-uCKWTl@PP=;(-sR&Mc4 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/next.png b/theme/adaptable/pix/next.png new file mode 100644 index 0000000000000000000000000000000000000000..b176eed6809fe6da9b2e663fc9456e07f32c1839 GIT binary patch literal 1102 zcmeAS@N?(olHy`uVBq!ia0vp@3=9lX9Bd2>3^^9p;u#niBuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFso#SM3hAM`dB6B=jtVb)aX^@7BGN-jeSKyVsdtB zi9%9pdS;%j()-=}l@u~lY?Z=IeGPmIoKrJ0J*tXQgRA^PlB=?lEmM^2?G$V(tSWK~ za#KqZ6)JLb@`|l0Y?Z*~TICg6frRyy6u?SKvTc<hj*9RNP;kyKN>wn`GuBNuFf>#! zGt)CPF*P$Y)KM@pFf`IPFw!?L(={})GBvX@GFN~CB|8P1qLehNAQv~NT}3Hrwn`Z# zB?VUc`sL;2dgaD?`9<mahL)C=ATx}Nbc<5bbc-wVN)jt{^NN*WCb*;)Cl_TFlw{`T zDS%8&Ov*1Uu~kxn8e5TD05>+T7#d8;`MLTPi3R$GdIlgbLHwFq;OmQDX>KlDb#X~h zD#E>34K5C;EJ)Q4N-fSWElN%eN=;J+xv9X)xhOTUB)=#mKR*W+iUAq<CHch}`2`Bj z!Db2?zKO}1c_0Bzunu2eE6=>*lEl2^R8JRMrHb4Fz0AxMD^p8XV{-#Xb5kQzBSS-1 zQ%fT=LklA#16MO=S4&qHQ<z?t{N&Qy)Vvay-V}sh3!HjEi2)QKRxYVUnPsUdZbkXI z3SduLW#V>=DNggCdQ)(_#SEujeLDpkeNfaQMKw$)$i)rB1gA<+3bj)}B>mL96kDYt zC40N=!84~ZFfbhOba4!+xHV-;qF}QDPwQsO6|GSRn8Y?Tv_>6p(miuXjgfov22YN& zu{%?DnpReq^Z$Cj_{-CStut3@o9^eF=x{dp^XmJWn_`04C0NfNJthD4ucM;MI<7s7 zC;V7;pv!6H{(CD$8;?Y?D$f%1=zoyDC~k|@#pQk`QYnUu8x*X&x}3yx!Vi53c-`}6 z(~U0IhF&|@bg6CknM>x1rR<2$R5aBJyuA0-n-j|odv}>NeX{@GX`q?E{Os?by~bw~ dDqjjDFueTu{*|o3?rcyF_jL7hS?83{1OVwGa-aYJ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/previous.png b/theme/adaptable/pix/previous.png new file mode 100644 index 0000000000000000000000000000000000000000..8202afbb2c3fe5ca1bcc62d5b646be46198f5bfd GIT binary patch literal 1109 zcmeAS@N?(olHy`uVBq!ia0vp@3=9lX9Bd2>3^^9p;u#niBuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFso#SM3hAM`dB6B=jtVb)aX^@7BGN-jeSKyVsdtB zi9%9pdS;%j()-=}l@u~lY?Z=IeGPmIoKrJ0J*tXQgRA^PlB=?lEmM^2?G$V(tSWK~ za#KqZ6)JLb@`|l0Y?Z*~TICg6frRyy6u?SKvTc<hj*9RNP;kyKN>wn`GuBNuFf>#! zGt)CPF*P$Y)KM@pFf`IPFw!?L(={})GBvX@GFN~CB|8P1qLehNAQv~NT}3Hrwn`Z# zB?VUc`sL;2dgaD?`9<mahL)C=ATx}Nbc<5bbc-wVN)jt{^NN*WCb*;)Cl_TFlw{`T zDS%8&Ov*1Uu~kxn8e5TD05>+T7#d8;`MLTPi3R$GdIlgbLHwFq;OmQDX>KlDb#X~h zD#E>34K5C;EJ)Q4N-fSWElN%eN=;J+xv9X)xhOTUB)=#mKR*W+iUAq<CHch}`2`Bj z!Db2?zKO}1c_0Bzunu2eE6=>*lEl2^R8JRMrHb4Fz0AxMD^p8XV{-#XV^bqjBSS-1 zQ%fT=LklA#16MO=S4&qHQ<z?t{N&Qy)Vvay-V}sh6P$WMi2)QKRxYVUnPsUdZbkXI z3SduLW#V>=Ax`t4dQ)(_#R#WfeLDpkeNfaQMKw$)$i)rB1gA<+3bj)}B>mL96kDYt zC40Mw^!mvR3=GFTT^vIyZcUlO$a`2pz;z{yP68wM)uz^{8LT>24(S+hZr<Rz!;$Ib z!BcmiPny4N=N^Xn_iuNc(3x`f*DR(+t4%@ETpJz2Z3Pdc*J=gKi(}SdUa>C2M8WVu zhAn3oPsbj0%d1+wwP#m`Y}a(E_q+SHrGCdjO?&J2XI3i2x>j2TE^w@wx_!&q!lf4( z+8tbXr+q#1N$;#i$d?&M-Mc5gP@DBZw((d?$wa4rQAy<)f-m&GvdQYqU97FFb8T~L kZeqkV$CsPp>>1e@EOWljf3V`&Z%{_}boFyt=akR{0NRal(f|Me literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/quiz/arrow.png b/theme/adaptable/pix/quiz/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..0b52b595010851f2a020f7c9ee78cdb8fbf71ca2 GIT binary patch literal 3059 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 E0CkV&)c^nh literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/quiz/qanswered.png b/theme/adaptable/pix/quiz/qanswered.png new file mode 100644 index 0000000000000000000000000000000000000000..e16882dfc32cb14fc63f53325c5169eee9c05fc7 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIM^5%7<!G)?`2?MkSuYHC<)F_D=AMbN@Z|N z$xljE@XSq2PYp^<OsOn9nQFtpz_8xa#WAGfR??sU|LvI#8(8N*`OSa+RKpcpg?k4( zPP;5nidWbw(y^cUqm9}M1$lwfN}5KDLA4HNxQ{$<{K52i_jiBUh8`6KGv-GM3jcVH zWH;Ur>F8%F>{-xog?WNBbE8B`FN45&wnzQ}uHlUq%u6^@xKFrDU=mlD$8{tr;I=$t zp-#X>#T|<s=P@QOXj4$AV?83@a6?{=@36)`wkjo7k3uHjSv_)GJxACq92yuI_WTXE VHGA50h=GBD!PC{xWt~$(69Dc?UpxQ+ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/quiz/qcheck.png b/theme/adaptable/pix/quiz/qcheck.png new file mode 100644 index 0000000000000000000000000000000000000000..ff29035a7352b03aa78b06c34ec391e9d2d768a4 GIT binary patch literal 1120 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIM^5%7<!G)?`2?MdY9?!9N_8ftPoI?pO%@E z%D_;;*cx&^Ti8+LpIP3@3rZYO*?gNSCMbDEuFi11V)j=sDVyiE{5IDKDaM}H^2}bp zQ`@?<<H@6oTK8PvE@c1TAXoJvMc=D6%2nguYK^3Y;<_Kt=J{+}`s%>By3hB_-`D1s z-Dlt}x@7XTf#nJB@t{S5nK?C1S${eh`WA6Iny7}}e&eUY=Ga{Pv-<gsD*ndku*22A z%Quy;opov3EcTYa4iDUC-PGFfUc8ViQi!R;=Key><0TyTAK&=&rr*-9vf|)Yer?+$ zkxDfVrn=|qeRl+(RlV%c>|rtGYhjSxoa^0FuFJA#=A|7wA+Xnf!m{0G-mOkrILGfB z--Nw$zts6{zGrtx@esG<T6?!0z4N~IT(y6c_;AUyBW<=a5~g2%B#3U8TKU8DkVw3M zulk~p!kc=$A7`xl8}`YosbGS|sg%Tq;wTUQ>ppoucD1wCs$Cae|EN4H@L^@YaQeL~ zci+3-Vp?y&C(9$nGmF<FK){tHamI!Khc+82Lz7KK_R6c-q$gJ!m8xXzE=h9P=vZxb z$98+*lDjtlG?WUbIB(^c^vCg^*7}qFPb+_(j@fV{?E93pvuEx4^uYS))0sP^<gQ;n z@wnJGu4d|$vdQ7@)7Dg0?V0sP%KlCGbi-#&ru|2XH~jvn8$b2Tbj==-qmM&Bzw&M= zjWRU9KR>75+V+v&dA+UIir02_n}^Mwp8hw><M+E&dnP{%{pla~_2ZxPvqzF2X&S1P zbv;+?oPJdA<D?tjtGOQ~<R0gkJGD*oQtrfGDpwN{X6^Z^A8Q|7vDdAh=WE~;$upNi zI=|N+(n>LzyOS^fSO4wff5e}B?|MF8gGoQ<!HJo>a)S%s3oJ}snpeDzNqqms+7H|E zvKQA`{9%c(SuM6U$aKEK9!8n6_A0p-nlmm>n|9VRs9k6N<XtaMB?X-cxa}MibY1m& zs<+tYnW0H9%WllPESo2@(9-Gqk>y^glYg!MD7fM8hM=E6*96_H^OL&PIQ>`tfjxg+ z4^(t_GB7Z37I;J!Gca%qgD@k*tT_@442%+<E{-7*lF5Jm|F>s0J8<H_i68SB6;HAR zu?VH)q_k{k^qHZwbLy+ep3Oh@Dzg1ePhgqJWwx`iu`^MW*?7v^)Y@~O+kX~Lzs_UH zvr9)o)xmg&WRnET|Mh?Wv*b9dF5cKR_2H5IXL#P^&)<LV{DI>ix>#3qxYzbxG~p}r zGd^Z;Oo2_aO`l21=<<viGeh6a+fn%9YtwJd|NVdKPaYCDzu?@_lW&i&);yyBe5vK% zi=Wl!-#BV<MlNQ5`OoWL%Wpck`Zux}INzCeR6u2K#Mjm{j=lezZa?|A*<Y;c#Ds$h zyk**F&KC5u7_t<d5ve(97XHBYf|&h`=MO$TxX}1(&cRm2u7i^mIoEJ#uuW>#WHCNq j=gO53VbOFXk%1whz0=^`$DbVx3=9mOu6{1-oD!M<k)#=l literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/quiz/qdash.png b/theme/adaptable/pix/quiz/qdash.png new file mode 100644 index 0000000000000000000000000000000000000000..cf5dfdce999d8db79c14c5e62501a84c00598452 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0y~yU|?lnU=ZP8V_;x#efs@20|SF(iEBhjaDG}zd16s2 zgJVj5QmTSyZen_BP-<dIW#P$G8wLgj1y2{pkcwMLdkQ{2s%JAfaHN5iC)4rR|NsB_ fBOMzUm>K#a<w~EW>{-abz`)??>gTe~DWM4ff+Z*i literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/quiz/qwhite.png b/theme/adaptable/pix/quiz/qwhite.png new file mode 100644 index 0000000000000000000000000000000000000000..1c479a00e95a01bce0725367ff60cae6f8b5ac86 GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0y~yV31>AVBqIqVqjpXx$)vO0|SF(iEBhjaDG}zd16s2 zgJVj5QmTSyZen_BP-<dIW#P$G8wLgjMNb#UkcwMLfByfsXU=61W<Gmh#l^*L{Zh7B gUldHG6b>-hyE98~-l%HCz`(%Z>FVdQ&MBb@0EdPqJOBUy literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/quiz/qwrong.png b/theme/adaptable/pix/quiz/qwrong.png new file mode 100644 index 0000000000000000000000000000000000000000..d92d868b32d9d37c09c9a61d738bdcb9179bdec0 GIT binary patch literal 1132 zcmeAS@N?(olHy`uVBq!ia0y~yU;weXIM^5%7<!G)?`2?MdY9?!9N_8ftPoI?pO%@E z%D_;;*cx&^Ti8+LpIP3@3rZYO*?gNSCMbDEuFi11V)j=sDVyiE{5IDKDaM}H^2}bp zQ`@?<<H@6oTK8PvE@c1TAXoJvMc=D6%2nguYK^3Y;<_Kt=J{+}`s%>By3hB_-`D1s z-Dlt}x@7XTf#nJB@t{S5nK?C1S${eh`WA6Iny7}}e&eUY=Ga{Pv-<gsD*ndku*22A z%Quy;opov3EcTYa4iDUC-PGFfUc8ViQi!R;=Key><0TyTAK&=&rr*-9vf|)Yer?+$ zkxDfVrn=|qeRl+(RlV%c>|rtGYhjSxoa^0FuFJA#=A|7wA+Xnf!m{0G-mOkrILGfB z--Nw$zts6{zGrtx@esG<T6?!0z4N~IT(y6c_;AUyBW<=a5~g2%B#3U8TKU8DkVw3M zulk~p!kc=$A7`xl8}`YosbGS|sg%Tq;wTUQ>ppoucD1wCs$Cae|EN4H@L^@YaQeL~ zci+3-Vp?y&C(9$nGmF<FK){tHamI!Khc+82Lz7KK_R6c-q$gJ!m8xXzE=h9P=vZxb z$98+*lDjtlG?WUbIB(^c^vCg^*7}qFPb+_(j@fV{?E93pvuEx4^uYS))0sP^<gQ;n z@wnJGu4d|$vdQ7@)7Dg0?V0sP%KlCGbi-#&ru|2XH~jvn8$b2Tbj==-qmM&Bzw&M= zjWRU9KR>75+V+v&dA+UIir02_n}^Mwp8hw><M+E&dnP{%{pla~_2ZxPvqzF2X&S1P zbv;+?oPJdA<D?tjtGOQ~<R0gkJGD*oQtrfGDpwN{X6^Z^A8Q|7vDdAh=WE~;$upNi zI=|N+(n>LzyOS^fSO4wff5e}B?|MF8gGoQ<!HJo>a)S%s3oJ}snpeDzNqqms+7H|E zvKQA`{9%c(SuM6U$aKEK9!8n6_A0p-nlmm>n|9VRs9k6N<XtaMB?X-cxa}MibY1m& zs<+tYnW0H9%WllPESo2@(9-Gqk>y^glYg!MD7fM8hM=E6*96_H^OL&PIQ>`tfjxg+ z4^(t_GB7Z37I;J!Gca%qgD@k*tT_@442&wCE{-7*lF5Jm|F>u6N=Qjz`Fv4zLCvyI zBO}qzH*fBBs1cZV{@l~8Zhq^SR&;V6NoRZ+r=}YGEHCHMzXykZA3xgDlQ_p=qXYM9 z$wxLZn>NSqVbgrcp#A5|-`7jC_2X@)v3XgnT&@4-(_87o_v7tT&)Zjhp3&I+RmekC z`9f=t!tPHJlDax6PsNK2{)PYk^Vj-t_x1Jpdy^A7-v9akpN09t-2`DrtA-|?_j2|r z6B1I>A24nD8F52G@BdHMNleUY6EahKdgAW!oLPOwV&~O+|NfT0wiS@xY_PiV$f|}S z1ydh|r~f4+zC4*ZGv$5l|9=(#_TT^bx#7pX{`vh+XU<G*OncSX%Tg)JF=LX1gjSQJ v#Djy(NzF$biWE$H0$LM%6PVl^m>Cq~TP|5mbWdeqU|{fc^>bP0l+XkK<Qg7d literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/screenshot.png b/theme/adaptable/pix/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f011a406c957907cff6ad739ca1c33c676d34984 GIT binary patch literal 153364 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≦Adxj`wnytPv^~uur_3ev= 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/search2.png b/theme/adaptable/pix/search2.png new file mode 100644 index 0000000000000000000000000000000000000000..c741075fe59963f8a0936897e14f130686f31ecf GIT binary patch literal 1280 zcmeAS@N?(olHy`uVBq!ia0y~yU=U|uU=ZVAVqjocA~FFaAX(xXQ4*Y=R#Ki=l*-_k zlAn~S;F+74o*I;zm{M7IGS!BGfmtOpB%&n3*T*V3KUXgiq(-kIw}1fzZ0suv5|gu2 zOB9k)(=+pImEP~(ucVNfVyhHx>TBRz;GCL~=}}db8eHWUl3bOYY?-2DZ>L~WVO5b^ zkegbPs8ErclUHn2VXFi-*D9~r3M8zrqySb@l5ML5aa4qFfP!;=QL2Kep0RGSfuW&- znVFuUiK&^Hp^k!)fuWJUfswv}nXaLUm8qGPk+}jCDA_646s4qD1-ZCE?J7!1vsKC{ zDJihh*Do(G*DE*H%P&gTH?*|01esxEq+67drdwQ@SCUwvn^&w1Gr=XbIJqdZpd>Rt zPXT0NVp4u-iLH_n)YyvL0=Thx#n50%&d=4aNG#Ad)H48i3F6n>0$*SJN^^7Js*6j4 zQW5UOYH)E#WkITbP-=00X;E@&P->bo$V~-S&PAz-CHX}m`T04pPz=b(FUc>?$S+WE z4mMNJ@J&q4%mWE%f_3=%T6yLbmn7yTr+T{BDplkb=w)W6Sh>1bTAG_Uy16-<8yXtA z8atU-8e5u~85%p88koA7Im7h2<R_QrrskEv^rpb|IvL>93rY;20I_mOEy^rQO>ryA z&s6|>+A0&bTU>CO2i2Q`(=Cp!IQ8n=DcI<Pq82HtVM0MJZXhN&Rf1BeodP21r{<;D zDitZ&+fC@0b(MjEvB=ZKF{I*F(x3nT?U|1@bT&#gux`IH|NQ*@%$i|-aeQJ<$Nn5= zxi6nTW5(j{C29)wr*<y4|Nofn#j={+n{Q4R++BP3_B-M9Q{ROHH#8KToL}_fO|SL! z^J`|!U^dS<_0hk@*YZM6&(&M`{ALGbBFf*J*9F(zKa<L~d(lUWWxM*Ht2xarekL6k zY&W+hV#lLz`Z??VS(q}Y%r<y0wM-_cDzR0~-cc_sr}6ji$F%}qUtX5~v$C&O{LQq( z;_->S%6y3eCr_N~vfo=XN0>qI^uABBch7z=7jbwJ7W3Oxq0cUE9;0dW_9q{j^*<!+ z-?ox9@AXB0mnFNBC(n5(H7BWS)-0_*UjmvBEaWxbwDCzpcXk%%lZ5td?JL*}lXNBj z?b*M-`uDNJ%jKCiD=(igY3b~Kx$B(qdsm$HV7q%SZsn0ib@N9I^7%T>=N84RyuD$m zxKo3_?I(^&j$uYN72a%r^mgrIn{k9$Yg?U~gdTHnQqXac1_ow^=}#I(Ie0B}L1mJs LtDnm{r-UW|#zV^& literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix/tile-background.png b/theme/adaptable/pix/tile-background.png new file mode 100644 index 0000000000000000000000000000000000000000..1618a435b29a20ff234341e9e5fe759df0f04e89 GIT binary patch literal 3330 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 K&t;ucLK6UQ=`wNv literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/f/archive.svg b/theme/adaptable/pix_core/f/archive.svg new file mode 100644 index 0000000..241bc7c --- /dev/null +++ b/theme/adaptable/pix_core/f/archive.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.6499993801117,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M11.099978,22.800002L11.099978,24.800002 19.199985,24.800002 19.199985,22.800002z M0,21.4L30.400001,21.4 30.400001,32.000001 0,32.000001z M11.000003,9.499997L11.000003,11.499998 19.099979,11.499998 19.099979,9.499997z M0,8.2000015L30.400001,8.2000015 30.400001,19.300002 27.699989,19.300002 26.799993,13.700002 7.5999773,19.200003 0,19.200003z M6.3999956,0L23.399999,0 29.099983,5.0000008 29.099983,6.0999993 1.2999883,6.0999993 1.1999822,5.0000008z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/audio.svg b/theme/adaptable/pix_core/f/audio.svg new file mode 100644 index 0000000..2f5fda9 --- /dev/null +++ b/theme/adaptable/pix_core/f/audio.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.0000046491606,11) scale(0.812499709427461,0.812499709427461) " fill="#FFFFFF" d="M6.786273,19.58038C4.8552475,20.543352 3.6762314,22.99128 4.065237,25.541203 4.450243,28.056129 6.2492657,29.899075 8.3792934,29.997072z M25.376723,19.580251L23.78369,29.997051C25.913715,29.899031 27.712743,28.056083 28.097726,25.541122 28.486738,22.991189 27.307741,20.543243 25.376723,19.580251z M16,0C24.823,0 32,8.0030079 32,17.841018 32,20.866021 31.309999,23.855024 30.002,26.485027 29.986563,26.51584 29.969746,26.545511 29.951663,26.574005L29.914128,26.627069 29.864518,26.820212C29.014498,29.873736 26.508523,31.999999 23.606687,31.999999 23.216699,31.999999 22.821676,31.959991 22.434679,31.88199 21.912671,31.774998 21.564675,31.277999 21.64567,30.750026L23.579709,18.101309C23.620694,17.830288 23.771697,17.58832 23.995698,17.432317 24.164454,17.315315 24.364144,17.253999 24.566082,17.253016 24.633395,17.252689 24.700958,17.259065 24.767708,17.272317 26.979605,17.722308 28.738765,19.392976 29.597425,21.560945L29.604015,21.578708 29.650576,21.362866C29.881711,20.212188 30,19.030144 30,17.841018 30,9.1060085 23.72,2.000001 16,2.000001 8.2810001,2.000001 2,9.1060085 2,17.841018 2,19.122894 2.1366634,20.391947 2.4033756,21.622048L2.4572048,21.853096 2.5655499,21.561043C3.4242134,19.393089 5.1833782,17.722435 7.3952808,17.272449 7.4622822,17.259199 7.5299702,17.252762 7.5973301,17.253012 7.7994108,17.253762 7.998538,17.314697 8.1672907,17.432444 8.3912935,17.588439 8.5422955,17.830432 8.5832958,18.101424L10.517322,30.75005C10.598323,31.278034 10.250318,31.77502 9.7283115,31.882016 9.3413057,31.960014 8.9463005,32.000012 8.5562963,32.000012L8.5552959,32.000012C5.8544168,32.000012,3.4959736,30.156921,2.4975262,27.438403L2.4337578,27.253311 2.3802185,27.190395C2.3527966,27.153262 2.3276253,27.113778 2.3050003,27.072028 0.79699898,24.296025 0,21.103022 0,17.841018 0,8.0030079 7.1779995,0 16,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/avi.svg b/theme/adaptable/pix_core/f/avi.svg new file mode 100644 index 0000000..f7af549 --- /dev/null +++ b/theme/adaptable/pix_core/f/avi.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.8133345246315,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.6080074,2C2.2780542,2,1.9999808,2.2279663,1.9999809,2.4970093L1.9999809,29.502014C1.9999808,29.565979 2.0049856,29.630981 2.0770063,29.757019 2.2189729,29.994019 2.6049554,30.080994 2.8799772,29.919983 6.4660018,27.979004 10.029932,26.046021 13.594961,24.111023 18.282903,21.567993 22.973776,19.021973 27.665746,16.487976 27.804785,16.377014 27.889744,16.297974 27.947727,16.210999 28.020725,16.088989 28.002781,15.952026 27.983738,15.883972 27.960788,15.802002 27.902805,15.687988 27.755712,15.612L18.92279,10.807983C13.593862,7.9099731,8.2720144,5.0169678,2.942965,2.1090088L2.9219689,2.098999C2.8160129,2.0369873,2.6979721,2,2.6080074,2z M2.6080074,0C3.0469677,0 3.498013,0.12402344 3.9090199,0.35797119 9.2348955,3.2639771 14.552837,6.1549683 19.878835,9.0510254L28.695766,13.846985C29.288778,14.153992 29.725786,14.690002 29.909744,15.346985 30.089796,15.994995 29.991773,16.698975 29.637773,17.278015 29.382772,17.669983 29.100792,17.904968 28.833704,18.11499L28.692714,18.20697C23.976818,20.753967 19.260799,23.312012 14.547832,25.869019 10.982926,27.804016 7.4189956,29.737 3.857995,31.664978 3.4670076,31.893005 3.0280471,32 2.5900631,32 1.7029866,32 0.82103718,31.555969 0.34801912,30.763 0.10998452,30.343994 2.3663961E-07,29.937012 0,29.502014L0,2.4970093C2.3663961E-07,1.1199951,1.1700329,0,2.6080074,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/bmp.svg b/theme/adaptable/pix_core/f/bmp.svg new file mode 100644 index 0000000..68cb792 --- /dev/null +++ b/theme/adaptable/pix_core/f/bmp.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(14.0809998512268,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M6.6779938,27.817001L7.1360016,27.817001C7.7749939,27.817001 8.2850037,28.056999 8.2850037,28.642998 8.2850037,29.266998 7.7599945,29.491997 7.1660004,29.491997 6.9559937,29.491997 6.7980042,29.485001 6.6779938,29.470001z M18.132004,25.742996C18.770004,25.742996 19.160995,26.043999 19.160995,26.608002 19.160995,27.200996 18.740005,27.539001 18.048996,27.539001 17.861008,27.539001 17.711014,27.530998 17.599014,27.500999L17.599014,25.789001C17.695999,25.765999,17.875992,25.742996,18.132004,25.742996z M7.2039948,25.722C7.7899933,25.722 8.1499939,25.939995 8.1499939,26.413002 8.1499939,26.840996 7.7899933,27.140999 7.151001,27.140999L6.6779938,27.140999 6.6779938,25.758995C6.7749939,25.737,6.9329987,25.722,7.2039948,25.722z M10.488998,25.074997L10.151001,30.138 11.014999,30.138 11.126999,28.102997C11.164993,27.410995,11.194992,26.608002,11.216995,25.946999L11.231995,25.946999C11.367996,26.577995,11.548004,27.268997,11.742996,27.915001L12.419006,30.077995 13.133011,30.077995 13.869003,27.884995C14.094986,27.245995,14.305008,26.561996,14.469986,25.946999L14.492996,25.946999C14.485001,26.629997,14.522995,27.418999,14.552994,28.072998L14.650009,30.138 15.551987,30.138 15.258987,25.074997 14.056992,25.074997 13.358994,27.102997C13.16301,27.696999,12.990005,28.32,12.854996,28.867996L12.831985,28.867996C12.705002,28.305,12.546997,27.703995,12.367004,27.111L11.705994,25.074997z M18.094986,25.038002C17.470993,25.038002,17.012985,25.083,16.681992,25.142998L16.681992,30.138 17.599014,30.138 17.599014,28.229996C17.719009,28.251999 17.875992,28.259995 18.048996,28.259995 18.702988,28.259995 19.280991,28.086998 19.649002,27.719002 19.935013,27.448997 20.085007,27.050995 20.085007,26.57 20.085007,26.097 19.889999,25.698997 19.589005,25.450996 19.266006,25.18 18.770004,25.038002 18.094986,25.038002z M7.1060028,25.038002C6.5879974,25.038002,6.0540009,25.083,5.7610016,25.142998L5.7610016,30.123001C5.9940033,30.153 6.3849945,30.191002 6.8959961,30.191002 7.8199921,30.191002 8.4129944,30.032997 8.7740021,29.716995 9.0670013,29.476997 9.2619934,29.124001 9.2619934,28.672997 9.2619934,27.974998 8.7740021,27.554001 8.2400055,27.418999L8.2400055,27.403999C8.7890015,27.200996 9.0820007,26.772995 9.0820007,26.306999 9.0820007,25.878998 8.8710022,25.547997 8.5559998,25.361 8.2100067,25.119995 7.7899933,25.038002 7.1060028,25.038002z M18.175003,10.675995C18.981003,10.675995 19.633987,11.330002 19.633987,12.134995 19.633987,12.941002 18.981003,13.594002 18.175003,13.594002 17.37001,13.594002 16.715988,12.941002 16.715988,12.134995 16.715988,11.330002 17.37001,10.675995 18.175003,10.675995z M10.048004,10.675995L12.203995,18.209 13.081985,12.598 16.486008,18.589996 17.719986,17.448997 19.633987,21.323997 4.772995,21.323997 5.7109985,14.433998 8.0119934,17.448997z M2.2830048,2.4179993L2.2830048,22.778 22.124008,22.778 22.124008,8.5849991 15.91301,8.5849991 15.91301,2.4179993z M0,0L17.120987,0 24.416,7.1419983 24.416,8.5849991 24.406998,8.5849991 24.406998,32 0,32z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/calc.svg b/theme/adaptable/pix_core/f/calc.svg new file mode 100644 index 0000000..ee83469 --- /dev/null +++ b/theme/adaptable/pix_core/f/calc.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#5E5555" /> + <path id="path1" transform="rotate(0,24,24) translate(14.639999628067,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M17.248002,23.997986C16.677995,23.997986,16.217971,24.460999,16.217971,25.031006L16.217971,26.063995C16.217971,26.632996,16.677995,27.095978,17.248002,27.095978L18.407001,27.095978C18.974994,27.095978,19.435992,26.632996,19.435992,26.063995L19.435992,25.031006C19.435992,24.460999,18.974994,23.997986,18.407001,23.997986z M10.94099,23.997986C10.372996,23.997986,9.911997,24.460999,9.911997,25.031006L9.911997,26.063995C9.911997,26.632996,10.372996,27.095978,10.94099,27.095978L12.099011,27.095978C12.66798,27.095978,13.12898,26.632996,13.12898,26.063995L13.12898,25.031006C13.12898,24.460999,12.66798,23.997986,12.099011,23.997986z M4.6339766,23.997986C4.0659825,23.997986,3.6040072,24.460999,3.6040072,25.031006L3.6040072,26.063995C3.6040072,26.632996,4.0659825,27.095978,4.6339766,27.095978L5.7919976,27.095978C6.3599912,27.095978,6.8230044,26.632996,6.8230044,26.063995L6.8230044,25.031006C6.8230044,24.460999,6.3599912,23.997986,5.7919976,23.997986z M17.248002,18.579987C16.677995,18.579987,16.217971,19.041992,16.217971,19.610992L16.217971,20.643005C16.217971,21.213989,16.677995,21.675995,17.248002,21.675995L18.407001,21.675995C18.974994,21.675995,19.435992,21.213989,19.435992,20.643005L19.435992,19.610992C19.435992,19.041992,18.974994,18.579987,18.407001,18.579987z M10.94099,18.579987C10.372996,18.579987,9.911997,19.041992,9.911997,19.610992L9.911997,20.643005C9.911997,21.213989,10.372996,21.675995,10.94099,21.675995L12.099011,21.675995C12.66798,21.675995,13.12898,21.213989,13.12898,20.643005L13.12898,19.610992C13.12898,19.041992,12.66798,18.579987,12.099011,18.579987z M4.6339766,18.579987C4.0659825,18.579987,3.6040072,19.041992,3.6040072,19.610992L3.6040072,20.643005C3.6040072,21.213989,4.0659825,21.675995,4.6339766,21.675995L5.7919976,21.675995C6.3599912,21.675995,6.8230044,21.213989,6.8230044,20.643005L6.8230044,19.610992C6.8230044,19.041992,6.3599912,18.579987,5.7919976,18.579987z M17.248002,13.160004C16.677995,13.160004,16.217971,13.622986,16.217971,14.191986L16.217971,15.223999C16.217971,15.794006,16.677995,16.255981,17.248002,16.255981L18.407001,16.255981C18.974994,16.255981,19.435992,15.794006,19.435992,15.223999L19.435992,14.191986C19.435992,13.622986,18.974994,13.160004,18.407001,13.160004z M10.94099,13.160004C10.372996,13.160004,9.911997,13.622986,9.911997,14.191986L9.911997,15.223999C9.911997,15.794006,10.372996,16.255981,10.94099,16.255981L12.099011,16.255981C12.66798,16.255981,13.12898,15.794006,13.12898,15.223999L13.12898,14.191986C13.12898,13.622986,12.66798,13.160004,12.099011,13.160004z M4.6339766,13.160004C4.0659825,13.160004,3.6040072,13.622986,3.6040072,14.191986L3.6040072,15.223999C3.6040072,15.794006,4.0659825,16.255981,4.6339766,16.255981L5.7919976,16.255981C6.3599912,16.255981,6.8230044,15.794006,6.8230044,15.223999L6.8230044,14.191986C6.8230044,13.622986,6.3599912,13.160004,5.7919976,13.160004z M4.7630049,3.6109924C3.9109532,3.6109924,3.2179593,4.3059998,3.2179593,5.1589966L3.2179593,8.6439819C3.2179593,9.4979858,3.9109532,10.191986,4.7630049,10.191986L18.27797,10.191986C19.130023,10.191986,19.823016,9.4979858,19.823016,8.6439819L19.823016,5.1589966C19.823016,4.3059998,19.130023,3.6109924,18.27797,3.6109924z M3.8609652,0L19.178973,0C21.308004,0,23.04,1.7349854,23.04,3.868988L23.04,28.127991C23.04,30.261993,21.308004,32,19.178973,32L3.8609652,32C1.7329727,32,-1.6755075E-07,30.261993,0,28.127991L0,3.868988C-1.6755075E-07,1.7349854,1.7329727,0,3.8609652,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/chart.svg b/theme/adaptable/pix_core/f/chart.svg new file mode 100644 index 0000000..8ba8eeb --- /dev/null +++ b/theme/adaptable/pix_core/f/chart.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M0,17.499996L8.7000122,17.499996 8.7000122,32 0,32z M13.600006,9.8999996L13.600006,30 18.400024,30 18.400024,9.8999996z M11.600006,7.9999981L20.300018,7.9999981 20.300018,32 11.600006,32z M23.300018,0L32,0 32,32 23.300018,32z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/database.svg b/theme/adaptable/pix_core/f/database.svg new file mode 100644 index 0000000..2583106 --- /dev/null +++ b/theme/adaptable/pix_core/f/database.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(12.625,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M26,22.617545L25.856231,22.72726C23.420451,24.535196 19.112828,25.688011 14,25.688011 8.8871708,25.688011 4.5795498,24.535196 2.1437683,22.727261L2,22.617545 2,25.465996C2,27.609997 6.9279995,29.999998 14,29.999998 21.072,29.999998 26,27.609997 26,25.465996z M2,16.306508L2,19.154008C2,21.299009 6.9279995,23.688011 14,23.688011 21.072,23.688011 26,21.299009 26,19.154008L26,16.306508 25.856231,16.416223C23.420451,18.224159 19.112828,19.376974 14,19.376974 8.8871708,19.376974 4.5795498,18.224159 2.1437683,16.416223z M2,9.9975402L2,12.842971C2,14.986972 6.9279995,17.376973 14,17.376973 21.072,17.376973 26,14.986972 26,12.842971L26,9.9975405 25.856231,10.107255C23.420451,11.915192 19.112828,13.068007 14,13.068007 8.8871708,13.068007 4.5795498,11.915192 2.1437683,10.107255z M14,2.000001C6.9279995,2.000001 2,4.3900023 2,6.5340033 2,8.6790045 6.9279995,11.068006 14,11.068006 21.072,11.068006 26,8.6790045 26,6.5340033 26,4.3900023 21.072,2.000001 14,2.000001z M14,0C21.981,0 28,2.8090014 28,6.5340033 28,6.5922066 27.998531,6.6501861 27.995607,6.7079349L27.992941,6.7430058 28,6.7430058 28,12.842971 28,19.154008 28,25.465996 28,25.578005 27.997171,25.578005 27.995607,25.639927C27.811427,29.278093 21.856297,32 14,32 6.1437016,32 0.18857193,29.278093 0.0043926239,25.639927L0.0032596588,25.59503 0,25.59503 0,25.465996 0,19.154008 0,12.842971 0,6.5340033 0,6.2939935 0.0094165802,6.2939935 0.01750946,6.1875257C0.37810326,2.6361818,6.2684059,0,14,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/document.svg b/theme/adaptable/pix_core/f/document.svg new file mode 100644 index 0000000..897d324 --- /dev/null +++ b/theme/adaptable/pix_core/f/document.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#2a5699;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.11" cy="24" r="24"/><path id="path1" class="cls-2" d="M23.34,32.91c.58,0,.94.28.94.79s-.39.84-1,.84a1.34,1.34,0,0,1-.41,0V32.93Zm-6.56-6.28H31.37v1.76H16.78ZM15,11.59v18.6H33.17v-13H27.43V11.59ZM13,9.38H28.61l6.66,6.51V38.63H13Zm3.83,14.06H31.37V25.2H16.78Zm0-3.43H31.41v1.76H16.82Z"/><path class="cls-1" d="M18,32h1.28a3.79,3.79,0,0,1,.86.09,1.4,1.4,0,0,1,.64.34,2.44,2.44,0,0,1,.75,2,3.4,3.4,0,0,1-.08.79,3.87,3.87,0,0,1-.22.66,1.82,1.82,0,0,1-.41.53,1.71,1.71,0,0,1-.43.28,2.43,2.43,0,0,1-.49.15,2.94,2.94,0,0,1-.6,0H18a.73.73,0,0,1-.39-.07.4.4,0,0,1-.17-.22,1.93,1.93,0,0,1,0-.37V32.63a.62.62,0,0,1,.15-.45A.54.54,0,0,1,18,32Zm.38.79v3.28h.75a2,2,0,0,0,.38,0,1.56,1.56,0,0,0,.28-.07,1.2,1.2,0,0,0,.26-.15,1.81,1.81,0,0,0,.49-1.43,2.06,2.06,0,0,0-.22-1.07.92.92,0,0,0-.53-.45,3,3,0,0,0-.77-.09Z"/><path class="cls-1" d="M24.47,31.93a2.54,2.54,0,0,1,1.29.3,2,2,0,0,1,.82.86,2.88,2.88,0,0,1,.28,1.31,3.38,3.38,0,0,1-.15,1,2.06,2.06,0,0,1-.45.79,1.9,1.9,0,0,1-.75.51,2.87,2.87,0,0,1-1,.17,2.5,2.5,0,0,1-1-.19,2.16,2.16,0,0,1-.75-.53,2,2,0,0,1-.45-.81,3.57,3.57,0,0,1-.15-1,2.87,2.87,0,0,1,.17-1,2.2,2.2,0,0,1,.47-.79,2.55,2.55,0,0,1,.73-.51A2.21,2.21,0,0,1,24.47,31.93Zm1.39,2.47a2.3,2.3,0,0,0-.17-.92,1.26,1.26,0,0,0-.49-.58,1.39,1.39,0,0,0-1.27-.09,1.43,1.43,0,0,0-.43.32,1.35,1.35,0,0,0-.28.54,2.3,2.3,0,0,0-.11.73,2.33,2.33,0,0,0,.11.75,1.37,1.37,0,0,0,.3.54,1.18,1.18,0,0,0,.43.32,1.65,1.65,0,0,0,.54.11,1.29,1.29,0,0,0,.69-.19,1.41,1.41,0,0,0,.51-.58A2.08,2.08,0,0,0,25.86,34.41Z"/><path class="cls-1" d="M31.8,35.32a1.38,1.38,0,0,1-.11.51,1.54,1.54,0,0,1-.36.53,1.79,1.79,0,0,1-.62.41,2.46,2.46,0,0,1-.88.17,2.62,2.62,0,0,1-.69-.07,2.52,2.52,0,0,1-.56-.22,1.9,1.9,0,0,1-.47-.41,1.78,1.78,0,0,1-.32-.51,5.64,5.64,0,0,1-.21-.6,2.57,2.57,0,0,1-.07-.68,3.08,3.08,0,0,1,.17-1.05,2.24,2.24,0,0,1,.49-.79,1.9,1.9,0,0,1,.75-.51,2.47,2.47,0,0,1,.9-.17,2.4,2.4,0,0,1,1.05.22,1.67,1.67,0,0,1,.69.58,1.24,1.24,0,0,1,.24.66.46.46,0,0,1-.11.3.34.34,0,0,1-.28.13.39.39,0,0,1-.28-.09,1.6,1.6,0,0,1-.21-.3,1.66,1.66,0,0,0-.45-.54,1,1,0,0,0-.64-.17,1.15,1.15,0,0,0-1,.45,2.12,2.12,0,0,0-.36,1.29,2.65,2.65,0,0,0,.15.94,1.23,1.23,0,0,0,.45.56,1.14,1.14,0,0,0,.68.19,1.23,1.23,0,0,0,.71-.21,1.19,1.19,0,0,0,.43-.62,1.26,1.26,0,0,1,.15-.32c.06-.09.17-.11.3-.11a.37.37,0,0,1,.3.13A.43.43,0,0,1,31.8,35.32Z"/><path class="cls-2" d="M20.78,17.94,20,15l-.79,2.91a3.51,3.51,0,0,1-.15.47.62.62,0,0,1-.19.26.54.54,0,0,1-.36.11.51.51,0,0,1-.3-.08.6.6,0,0,1-.19-.19,1.11,1.11,0,0,1-.11-.3c0-.11-.06-.22-.08-.32L17,14.66a1.58,1.58,0,0,1-.08-.43.45.45,0,0,1,.45-.45.37.37,0,0,1,.36.17,2.62,2.62,0,0,1,.17.51l.64,2.81.71-2.64a2.78,2.78,0,0,1,.15-.47,1,1,0,0,1,.21-.28.52.52,0,0,1,.39-.11.53.53,0,0,1,.38.11.85.85,0,0,1,.19.26c0,.09.08.24.15.47l.71,2.64.64-2.81a2,2,0,0,1,.09-.34.78.78,0,0,1,.15-.22.48.48,0,0,1,.62,0,.49.49,0,0,1,.13.32,1.7,1.7,0,0,1-.08.43l-.81,3.24c-.06.22-.09.38-.13.49a.53.53,0,0,1-.19.26.56.56,0,0,1-.37.11.54.54,0,0,1-.36-.11.57.57,0,0,1-.19-.24A2.62,2.62,0,0,1,20.78,17.94Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/eps.svg b/theme/adaptable/pix_core/f/eps.svg new file mode 100644 index 0000000..5aad0a7 --- /dev/null +++ b/theme/adaptable/pix_core/f/eps.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(14.0846556425095,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M12.236001,25.650002C12.874002,25.650002 13.264994,25.950996 13.264994,26.514 13.264994,27.106995 12.844004,27.445 12.152993,27.445 11.965005,27.445 11.815011,27.436996 11.703012,27.406998L11.703012,25.695C11.799996,25.671997,11.979989,25.650002,12.236001,25.650002z M6.7269945,24.980995L6.7269945,30.043999 9.7850104,30.043999 9.7850104,29.285995 7.6510062,29.285995 7.6510062,27.797997 9.5589972,27.797997 9.5589972,27.046997 7.6510062,27.046997 7.6510062,25.739998 9.6720038,25.739998 9.6720038,24.980995z M12.198007,24.943001C11.57499,24.943001,11.117012,24.988998,10.785988,25.048996L10.785988,30.043999 11.703012,30.043999 11.703012,28.136002C11.823007,28.157997 11.979989,28.166 12.152993,28.166 12.806986,28.166 13.384989,27.992996 13.753,27.625 14.038004,27.354996 14.189006,26.956001 14.189006,26.475998 14.189006,26.002998 13.992991,25.604996 13.693003,25.356995 13.370005,25.085999 12.874002,24.943001 12.198007,24.943001z M16.819989,24.897995C15.72401,24.897995 15.033,25.528999 15.033,26.355995 15.033,27.084 15.56599,27.528 16.400006,27.827995 17.046002,28.060997 17.301007,28.285995 17.301007,28.669998 17.301007,29.083 16.97099,29.360001 16.376996,29.360001 15.904003,29.360001 15.453014,29.209999 15.152995,29.036995L14.949992,29.804001C15.228007,29.969002 15.784008,30.118996 16.316998,30.118996 17.624005,30.118996 18.240003,29.413002 18.240003,28.601997 18.240003,27.873001 17.811994,27.43 16.932996,27.099998 16.257001,26.835999 15.964001,26.655998 15.964001,26.265999 15.964001,25.973 16.220014,25.656998 16.806012,25.656998 17.279004,25.656998 17.632001,25.799995 17.811994,25.897995L18.037,25.153999C17.775006,25.018997,17.369,24.897995,16.819989,24.897995z M18.999007,19.320999L18.999007,19.775002 19.453017,19.775002 19.453017,19.320999z M8.3339901,17.847L8.3339901,18.300995 8.7880011,18.300995 8.7880011,17.847z M19.290999,13.720001L19.290999,14.586998 20.158004,14.586998 20.158004,13.720001z M4.2500029,13.720001L4.2500029,14.586998 5.1170082,14.586998 5.1170082,13.720001z M9.5869818,11.919998L9.5869818,12.786995 10.453987,12.786995 10.453987,11.919998z M7.7519894,10.484001C8.7280035,10.497002,9.4369879,10.970001,9.9709845,11.605995L10.767982,11.605995 10.767982,12.889C10.872993,13.109001 10.970009,13.327995 11.053993,13.539001 11.224983,13.971001 11.371986,14.434998 11.513986,14.884995 11.910012,16.136002 12.285989,17.328995 13.172007,17.944L14.549998,17.944 14.549998,18.368996C14.604014,18.370995 14.654002,18.376999 14.708995,18.376999 15.690014,18.363998 17.284009,17.442001 18.337995,16.278 18.763015,15.808998 19.061019,15.340996 19.235,14.900002L18.978011,14.900002 18.978011,13.406998 20.471999,13.406998 20.471999,14.900002 19.891006,14.900002C19.707015,15.484001 19.345992,16.098 18.802016,16.698997 17.842999,17.756996 16.552014,18.581001 15.473003,18.876999L18.686017,19.316002 18.686017,19.007996 19.766006,19.007996 19.766006,20.087997 18.686017,20.087997 18.686017,19.631996 14.549998,19.067001 14.549998,19.437996 13.056009,19.437996 13.056009,18.862999 9.1009894,18.322998 9.1009894,18.613998 8.0199948,18.613998 8.0199948,17.533997 9.1009894,17.533997 9.1009894,18.006996 12.902994,18.526001C11.785989,17.804001 11.331001,16.378998 10.917,15.073997 10.777992,14.633995 10.63401,14.181 10.470985,13.769997 10.374001,13.525002 10.273994,13.306999 10.172005,13.099998L9.2739935,13.099998 9.2739935,11.767998C8.6860104,11.191002 8.0000057,11.002998 7.1519828,11.167 6.3859911,11.314995 5.647007,11.824997 5.2699928,12.466995 5.1349831,12.696999 5.0119968,13.022995 5.0069919,13.406998L5.4310036,13.406998 5.4310036,14.900002 3.9369841,14.900002 3.9369841,13.406998 4.3849826,13.406998C4.3859897,12.977997 4.4929843,12.552002 4.7299838,12.148994 5.2019997,11.347 6.0839887,10.735001 7.0329943,10.552002 7.2879996,10.501999 7.5269828,10.480995 7.7519894,10.484001z M2.2839985,2.4179993L2.2839985,22.778 22.124009,22.778 22.124009,8.5029984 15.837994,8.5029984 15.837994,2.4179993z M0,0L15.837994,0 17.129987,0 24.407001,7.125 24.407001,32 0,32z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/flash.svg b/theme/adaptable/pix_core/f/flash.svg new file mode 100644 index 0000000..41042bd --- /dev/null +++ b/theme/adaptable/pix_core/f/flash.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,11.1872760057449) scale(0.8125,0.8125) " fill="#FFFFFF" d="M23.274994,0L29.303009,0 29.303009,25.761968C29.303009,25.761968,29.462006,27.664981,32,27.823977L32,31.330962C32,31.330962,23.274994,33.058957,23.274994,26.554965z M0,0L16.691986,0 18.084991,4.6000017 5.8699951,4.6000017 5.8699951,13.721987 16.691986,13.721987 16.691986,19.023982 5.9490051,19.023982 5.9490051,31.330962 0,31.330962z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/folder.svg b/theme/adaptable/pix_core/f/folder.svg new file mode 100644 index 0000000..10be6de --- /dev/null +++ b/theme/adaptable/pix_core/f/folder.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,15.3549997210503) scale(0.8125,0.8125) " fill="#FFFFFF" d="M7.5160018,7.0210069L32,7.0210069 26.112001,21.280001 2.2460016,21.280001z M0,0L9.2969996,0 11.158,4.0930236 26.091997,4.0930236 26.087999,5.3469933 6.3500015,5.3469933 0.46300124,21.280001 0,21.280001z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/gif.svg b/theme/adaptable/pix_core/f/gif.svg new file mode 100644 index 0000000..7bbf159 --- /dev/null +++ b/theme/adaptable/pix_core/f/gif.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(14.0846564173698,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M14.613993,25.084999L14.613993,30.147995 15.538006,30.147995 15.538006,28 17.401013,28 17.401013,27.240997 15.538006,27.240997 15.538006,25.844002 17.535992,25.844002 17.535992,25.084999z M12.441995,25.084999L12.441995,30.147995 13.366007,30.147995 13.366007,25.084999z M9.8029859,25.032997C8.1279972,25.032997 6.9859975,26.061996 6.9859975,27.661995 6.9790089,28.428001 7.2410028,29.097 7.6919916,29.525002 8.1730106,29.983002 8.8190067,30.200996 9.6749952,30.200996 10.35099,30.200996 10.982003,30.035995 11.305001,29.915001L11.305001,27.353996 9.5170057,27.353996 9.5170057,28.083 10.410988,28.083 10.410988,29.345001C10.290993,29.404999 10.043007,29.449997 9.7129896,29.449997 8.6609867,29.449997 7.9549925,28.765999 7.9549925,27.616997 7.9549925,26.436996 8.7139957,25.798996 9.7949903,25.798996 10.328987,25.798996 10.667,25.896996 10.944985,26.016998L11.147988,25.272995C10.91401,25.159996,10.426002,25.032997,9.8029859,25.032997z M10.250984,14.919998C11.957986,14.919998 13.341013,16.304001 13.341013,18.011002 13.341013,19.716995 11.957986,21.100998 10.250984,21.100998 8.5430057,21.100998 7.1600092,19.716995 7.1600092,18.011002 7.1600092,16.304001 8.5430057,14.919998 10.250984,14.919998z M7.8440001,8.348999L11.030984,13.869995 4.656986,13.869995z M2.2839985,2.4179993L2.2839985,22.778 22.124009,22.778 22.124009,8.5899963 15.742992,8.5899963 15.742992,2.4179993z M0,0L16.945996,0 24.407,7.3050003 24.407,32 0,32z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/html.svg b/theme/adaptable/pix_core/f/html.svg new file mode 100644 index 0000000..344e92a --- /dev/null +++ b/theme/adaptable/pix_core/f/html.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,12.366218328476) scale(0.8125,0.8125) " fill="#FFFFFF" d="M21.380005,4.4729932L32,12.339999 32,16.440006 21.380005,24.272009 21.380005,20.008001 28.333008,14.422001 28.333008,14.356006 21.380005,8.770007z M10.622009,4.4729932L10.622009,8.7390011 3.6699829,14.325001 3.6699829,14.388996 10.622009,19.976995 10.622009,24.272009 0,16.407001 0,12.306994z M16.969971,0L19.846985,0.49800156 14.997009,28.637001 12.122009,28.140999z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/image.svg b/theme/adaptable/pix_core/f/image.svg new file mode 100644 index 0000000..f66006b --- /dev/null +++ b/theme/adaptable/pix_core/f/image.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,13.1990312933922) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.677002,5.2939989L2.677002,21.356965 4.8850098,21.356965 9.9119873,14.835001 14.226074,19.338962 20.602051,9.7720205 27.702026,21.355989 29.32605,21.355989 29.327026,21.356965 29.327026,5.2939989z M0,0L32,0 32,26.586999 0,26.586999z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/impress.svg b/theme/adaptable/pix_core/f/impress.svg new file mode 100644 index 0000000..32ab99c --- /dev/null +++ b/theme/adaptable/pix_core/f/impress.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#A2A2A2" /> + <path id="path1" transform="rotate(0,24,24) translate(14.8764382004738,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M3.3169894,18.909973L3.3169894,20.480957 14.341993,20.480957 14.341993,18.909973z M3.3169894,12.10199L3.3169894,13.672974 19.114001,13.672974 19.114001,12.10199z M16.406999,1.1129761L21.374013,6.3989868 17.453996,6.3989868C16.875993,6.3989868,16.406999,5.9299927,16.406999,5.3519897z M2.2689838,0L14.893996,0 14.893996,5.6429443C14.893996,6.8959961,15.910995,7.9119873,17.163987,7.9119873L22.457999,7.9119873 22.457999,29.730957C22.457999,30.983948,21.442006,32,20.189015,32L2.2689838,32C1.0159922,32,4.4543413E-08,30.983948,0,29.730957L0,2.2689819C4.4543413E-08,1.0159912,1.0159922,0,2.2689838,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/jpeg.svg b/theme/adaptable/pix_core/f/jpeg.svg new file mode 100644 index 0000000..cbfac64 --- /dev/null +++ b/theme/adaptable/pix_core/f/jpeg.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.11" cy="24" r="24"/><path id="path1" class="cls-2" d="M22.66,33c.58,0,.94.28.94.79s-.38.85-1,.85a1.71,1.71,0,0,1-.41,0V33A2.29,2.29,0,0,1,22.66,33Zm-3.47-.87v3.22c0,.87-.32,1.11-.84,1.11a2,2,0,0,1-.57-.09l-.11.74a2.47,2.47,0,0,0,.77.12c1,0,1.67-.46,1.67-1.85V32.1Zm3.51,0a8,8,0,0,0-1.4.1v5h.91V35.23a2.76,2.76,0,0,0,.45,0,2.22,2.22,0,0,0,1.59-.54,1.52,1.52,0,0,0,.43-1.14,1.42,1.42,0,0,0-.49-1.11A2.27,2.27,0,0,0,22.7,32.06Zm5.48,0a2.55,2.55,0,0,0-2.79,2.61,2.49,2.49,0,0,0,.7,1.85,2.71,2.71,0,0,0,2,.67,5,5,0,0,0,1.62-.28V34.35H27.9v.72h.89v1.25a1.78,1.78,0,0,1-.69.1,1.66,1.66,0,0,1-1.74-1.82,1.68,1.68,0,0,1,1.82-1.8,2.71,2.71,0,0,1,1.14.22l.2-.74A3.32,3.32,0,0,0,28.18,32ZM18.09,19.73v8.11h4.73a11,11,0,0,0,1.37-.73c.65-.3,2.06-.84,2.22-.86s.53-.13.86-.23c-.27-1.34-2.17-2.3-3-3.39a6.91,6.91,0,0,0-1.17,1.45,3.23,3.23,0,0,1,.67-1.53,3.43,3.43,0,0,0-1.54.09c-1.06.28.51-.64,1.56-.69a9.48,9.48,0,0,0-1-1.12,2.28,2.28,0,0,1,1.17.78c.06-.44.23-1.11.65-1.27s-.29.07-.17,1.36l.12,0c1.09-.35,3.61,1.17,2,.68-1.3-.4-1.82-.15-2,0,1.2,1,3.53,1.88,4.4,3.28a11.17,11.17,0,0,1,1.39-.25V19.73ZM17,18.61H31.55V29H17Zm-1.92-7V30.2H33.18v-13H27.44V11.59ZM13,9.38H28.62l6.65,6.51V38.63H13Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/markup.svg b/theme/adaptable/pix_core/f/markup.svg new file mode 100644 index 0000000..344e92a --- /dev/null +++ b/theme/adaptable/pix_core/f/markup.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,12.366218328476) scale(0.8125,0.8125) " fill="#FFFFFF" d="M21.380005,4.4729932L32,12.339999 32,16.440006 21.380005,24.272009 21.380005,20.008001 28.333008,14.422001 28.333008,14.356006 21.380005,8.770007z M10.622009,4.4729932L10.622009,8.7390011 3.6699829,14.325001 3.6699829,14.388996 10.622009,19.976995 10.622009,24.272009 0,16.407001 0,12.306994z M16.969971,0L19.846985,0.49800156 14.997009,28.637001 12.122009,28.140999z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/mov.svg b/theme/adaptable/pix_core/f/mov.svg new file mode 100644 index 0000000..f7af549 --- /dev/null +++ b/theme/adaptable/pix_core/f/mov.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.8133345246315,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.6080074,2C2.2780542,2,1.9999808,2.2279663,1.9999809,2.4970093L1.9999809,29.502014C1.9999808,29.565979 2.0049856,29.630981 2.0770063,29.757019 2.2189729,29.994019 2.6049554,30.080994 2.8799772,29.919983 6.4660018,27.979004 10.029932,26.046021 13.594961,24.111023 18.282903,21.567993 22.973776,19.021973 27.665746,16.487976 27.804785,16.377014 27.889744,16.297974 27.947727,16.210999 28.020725,16.088989 28.002781,15.952026 27.983738,15.883972 27.960788,15.802002 27.902805,15.687988 27.755712,15.612L18.92279,10.807983C13.593862,7.9099731,8.2720144,5.0169678,2.942965,2.1090088L2.9219689,2.098999C2.8160129,2.0369873,2.6979721,2,2.6080074,2z M2.6080074,0C3.0469677,0 3.498013,0.12402344 3.9090199,0.35797119 9.2348955,3.2639771 14.552837,6.1549683 19.878835,9.0510254L28.695766,13.846985C29.288778,14.153992 29.725786,14.690002 29.909744,15.346985 30.089796,15.994995 29.991773,16.698975 29.637773,17.278015 29.382772,17.669983 29.100792,17.904968 28.833704,18.11499L28.692714,18.20697C23.976818,20.753967 19.260799,23.312012 14.547832,25.869019 10.982926,27.804016 7.4189956,29.737 3.857995,31.664978 3.4670076,31.893005 3.0280471,32 2.5900631,32 1.7029866,32 0.82103718,31.555969 0.34801912,30.763 0.10998452,30.343994 2.3663961E-07,29.937012 0,29.502014L0,2.4970093C2.3663961E-07,1.1199951,1.1700329,0,2.6080074,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/mp3.svg b/theme/adaptable/pix_core/f/mp3.svg new file mode 100644 index 0000000..bff01fa --- /dev/null +++ b/theme/adaptable/pix_core/f/mp3.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M25.29,32.91c.58,0,.94.28.94.79s-.38.85-1,.85a1.7,1.7,0,0,1-.41,0V32.95A2.29,2.29,0,0,1,25.29,32.91Zm3.91-.52a2.41,2.41,0,0,0-1.28.34l.19.61a1.8,1.8,0,0,1,.91-.28c.49,0,.71.25.71.56s-.49.63-.89.63h-.38v.62h.39c.51,0,1,.23,1,.76,0,.35-.25.71-.9.71a2.2,2.2,0,0,1-1-.28l-.19.64A2.43,2.43,0,0,0,29,37c1.11,0,1.74-.6,1.74-1.35a1.13,1.13,0,0,0-1-1.11h0a1.09,1.09,0,0,0,.82-1C30.59,32.89,30.12,32.38,29.2,32.38ZM18.3,32.3,18,36.92h.79l.1-1.86c0-.63.06-1.37.08-2h0c.12.58.29,1.21.47,1.8l.62,2h.65l.67-2c.21-.58.4-1.21.55-1.77h0c0,.62,0,1.35.05,1.94l.09,1.89h.82l-.27-4.63h-1.1l-.64,1.85c-.18.54-.34,1.11-.46,1.61h0c-.12-.51-.26-1.06-.43-1.61l-.6-1.86Zm7,0a7.34,7.34,0,0,0-1.29.1v4.57h.84V35.18a2.54,2.54,0,0,0,.41,0,2,2,0,0,0,1.46-.49,1.4,1.4,0,0,0,.4-1.05,1.31,1.31,0,0,0-.45-1A2.09,2.09,0,0,0,25.26,32.26ZM21.92,21.69l.36,2.71a1.55,1.55,0,0,0,.85-1.6A1.3,1.3,0,0,0,21.92,21.69Zm-.54-2.16c-.94.75-2,1.69-2,3a2.1,2.1,0,0,0,2.26,2l.32-.05c.07,0,0-.1,0-.16l0-.2-.1-.8-.2-1.61a1.17,1.17,0,0,0-.86,1.39.91.91,0,0,0,.21.35l.15.14c.06,0,.15.08.2.13a.08.08,0,0,1,0,.14c-.06,0-.14,0-.19-.06a1.51,1.51,0,0,1-.37-.28,1.61,1.61,0,0,1-.33-1.71,1.9,1.9,0,0,1,.58-.75,1.67,1.67,0,0,1,.38-.22l.1,0c.06,0,0-.06,0-.11l0-.35Zm1-4.37-.13,0a1.17,1.17,0,0,0-.57.63A3.58,3.58,0,0,0,21.45,18a3.22,3.22,0,0,0,1.39-2.28C22.85,15.45,22.67,15.14,22.39,15.16Zm0-1.24c.12,0,.21.16.26.24a3.18,3.18,0,0,1,.25.62,4.94,4.94,0,0,1,.25,1.54,3.93,3.93,0,0,1-.55,1.84,4.7,4.7,0,0,1-.94,1.15c-.06.05,0,.2,0,.28l0,.35.11.8a1.67,1.67,0,0,1,1.52.67A2.11,2.11,0,0,1,23.67,23a2.15,2.15,0,0,1-1,1.39,2.88,2.88,0,0,1-.39.21l.08.64a9.14,9.14,0,0,1,.14,1.21,1.58,1.58,0,0,1-.66,1.39,1.48,1.48,0,0,1-1.57,0,1.27,1.27,0,0,1-.59-1.49.77.77,0,0,1,1.34-.22.83.83,0,0,1,0,1,.69.69,0,0,1-.68.21c0,.3.46.44.7.43a1.22,1.22,0,0,0,.91-.41,1.53,1.53,0,0,0,.29-1,7.32,7.32,0,0,0-.1-.82L22,24.74a2.55,2.55,0,0,1-2.9-1.58,3.17,3.17,0,0,1-.24-1.79,3.68,3.68,0,0,1,.8-1.63A12.21,12.21,0,0,1,21,18.4l.16-.15s.05,0,0-.1l0-.15q0-.31-.07-.63a6.32,6.32,0,0,1,0-1.11,4.54,4.54,0,0,1,.39-1.43,2.41,2.41,0,0,1,.39-.62A.85.85,0,0,1,22.38,13.92Zm-7.45-2.33V30.2H33.07v-13H27.32V11.59ZM12.85,9.38H28.5l6.65,6.51V38.63H12.85Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/mpeg.svg b/theme/adaptable/pix_core/f/mpeg.svg new file mode 100644 index 0000000..f7af549 --- /dev/null +++ b/theme/adaptable/pix_core/f/mpeg.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.8133345246315,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.6080074,2C2.2780542,2,1.9999808,2.2279663,1.9999809,2.4970093L1.9999809,29.502014C1.9999808,29.565979 2.0049856,29.630981 2.0770063,29.757019 2.2189729,29.994019 2.6049554,30.080994 2.8799772,29.919983 6.4660018,27.979004 10.029932,26.046021 13.594961,24.111023 18.282903,21.567993 22.973776,19.021973 27.665746,16.487976 27.804785,16.377014 27.889744,16.297974 27.947727,16.210999 28.020725,16.088989 28.002781,15.952026 27.983738,15.883972 27.960788,15.802002 27.902805,15.687988 27.755712,15.612L18.92279,10.807983C13.593862,7.9099731,8.2720144,5.0169678,2.942965,2.1090088L2.9219689,2.098999C2.8160129,2.0369873,2.6979721,2,2.6080074,2z M2.6080074,0C3.0469677,0 3.498013,0.12402344 3.9090199,0.35797119 9.2348955,3.2639771 14.552837,6.1549683 19.878835,9.0510254L28.695766,13.846985C29.288778,14.153992 29.725786,14.690002 29.909744,15.346985 30.089796,15.994995 29.991773,16.698975 29.637773,17.278015 29.382772,17.669983 29.100792,17.904968 28.833704,18.11499L28.692714,18.20697C23.976818,20.753967 19.260799,23.312012 14.547832,25.869019 10.982926,27.804016 7.4189956,29.737 3.857995,31.664978 3.4670076,31.893005 3.0280471,32 2.5900631,32 1.7029866,32 0.82103718,31.555969 0.34801912,30.763 0.10998452,30.343994 2.3663961E-07,29.937012 0,29.502014L0,2.4970093C2.3663961E-07,1.1199951,1.1700329,0,2.6080074,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/oth.svg b/theme/adaptable/pix_core/f/oth.svg new file mode 100644 index 0000000..32ab99c --- /dev/null +++ b/theme/adaptable/pix_core/f/oth.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#A2A2A2" /> + <path id="path1" transform="rotate(0,24,24) translate(14.8764382004738,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M3.3169894,18.909973L3.3169894,20.480957 14.341993,20.480957 14.341993,18.909973z M3.3169894,12.10199L3.3169894,13.672974 19.114001,13.672974 19.114001,12.10199z M16.406999,1.1129761L21.374013,6.3989868 17.453996,6.3989868C16.875993,6.3989868,16.406999,5.9299927,16.406999,5.3519897z M2.2689838,0L14.893996,0 14.893996,5.6429443C14.893996,6.8959961,15.910995,7.9119873,17.163987,7.9119873L22.457999,7.9119873 22.457999,29.730957C22.457999,30.983948,21.442006,32,20.189015,32L2.2689838,32C1.0159922,32,4.4543413E-08,30.983948,0,29.730957L0,2.2689819C4.4543413E-08,1.0159912,1.0159922,0,2.2689838,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/pdf.svg b/theme/adaptable/pix_core/f/pdf.svg new file mode 100644 index 0000000..31a7173 --- /dev/null +++ b/theme/adaptable/pix_core/f/pdf.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1,.cls-4{fill:#da251c;}.cls-2{fill:#fff;}.cls-3{fill:none;}.cls-4{isolation:isolate;font-size:6.75px;font-family:ArialRoundedMTBold, Arial Rounded MT Bold;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.11" cy="24" r="24"/><path id="path1" class="cls-2" d="M23.34,32.91c.58,0,.94.28.94.79s-.39.84-1,.84a1.34,1.34,0,0,1-.41,0V32.93ZM15,11.59v18.6H33.17v-13H27.43V11.59ZM13,9.38H28.61l6.66,6.51V38.63H13Z"/><rect class="cls-3" x="16.82" y="31.93" width="26.06" height="8.81"/><text class="cls-4" transform="translate(16.82 36.84)">PDF</text><path id="path1-2" data-name="path1" class="cls-2" d="M19.76,24.07a3.64,3.64,0,0,0-1.65,1.2c0-.06-.09.11-.09.11l.09-.11a.78.78,0,0,1-.06.17c-.15.38.3.38.3.38C19.61,25.61,19.76,24.07,19.76,24.07ZM27,22.78a3.34,3.34,0,0,0-.37,0,3.13,3.13,0,0,0,2.59.9c.51-.08-.26-.54-.26-.54A5,5,0,0,0,27,22.78Zm-4.58-3.17a11.48,11.48,0,0,1-1.09,3.26l3.36-.54A14.24,14.24,0,0,1,22.41,19.61ZM22.48,15h0c-.06,0-.09.06-.13.15a5.69,5.69,0,0,0,.26,2.47,4.17,4.17,0,0,0,.09-2.16S22.61,15,22.48,15Zm-.43-1c.36,0,.67.73.67.73a4.16,4.16,0,0,1,.06,3.77,9,9,0,0,0,3,3.84,9,9,0,0,1,3.34.41c1.11.62.73,1.22.73,1.22-1.16,1.39-4.57-.79-4.57-.79l-4.39.79a4.19,4.19,0,0,1-2.76,2.89c-1,.15-.88-1-.88-1A4.15,4.15,0,0,1,20,23.53c.79-.9,2.06-5.12,2.06-5.12-1.29-3.11-.37-4.16-.37-4.16C21.83,14,21.94,14,22.05,13.93Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/png.svg b/theme/adaptable/pix_core/f/png.svg new file mode 100644 index 0000000..9db2e0d --- /dev/null +++ b/theme/adaptable/pix_core/f/png.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.09" cy="24" r="24"/><path id="path1" class="cls-2" d="M19.6,32.91c.58,0,.94.28.94.79s-.38.85-1,.85a1.69,1.69,0,0,1-.41,0V33A2.28,2.28,0,0,1,19.6,32.91Zm2.69-.61v4.63h.78V35.28c0-.76,0-1.37,0-2h0a16,16,0,0,0,.83,1.6l1.21,2H26V32.3h-.78v1.61c0,.71,0,1.3.08,1.92h0a12.74,12.74,0,0,0-.79-1.55l-1.19-2Zm-2.72,0a7.32,7.32,0,0,0-1.29.1v4.57h.84V35.19a2.42,2.42,0,0,0,.41,0A2,2,0,0,0,21,34.72a1.4,1.4,0,0,0,.4-1.05,1.31,1.31,0,0,0-.45-1A2.09,2.09,0,0,0,19.57,32.27Zm9.87,0a2.35,2.35,0,0,0-2.57,2.4,2.3,2.3,0,0,0,.65,1.7,2.49,2.49,0,0,0,1.81.62,4.59,4.59,0,0,0,1.49-.26V34.38H29.18V35H30V36.2a1.63,1.63,0,0,1-.64.1,1.53,1.53,0,0,1-1.61-1.68A1.55,1.55,0,0,1,29.43,33a2.5,2.5,0,0,1,1.05.2l.19-.68A3.06,3.06,0,0,0,29.44,32.26Zm-.87-5.86h2.22v2.22H28.57Zm-4.44,0h2.22v2.22H24.13Zm-4.44-.08h2.22v2.22H19.69ZM17.47,24.1h2.22v2.22H17.47Zm11-2.22h2.22V24.1H28.57v2.22H26.35V24.1h2.14Zm-4.44,0h2.22V24.1H24.13v2.22H21.91V24.1h2.14Zm-4.44-.08h2.22V24H19.61Zm6.66-2.22h2.22V21.8H26.27Zm-4.44,0h2.22V21.8H21.83Zm-4.44,0h2.22V21.8H17.39Zm-2.37-8V30.2H33.16v-13H27.41V11.59ZM12.94,9.38H28.59l6.65,6.51V38.63H12.94Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/powerpoint.svg b/theme/adaptable/pix_core/f/powerpoint.svg new file mode 100644 index 0000000..d925fdd --- /dev/null +++ b/theme/adaptable/pix_core/f/powerpoint.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#d24625;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.18" cy="24" r="24"/><path id="path1" class="cls-2" d="M24.51,32.82c.58,0,.94.28.94.79s-.38.85-1,.85a1.7,1.7,0,0,1-.41,0V32.86A2.31,2.31,0,0,1,24.51,32.82Zm-4,0c.58,0,.94.28.94.79s-.38.85-1,.85a1.69,1.69,0,0,1-.41,0V32.86A2.29,2.29,0,0,1,20.5,32.82Zm6.27-.61v.71h1.32v3.92h.84V32.92h1.33v-.71Zm-2.29,0a7.32,7.32,0,0,0-1.29.1v4.57H24V35.09a2.43,2.43,0,0,0,.41,0,2,2,0,0,0,1.46-.49,1.4,1.4,0,0,0,.4-1.05,1.31,1.31,0,0,0-.45-1A2.09,2.09,0,0,0,24.48,32.17Zm-4,0a7.32,7.32,0,0,0-1.29.1v4.57H20V35.09a2.42,2.42,0,0,0,.41,0,2,2,0,0,0,1.46-.49,1.4,1.4,0,0,0,.4-1.05,1.31,1.31,0,0,0-.45-1A2.09,2.09,0,0,0,20.47,32.17Zm6.07-5.66h5.13v1.11H26.54Zm0-2.12h5.13V25.5H26.54Zm0-2.1h5.13V23.4H26.54Zm-5.77-1.86a4.12,4.12,0,0,1,1,.1l-.92,4,3.9-1.2a4.07,4.07,0,1,1-4-2.91Zm-5.65-8.85V30.2H33.25v-13H27.5V11.59ZM13,9.38H28.69l6.65,6.51V38.63H13Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/quicktime.svg b/theme/adaptable/pix_core/f/quicktime.svg new file mode 100644 index 0000000..f7af549 --- /dev/null +++ b/theme/adaptable/pix_core/f/quicktime.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.8133345246315,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.6080074,2C2.2780542,2,1.9999808,2.2279663,1.9999809,2.4970093L1.9999809,29.502014C1.9999808,29.565979 2.0049856,29.630981 2.0770063,29.757019 2.2189729,29.994019 2.6049554,30.080994 2.8799772,29.919983 6.4660018,27.979004 10.029932,26.046021 13.594961,24.111023 18.282903,21.567993 22.973776,19.021973 27.665746,16.487976 27.804785,16.377014 27.889744,16.297974 27.947727,16.210999 28.020725,16.088989 28.002781,15.952026 27.983738,15.883972 27.960788,15.802002 27.902805,15.687988 27.755712,15.612L18.92279,10.807983C13.593862,7.9099731,8.2720144,5.0169678,2.942965,2.1090088L2.9219689,2.098999C2.8160129,2.0369873,2.6979721,2,2.6080074,2z M2.6080074,0C3.0469677,0 3.498013,0.12402344 3.9090199,0.35797119 9.2348955,3.2639771 14.552837,6.1549683 19.878835,9.0510254L28.695766,13.846985C29.288778,14.153992 29.725786,14.690002 29.909744,15.346985 30.089796,15.994995 29.991773,16.698975 29.637773,17.278015 29.382772,17.669983 29.100792,17.904968 28.833704,18.11499L28.692714,18.20697C23.976818,20.753967 19.260799,23.312012 14.547832,25.869019 10.982926,27.804016 7.4189956,29.737 3.857995,31.664978 3.4670076,31.893005 3.0280471,32 2.5900631,32 1.7029866,32 0.82103718,31.555969 0.34801912,30.763 0.10998452,30.343994 2.3663961E-07,29.937012 0,29.502014L0,2.4970093C2.3663961E-07,1.1199951,1.1700329,0,2.6080074,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/sourcecode.svg b/theme/adaptable/pix_core/f/sourcecode.svg new file mode 100644 index 0000000..b9b99be --- /dev/null +++ b/theme/adaptable/pix_core/f/sourcecode.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,16.1683187484741) scale(0.8125,0.8125) " fill="#FFFFFF" d="M10.337,0.00099945068L10.337,4.1539962 3.573,9.5909917 3.573,9.6569917 10.34,15.093987 10.34,19.277985 0.00099945068,11.61699 0,11.615991 0,7.6279933z M21.659,0L32,7.6629932 32,7.6639936 32,11.651991 21.659,19.276984 21.659,15.129988 28.427,9.6899917 28.427,9.6229918 21.659,4.1869962z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/spreadsheet.svg b/theme/adaptable/pix_core/f/spreadsheet.svg new file mode 100644 index 0000000..3b243ef --- /dev/null +++ b/theme/adaptable/pix_core/f/spreadsheet.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#02723B" /> + <path id="path1" transform="rotate(0,24,24) translate(11,11.6187486614626) scale(0.812524650993786,0.812524650993786) " fill="#FFFFFF" d="M24.657059,21.64202L24.657059,24.793019 29.859102,24.793019 29.859102,21.64202z M24.657059,17.491021L24.657059,20.64202 29.859102,20.64202 29.859102,17.491021z M24.657059,13.340021L24.657059,16.491021 29.859102,16.491021 29.859102,13.340021z M13.054056,9.3130452L10.936008,9.421993 9.4030447,13.339044 7.9020019,9.7839925 5.8020201,9.7839925 8.0030155,14.915033 5.8040342,19.910025 7.6690302,20.058036 9.2030315,16.390984 10.803012,20.358023 10.804049,20.356986 13.272014,20.356986 10.53702,14.689996z M24.657059,9.1880452L24.657059,12.339045 29.859102,12.339045 29.859102,9.1880452z M24.657059,5.0370461L24.657059,8.1880452 29.859102,8.1880452 29.859102,5.0370461z M31.031412,3.4354717C31.182783,3.4359052,31.990089,3.497225,31.990089,4.7560403L31.990089,25.60204 31.989113,25.60204C31.989113,25.60204,32.17411,26.575001,30.946079,26.645009L19.072081,26.645009 19.072081,24.645009 23.606091,24.645009 23.606091,21.49401 19.072081,21.49401 19.072081,20.543021 23.606091,20.543021 23.606091,17.392022 19.072081,17.392022 19.072081,16.441033 23.606091,16.441033 23.606091,13.290033 19.072081,13.290033 19.072081,12.340022 23.606091,12.340022 23.606091,9.1890218 19.072081,9.1890218 19.072081,8.2390096 23.606091,8.2390096 23.606091,5.0870339 19.072081,5.0870339 19.072081,3.4360334 31.007114,3.4360334C31.007114,3.4360334,31.015755,3.4354267,31.031412,3.4354717z M18.405087,0L18.405087,30.476001 0,27.275014 0,3.0670151z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/svg.svg b/theme/adaptable/pix_core/f/svg.svg new file mode 100644 index 0000000..f66006b --- /dev/null +++ b/theme/adaptable/pix_core/f/svg.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,13.1990312933922) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.677002,5.2939989L2.677002,21.356965 4.8850098,21.356965 9.9119873,14.835001 14.226074,19.338962 20.602051,9.7720205 27.702026,21.355989 29.32605,21.355989 29.327026,21.356965 29.327026,5.2939989z M0,0L32,0 32,26.586999 0,26.586999z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/text.svg b/theme/adaptable/pix_core/f/text.svg new file mode 100644 index 0000000..1fe900a --- /dev/null +++ b/theme/adaptable/pix_core/f/text.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(13.591873884201,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M11.563998,15.950012L20.536015,15.950012 20.635014,18.541992 20.336003,18.541992C20.237004,18.044006 20.237004,17.744995 20.136998,17.544983 19.937993,17.245972 19.738011,17.046997 19.439,16.846985 19.140019,16.747986 18.841008,16.64801 18.342992,16.64801L16.748021,16.64801 16.748021,25.021973C16.748021,25.719971 16.84702,26.117981 16.946996,26.317993 17.146001,26.517029 17.445012,26.617004 17.944005,26.617004L18.342992,26.617004 18.342992,26.916016 13.657017,26.916016 13.657017,26.617004 14.056004,26.617004C14.55402,26.617004 14.854008,26.517029 15.053013,26.218018 15.153019,26.018982 15.252018,25.619995 15.252018,25.021973L15.252018,16.64801 13.955997,16.64801C13.458011,16.64801 13.058993,16.64801 12.860019,16.747986 12.561007,16.846985 12.360995,17.046997 12.16199,17.346008 11.963016,17.64502 11.863009,18.044006 11.763003,18.541992L11.463992,18.541992z M12.062015,2.6920166L12.062015,8.572998C12.062015,8.572998,11.963016,12.460999,7.6759967,12.161987L2.6919865,12.161987 2.6919865,28.510986C2.6919868,28.909973,2.990998,29.208984,3.3890085,29.208984L22.230992,29.208984C22.629003,29.208984,22.928014,28.909973,22.928014,28.510986L22.928014,3.3889771C22.928014,2.9910278,22.629003,2.6920166,22.230992,2.6920166z M10.866,0L22.230992,0C24.125007,0,25.620002,1.4949951,25.620002,3.3889771L25.620002,28.611023C25.620002,30.505005,24.125007,32,22.230992,32L3.3890085,32C1.4949956,31.900024,4.3499313E-08,30.405029,0,28.510986L0,11.463989 2.4920048,8.8720093 8.4729938,2.59198z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/unknown.svg b/theme/adaptable/pix_core/f/unknown.svg new file mode 100644 index 0000000..32ab99c --- /dev/null +++ b/theme/adaptable/pix_core/f/unknown.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#A2A2A2" /> + <path id="path1" transform="rotate(0,24,24) translate(14.8764382004738,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M3.3169894,18.909973L3.3169894,20.480957 14.341993,20.480957 14.341993,18.909973z M3.3169894,12.10199L3.3169894,13.672974 19.114001,13.672974 19.114001,12.10199z M16.406999,1.1129761L21.374013,6.3989868 17.453996,6.3989868C16.875993,6.3989868,16.406999,5.9299927,16.406999,5.3519897z M2.2689838,0L14.893996,0 14.893996,5.6429443C14.893996,6.8959961,15.910995,7.9119873,17.163987,7.9119873L22.457999,7.9119873 22.457999,29.730957C22.457999,30.983948,21.442006,32,20.189015,32L2.2689838,32C1.0159922,32,4.4543413E-08,30.983948,0,29.730957L0,2.2689819C4.4543413E-08,1.0159912,1.0159922,0,2.2689838,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/url.svg b/theme/adaptable/pix_core/f/url.svg new file mode 100644 index 0000000..598c144 --- /dev/null +++ b/theme/adaptable/pix_core/f/url.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11,11.0004098882913) scale(0.812474527263561,0.812474527263561) " fill="#FFFFFF" d="M10.519881,14.945064C11.300896,14.955074,12.079896,15.102078,12.819896,15.386075L10.878893,17.326085C9.6458764,17.201085,8.3688531,17.591071,7.4258485,18.533091L3.5977991,22.360102C1.9327704,24.026093 1.9317786,26.736124 3.5977991,28.402117 5.2638197,30.068108 7.9748359,30.068108 9.6398644,28.402117L13.467914,24.575106C14.410919,23.632109,14.800938,22.355097,14.675937,21.121083L16.615963,19.180096C17.523962,21.546103,17.034943,24.325104,15.126931,26.234108L11.298882,30.061119C8.7138591,32.64612 4.5238051,32.64612 1.9387672,30.062127 -0.64625573,27.477126 -0.64625573,23.287106 1.9387672,20.701098L5.7668166,16.875065C7.077837,15.564055,8.8018727,14.923061,10.519881,14.945064z M19.825993,10.418039L21.580012,12.17305 12.089907,21.665091 10.333873,19.911087z M25.383069,0C27.077088,3.0538786E-08 28.771109,0.64599859 30.063117,1.9390028 32.647133,4.5230274 32.647133,8.7120385 30.062109,11.298047L26.235068,15.125058C24.327055,17.033085,21.547022,17.52207,19.181973,16.614077L21.121998,14.674067C22.356022,14.799068,23.633046,14.409051,24.57605,13.466054L28.403094,9.6400513C30.068121,7.9740295 30.069129,5.2630205 28.403094,3.5980362 26.738064,1.9320143 24.027033,1.9320143 22.362034,3.5980362L18.533986,7.4240398C17.591958,8.3670363,17.201968,9.643042,17.32697,10.877056L15.385937,12.817066C14.478913,10.452035,14.968939,7.6730337,16.875945,5.7660436L20.703019,1.9390028C21.995026,0.64599859,23.689047,3.0538786E-08,25.383069,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/video.svg b/theme/adaptable/pix_core/f/video.svg new file mode 100644 index 0000000..f7af549 --- /dev/null +++ b/theme/adaptable/pix_core/f/video.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.8133345246315,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.6080074,2C2.2780542,2,1.9999808,2.2279663,1.9999809,2.4970093L1.9999809,29.502014C1.9999808,29.565979 2.0049856,29.630981 2.0770063,29.757019 2.2189729,29.994019 2.6049554,30.080994 2.8799772,29.919983 6.4660018,27.979004 10.029932,26.046021 13.594961,24.111023 18.282903,21.567993 22.973776,19.021973 27.665746,16.487976 27.804785,16.377014 27.889744,16.297974 27.947727,16.210999 28.020725,16.088989 28.002781,15.952026 27.983738,15.883972 27.960788,15.802002 27.902805,15.687988 27.755712,15.612L18.92279,10.807983C13.593862,7.9099731,8.2720144,5.0169678,2.942965,2.1090088L2.9219689,2.098999C2.8160129,2.0369873,2.6979721,2,2.6080074,2z M2.6080074,0C3.0469677,0 3.498013,0.12402344 3.9090199,0.35797119 9.2348955,3.2639771 14.552837,6.1549683 19.878835,9.0510254L28.695766,13.846985C29.288778,14.153992 29.725786,14.690002 29.909744,15.346985 30.089796,15.994995 29.991773,16.698975 29.637773,17.278015 29.382772,17.669983 29.100792,17.904968 28.833704,18.11499L28.692714,18.20697C23.976818,20.753967 19.260799,23.312012 14.547832,25.869019 10.982926,27.804016 7.4189956,29.737 3.857995,31.664978 3.4670076,31.893005 3.0280471,32 2.5900631,32 1.7029866,32 0.82103718,31.555969 0.34801912,30.763 0.10998452,30.343994 2.3663961E-07,29.937012 0,29.502014L0,2.4970093C2.3663961E-07,1.1199951,1.1700329,0,2.6080074,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/wav.svg b/theme/adaptable/pix_core/f/wav.svg new file mode 100644 index 0000000..2f5fda9 --- /dev/null +++ b/theme/adaptable/pix_core/f/wav.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.0000046491606,11) scale(0.812499709427461,0.812499709427461) " fill="#FFFFFF" d="M6.786273,19.58038C4.8552475,20.543352 3.6762314,22.99128 4.065237,25.541203 4.450243,28.056129 6.2492657,29.899075 8.3792934,29.997072z M25.376723,19.580251L23.78369,29.997051C25.913715,29.899031 27.712743,28.056083 28.097726,25.541122 28.486738,22.991189 27.307741,20.543243 25.376723,19.580251z M16,0C24.823,0 32,8.0030079 32,17.841018 32,20.866021 31.309999,23.855024 30.002,26.485027 29.986563,26.51584 29.969746,26.545511 29.951663,26.574005L29.914128,26.627069 29.864518,26.820212C29.014498,29.873736 26.508523,31.999999 23.606687,31.999999 23.216699,31.999999 22.821676,31.959991 22.434679,31.88199 21.912671,31.774998 21.564675,31.277999 21.64567,30.750026L23.579709,18.101309C23.620694,17.830288 23.771697,17.58832 23.995698,17.432317 24.164454,17.315315 24.364144,17.253999 24.566082,17.253016 24.633395,17.252689 24.700958,17.259065 24.767708,17.272317 26.979605,17.722308 28.738765,19.392976 29.597425,21.560945L29.604015,21.578708 29.650576,21.362866C29.881711,20.212188 30,19.030144 30,17.841018 30,9.1060085 23.72,2.000001 16,2.000001 8.2810001,2.000001 2,9.1060085 2,17.841018 2,19.122894 2.1366634,20.391947 2.4033756,21.622048L2.4572048,21.853096 2.5655499,21.561043C3.4242134,19.393089 5.1833782,17.722435 7.3952808,17.272449 7.4622822,17.259199 7.5299702,17.252762 7.5973301,17.253012 7.7994108,17.253762 7.998538,17.314697 8.1672907,17.432444 8.3912935,17.588439 8.5422955,17.830432 8.5832958,18.101424L10.517322,30.75005C10.598323,31.278034 10.250318,31.77502 9.7283115,31.882016 9.3413057,31.960014 8.9463005,32.000012 8.5562963,32.000012L8.5552959,32.000012C5.8544168,32.000012,3.4959736,30.156921,2.4975262,27.438403L2.4337578,27.253311 2.3802185,27.190395C2.3527966,27.153262 2.3276253,27.113778 2.3050003,27.072028 0.79699898,24.296025 0,21.103022 0,17.841018 0,8.0030079 7.1779995,0 16,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/wmv.svg b/theme/adaptable/pix_core/f/wmv.svg new file mode 100644 index 0000000..f7af549 --- /dev/null +++ b/theme/adaptable/pix_core/f/wmv.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path id="path1" transform="rotate(0,24,24) translate(11.8133345246315,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M2.6080074,2C2.2780542,2,1.9999808,2.2279663,1.9999809,2.4970093L1.9999809,29.502014C1.9999808,29.565979 2.0049856,29.630981 2.0770063,29.757019 2.2189729,29.994019 2.6049554,30.080994 2.8799772,29.919983 6.4660018,27.979004 10.029932,26.046021 13.594961,24.111023 18.282903,21.567993 22.973776,19.021973 27.665746,16.487976 27.804785,16.377014 27.889744,16.297974 27.947727,16.210999 28.020725,16.088989 28.002781,15.952026 27.983738,15.883972 27.960788,15.802002 27.902805,15.687988 27.755712,15.612L18.92279,10.807983C13.593862,7.9099731,8.2720144,5.0169678,2.942965,2.1090088L2.9219689,2.098999C2.8160129,2.0369873,2.6979721,2,2.6080074,2z M2.6080074,0C3.0469677,0 3.498013,0.12402344 3.9090199,0.35797119 9.2348955,3.2639771 14.552837,6.1549683 19.878835,9.0510254L28.695766,13.846985C29.288778,14.153992 29.725786,14.690002 29.909744,15.346985 30.089796,15.994995 29.991773,16.698975 29.637773,17.278015 29.382772,17.669983 29.100792,17.904968 28.833704,18.11499L28.692714,18.20697C23.976818,20.753967 19.260799,23.312012 14.547832,25.869019 10.982926,27.804016 7.4189956,29.737 3.857995,31.664978 3.4670076,31.893005 3.0280471,32 2.5900631,32 1.7029866,32 0.82103718,31.555969 0.34801912,30.763 0.10998452,30.343994 2.3663961E-07,29.937012 0,29.502014L0,2.4970093C2.3663961E-07,1.1199951,1.1700329,0,2.6080074,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/f/writer.svg b/theme/adaptable/pix_core/f/writer.svg new file mode 100644 index 0000000..32ab99c --- /dev/null +++ b/theme/adaptable/pix_core/f/writer.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#A2A2A2" /> + <path id="path1" transform="rotate(0,24,24) translate(14.8764382004738,11) scale(0.8125,0.8125) " fill="#FFFFFF" d="M3.3169894,18.909973L3.3169894,20.480957 14.341993,20.480957 14.341993,18.909973z M3.3169894,12.10199L3.3169894,13.672974 19.114001,13.672974 19.114001,12.10199z M16.406999,1.1129761L21.374013,6.3989868 17.453996,6.3989868C16.875993,6.3989868,16.406999,5.9299927,16.406999,5.3519897z M2.2689838,0L14.893996,0 14.893996,5.6429443C14.893996,6.8959961,15.910995,7.9119873,17.163987,7.9119873L22.457999,7.9119873 22.457999,29.730957C22.457999,30.983948,21.442006,32,20.189015,32L2.2689838,32C1.0159922,32,4.4543413E-08,30.983948,0,29.730957L0,2.2689819C4.4543413E-08,1.0159912,1.0159922,0,2.2689838,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_core/i/loading.gif b/theme/adaptable/pix_core/i/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..e15844396f9b8c4d915f5cf595e2a22014a24426 GIT binary patch literal 4176 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/i/loading_small.gif b/theme/adaptable/pix_core/i/loading_small.gif new file mode 100644 index 0000000000000000000000000000000000000000..078b55fafd40f60c392082f0ce8dcdfd552dd15c GIT binary patch literal 1456 zcmZ?wbhEHb6krfw_{PBS|Nnmm28Lh1eodM*siULg-Me=uPMo-L<Ho#s^B5Qy`uh5g z9Xn=ZWE2z>^ybZ*Wy_XLnKI?mr%z2yO)@evn>KA)vSi7uS+l-;`SRn(j}<FcoI7{U z#l?k(hbJK+;oiM_FJ8RZvuBT%mX?{BnVXy2l`B_{964fRW8>%NXJ=>U;o))N!i7_( zPDMpUZP~Kr)vH$p1qEx?tl6<+$F5zw;^N|BVq%h#lG@tZ9z1wZS6BDv&!0t$7D-4* z=;-K#g@v)Ouml7IoH=vG!ongVB0@?^>cfW**REZgGiT1DM~|L8d*<NauxiyR6%`c) z1%-9%))f^Md3kx6n3!Z`WnH>->A-;lYHDgVH8oC7P9Y&7TwGin92^@qY<TkIiI0!Z zw{PFJZQGWXmX?u`ap%sR1q&7k2?;4FDKStRDE{a6a}5c0b_{Se(lcOYWME)W{3qpH zl$uzQnxasiS(2fUn3Y(Olb@KPmzkHA&!G5|g_DDUfk6l45>O;EuxB(dG&L(Isi`)1 zGH?q?F(`6!YIHTR@o-AZP3P2X66|D|C?m$Ltggi%AvwQ^omW^^eu_jFp9t%eE+!8C zol}@6itb}j=~&1qcK9d*E0=)y;ft46?%UESD0!`O)tctJDk}Fbs&-ty*oe*YY>?&b zCZ^`ion6M9ocfklwvv|3+WJ!B`n?tc)|}P`%{rXCMtVYKtctyyrp>yVhOC{UHg+~f z&3Xo#yA13Vx(*#?P*pk7q@vn%>;%K9a~Cf)OB}h{DJXfpQ$kX(>;B2(O%IQ#p1ynn ztNmclGjZ@YcXTo^b8?C@s0wLy3O2Fyia9uPs<}1`N=ktJWMS9q*0HEl&dJ%ty{n5) z#AVACP>@Yf*~y^TtFiwePp{^|W^QHm$)|U&?Yi8w;A)eE<kXHk3@Qh%omSm<5xeKp zL7s1EZEI3hZEqHAR#9Pa;r0t^5tNi@<@e+aY@ONa;pHFP-70Uux~vrxNDQhIx<R3| zadW%=HjC9wdO~KaR=Wl8&u<g(=JYY>nbE4ajni~m-vm(~&Jd&S&L-c$d;8mPdmP#B zo>oCgi8cwz-j=EDv%BZbZ(`!$@0idH36y2+EGxwpfJ0&dx1Ve4+-B{th_F3Plc$10 zA=GZATgyzvl^X8hkxtR=JS#OB+@s{XL1_lN<rp4Ex4d0-!aS8OhDGz2F6b5u;BSjn zh;4Qg=2VnyaS7IuYL4MNs0j)WeoxMXW=UsG8zbX``tot{+%3L=MhyA~EmngP40(1d zD5-VLYXW70c`98z%QaVZf&8(gMcXfE8v~o8jDO;^7PI9tVoV(TP19#GB*sJ<^D!?M zZ93Z&9p%BmvRrI8C=Xz_8=Tr&TAP@XY`vOPRoj}}k~w`O8B|o71zTb`=lZuwNX}}K zOtDikYLjR+us7^)0r_%6i}76jZ7O~Gb1ioFfjqXYRnUm(c(baLcI>KF9Zuf$EejVn Y1;+=wOq<bktH1ZmvqlDMQ2qvE0Kgd6(*OVf literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/i/notifications-black.png b/theme/adaptable/pix_core/i/notifications-black.png new file mode 100644 index 0000000000000000000000000000000000000000..f143be29c926b9677c452789f7e2b90c41f57a95 GIT binary patch literal 268 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7*pj^6T^Rm@;DWu&Co?cG z9Q1T?46*2)JK37+kb{V;zm86mlhjf99Spih&%3?JJ?gr0uIAc1O>=j#m{0Ix61(`U z*GDMr)1y}I&z0ZL%x+}d|LoCz6DGMwiE|S;WEyyWbQni+n>H*K)v1aXm8&>(tk(L1 z27_7`-*P*LaK;xu7;IS&RJTrL+E9|arLpCeyU&tsR}S)Zh{(DG&A!a!A-Y_8`J|sO z<CJr1m^oMs)c$8i9T1xlnJxVw_3Zism&>mU7qHzsa5Pk8hgpN#hG~U53nuMLJ><sy UEh$Wqfq{X+)78&qol`;+051_|<NyEw literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/i/notifications-black.svg b/theme/adaptable/pix_core/i/notifications-black.svg new file mode 100644 index 0000000..0ab2fa7 --- /dev/null +++ b/theme/adaptable/pix_core/i/notifications-black.svg @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="16" + height="16" + viewBox="0 0 16 16" + id="svg4706" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="notifications-black.svg"> + <defs + id="defs4708" /> + <sodipodi:namedview + id="base" + pagecolor="#02ffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="31.678384" + inkscape:cx="4.3629016" + inkscape:cy="7.999192" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1366" + inkscape:window-height="706" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + borderlayer="false"> + <inkscape:grid + type="xygrid" + id="grid5275" /> + </sodipodi:namedview> + <metadata + id="metadata4711"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1036.3622)"> + <path + style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 26.577475,1026.4201 c -0.045,-1.9643 2.9286,-1.5089 2.9911,-3.5268 0,0 0.625,-3.4821 0.8929,-5.491 0.045,-1.6072 1.9196,-2.0982 1.9196,-2.0982 0,0 0.044,-0.8929 1.1607,-0.8929 0.9821,0.1339 1.1161,0.9375 1.1161,0.9375 0,0 1.9196,0.5357 2.0089,1.9643 0.2672,1.8303 0.7141,5.4911 0.7141,5.4911 0.1252,1.9732 3.0802,1.6964 3.0356,3.616 l -5.8929,0 c 0,0 -0.2232,0.9375 -1.0714,0.9375 -1.1161,-0.045 -1.0268,-0.9375 -1.0268,-0.9375 z" + id="path4169" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccc" /> + <path + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 0.35050582,1050.3632 c -0.045,-1.9643 2.64949418,-2.001 3.27520528,-4.0319 0,0 0.5618655,-3.6715 0.8297655,-5.6804 0.045,-1.6072 2.5509452,-2.2245 2.5509452,-2.2245 0,0 -0.1138363,-0.7666 1.0028637,-0.7666 1.1169836,-0.013 1.1161,0.7481 1.1161,0.7481 0,0 2.4214565,0.7308 2.5771115,2.1537 0.201144,1.8387 0.840369,5.7752 0.840369,5.7752 0.457134,2.0254 3.206469,2.0436 3.161869,3.9632 l -5.482526,0.063 c 0,0 -0.7598437,1.8213 -2.239389,1.8213 -1.6527435,-0.045 -2.1316543,-1.7582 -2.1316543,-1.7582 z" + id="path4169-0" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccscccccc" /> + </g> +</svg> diff --git a/theme/adaptable/pix_core/i/notifications.png b/theme/adaptable/pix_core/i/notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..53a19fcfba44bd57bbe3010bbad6f75710b2732a GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7*pj^6T^Rm@;DWu&Co?cG zobhyV46*2)I(aW2qoYXcc?}V#O}rncS1@?peCqlo>SMqQjnFv9cJ~iVS~pX=|C($z zOeyb~>|<#Dyyp8oPq$AT9gR1he{5|(aCY87`-5y7Sc~qR%3UkSbR%%>+4TkYh4ro! z8*kM=W|45Zlm7xI!<O2HJca|i57{snXs>0`eIWRugMUG&*}<qF5d+7Rz4CX36?k97 zX|M{37yMO@d7<QajLB`%9skQywoEc9&GPp2e_m&H?2fiL>$d}fCmMpMp08ji_2jO$ fl}O%Xbf5cOhrXGx%~T-<1_lOCS3j3^P6<r_I%#d$ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/i/notifications.svg b/theme/adaptable/pix_core/i/notifications.svg new file mode 100644 index 0000000..e71e2e6 --- /dev/null +++ b/theme/adaptable/pix_core/i/notifications.svg @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="16" + height="16" + viewBox="0 0 16 16" + id="svg4706" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="notifications.svg"> + <defs + id="defs4708" /> + <sodipodi:namedview + id="base" + pagecolor="#02ffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="31.678384" + inkscape:cx="9.0190735" + inkscape:cy="7.999192" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1366" + inkscape:window-height="706" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + borderlayer="false"> + <inkscape:grid + type="xygrid" + id="grid5275" /> + </sodipodi:namedview> + <metadata + id="metadata4711"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1036.3622)"> + <path + style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 26.577475,1026.4201 c -0.045,-1.9643 2.9286,-1.5089 2.9911,-3.5268 0,0 0.625,-3.4821 0.8929,-5.491 0.045,-1.6072 1.9196,-2.0982 1.9196,-2.0982 0,0 0.044,-0.8929 1.1607,-0.8929 0.9821,0.1339 1.1161,0.9375 1.1161,0.9375 0,0 1.9196,0.5357 2.0089,1.9643 0.2672,1.8303 0.7141,5.4911 0.7141,5.4911 0.1252,1.9732 3.0802,1.6964 3.0356,3.616 l -5.8929,0 c 0,0 -0.2232,0.9375 -1.0714,0.9375 -1.1161,-0.045 -1.0268,-0.9375 -1.0268,-0.9375 z" + id="path4169" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccccc" /> + <path + style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 0.35050582,1050.3632 c -0.045,-1.9643 2.64949418,-2.001 3.27520528,-4.0319 0,0 0.5618655,-3.6715 0.8297655,-5.6804 0.045,-1.6072 2.5509452,-2.2245 2.5509452,-2.2245 0,0 -0.1138363,-0.7666 1.0028637,-0.7666 1.1169836,-0.013 1.1161,0.7481 1.1161,0.7481 0,0 2.4214565,0.7308 2.5771115,2.1537 0.201144,1.8387 0.840369,5.7752 0.840369,5.7752 0.457134,2.0254 3.206469,2.0436 3.161869,3.9632 l -5.482526,0.063 c 0,0 -0.7598437,1.8213 -2.239389,1.8213 -1.6527435,-0.045 -2.1316543,-1.7582 -2.1316543,-1.7582 z" + id="path4169-0" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccscccccc" /> + </g> +</svg> diff --git a/theme/adaptable/pix_core/s/angry.png b/theme/adaptable/pix_core/s/angry.png new file mode 100644 index 0000000000000000000000000000000000000000..eb0d7fabb956717f5bddcb83e615735f761ae6af GIT binary patch literal 813 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGr2>3HTp1V`{`>hov9S2> z>-)^g>c5}we?PxhHa7o#eP7wy{`UoO|NHs9b8z_Y=l9>+8zS=3*7lXH?GHD%_l}Nl z9UR^{I)3o>{$OYKFCz50jm`hSfd9U}QetWk&CLG)|F0mW_1D|`m$&z47ngSq4o@vD z|E9$K_w#%G`tAQ0Fa94m@MA{XpMvz)9-iOa+^##jopx|JXy^3W&hCYcjlQ;}vXtik z*qHwj;a|Ug`~UUp|LN2J_x5%-bR2DH`8m1ie`DkStgOVi%;12Se<49n+&$J<+8Z0# zO)|1d)we9rHqF&CiB&gpQ`FN|(EESk!vFK<uWsLUW!ui>OV=!0y82gZ`LE`(UkxR{ zs|$XY=f>K3#@Ko0ncC->*xfZUd&<DTAX^gT7tFx?`}4cEH&5@|(cNB|8EVV*^xM&$ z8++OW1)e`zd;QNna|Q;+rJgR1Ar_~nPEIX9WFXS!UMO9>-*eZcM?CEhjbfhd`}=?Y zMsG2VpmXQu>7PCM+bq0j^H+W!yXlA1%x=r*%|GdI?_&frqd<hM_3B^WZbrZT%l7O| zp8ouIeXjF&u6b;x`$Am*VuV1L#Rb`S*E1NdS<X4$FZrsVROi$EwNDRD*c+Vp;RsVg z&;66eTf`EWr(ZJhmsj|h;uJY$ii=U7{Ml|calKXZg(v;aYhLhos`BK1Er(luZ0rt= zv8PqEp9dZAeELLZ;`&I&5N@d)hDtFdk1#)mNe+^n8*NqyNuAjto~GLr@~h#CHcRG4 zgSG{S0+JRkyHlIOzk~1I=H=_|@N5)vX!|6vLo#uWl|t#UPL_>ao$8iLYr=Wlr4D4e z96Ea2=oQ!24@TyXmX}@BkacC8%k%Y9*z2`#dpIS__0qR{ZoJx&zK)+GT|q{wwClB4 zo1EmbM<1_D_>$1&wN%*9;E;hAle*=vvlHabx}V#?v}-;8+k>;#1y${{xYizi`tpj0 z|DD8T-v3O$s`-y+>7<VJ#jmDp*|KVxv6So0%A_w9d&|0879D+e)&Ku&`Q+HRs^$jo RMGOoK44$rjF6*2UngGTofSUjS literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/approve.png b/theme/adaptable/pix_core/s/approve.png new file mode 100644 index 0000000000000000000000000000000000000000..7e666a685cdae76bf77d76d4d118470ef0acce24 GIT binary patch literal 774 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tG{s;JkxPs_wA-{HV{5|dQ z_oVyZQyza$x&PiP_V2X&ue}n#_RE1p4#@vG>GA)h`~O3_|IhgT+AH>Z5C8Ao{Qu4e z{XOOOd!P89V`hI2DF2^n{A-WUzq7u7Pq}~WZ86ZX{<(wk+tS=$yLj(!c3s=!^n1T- zQm|c|zpaVB^`FC9zxIlL>~H=wso~#^+RsyJzs#ukzBc90aj!3j-M=4od$G&y(KgpZ zbDXzLa$eZv=wxa0=cwWT7rXvl$@zCF^~d_8KPT+}Ki%-}PWRulK{waC-dN{)dZ|l8 zmP1pveYCG#q_=IThi$OCt+Ta_xuMPP14_Ska{Pb3<==zZ|L)KJcYn&?3o+kU1u1v) z`7kgr$dm;61v4=J?)?1r=IMPqIx90nZ3Vc#y?guY$<dt~Ss$%^rPZ^afq`+Br;B5V z#p$V&uU8#1;A!<f)aJx4l5thS!-J#xedvOm7d8LWS1x^~+){Y3exiou@o71y&BGJ8 zJMxy_eOJ=Wcw6%4x#urR4<2~9+xl9*^Wujuc)zveuif0e^PKugwMB0o>VLj^@?b{C zOM(5?u@<Vk&d>k-;-KWMxW)f2O!yM~t~uqy(~N&n%A9N`-Fok3@`t|oagaZ9@)9)x zU*Ah9JZwzYBpFh)6iazti})G({3zs13D|n2I&1Txf~^l1E=yXmd&j}vz*4(ocd{ng z2SuE&h<ob)bfWZ}f|+X%@Rl_s*-7h1E)BAot>1L$#D)5&N7lAk{CoYnCrIRja<Xa4 zhj>NBfQi$2-!M+Ev^=VRIF+qONjf!H&wPc)+@G1N8v~-xatTaL^Im#6Ww(T^n84L( zTjrhoy2~cod#zTk$>%@v<q{pYoQxtaBx?C^NbxcsyYfls=~ADSmwGC{>!yd9&kR_* z#D1o)bYrr}v3dNrt!`~AOK$3Szk8&5zMRA>g|n%QpQAadT3XiSh|XP|^PF?u?mcza l7aurox8u)7?RS6VcjQHH>lQpXkAZ=K!PC{xWt~$(69D7~iBkXo literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/biggrin.png b/theme/adaptable/pix_core/s/biggrin.png new file mode 100644 index 0000000000000000000000000000000000000000..bdebf3a46aa5d7bdfe11bb08020c210df5bf216f GIT binary patch literal 1178 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGe+2l1xH2#>{MyO-|4!2X zBR0Qwa{fK(9)9N2-&5}YPP_j-<?(y3=-*Qwf6sV;$OH0!PI~-5>Hhza-v2YczxIk{ zow@gW5C8Ao{E=tB{N5+==YaCheG0MXzek?^{Ar`izYF21mwx;^>-YDR+o!Gi&kmTB zoj&_}pZK3+W<U2xv|oM_b@ubWv%b;iKEFDX^lQIt!ikSj=f3|v<z9O2zk!bRzkmOJ zZf6KT^Xc6_x57Ak4?7zhQyVh_YgtLnU%PmAzWV=rpKRxy{}tE&<z4xkcIoGfU9Nj) zIL~cx>@9JuNp}eKunlyz)lxH6RWy_mQ(OD|f9=ix4L5!#TmU)m|BGG!uH^i?l=}CS z>z@<$e~uddIjq%t_iyo~AImQvSaRvW|EC)!>{~Hq(yV{?CU$pDJbk{qsiAZ2)`rTm zhJQEf|J|s%b+#-wtMtLyyzr2O3-J+WW5f1FhW<Spv?a)YhQEKGhnKyz=chw%4>r4A zU+WrS>AZQO)5=b#$rX;?rjDthcCr4puC_K;7h4<YTK_(v^lPu!uf3w1Ui_c;=>Nof z|GTgMsK5HD_xi`G3-2f0eBFBWRnGYrGjBZTyK*;n%eC~)*UFBbt~zq$|MM;06V`@y zEdBRj_P_hH|J`msf3E5B*@o+9tNl}Qf?|{YUWoa8I`s4DkYoFU{+{yqzR&jK7PAlQ zHNS4;&a5<XU|?V{C<*clX86a%`t$SqoBQ6L>29yg47C;D`u64B+kHDWw&{L&_x#z@ zCpXTYJF;`frnPIAP3=*Y_flnk#lXO%=;`7ZVsU!v<j~|t4kE3b?^<`Q+5KLH+ohl@ zxNFOnD|b_0zl$v|zrKHGvGZ-!DRy6~txo<-duMZ|gRk+(f<Ublqt>A02~SpXm#+F{ zwWZlBe8z2Ynb$eh^}=rpH#r1!Z~c36dqDa^(e$s=@2RraEs{C+*L=%dPl@@?tCI5T z{;#f@(thmJ+|Tnfn(g>q-Qq6rURn5O$?1jDSL|EFoT2bw4x8Qii`fg8N**!ZEE2W& zxx&pa5f>aj6^W>2H0E$RJ=<fEw{qixBMWEo{t~ZcoW|Ce#=Ot9e!^O}6M<c<tQ_|a z=0)sgIK=ekvh9nbhn9(O8~ypY_vp<omMuG?_CJ*E=rQ<X@Tx{jWZoS$dEUjjP4XW% zo|IFS|MBvwz~Lg9(=B0{|7&-5m(*@J$nkXfjK5*JH*IW<u3W!-_2R{=m(5L#YOlXj zIr&UpHDX86`O@XnR8C4W&H1MG=l#b_ofw76Bqv3&R@ZsP5f<|V=LOE#U@Ek+P0?1J zv0sMU{f@xh^$)jiRg+n_(W&Y%H>bcUA-V0hlmE>TRGQfHf%Vspojly3MaIi&=Pq3F zbY<tpjh!n~yOOTme*Cc|GEVhS%<hOki@uw0VljKBe%#&txcYN*rfFB^-<y7P`ug{F zGlTaFq+Q#7s6b}(quH*S?JwV7T=QIcU53a#x7jC4!gNk8+oW-KLv{b99ct^kiVUU+ zZ__=#ELnQH+M_*zTjV#!#cHW?WOhdBTOOJHpFRHU#+&CgG%hkQFfe$!`njxgN@xNA DJ%)J& literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/blackeye.png b/theme/adaptable/pix_core/s/blackeye.png new file mode 100644 index 0000000000000000000000000000000000000000..c2714165f87fdf8e0b3ab15e5fb882c97cbafeb9 GIT binary patch literal 929 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tG^#goDTp1V`{y&NTwUh1t zM(bZYIscw?|GkI*?<x1+d&U2qcK@|k;_oSszh^xDp7Qu}(&PV0_y31<|DW;wd&>RS zUa`NY+<xyB{d3Ih&jICM`(=ObQ}}y8{_lQ;zo)$aT?qeo*6-IIp?_yViVbwEf9_!T zbJG3CG53!L-AweY?aZuy@0a_vi|6Kg*L|~GHuO17t8z3qvbna#_2@j8gR`9rV(n8y z?IOHwoh)r$?{#~z%XQmi=ZWQx)oJ#z{&s%OHZC?c|NsAAwq(`68?{fjyY8Omyu8Ed z&k6fKM-BfR*8a6u^#6-p|E}bIJ>s^g*{Q3@p)uROEXm%(&gR31kN=-;`1fG;-?KsI zmbtX$Ib=uKhq&ARKBW2o`Idk8r~JFq_wRQ5uM6pq_a^?m5OZ~7<b~y~r<b~T*xQ_5 zk#c~6fkCk($S;_I=`YLA&+l%Y*tes*y)x8Rfa}e-CueqU>}ku&c>Db6qqUu0emV>c zObnhbjv*GOr%t{e_Siwd<+^3^WbqqUY)@@dVLy2@?)jVs)m*>-|9N|kC3DwQJhby% z?seDJbglXN1Jiq4of;i9guH*nP53VSUQPIcjLNt2uY14WUtd?#sOw&RKE;2J?Y^{4 z-giE`{4cT1JFKU^PWfN1jo{`~MhmCqk8S4X{ocO+pO!+B^5G|$TRT>SxJu0V_|rb- zuc7GT)wf?*T>8H0>gDMb_Ztt37^~c}m+lR^wW6ccmF36%J&b`3bCNg@Yn*=mV0+)r zH9E=bo6H#^SJtS{4&B&Pb^rC5nJhb|9$gi)E&IO#JHw*fd)Er|)^II|4RGdJu#L%i z-ZXEvX)6?;_iPF|u<G!O8B$6NY+m&jS4n&l(lDMP_I0YRi1-4wm5MI6lSHf3l6;xM z7S7JT5h=TS!LOBy3=G9@I4|dP9uEoMr4Zud%`ml~xuTudS^w}XKUq;$-B<F0*P4`4 za&9qNEZR3Cea;3`d+DOyDbo_#?Kn3j?^?orHcgRNTFzqz>sHm|ZF>6AX>t})mg#}a z?poXIf}>pTWjpM#D~mjEQdxb!ra)}`cR}+r6O5DBYkgey<ItXnM2{1<-=4}%b8ric zdYxFF>HkABWyy)ku=zDLyRI#7PPgPS`DJ_EGQIuqKeherTGf^H(?hPT?hOf*&1^Xp sX!f6PUiae#^9AY+YTi{Jwg1m?HnG^ac-`r53=9kmp00i_>zopr0JH(dJOBUy literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/blush.png b/theme/adaptable/pix_core/s/blush.png new file mode 100644 index 0000000000000000000000000000000000000000..130c795fd482edb2642ac1f210a943875ea993df GIT binary patch literal 866 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGEdqQ(Tp1V`{-5^ywUh1F zPR_q4-GA@l|9i^)_g=Aor`><;mH2zg<L?;{5P3lU&q<H}C*A)a(*1wN_wOn9f2Tcu z?G^jIPyEj@vp)xvf9;q3xliHmDer$5!hh`%`gazj=KufyAA4I2bgX~wVEB7B@ZZ(k zzZata|M~L&=j;DJUi{j{^Y_`Be>dyq)N1_RC;Ri~um69)|NnaT|Cby8KA!*ga`XDN zo0l(L{qO1GnbYS@nlK|btMu<l_stWX{+zIXyv^><VXY<2TEF&+{(rIS--DUoR%HI% z68CFo<e#I4zYeJU`}yPVhxfm(UH<oG@7MJ!KhK&}UD5PqZppXRIf-$ZKh`Jz+8^}m zgxkjhPVe?PJ>ThYYn}DaZ5C%1o3HKG{r_y!>x<L=-Rb*xyZzUdvabvMzZ`Zsy3qOi zD$7H2Or}+;eqS$<#Kt&<fq_A;B*-tAf%)&x&+l%Y-nXN>y)rY@R)Fix({D$2ZtQ7$ z`~1;b#_RJr85kIMd%8G=Se%|Z`FYVH1A!Lj_l;lb(pmT<-hA<Tp)%o~v+w%%|Jf{4 zc-?r6XV%!;+`B!sWZIL7^$Fa+_vS6X{Pt^muJfCX4{B`qnm#_bvA}-Uhd)1rn&pHS ze|pncvQmb*vtjM|c&+J|3{GsB$}z?JW6bA+pY9cZ<vHM!|F`6o^L1-p?Frg7;<vtE zwY>QM$mDw`Qym`di`2NHu52SvbM^MdxxGh@^<@Ni=<j{@ShjC9|3{TF&riKCI^C|_ zdu#0DER->+<D?UJ-~s*#N;^w;x}Kk+(<R4M!FWw@`{Ii)%pUF1;Sp!O!?3#JcgSt` zn5nL39U0XRNIlZKS|)fsCDFimL+6f7<(s!R?a=I2GM*kV{bEYw)YfMPv)9a#pRHP# zlxbX86L-=`{Qnti*PRx&2cOL3lKHCBslHsi>r#^3zQA^`>DezH&zf!**^&QMHoQGS z@z|dWVZx3pChVBhXI$*M)cl$7abEZB?hZ08mh4}=mWpk5KhnzBC(~e)ym_xSyV0Tf zoAlnA@FW>*yHUS3ebuDcs~LMfuoh4Lxu#%q<)*h=_W2x1zux6`RwJt?CqCuIdFf9( lWO^)P=1nmDeg6;BREw<OhO1|{GcYhPc)I$ztaD0e0st*D*1-S( literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/clown.png b/theme/adaptable/pix_core/s/clown.png new file mode 100644 index 0000000000000000000000000000000000000000..2adf4c91a926dc5e57087b44d8e3cd6e47142ba0 GIT binary patch literal 1472 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 GLK6VAj~9vn literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/cool.png b/theme/adaptable/pix_core/s/cool.png new file mode 100644 index 0000000000000000000000000000000000000000..94dfa78d7019c71bd2347cc86cf4721981737703 GIT binary patch literal 892 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGtpj{QTp1V`{$C6EwS(=~ zPR_q4-GA@l|9i^)_g?XTr``Xa^7wnk<L?3aKPNr@pLG9!NcaC4-@m6ke(jY2lVbnQ z2mL+e_It1BpJQf!4k-WHFZ*+!!vC4ZzxD{ts?g}jcldYK_wOlp10Cz1I~e@p(miag z|DJSnwzBpLPqec2GBtHx*yL!eZ}aD{)~{XM|E^@Y2gf<M2me0l{_UvSt37T%k2v*} zI%Y@Me%WR1W@Wjp!(>H`(VwG+zxK)e+AI41#V+rtWIJd7-^X1G;+&SYIsQ3eAMR-z z?_+snlF9$48~)v!`0qxIi*Ll=vq7H^xqmw7_VtkSo*B+Nr#a8Ab38W7dhY})TT?4v z2g^wX7IjJH#Sx|_rkJd2H2!@+>Gyv5U%LhVKi~52!R&wcXaBq1{_keP@AC<{kyh)P zOxHG<9_dqg=dhm_6s9FXe!&dPe}8^{ck{%)?)J=3TLG@OJ1W0Ddva#y#-29TM_1Rc zoj&=)bOr{-3!W~HAr_~nPQLEsWXRK&s_%GU?`5v`LJN))0xVPB-mA{9{ja`(b1E~J zcuo1s|K^^zOHw#^^4jlanMJ?l+rF^h=GpgSj}<=tI{xp~Vqxps>wjNJEZSne`}5jA zKAZY3Us$TYe@gj7{mZ)!?+orcY~N%&_t$R!75|fy{l3|6{99Ks!K*C$<duVotjBCq zB35bfaHcHI)>!AdnXyenzWd5U*5uBs0UBGR60^R2F<IUA-Q~mt?l+h31}STwkkh++ zXM--!A&C|D^%4tMW+%pYJ-C!=RXcM5L*}$)EeH9+l@6!PP`u7?UFoox^13du#0`fe zZ;O2GEN=MBF{^WP^<;w?4?O~(o~n{k*n3XpRN%zPjQeJ&@HhR*|H7&2r@ElXeK*rj zh1fznhnw%T7R+4X>9{Ub<Ib}HclUSO0y3U5ypow+a=;<!PMbI@bLA7gJz4q!y%}68 zD>ItoCQCneXQ@-j@|oBl8XUpPBK3Xi{A!^N9=Rnq4=AS?)ID(PmyyUQZj##NcT4uu z&2uOI_MI$T7IjueeQDn1_A)CD+l0D(7i_N?OYgnBqVw;H*JpZ<zY1D=T+Y(`)GsT! z`GMM^@4Hodxo<vy`MokWSWNi2TK8E~`y=!JGqyj@GnT*M{)>Tufx*+&&t;ucLK6T8 C=e9Qh literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/dead.png b/theme/adaptable/pix_core/s/dead.png new file mode 100644 index 0000000000000000000000000000000000000000..1a73a088a2de60a15f27f286ad85a54726cda8b8 GIT binary patch literal 725 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGz6JP%xH2#>{J$RbYbX29 zoy@;>a{fK({(BGq-&5|t_lo~J?fz@8_}^0=f6sV;$b<5KPI~-5>Hhz)?*B8se^0sp z+9&q+l-uvUqJNH={W+lgYrpLOnZ|!ldH=f*{_m{cuRTKl&iej7pzw2_!rxQw20GS1 zw=?`Xto3`p{I6ZyUk<zF$J+h7QTuw2+uR1nwtR;_C+z<mHT<<t^4A`r|1Wm^yOQ(o zQtG9ZF30D)?4RjuZ*F61VEzB;hJSau|DFwcwAFR%B&VfqPEFYkDIs>r!FI9!w*Q}R z`S)P<zx%WQ-JkOJLd>o8t~b`XuIzN0UhO!c%%Q8up(@p(D$PDK($1yZ_bvkigHlP5 zUoZpn-+w<pzq@&2-_Gv#%FIw(L9RF7o}Agav4{2T^QVv2wzXx4{w!r+V9fD!aSX9I zJ$3TUswM-0R&6CMt*+{~e2giA7k5Rk_5Qy1{eL6g$x|}y_I%j?v1Z=kGc7#o?h`bg zZh9G_HTUdPA;Yqr1!g<0YD{&S`Mvx3=bL~0x>tyPU;TXUKjj-eHA%<!SG}^9@^4oC zbiOv@{KcMM*8I%6ZSQ#J{!enNowtgsvFY&j{M}EkPe?p;iu?I}|F5OiBKri+MJ}q} zovS5g^^AGR(gzNGAzQz$bZ1&@@PXH8#bbwQQ?44WSFt`=#Cd5-fdcO)VYXvZAJX#g zcN)3Mc(|6TuRA8FqR60Q{w?6~OVx!(6LkdJY&!Z?ES@>?Oj)SkwedsP{^;GtD#{A1 zai*J(OV8cVcH6~?TTlB__>UE3$3O3@b2Ctybh7mNivN~JmrAM$h3qo;&wJ-D=f%t| zMH|%}B)4thO>_Br@#(uL^WDF1XFPm<uz$xoDZA3z#qARuejA5GZeY61Qzdq2+nQsw od%R+WCVK{+(pam!@2CAVhUB??_sg;v7#J8lUHx3vIVCg!0Ftq8C;$Ke literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/evil.png b/theme/adaptable/pix_core/s/evil.png new file mode 100644 index 0000000000000000000000000000000000000000..984254ced37d473a8c7012af76e8c24359164c2c GIT binary patch literal 969 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGlLCA~Tp1V`{^z(qv9Ne; zXZPRN_nDQ|e?Q;<etxfPZ2tTD{`d8L=iu<)&kuyWy+63PfCT;g{vLPxd(8c%t?l1a z?yqcZ-#I$Ib8z@`(*4gVw?C)d|DSaKf6D#;Sq~87gzMiE?*Ae}pWE2{J?;MYl=}}i zH;^H|zJE`-ONpsHG&B4E|G$j5`v0VaPoCZnT;1O}I6Sqq{2%A@Z;i+QnKS=*d;jqE ze&yl$*~R6xo!tu?8v`BdW&_K7EfZCFJp~yZOIhvzKY#w+=>5&jZKjF!L}RP(-@kwU z{N>fF*AE{){(tM%|LfQPFIx0}?%ZipX8-T&t155&Ur}))Bj;~cvcFG6rgzwZfDk{A zkbfaT9<G6peEt7N`}_~~`R(QPcbms~C)ZCdE@_&^DH_HhDh7s%`nvLZKW4Q3Z*KlM zx#@pX)32FTzgo+GR~P&)&;2tc=3jKg-_^nYYQ6vN_x!up<M$kQa}Con1_lO=k|4ie z2A1F3Kfil>^Yp&%&dN|*0j{UNzkj=WG%F(|I>^>sSw`sJ^GDaN>}<c4;P8imf$^QE zi(`nz>E21t%Vs+Ww2EKZedkf!C2_G0Dpz)=dcNCxZ13{#PyQM+KW^~rEB^C0A^myW zjGK`+l4s1=B&I3H$GYd9gNDp>2_sX-wvE$`dsG+KB~KUrmoaJM%!P(VJzCC}H>S_O zUtf9n|0VI+bqjkYCB_7@?c4XaWz*q)MpsGo!%LXX%-`{Jvd%NLlL}v>`L-v@8UGcM zOu8@n<jO_=wMX7}tX2PQ_pc>)+U^5f`_CL?abC2$ZDws%-Y(u}J13b|o=h%%@c)>+ zf_}1i;Xi|!f(9*yT1&e21{sDnq+4>d39zuY`+nVKcDnQCt+lsoY#!LWWp-vT+S$4+ z?&^EJoWGCw%+9vTNTe84C~n=hgz4%r!&k?9wcbs;wxC7fF#B7M;^^NQH|`($;Zc3O z(lvl{-=;mw&CRyR_LOhwZQHedPt)=VD?RhpWOYYrAJ;8fdu`FStb#hVni8Lc>e|Z> zS~l+7*ZX=Y$G`djQNx_xTXxk4bwvx*av2x1E&6t6y_Kfll3n4IIe`@~cD;Qw$ErN` zby@axfd%HePgwpMm^|3EF6u?8PJocop^od{&9AJyf9Hy|Yu;=B8@89U?{U|~+f1L) zy!@{6cH<9rulOb&44QXk^XV=DqpRf}6;FID=jw$UF{Su7{p~gp`f^g?SJPpkYMXk7 Z9@kcxIeRa&F)%PNc)I$ztaD0e0s!*M;nDyA literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/heart.png b/theme/adaptable/pix_core/s/heart.png new file mode 100644 index 0000000000000000000000000000000000000000..2f86fb0a3b9a256a93fd22dc7972c8cc6ab94b93 GIT binary patch literal 621 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGZU^{;xH2#>yp&OUF0J~E zU*?63`f(A_7c%NEWYjK+OJ5b0c`2p-Qcmrftn5iK@s~2{XT-$Mib=eXRX;B-bxBP6 zyqMHQahXeE(l2B*o`ZE=k(7QRqkd6B>b#W7bxFB<Qi@>H)M2E?IWfr>GHNfBw606a zfdpmMFN;aNkXCytrTkV!<%Nv;Arav(Iq^qDL>@^gKap0sBrg40&*Y`N);&qZ4}m`K z-Cf_gxV%)>IVLLlB|Y}Gq{2Bd$&+H@pHrefMg+aJcYJH_@Yde`wVCBh4Sfyac}xrp z4EiNOe!&cZZmfTQ{+L<oVXY|g>D}Y&NA_*ovb3|gEHgdXitFFE&oA%Zxq9LJiKFYQ zOA2NAQuP8&85kIaJY5_^EKcve@VuzWL8R?rv%;k-5~kO_#C_kO;OcUD^6&kNWO%KO zPJFujeEO%l!+hM;pMz%eh(`a}JoVv{XHPcWwMgF2eauQt;I-L;`)|HfX;rNiJ>p&N z$NyLFz=Q0cf<{6gkFR-Me0}M@XJ6gt=lRs;wy_C(HCp)Z?7z7gg@=snuJvbr5DEI9 zn0jx6d$Zqzee6e7&ShL$u+_s!O}aqph0iO|iEQGZ9J@-T?s!+X+*sBaRC-8|_lovU z?iX?$OASxzK9DezQ@P*KJ|T0;7tu$I9IxIfEn0Wz+JXzVdiV5uw$yyL{I<~HVzKQ~ z?i-17yWcIX;ZdEJf8^&`%_&9i{`lrwD_oviseP}Ri|sjEh>`D&-S%;caesd(m9R1} PFfe$!`njxgN@xNALmCZJ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/kiss.png b/theme/adaptable/pix_core/s/kiss.png new file mode 100644 index 0000000000000000000000000000000000000000..1bf90311f35b7721bcc0f526704c5eb446ef03ab GIT binary patch literal 873 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tG!vcIlTp1V`{vUAvxs&<V zPR_q4-T$8Q_<PFz-)S)8jK|+o9)C`H{6Fdb|BUbNeG-2U$p1g6{c9)3uf1Zw_wfJT z&HwLw(0MV*zo*=O?-l*ENBqw*vp)xvf9_MbDJg$KRP3yn)U#rR3*u7$&iY;wlYZAK zcS&6Onz-!WQ|>Qh)D3j3_sw+vcEs)Xez~9988-Ae&2My!^|Sl6i|5~s+CL}Vf1Pmu zz02j#3H$#qc1^ByEKaa@v9_@?u{PATeywZt=djkxB^uA<)&D=;@b6Cdzbm=_E@%9` z824tc+vDwSmsYx-TI{mA+o?LuJ~PrTCB)9h$>x2K#h;@_FIDwkmTEkdQobf3`)jY* z2~n|Mdqw|0-}3Ll?0@%X|GPis-=(zADN%pVhWtGn_~o$s?-OqCJ>2eYbo~(S^nH!v zkNx(s{<cPXHgDJHf8D9~%1!U*Ce4@TnlI8dUPNfz)R*|YfiK-@R{;Y9gL+AjUoZpn zzn`Dq-rU#SUKwi3wIfsT@3(hvpFMkW=G4xOZ9l%9J+Xf6jJD>8rFA}w7#J95d%8G= zSe%|ZIkfzcgFvgI#Zrg6v1*(x3P)qRte0N7yZ8V9rL%ckbk1_$xAc5Ar`p)s+IqDV z^VXvo7qYijHD6p5HbMRDt{AajFV0L}{Od@J+T5LNovoZkyH85n>}6t_9JMTeKI>kC zQ=cv*TJfhnwAa4$>)vGUfCW2mt*_Us<>>Ocz473ivyDca+Uo+;|Gh8RDdQkCL+I5O zhGz%P8oXmx_;_Q&L5*WN%?U?$h^+f)5nV1Q$<y$VmAT(JoToKZadxqo<d?;gTo<c$ ze3E(0+RP@oWs_9RjVLj@qZ1Mqs5C#)Tj2gs^~s$HyDvOYdb;6Z(Z{e?%;wG8Jq5Z9 zOd0meUwGXdeW+LI;mof6h109*Ycej{@7{5><z4r_C(_dkXZbB$Zrsn`ZD5fU7{AB$ zyXCRtdmSw9@cg#CVEy{hffHYAI#zC9%3}R@qV%g)k0qz})ph^6cq}1fx~lU_xxMoy z#Yfzi>ig;?{A7>nZOy7&f7RqtD?y1b`VSx0O`iSvw-(dx(o>g9t>)VDA1|D8bzMni nZue%Xu1jZhZeB^g^V7btrTV^q<@^!`1_lOCS3j3^P6<r_NmIB6 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/mixed.png b/theme/adaptable/pix_core/s/mixed.png new file mode 100644 index 0000000000000000000000000000000000000000..e005a582966371fd6ee4431be71478e92d9f6157 GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGJ_q=OxH2#>{6FpYYbX29 zoy@;>a{fK({`Zvo@4aIGPP_lwEAjV~$KNv^e@}V*IqC8Lr2GFvy8qAk{ypXXYp>Yv zJ^a6S^Zz~N_IscBpJQf!4k-WHFZ*+!!rue(fA=f=J>~uHLioS4e!unz{W}ZNVxVLF zb34QD{c?9Vx&Aq<^=q&2uU*{#E@%Bb?*8eZ+o{Da7gxA!ndtQAgne1E{hy<T|6lC- z_h9DpovshJxK6Hc>@9Z4j<SpMw{^0z`TumozdPOkZr1<1QT6v?+~2c7Z}z&a>T;Ub z=-8NLUy@*-7G~$=U}I)z{r~xvqw`(XbUQWYIMk*)_&M9ynA*IVd2KfX1A|gYkY6wZ z^WT3zKi@pPZ%21~WvHzH*PCze-aa|Hb7N0iR>s@sPamy4wqv3?0|R4;r;B5V#p$V+ z)61I-MA#m5Rfp`}nst44gTjM*yYqJc|Gz$y!}3AH<;%~dpYK(f{-o#|e`AS~7-zfk zMi;9Gg7Hh_PNi~8U)lZm&!0jYsr`ovtLt`D6<x~tcw;Kho2U9~pNSmbl;^tXs?Ot( zb2sZhJ)63CqWE9?-n0)TQy;Rf%#^(6Re0~=Qy#mlNmtXQ{%ujx_Be1U^4jZIR<pZr z*6tT$Ti_Py$kSk?Fl{cUcj<}w`$|ukuTfyy(ti6Mdt#W4`s}Cbi9RP3!vxrRR`Er2 zha3#?akwe?$+%tS3G23>S(_UUzge%{AoV2r$7QEIp7EwtXYDVCce?rWd%X|2WtF3) z`(bgL^UK`MrQF`lUW|2-0=H_FuDlh?ytD6JbirIj$&&k`hXb_OBxZU^8X7$KRr+*; zbJhDEnU=$U&dJ`=={q^a?tOphy2e`G-o5%oiuIwZ4u)y3oZ^*KAFigCxk~5sDz`|P s^A&&h9DU)dA;A;t&i(FH{cl#;l$5<6{>}W#z`(%Z>FVdQ&MBb@05CRi?*IS* literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/no.png b/theme/adaptable/pix_core/s/no.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce65b0da76389aac7a1234ae109b8dbf9e33f88 GIT binary patch literal 547 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGHU{{FxH2#>{J*#3?Z(P? zTWa2Isrz?l%g5av|L<;pzoY5Pk%|BJH~hc1<Kw>W&j%-cJ~Zj`;fY_4P5FO+*XQHY z{#;)EZfnEGy<LBAZ2W(32N>=Ae6aujT@d@rfr<a`@A!Xz=l^@#|KHp3`S7&=cenq) zyZ!5-ng8$X{C{=-|9d-LuPgnyr{nGBnt!)9|GlyP-PXFlH#h#ewBqOa#h><d|GT^G z-<>UguC4ujW!3jnbG{y%`esAL`yEZPCl=geU|`TF3GxeOc>eFt&lk6X-nX&*KGoNn z>1?2>^zGyG`-fM}>~AT{Pm2i)^|Fx^<4K9pS7l&e`04557-DgH>ZF@hO$H(@morwb z4a<G|YG1XJ0ITTH@AU?%%;HXcr(-_OmHhNKU8r#XEI&`p(kWVjFSkB0Un0GxW6SO( zF6S%%2g~(5JiL5)&4I$iHMT$We=<M%a^68(+vT?E33g}3LpReKMI#lp1O;=M`FX@T zHqFdGtdPjy@>fY9|Ab17tlmksg#rsas#97gNJe+`Gr3f{UvRuty<!dLB_IB+(YAZq zB{th`DcN=G{er1y-^A8)xII>8>Ta5RJNXdXs~sQe%|3h+Rc(AV$9KtWzMskg<>4Dv z%`CtBklD8R=5uCWdEbq*j;8<b-|84Sw@W*#=6}zT!&Z!^=UIk5lV81;fq{X+)78&q Iol`;+0FYoRM*si- literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/sad.png b/theme/adaptable/pix_core/s/sad.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a0f514e3d252009dee280a291cae1ec7794b9f GIT binary patch literal 739 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGeg^o2xH2#>{J$3RYbVFg zoy>nvy8qt8|M!%~-&5|t_lo^H?fz@81c-aa14JH>|8vsg|4H}%hjjm+@%^<|?BDsI zzo*=O?-T!X%<Rtr<zM?{f9_NGKhyZv9-)6{egB?vH_);Ext-z9Nw;6Sc&top{v6ht zQKfcolk3}kE*F-&Y@X=!=Y;)(O|~KKwilLJ{5fj)`+&l)eKNoHivEAG>)+*!m%Ck_ z?{xV8bi==U6aU?;|97M6-<90I7vsJhc7K1s?Z+|KuSZ<3taiP$(&h9Lm;E!H=QTP` zu5j!wcDS|9dR>q1nl7DLHR}JLZ~6CN_P_hH|J`o?dp6|n*}%LQyR1k%>z3Z*3=9l% zB|(0{49vg(eSY`$=IMPqy4x!=Lv01PzCC+#bmzvNHr7XLH5>NcWnf^e_H=O!u{b?- za(aD}fxt2C9qe6;qjJwPG;xc3Us0W1zW;xFW{2REL)`!Me@wLTpLF`}(}#REogNww zzOp$@Y&q`eckItx!+<{<m&EojXa9RX-{9nt{PKRj+wbEGoUarw-CwiCJwZL&a#8a) ziGP!g&&%&CJ<#Yj@7Vlz`<s+C=Sd_j=CcUqn#}Qc`qyGcjze?&u1F}KV+nXwboU#x zQai6;vhU92vQKRTzI_UOJ?&%NS_QL~4?g~OvRfo)oY`{N^Bv<0yBv-NYhO0$JrDZM z1vMM6I?b%(S?0#0W1FOy8y=hbcePa4ugzh*R<GT1V0%KwgbmtK9-;?ARE%ysv{8`X zx$Gd@`Bf3>e4^FnZfb!Syso6I<Zuw2^0eg%XQ9f~2TsfzS%Y-Un>!9YFG`Qu)FLs@ zj3;1~tJBw*?3Q_Frfrs7@h`(P_gZ58mzulRb{@<9qZH>~>SY(YD)!fkmejU+`3Ku2 zH!d~*Di$Uv{cNtBzW5hIozp*>VjlLH|7SOz8Y5)-|M&(51_lOCS3j3^P6<r_I9zii literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/shy.png b/theme/adaptable/pix_core/s/shy.png new file mode 100644 index 0000000000000000000000000000000000000000..130c795fd482edb2642ac1f210a943875ea993df GIT binary patch literal 866 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGEdqQ(Tp1V`{-5^ywUh1F zPR_q4-GA@l|9i^)_g=Aor`><;mH2zg<L?;{5P3lU&q<H}C*A)a(*1wN_wOn9f2Tcu z?G^jIPyEj@vp)xvf9;q3xliHmDer$5!hh`%`gazj=KufyAA4I2bgX~wVEB7B@ZZ(k zzZata|M~L&=j;DJUi{j{^Y_`Be>dyq)N1_RC;Ri~um69)|NnaT|Cby8KA!*ga`XDN zo0l(L{qO1GnbYS@nlK|btMu<l_stWX{+zIXyv^><VXY<2TEF&+{(rIS--DUoR%HI% z68CFo<e#I4zYeJU`}yPVhxfm(UH<oG@7MJ!KhK&}UD5PqZppXRIf-$ZKh`Jz+8^}m zgxkjhPVe?PJ>ThYYn}DaZ5C%1o3HKG{r_y!>x<L=-Rb*xyZzUdvabvMzZ`Zsy3qOi zD$7H2Or}+;eqS$<#Kt&<fq_A;B*-tAf%)&x&+l%Y-nXN>y)rY@R)Fix({D$2ZtQ7$ z`~1;b#_RJr85kIMd%8G=Se%|Z`FYVH1A!Lj_l;lb(pmT<-hA<Tp)%o~v+w%%|Jf{4 zc-?r6XV%!;+`B!sWZIL7^$Fa+_vS6X{Pt^muJfCX4{B`qnm#_bvA}-Uhd)1rn&pHS ze|pncvQmb*vtjM|c&+J|3{GsB$}z?JW6bA+pY9cZ<vHM!|F`6o^L1-p?Frg7;<vtE zwY>QM$mDw`Qym`di`2NHu52SvbM^MdxxGh@^<@Ni=<j{@ShjC9|3{TF&riKCI^C|_ zdu#0DER->+<D?UJ-~s*#N;^w;x}Kk+(<R4M!FWw@`{Ii)%pUF1;Sp!O!?3#JcgSt` zn5nL39U0XRNIlZKS|)fsCDFimL+6f7<(s!R?a=I2GM*kV{bEYw)YfMPv)9a#pRHP# zlxbX86L-=`{Qnti*PRx&2cOL3lKHCBslHsi>r#^3zQA^`>DezH&zf!**^&QMHoQGS z@z|dWVZx3pChVBhXI$*M)cl$7abEZB?hZ08mh4}=mWpk5KhnzBC(~e)ym_xSyV0Tf zoAlnA@FW>*yHUS3ebuDcs~LMfuoh4Lxu#%q<)*h=_W2x1zux6`RwJt?CqCuIdFf9( lWO^)P=1nmDeg6;BREw<OhO1|{GcYhPc)I$ztaD0e0st*D*1-S( literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/sleepy.png b/theme/adaptable/pix_core/s/sleepy.png new file mode 100644 index 0000000000000000000000000000000000000000..6d3bcdd86ef0c0d96062a7efb3b11d338df1a6de GIT binary patch literal 1190 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGe+T%4xH2#>{J-S?YbX29 zoy@;>a{fK({`Zvoztir&_KN>K<?;8dCx|>K|MQgB|C8?j59|Iv<NNoN`>%as{9CU6 z-oyWUH~+u$L4QxV{oX73YmfNved2suuKe0B%eVE)|Cz@8Tdvq|UiJ5s_pd!d{9CT@ zZ@Kd4nAx8L%Ky&#e%T?;xAh9hkOK+=TP}-jy(qBdir|)OyxT8w?YPFj<<j3%?)+P> z80c94+|Kalg!8Xm+`mt_{W)R(=djkl8?~#uoPO<-`L$Q{`7##at(WAsomSj@#A?Ux z|1WlJJY4eca>kJ(8GlZByg%Uf`H1ViO|FwF9Gi3Ot5WTQ-E6H*ZT=iJ{IXl?{!$L{ zEoU{h9?;vg?f=saHk(#CY*-SvedfPA-E$5$7OyF6*pR>PNcOd3aevPSy*nB3{iyq! z1Fp|^xjx+LdUmPH&Z*9uCpxe1b6VQww4lkcD8WA3*Y?*@8xMOM6MgF+2hG0i)%(0t z?e_tt-}~i1Y>_*+RO0e7;a|H2%(rg;|9p$bj`ijn*Zg}h```W9WqW7%te^4k{*-A4 zT9zHE-gcyL;hw^OSMvT{$^CmVarO4tzZYU|9FOT-9rN^f*wj^lUrzcw-R^c}wd;lD zt~U?3oZI0tyUsB+)GjH=ZpBJlA17NE8yho2>-PuDcC0qtu)_55PLn%Z4L@#Iez8&N z&04{mOL-nGW2<V&D`sF|FfIx53ua*c`|s!HoBMWlw^wF{*$Q&K`S$VM+h<SCoH}~2 zr-t>%*SC-E+}ycw?cCn(wxnQ39d${;Z<UU385o$<JzX3_EKX0ITpBLzD01|%Uz2zA zirwG$`j|<pDk>gPJh+#&D|m~?-Me@9{+%&5@}=ClOuqK}^1Hvk*=zj%?3unS;T3Pw z7CWvtmpEIt)vGkA{O!upvW=zbk7F4un5PJ37wRV6TsXNaKIG=*i^Wn8#LQy4?U+|+ zS#4r3{Owp-)@@$YdW(U1?$?gMWjgMs1-5(r`?p$9pJPt+-k!Jj8D}lx_4xZZ=|GWk z5x)!D?G4$c?AvGVeLN%Pe)YzQTgx|d%C&x|suP@d!74VsnSIGWZR>ZJEvua+n&Vvd z2sB4X7~Zjuc_qLUrY~*ZvG#|Q(EHz3uirOl|KGuD_~n#JNyH}siTGb14mP|#zW%}c z`CG0(wfDb$c)n?t)P`n{b<!V&YIi?--gro#JzUH}ns<idqTO5V-!rj>^D$jDNnq*B z*FMM^(Z9cmjYr|2zSm>XX(c8L+7JEMD$T+awerY@d=8Dm`LpfR=NZ41^JXfrT6yxv z@ta?+%=Y;lE#`6Fce&c-oBo~;Ja_Inb1_X_)%sA$BiYNv$qaHqpYs==OS4M}E)<ZM zHEZVFI>)0@8#$wSSu+%8Sv}mafI-Hv^LXsin&ST7LYwUTP55562Qo=6FFvO0#jkif zL`dvr{mUgir~ipPJ@tllBBz)~=jX|>SyLz7xUQr(E%&&_+xd$h&s<-y<I3U7M>_J2 zPF7#zxajd!R7Q<g{Db~aW|@WCPJY~fI(zlH3z=JYeUgg4tM=|s{A8ES=5L=XzF}Zs OVDNPHb6Mw<&;$Thx?;Tm literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/smiley.png b/theme/adaptable/pix_core/s/smiley.png new file mode 100644 index 0000000000000000000000000000000000000000..5c84740cb6d35ae179585776edc7aada5ed3fb98 GIT binary patch literal 710 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tG?gjXSxH2#>{J$3RYbX29 zoy@;>a{fK({(BGq-%}ocPr3izEB5cS`>(wcAnq9t5P3lU&q<H}C*A)a(*1wN_t##r zzo*=O?-T!X%<Rtr<zM?{f9_NGKhyZ%h46o8{eJBc`ghj%?<sc!9qXUl8N3~B{v6i& zy<hg%Ug2N6xW6BBd$ZT=(N@>p)16m$JN1=1{yAZvA8Q}wV{5Eu{pYCR{};RdJ(&6L za>mj&#|2G}|DSI7cc=T`&H8^gs{UQc{d+O)@7bV_2i;!ma=p6RWkbJHSD`~~x<gu+ zU8skxi?z+~14{p&Z#kZMPZ#9vk|4ie2Ijy2etv%U_U7q*JG$E|Ged0!xV}9(x^rVs z8|(9@kJgq?^*YbMz*z3-;uvCadTQ|XvL*+So(7FYLV=4fij*zYR7%M2-23Ezy~b@8 zZW{~zGdqn>pS!pB_a~O1r8Ro9{nCqn25GtJi5o9odTfH`wD(`H_unk}wz_r2zI*@n z8p!i>$}F6)>(c)A=GL<Qs3#gc?A~t$zZU%edEcYkasR<j^?vnRB0@rvH1yn}Y)WRT zoVoCIwcnDGuqT%v7n~DXTgv=kyJqfhg%?6;nkObKKgz(cVR4)6B1Xg6LPk$K3Yi6L zvOXBxPe|)HF+tUJcWYO4)(xdwn+&bFPfSSL`Hg*x{^{ZymyfXd^PDuO+~jJc)7awU z!r%2|M(2fvBBox}es8)H{QFFpLOfPymh$*bHdTB5k+bJyuG2bCz3#QeA3xU0n;%&! zxX@?)?o%>+*EXD9=-V*o6q}mQ(;f4kTTO5IU@NophVX&y85!C9yBHr{{~B@q_x8&f z3%~lLXM}vYx3qp2r{eK%{R>XbNtH@JyhUMBs_DCrT9Y(B>{0OY`rK*$pMOnu+}4DG SslE&h3=E#GelF{r5}E*27G=u- literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/surprise.png b/theme/adaptable/pix_core/s/surprise.png new file mode 100644 index 0000000000000000000000000000000000000000..fee206ef5ea251632e355942788ee179640462b0 GIT binary patch literal 1221 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGKLz-NxH2#>{MyO<|BS=` zgV}f1I^Ex3^=l{R-&5{?PrLs;<MH>D$DflPH�RKjr@aj4w$1r2FsP{C`imKiy#T zWP{<2RgO1SI{w)&`}dUljg^kqS32BXWBF^Z*!>MozxVL}J>`9SjqSe+;s4J1{XOM& zYmMWB4NkxJiay)s^5>Y@zq23%-)__SxliHmeudY&UGA*5yS>)=+6ssN`wjljcYv7r z|Nnmj9qT_Q-K50SbTuvh-KhF|A?DwM$$!sB{QG#}b9Vf*ZO#wYJ4ISK8S2`oD;xdV zC-eRL_y2#t{rhm{-}~eLUvB&NbmPBgEB@VX{(Cj&-{s7|XZ?R3cYD0u?duWOFNa(| z9dLfN)A{jc=cFLJ1&vyA66({d)czb){k2#0*Djv_e?Gr@`TD~7OaEVNTC{NaoLLM1 zJ(&IPUQb49!QZQS*OQ`rJj4E;_I|$8_3a)PD>IkFbDbxaJGSIH{5ob|nPUHNi=C6D zjk%%CwKZ0!7ny0QnkvidKii<aqFq~FO6$juAOHV;|NrCZ|L>1(-nf1J+RYu?_xyjc zd&joj|DG=W`()|wM+;wGn)B~Y-?u9b|E}cyy_k4=SHk5@k$=yJZwe1t9~OLZt^eMc zF1x2WKi}z)A8Vf%Z5QijcWaf6slJV=fz^vGW`B+v?U-WtXr00O9=*AB8q+FOZG%p> zFfcHflmz(&GyG#=`up$atpn|qp-y&OUp~Km^Xloo?OS`YHUE8k|L*O}=TD#9Id|sB z&b9sBZ5gR?Ar=B0Youp{F)%RsdAc};Se%|ZsWe>LQRJxk!L1$(9q!(}f974v<Ou?b z0^*NUEJVuNckSN2{r=|s@@Z4Ped+&Xtv>ntyS>(*!&-C~tk2NUTeV6{D{sYx|A*J< z+?B3ikuqI6wf=YNZ}|#2wnkxAi->RSbHg7M^S4}<z0<#hyFmA1{-=KaJCnpOcP|M1 zQ0&QMF>N~clo{{#$NV;Ax4fq^*}Hgya{0bDo^|zA=W~sUZf6wTul-c&`>V)n_Wy?G z8+Z09t#fo>i}kvj#&)CBk$1`|#*Ae$k`C8Sr8W3{6{zhMtkH<sf7k0}XhZgtxP6t( ztnz14rdnz<ESP>tZns6ts-Kd2&WrZ8Uy5sZlgTsZfP?kY+C>(P_f+lmgcsd#&$M=U z#=+KQSkQjyonW<n)j<WlzuzY2dF^F5m&GHJc))*RSZdjw-sw9e{USRWa~mpFdQBHR zaN}sU{wY6Meg(DZMd>e+opwIgR&y}&Qm~g}OfgZI?Ko@J{P~j(==^qP?5w%n*?705 zhuM9W=)_1)i^nY@2@f<BU8>JdUUW+3z14*qeT~LH=AYO1>R@y6`SAABMT?a-SxYyp zy42LXc8$UDCaLX<{ybdNAfx8Nv(#|n#;2*rju@C39`biMC&;>*C7JCQ&nbPLNui?W zxwP7^EK^mfP0FdAyVTBWp;+jx<+UYGThy1dG%pQ0^D*v`ou;_St4jI$ROxLIZMo|o z?^!QymZ3D~#cgr1)1S)&*X9ZpT`5!*o3-ucvUI!8>->KBDo+oaJNKMV*!EMqkNGx! pj#;%<@5)QA?^p7Ae#HM_s<hZ+Ef}y(mVtqR!PC{xWt~$(698p|eSH7` literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/thoughtful.png b/theme/adaptable/pix_core/s/thoughtful.png new file mode 100644 index 0000000000000000000000000000000000000000..8fc1cfb01a514fd555e20fbc909d5ba5097f7a28 GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tG9tQY?xH2#>{6FpYYbV?9 zebT>na{fK({(BGq-%}ocPr3izEB5cSJBWS814JH>|8vsg|4H}%hjjm+@%?w&<JUg1 z-}}V>95eg1U-sudg?|^qf9(<ach>jsDfj>X{~PF7|J=dw?`F;4Grs>Wr~iMk;qTd? zKZmt`@0b6zi~IkV>;K;F|NmmgzZYx&UMu>0G2YbB;m-;C02kXoM-6}Nll-+u==bm6 z|L%7D|MBeZo%=U$+&+GA$Lf{q=g(d8@4@VU4`)p5pZ@RO#OlhXzt>CW_U8S&lKb~+ z&f4i&7dB-k#AbXt5%=%?$$#%p)YYAS#lXNIQxfDC%)tD2+t1JM-rhXDue-f6Gt^dq z>*=?nJA2xmKU$l8*?t)V17nY;i(`nz>8X>i7aeloab;e%{eqK+(#B;szob>YpYZ2D zyX!R_v!gz0&yQ~2Ri5fOE&gAlwj3Ypo_h`&GUb*hPOjL$fw|Lu)}HI}_F+dIIdy{8 zZ@;OWm3wZJ?~&4_^=GrS?o8e&J25nuy<g+n@2gRaGB46n|E^3Dmhn`-ArShq?%xjE zIgUkkIsKJY(?r)@@c%A7IYzHsD`?7|qr$RkybarKuQB%9$aGHQ+9550t99Lc#!L28 z_3$Kig%-FRG*vQPt+ei>%^@xaeUCN)2TsA`r!(a@@&&lR@t>Z|HPzwsL`8-T$GPnp z?q5sfpViJ2ckaLm9-j0UoR<%n8f;?YyqYkhjiLPD3B3sq^=x8PE=njB&SVyHF6KLQ zJh!N1vqS`QKA)KMyd@l;7R*cPkm&z4^PZ?lmJ*l$g;phda}O)aWz0QlcQ|(%^tEjg zcz^BGLEAO^Z+g$JykM$mxBhz6%X`<>U#`8TyXj?7^s2X;6kDSA-B(+<<LkU-UwL9D stWlX$cQ4|?bHPK8xsSbhQ~#M;@N~FEL!$Wu1_lNOPgg&ebxsLQ0MUAOe*gdg literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/tongueout.png b/theme/adaptable/pix_core/s/tongueout.png new file mode 100644 index 0000000000000000000000000000000000000000..221a0dc806502c6755ba105fbadd5bc9aa2cacde GIT binary patch literal 944 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGvjcoWTp1V`{$C6EwUhPl zN%y~}JpP_?|Gii2-)S)WjK|+o9)C`H{6Fdb|B&wgGrr$i+<xuk{Iyr?*J_2|d-(sJ za{Ikc{LeA7-}@wf?veO&K>62x*`NCq{vMG3yI<k|OyhqS!vCH1`?W{t-&x<kr`!#6 ztiR+W{Qv*|=XQqQ`{jP^;{JED`rqaBzvqJ2^f>)FYWU}{_W!@%{=GZ&|HZEV&o}(L z*Zc2Q!@sL}e=j9|&WihcA?Extm#veW{+zJ?|M}X*3s?R<p7-y;?0@$s|Gi%PAuIlC zQS6uc=#R;fUrNH>$A!L)2zV9b^Yf_tOCR^2$K7skaDC?Pyu96ML9=60kX^K|-LJi( zzjg}!`t|Go&zH}izudd$;Q!}amM>lX@7aofPnXV}y=ca?xgBl&O%0v@ZnqcZS7xRa z{=Jg<Dl0BDFz!WW?9VyDKW7L3J>%=_;QMaB+rzDH*Vecmo$qpFuFJG)#{gGbZwDKD z3!5Jc^?x7I{61HKQI6pe0|SF{NswPK1M}a{Z*QL7*WF$jYAe9?_vg2d@17jpxnsx1 zp0=zE*5^+jt-ZISC6IxE@uH`TV~EA+sgugXr40p+JT5jjvz@|_xjOo&*^OhW2UQn- z=TqF<e$Tq5#_iF)Lc0gHKmKl9zFac%xr%Ki%e)MY7vHP?9t^lr+#VzSQT6_$R{fyY z*RtcD6#w6Ka@o#hRo@E~tr>25-gtZ=o<BwS$+D-%1m(^w`m1~E?Z4K1&*c+-T)e%% zEq~SFQ~!;=pHnRVzUh&A8?Rw*hsvsJ&ewT_mPVI4#a~>>Z&)9B_wVmcW9tyZkaHTD z%T9N*v;3O*pIdRCv2yty?zRcnzWW<i_c-=dG9R`-!@8)SQKE?@&A-J$c#+SP^na{} z+0#_&FS9hycc@%x;$7YC?Q)do^Y!}|?hEsLHK<rQ%O=~%Gu2?rLDxsd$+l7o&52tJ z0%bPo>BX&gzUS>~c60wug&3WLPfl!anqL)4R~hV7vi=$<Jfm3pS;6c}73=i7MNT{M z%wA?`-*2RIWK);M?RD#S&vfwQ5I%Eeg_a9vyF%}buL)~Jyk^aEdKt`lDadZ~d>PHY zYYTn-7Ujrg{e5|MajxP~jV+y=Qv$c{uX$Q??X9N7wuQaD3v-q}ir~NN=<dAB$hPTQ z?J~<vN=qC6T|E-j`(x+Iom1STKixlcSu|sfwEX&Po{~p3a%V(mmX)yYID6$d0|Ntt Mr>mdKI;Vst0JO^S&j0`b literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/wideeyes.png b/theme/adaptable/pix_core/s/wideeyes.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5b6e516f9b75047ec9663556f209016da6c794 GIT binary patch literal 792 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tG`2u`GTp1V`{-5^ywUh1F zPR_q4-GA@l|9i^g?<x1+d&T~pb_cP~c>F!(@#mz+|C8?j59$6t<NIr`*uV2Ze^0sn z-Y5R&nAz`r5<mAy{5hcfYrpKzeF}dM$p781@N19Izq7u7Pr3j9|KC8z`sWUY|1Z}6 z|NY_rms{y6`G3!a{{Q>!+0*9-X1iFKx%|6P{^zjPf(FfHOIH0o7xw3*`^STBg>m+e zw%EDY*f?9+m>SqDZ`1yLK;hRunO}QFf9>M=_k8=m%UL(qyT07*_~(TEhYuhBJ(&IP zPWQi?_5W^^Eo^qWwa)hRBD2GDO#U1-`g6qa*KUDdySV?|pYr6)l)o1fuWyb0dm-lU z*`O0kTraM$+%?s3W5524Ds`(jYj!X&Fvyn#`2{mD|NZ&--P@a|_jR{dhS~~neS7xg z=*}HGHukh-WjtD|UAd8sfq`+Br;B5V#p$WR*NYAr2=FYM?oh=sVUES+gKA&yDVAKi z{r|s5iua9<#2F{|tt~5!n(L?jzv1ZnLhD?!<+mSimzi+BrY}$2LT2CQ+P`0tbo%WN z9C*4i;;!|}o=FM^vTt6LcbmIi{%g2yhS$P%*Y=-g-C-ts@45I^JN@s!7c7pMr+cMJ zzUkP{$>*MncW9gu-@x<nkN78TnIn}B=0A#JE2cbNx#LXIESH@Zg{|%>iYrScS^Aov zbhuRX>!sMPBVv=4GTm1kWL6NK<d-6x>69{YOU#uAY#qXrI@}g>Iz3X#ytVQm=L97c z@9UddmbhHn5Y+rOg7pf|aU%t5rA#()&)cV*q{8c&8hI|9Tj#kwcec6!YwVgpwyLFG zORpHekUCtoy0_Iw*IwpXwxnOUM&y^{OnphF{u-aIDm|9olX|{(r>?-$1XU>w)*lB7 z4wq!l$ntUAW9*i<pXu`YGYj`Vlbo?GHACUv`@YhGlX(kYei2*Edsn}1{e_)I4{B#< zX1jFG(^o1`-Wq+@ZSz$(t@~_zwf8zs=ecgV6`Q#6SK%K<^_0{*OiyRUGcYhPc)I$z JtaD0e0ss`gmp=di literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/wink.png b/theme/adaptable/pix_core/s/wink.png new file mode 100644 index 0000000000000000000000000000000000000000..37cfe0e491550ea3009b4359e17f99eb4ecc7ec7 GIT binary patch literal 817 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGH3NJ?Tp1V`{$C6EwUh1F zPR_q4-T$6)|99H`*ItRgr#$|i@c@wr<o}%X_<z#<{~_J~XMF#ja{skg?Dro2-@Ez$ zoe%nZ%I)`F(O-MS{~R;>b3pmme%YV<6#maN{(H*%*B+tYd&Ph66aRM>q{~3Z`sWUY zKZmt`?c)A>%5`po<G&lVzfQRSIbm;QV*Tf+;j~KC-v<<a?UVWcV%NV1Gyh%A_;}Fm z`!UyB8(c50a9Q8y^meafXQ4xNntgt(U3$1(jIZsDwbr|)82*2{;oqI^e^+w<o(+1v z$L-ru*T>sjPcL=(c+h#@Oy`|bofow@O|N!*x!d8vW?L6)n@cM!_s%d{)v3L-MeEmI zv0r;df9)3d|9s2ei-~_P#Jt$)c4&_C%1)<-EQhir`?E{TPcAfD)2(|_Ch-Xa1A|gY zkY6wZ^WUGJZ|>XC-Cmg)YAe9?=G(is&z_t)wR2-n8|&LgM~|$%&T?}d0|Vn!PZ!4! zi_=plKP@{HAkgAnpmBv;Bx|<nj@{m@<*RT1ws$Gc7h?MUq5b{!ImXP^H*@&DIV2lP zezBT+FJV(=*$ww;udgtlkcsY#`2S(g%?!Ikv$_wztv6>E^2}O2!&LfTt&t#u*z#Eh z>cS~D&F8nyldteR$fuOI^MB=IQKQMf-mE{S;`hcRfpbaC<QH$YhiMxyuAgXTQ`h{k zhQ*qraLIDsh`L1=f0lSmNf2dLOQ_Tnal4#h%e-NS2s7J+3{mggfR|US89o{tE@1bn zI`g^4@6y{ka}QS^ZMG7Jm!Fhe7d5E{hF*3#7RdX=<ADDag&4iM{?jgcfwGM8r_O)! zUy?D8M{tXmr<0j@%J+ulXOfL7E*{kBN^A|=m6dSf%c(87ESCJWk=f5LzyHqfb=5## z&9;_FYU%abg)CZoTz)j?aa!)KG4Og2>lLyo$azoHd>M_D&wt~%Q~rn9MlJ8(dtBnt z*=Lcvm2O|xP`D=;f3)@yujcR1>jW$-_jTPp!P#A*vri;oOYgT>`So8v`E;+aEse0P duKE0b<>Tz_P4NfK85kHCJYD@<);T3K0RTW{l@R~{ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/s/yes.png b/theme/adaptable/pix_core/s/yes.png new file mode 100644 index 0000000000000000000000000000000000000000..42c64923cef017aaa98b682cb4a80cadc2cd2b6d GIT binary patch literal 532 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4rT@h2A3sW#~2tGRtNZmxH2#>{J*#3?Z&Ei zn`_@~t^5CA`@cI|-|uMtd}Px9dpkew>G*tT!so*iKOda<`N*_yM`!%Gy!_Y2b^q_} z__(+G@68SW@9hAwcej7s)BbU9_s4x*5RuOZ`~TnH{{P;NFGnW+zrW-E{hj~s?)-dc z(*L{L|G(Jz|K5(*>q_5ksrh?j{l~ps|L<=5cYE{Sn;U;!TJiJ3;`iGczMq=?X<yHq z4ds8Xt@(Xr<=3NAKJ03HaQ#pt0|SG4NswPK!?S;Xeg-{%-)elluQ|g(T}kZc$7fIP z-M)F|_~Df^`&vr!)1pJXxS1waUS?om_~_~47-DgH>cr<lO$Gujrr+*WuTOU{;m~^j zKV?>tJx5&H9s86W`Fgu&-?7i)ZBtbbsA0My^Er-(ft&rpGlp}bw_TX5rDoWxY$&Vv zqOw8pc`wHjzH<{8{>qu=|5DhhA|CRN%Odb|GKW_>*NW9WCH}E>R}WO&Uuu+fm-+A6 z==WCD5)Jnc2$Zq0)cDJvnmKLDy*G<ORxD#|o2_iqb4e{p_|Z}3ADK;=52O~c-*zz! z`0T+^XQ(#orHH<zIOEO3BGnw7a$$B%F8p(E{1LzNiZS&2N8KFm9Zh>nugXPjxY{LL q7@XfIma|p8_5EGN(@_PlnNNn9?|vuqvy_2>fx*+&&t;ucLK6U}8yu+s literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/t/message-black.png b/theme/adaptable/pix_core/t/message-black.png new file mode 100644 index 0000000000000000000000000000000000000000..018e6ab9596bab805b1f9d777f99eee1b4f48039 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4i*LmhONKMUokK+FeQ1ryD<E3??1Djfq~(O zr;B5V#`)xg1#$w(8`3!vHl#f`sFr!Spy8qZ;d+BV2RBS|WDT`ye8?Xn#Aa_0p|s?w z#J+`|%@1VR!q^l$nnZLiDP$bjFz+C55@Q)_6i;<y7^ANsThNS4>5@Mr?HFHHOptt+ zpk`EaV!^Qs+-Xal?o8}g`Pr~=uH%G-vmVOdIK4?BGqi0x&vQwpMj2Z+PPVdy0$FD5 zzsx1_5*<#=ZBrGyADSHY-w}EA#-S<VFKq1-7Kj_~N!-D2!q1<;+}30ySjfR}RyA9M UwKd}j0|Nttr>mdKI;Vst02}OJlmGw# literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/t/message-black.svg b/theme/adaptable/pix_core/t/message-black.svg new file mode 100644 index 0000000..d71aa1d --- /dev/null +++ b/theme/adaptable/pix_core/t/message-black.svg @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + id="svg5277" + version="1.1" + inkscape:version="0.91 r13725" + width="16" + height="16" + viewBox="0 0 16 16" + sodipodi:docname="message-black.svg"> + <metadata + id="metadata5283"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs5281" /> + <sodipodi:namedview + pagecolor="#000000" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="706" + id="namedview5279" + showgrid="false" + inkscape:zoom="14.75" + inkscape:cx="8" + inkscape:cy="8" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + inkscape:current-layer="svg5277" /> + <image + width="16" + height="16" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABE0lE +QVQ4jcWSPS8EURSGn7uJFcWSXSGa3WYpRKLSbKKemkq2srVGoRKJ1q/wA/gVKo2I6BWiksgmviJL +hkdhRgZjZ1Se7p573/ece86B/yZ8D6jTwDxQB26A8xDCY6GTuqweqa9+ZaAeqLPDxD01dji3aidP +3FKfCsQpF+qI2kj1FSACxkr0C6ANLAD7apQaxCXFKS/AOHCoNlAnk/+V4VSdUO+T83olhNAH1oCH +gsxXQBfYBWpJrPp5q27+kvVS3VPr6o76lsQHajMVz6l99UxdVZvqojqjjqor6nHGNFZ72exTZhYl +GdWGeqI+51S1lb79scpZ1Bofo2sBd8ASsJ30qxNCuC7oW65pVY3U9p/FebwDYVJvZo1SWyUAAAAA +SUVORK5CYII= +" + id="image5285" + x="0" + y="0" /> + <path + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 2.2372881,10.508475 c 0,0 -0.2711864,1.762711 -1.0169491,2.237288 1.6271187,0.135593 3.1864407,-1.762712 3.1864407,-1.762712 2.3200014,0.708023 2.7570621,0.338983 4.4067797,-0.0678 1.6949156,-0.610166 3.4051866,-1.8208371 3.2542376,-4.6779628 0,0 0.463823,-2.6733371 -3.9322038,-3.7288136 C 5.4237288,1.9661018 3.322034,2.7796611 3.322034,2.7796611 0.20338987,4 0.20338983,6.5084746 0.20338983,6.5084746 0.27118644,9.6271187 2.2372881,10.508475 2.2372881,10.508475 Z" + id="path5827" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccc" /> + <path + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 7.118644,12.542373 c 0,0 2.5084749,-0.20339 3.728814,-0.881356 0,0 1.830509,-1.220339 2.305085,-2.3728815 0.745763,-1.4915258 0.135589,-3.5254237 0.135589,-3.5254237 1.423729,0 2.372882,2.1016948 2.372882,2.5084745 0,0 0.704988,1.8167987 -0.135589,2.9152557 -0.881356,1.220339 -1.084746,1.830507 -1.084746,1.830507 0,0 -0.542374,1.355932 0.677965,2.237288 -1.694915,0 -3.322034,-1.898304 -3.322034,-1.898304 0,0 -1.084745,3e-6 -2.4406778,-0.13559 z" + id="path5829" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccc" /> +</svg> diff --git a/theme/adaptable/pix_core/t/message.png b/theme/adaptable/pix_core/t/message.png new file mode 100644 index 0000000000000000000000000000000000000000..e7cc6c9c0b2ef1974a3ea1adaac6d06d25318b4c GIT binary patch literal 350 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7*pj^6T^Rm@;DWu&Co?cG z3VXUZhFJ6-on))e5-8F(e|M+o(Mhq2v+ky8x-7bsvuNHduWg&Qx-Asyyte)W^AAz2 z-i{Zdizb~=+o$B+=AP)rmYg0k@7=uW``_nOpKJWDR>QyI@&TIw_PmDA0*8_pPTsXf zZsq#A)CF=YHXlA>Yg%TpJxksHPSAX=Bbwr`I&OJ<6;)t5=Jkbp0k00j@2n|OM^uk$ z>L0KuSYZ~RKKcLDhaqzV-cSBqrP^|V{aQnN)0uDw@f)mJo@;(puFm^6>1)v4;McGE zeoZ^Vo@Fe0er4@fpV-jZuNGM??pvk(s_>XeSoO^}!D(Wul2_ZR7TG-y**bOK-;Jjt zuSm%+&Soq>Ahcobm({{fJM>;eugeVWz4C4T(aJr{i9z{ky+P5c3=9kmp00i_>zopr E0RP{cTmS$7 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_core/t/message.svg b/theme/adaptable/pix_core/t/message.svg new file mode 100644 index 0000000..20158c1 --- /dev/null +++ b/theme/adaptable/pix_core/t/message.svg @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + id="svg5277" + version="1.1" + inkscape:version="0.91 r13725" + width="16" + height="16" + viewBox="0 0 16 16" + sodipodi:docname="notifications.svg"> + <metadata + id="metadata5283"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs5281" /> + <sodipodi:namedview + pagecolor="#000000" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="706" + id="namedview5279" + showgrid="false" + inkscape:zoom="14.75" + inkscape:cx="8" + inkscape:cy="8" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + inkscape:current-layer="svg5277" /> + <image + width="16" + height="16" + preserveAspectRatio="none" + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABE0lE +QVQ4jcWSPS8EURSGn7uJFcWSXSGa3WYpRKLSbKKemkq2srVGoRKJ1q/wA/gVKo2I6BWiksgmviJL +hkdhRgZjZ1Se7p573/ece86B/yZ8D6jTwDxQB26A8xDCY6GTuqweqa9+ZaAeqLPDxD01dji3aidP +3FKfCsQpF+qI2kj1FSACxkr0C6ANLAD7apQaxCXFKS/AOHCoNlAnk/+V4VSdUO+T83olhNAH1oCH +gsxXQBfYBWpJrPp5q27+kvVS3VPr6o76lsQHajMVz6l99UxdVZvqojqjjqor6nHGNFZ72exTZhYl +GdWGeqI+51S1lb79scpZ1Bofo2sBd8ASsJ30qxNCuC7oW65pVY3U9p/FebwDYVJvZo1SWyUAAAAA +SUVORK5CYII= +" + id="image5285" + x="0" + y="0" /> + <path + style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 2.2372881,10.508475 c 0,0 -0.2711864,1.762711 -1.0169491,2.237288 1.6271187,0.135593 3.1864407,-1.762712 3.1864407,-1.762712 2.3200014,0.708023 2.7570621,0.338983 4.4067797,-0.0678 1.6949156,-0.610166 3.4051866,-1.8208371 3.2542376,-4.6779628 0,0 0.463823,-2.6733371 -3.9322038,-3.7288136 C 5.4237288,1.9661018 3.322034,2.7796611 3.322034,2.7796611 0.20338987,4 0.20338983,6.5084746 0.20338983,6.5084746 0.27118644,9.6271187 2.2372881,10.508475 2.2372881,10.508475 Z" + id="path5827" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccc" /> + <path + style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 7.118644,12.542373 c 0,0 2.5084749,-0.20339 3.728814,-0.881356 0,0 1.830509,-1.220339 2.305085,-2.3728815 0.745763,-1.4915258 0.135589,-3.5254237 0.135589,-3.5254237 1.423729,0 2.372882,2.1016948 2.372882,2.5084745 0,0 0.704988,1.8167987 -0.135589,2.9152557 -0.881356,1.220339 -1.084746,1.830507 -1.084746,1.830507 0,0 -0.542374,1.355932 0.677965,2.237288 -1.694915,0 -3.322034,-1.898304 -3.322034,-1.898304 0,0 -1.084745,3e-6 -2.4406778,-0.13559 z" + id="path5829" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccccc" /> +</svg> diff --git a/theme/adaptable/pix_plugins/mod/activequiz/icon.png b/theme/adaptable/pix_plugins/mod/activequiz/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..79326e82c253a371fa11a684f07c4617b723c422 GIT binary patch literal 1164 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;8PZ!6KjC)sS`Fbd)OB}C1c3_Izq_q=54hAgzcuB|f?L4*Hk1D+V&nDis-75RO z$w`lUce~8@4UHbvIhj=@yW1;n-I>FhooD70EU<Zj#tW?*rYZ>zp8WgHJ<EQS-E-~@ zv-6=dlh6FOKW}~hxplz$>pT0Fe2QY}V3sma{kb{3fiXpRRp5?uf7z=PC!ESIXONx2 zZuhWi6Yr8nA1Bj$_cRz8YvN?=erX?X^7y6YvV?QVr&(+%zdjsE)mfw-a(F^unP-zf z^Xcsja)te=h38*KI_O9;&p09%$-X7e`_$8f4;|IgOx-q&?G4kF5>p%3I3F;dbizx} z<LTBXqD+@^Pfa<H_E(5Id}q(*)Lj>k?uyon+NHPN$jM~I<6t*S%e5P}eOI@B+9b<z z$zjP^rj>h^G4GFV{9pLVe2Sz0<)2G?)u;4@y-_fn9MJ2tnsrjxt&0otn(phaH-2VY zzkhMEv0CHHkC9CCI`=8stjOE6=WF-Xr%O(7o#X6So-dhNdujz|-9`&{lNF2mUjDG1 zKg){oe2eEBvqhy^>yHYrnz(m^!=BmSPG+(;7`|7zxZvFM#rGKl*6lScowmKsO;YBS z^r2gkPmM%nPw?49&)voR=en=)1I~|!pYPu`cl$@#9TWOv776T3v7J^CHz$4j9=-#K zU$+V$V9v;sWMZ&>%G7<-a`{n~vsYgP-*@iZk;<v@PVwEZWQ})<S<6Mu`olD?f0K1Q zpw9g7VLG>OUNG;u4|^<*J8n%;XPh7H_WIrnX5pHh-`LiFGCZ|_y)>t7Y434{mcNEI z@;zVHJJ{V^UNK{Lh<}&Eh2N|UOBe5txK-h!^QB{hy<5hQeJhL2KlsKo-^{DrThF4$ z6nQ~h@kOt|kAg+I6aE%|m^Am({o1V!JsyG@a{E+FYUEQd9RB{W<nqt*`^DS#umr_+ zUb@J&^HOkDdg$jnx9)L=l^bpQeX%ipbuerD)c){SQ9Byeez%yAyVK>nqZiXWfr|_L zGJa?;7cjW9iAAnWeAb;OiC;~-|DOxAUVFnUVL{yb$)~cT?{dwmUd(^tSi0-HIMdae zA0_t1yj&UEm}9+A>hZfay9ueQB&GiCdb!fS|Gdz*;~c+})flCjBDWdxE!i!YajKiG z{fxu47h(LXX4l3_3;my-_tq<=VZK#n+h4<^h3>LUcXq3moc`qQWcuZr!?Z1ZM=Y;> zi`z1Dhxhdd%T#Y(6Bi15v1`hnQyg}wEw>_%o-?UZTqyN9Y-d1FiH23xlA_%E@08MZ zmo7DV>XbA=pgiM>E!(5Dhx@jr`1RPP%XlR0yX>(o*-YzKpPy3VH>ahSGubC@V5!fv zz2NZZ#xm3UqBXOnw+d~Xt5Dosdf4ju=11P_0^xJ7sPCMqS^GHHUp+)wGj?KZ>G~tQ zx#ll2XFK|{M0FfF`Z2fvz;u_enQv1UAK%JRq9FWmPoNB=UfM44%jd!$ebx;Sa1B`) eexdi1{p#g=CtZ;im1baIVDNPHb6Mw<&;$TR$0oY~ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/activequiz/icon.svg b/theme/adaptable/pix_plugins/mod/activequiz/icon.svg new file mode 100644 index 0000000..ac94df2 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/activequiz/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#8cc63f;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="23.77" r="24"/><path id="path1" class="cls-2" d="M18.49,31.7H38.63v1.84H18.49Zm-5.89-.15A1.41,1.41,0,1,0,14,33,1.4,1.4,0,0,0,12.6,31.55Zm0-1.84A3.22,3.22,0,1,1,9.38,33,3.22,3.22,0,0,1,12.6,29.71Zm5.89-7.16H38.63v1.84H18.49Zm-5.89-.19A1.41,1.41,0,1,0,14,23.77,1.4,1.4,0,0,0,12.6,22.36Zm0-1.82a3.22,3.22,0,1,1-3.22,3.22A3.22,3.22,0,0,1,12.6,20.54Zm5.89-7.14H38.63v1.84H18.49Zm-5.89-.21A1.41,1.41,0,1,0,14,14.6,1.4,1.4,0,0,0,12.6,13.19Zm0-1.84a3.22,3.22,0,1,1-3.22,3.23A3.22,3.22,0,0,1,12.6,11.36Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/adaptivequiz/icon.png b/theme/adaptable/pix_plugins/mod/adaptivequiz/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9b771769e33cf72363aa0017eda5d1fa74dea2 GIT binary patch literal 1101 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tK;o-U3d8TX#f^3S+kCUV@q@2JeGGm?z6#e|YNPAABjMEhrjE6txa=au8lzN>u6 z)2E5lNPaPpFT0W0#!_}G^_^j^!nu|ELKo`T<-T4ZDP4LXmr2)bit3R_9^QKg?KA(W zM;3;7NAF+I{8asA?bmzn|NgXPmFi_%?z(Nos)oWN4Vl~*mpe#aV6<KP;#cN;S&1nP z0a46B2Yxi1zt8;1Auqs4T}?scJVWu0ZeE4rAf-Oel<q8^2&Q$Psy6c7@zSokY07kS z(JZE-<pon6W4yh!tM<)*(82t4dc#<^SH@vnswl5IZ4uRi;2TzfgVci(8$#4}_mG zuH%lKBP_7&bMvRQD$`_kPKel${X=+uiQXGCCZ!NNsZUL|nI*Gwc{c{NADA@vQ|Aer z$BWZf?zehSxYgnB)q@*2d$?tTW5om{zd5d7?Q}_E{@(3AF7ph}U+vGnb2n;9kalHa z=aPvVqje{2oV2gv-K>>0e|NZPXb5wi(wIDd^S|eB&0g<#rtPkOW0lyNf9E<5ZS-{5 zQ+xVWn${mihO4*CwD;>z>ba{RI8)uO@aB|C`&|c8el%sXlzp$?tZXn{cU4x@#nQt} zv+kblYgym!eqP8{SiS1mo=jfme(mo!Z~ng;%KfF0Z3atF*`(;JzmKTv33HvQnf1e5 z=KrTZaos+NH(o2S3e@~gj%D4qWpd5W@5W4w9jBg8bh{^XYW=4VH~Hh*u3UVSm*;FG z=4_wy?#YciCk;jCEiH=pq}1}Q>D7#e#<_pp>%4MSsZ>R{cbqD8I%sGw*y(A;TOs}I zas0%NGqd;iPI$FZ+t=G~iQc^3X<GRzn>uWqjmnyxUf$Mwy`%Bh$Hv7v?>ii79(B1Y z?PfCJR7p0E^-s}`?tUjw(B$>DbM?N5U+Vfero`E$7}Yd(*c=v@UnjtI>RBZ-|Ec@c zn>gI&aQyvKbM?%Ywog}YZA<ssZfbHr_uSOEZc6fP7ncj2-FqrCy)N=lx<>znI~k=d z6HJbUR7{&-@=Tm*mkLMCUH?y)rnDKY+#0fIbzKB=)q;x|6LeSA-~4*$V`u-4U+>Ig zpGSLEOgQoFQrEveLATFkza2VHG|g<i{(pzBQQ71U8)v0m2|^zfCO(ZmoN;d32Qj<- z^<SD!C3c>AIC<NISAs=zcMI(bjlc8iznktApA`F<zJDidoETQJ)p%m+8==Up+wOk5 z_WHPK*KYf&uiuPge>)xGs?1@Nu)A@CDZ*d7>eMQ+U8+`FKHfW9%h<nBz2r?Scc-m8 z?|-M)GPb3n#Yx}GZ?>P(|1M=2>6cMs@_=EsW^bycuHKHv=}&)uV}5ifwg2w1GT(py zkFU|?uAcTx;MdmUm9v6p>=Axw{ydnOTS@Bw)_&WZ6B6z{igkIP`PD2><h{4AlVV_C OVDNPHb6Mw<&;$UO!VQuD literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/adaptivequiz/icon.svg b/theme/adaptable/pix_plugins/mod/adaptivequiz/icon.svg new file mode 100644 index 0000000..de334a5 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/adaptivequiz/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#8cc63f;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M18.49,31.93H38.63v1.84H18.49Zm-5.89-.15A1.41,1.41,0,1,0,14,33.19,1.4,1.4,0,0,0,12.6,31.78Zm0-1.84a3.22,3.22,0,1,1-3.22,3.24A3.22,3.22,0,0,1,12.6,29.94Zm5.89-7.16H38.63v1.84H18.49Zm-5.89-.19A1.41,1.41,0,1,0,14,24,1.4,1.4,0,0,0,12.6,22.59Zm0-1.82A3.22,3.22,0,1,1,9.38,24,3.22,3.22,0,0,1,12.6,20.78Zm5.89-7.14H38.63v1.84H18.49Zm-5.89-.21A1.41,1.41,0,1,0,14,14.83,1.4,1.4,0,0,0,12.6,13.42Zm0-1.84a3.22,3.22,0,1,1-3.22,3.23A3.22,3.22,0,0,1,12.6,11.59Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/assign/icon.png b/theme/adaptable/pix_plugins/mod/assign/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce27f6d0c664e518829d2548b2814aa0fe983946 GIT binary patch literal 5758 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 M)78&qol`;+0OgajOaK4? literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/assign/icon.svg b/theme/adaptable/pix_plugins/mod/assign/icon.svg new file mode 100644 index 0000000..cb4df17 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/assign/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M33.53,27.56l1.29,1.29-5.89,5.89-3.47-3.47L26.76,30l2.18,2.18Zm-3.38-2.81a6.39,6.39,0,1,0,6.39,6.39A6.4,6.4,0,0,0,30.15,24.75ZM24.68,13.24v3.28H28l-.39-.39ZM13.71,12V33h8.42l0-.17a8.08,8.08,0,0,1-.17-1.65A8.22,8.22,0,0,1,29.1,23l.15,0V18.36H23.78a.92.92,0,0,1-.92-.92V12Zm10-1.84a.64.64,0,0,1,.26,0h0l0,0,0,0h0l.11.06h0l0,0,0,0,0,0,.06.06h0l4.91,4.91,1.48,1.48h0l0,0h0l0,0,0,0,0,0,0,0v0h0l0,0,0,0,0,0,0,0h0v.11h0l0,0v5.76l.11,0A8.23,8.23,0,1,1,22.8,35l-.13-.26H12.79a.92.92,0,0,1-.92-.92V11a.92.92,0,0,1,.92-.92Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/assignment/icon.png b/theme/adaptable/pix_plugins/mod/assignment/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce27f6d0c664e518829d2548b2814aa0fe983946 GIT binary patch literal 5758 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 M)78&qol`;+0OgajOaK4? literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/assignment/icon.svg b/theme/adaptable/pix_plugins/mod/assignment/icon.svg new file mode 100644 index 0000000..cb4df17 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/assignment/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M33.53,27.56l1.29,1.29-5.89,5.89-3.47-3.47L26.76,30l2.18,2.18Zm-3.38-2.81a6.39,6.39,0,1,0,6.39,6.39A6.4,6.4,0,0,0,30.15,24.75ZM24.68,13.24v3.28H28l-.39-.39ZM13.71,12V33h8.42l0-.17a8.08,8.08,0,0,1-.17-1.65A8.22,8.22,0,0,1,29.1,23l.15,0V18.36H23.78a.92.92,0,0,1-.92-.92V12Zm10-1.84a.64.64,0,0,1,.26,0h0l0,0,0,0h0l.11.06h0l0,0,0,0,0,0,.06.06h0l4.91,4.91,1.48,1.48h0l0,0h0l0,0,0,0,0,0,0,0v0h0l0,0,0,0,0,0,0,0h0v.11h0l0,0v5.76l.11,0A8.23,8.23,0,1,1,22.8,35l-.13-.26H12.79a.92.92,0,0,1-.92-.92V11a.92.92,0,0,1,.92-.92Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/attendance/icon.png b/theme/adaptable/pix_plugins/mod/attendance/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ac36e3bf27cea906b81c98152d5c60eccf158866 GIT binary patch literal 1154 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l-rPZ!6KjC)UK?JvINC2-t6P2=sy>_B0yrDt1S`~EU`ICUZC)XQm)>=kzO+m=a& zFA4N|yufaexX-NsRo*S<m8*hurVBd>O*&w9?zZGHwy^TwckfppzgJ_Io*cA_??t$1 z#iRp2xqon0G_=33o}IThT%v*FF59dFi3k7YH_Bbm<x;a}5ZuEovoThRVeTG>P0RtS zxMCRA?}*;Tc<<qF`|a!wE7dOCJrLTpBTV}K`?bkBsRI9)cI<P182P4j?~Q6^ZjGy~ z_daa+B6&am`St^O9Q+^B6utQvl6J)k*zr74n$7KbAa3*QFZm`LrZal1`m@h0Vg0&y zZhIa-*17pI(&FeeWghXFo6dfI&Y#+NxV7hOC__-<XVI10^S3KsTkc=EBYt5>Z1|fE ztX4bC<@lbyKghVJE;nZN#R=cnUW<)fv^hgYoWbSy(W=?a%oi8rHm7o~;Fo{!rDW>$ zO$&k=Uy7(r_GOUC)Xw1E!mVFglyv9i8=km*A`gH6P2K0_+>rU}lk1P}VCf&s3nD8d zbzKBJ#5U@D*b{m-d-a#!0joQ&+MM5!SeMns-T7POeQ{1SOIID+gI$lbrgEK0JEnD5 zvB&LGOd&(NMyUSl1Pz~QtX@a!m+K0$Y}l`V;8=2}^li4|H%(@2XHekoJ)XpST=4Ae zZ5j7IZ(~}p>!Q$P&L4Gu$~Q-9d@SwxZ+*_Jg?+`eV73o$cLwap6Z!trqAKWKMdw2U z%jN#d%iqnN&FE1yw|a?C|D12nw%+<<$gCmGcgO4Kk`oFY&o8p-Y5JAeUicWaz+OxF zc<oK&z%9#Xo?X%^(LcS?&u>XZ=Df|DpZ+_sZE5vY8-e@IOE0hATe<dG>N^)*?tOKp zPA|_c4;GBd-g-Y?%;&SY)Ul|)`!*(3tPRVKlzeHTp~${g@^xHo<m>0pKkW-LT$ga$ zZN}x`%VDp>lwLKh)6RamDE$0YiHe^LE?cFp<xQKjzH-vFlkwIzlCP%z(hqi@^~CAn zJ-d^wG2yp3eGBzO%8q_5>3;uz@oNYD%gU2GO_Bq3+io{2bi6lOEU8wrY2o!tbF<7| zwQ@(AUw-kz;-k8dyuH*}S^4Ag{Vn#_BePNiY$iFXOLx5cv~xnx%bO<(RXEa(f5vC; zy*R-zC_8}t)zzsVSQ=TDiPf1Hc@@M?Hp@0W>G1paCcj^e2N>?JcW1oPn4eu-A2Ta^ z>w~|)ztpTd?&16V`Ra9I>v&(9K5VrM(`=P8=HD3;p&A<)Yi{Rm-FH%Map0e0hZT9) z)Gi-6da@#Cp0D3>^M}!MGPFPD87|aKU!ve4rZXpWir4dxHXq;1+h4j~`O9SG+8wbc zW)zrdt^2ZquiV?vwvK<t9mj|IkJrtal%G-dyoos|FVjo&a+vgwJ*N+?pMT=X!PD$z zv&(+De7hdMdDeltX%Xt%yv<MO`YikRrv6KCbA>{#+}(cLNe5qqPp$s5^KAZO=KEV* U{~Z6qz`(%Z>FVdQ&MBb@06u^y7XSbN literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/attendance/icon.svg b/theme/adaptable/pix_plugins/mod/attendance/icon.svg new file mode 100644 index 0000000..40a53d0 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/attendance/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#f33;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M33.15,28.59A1.41,1.41,0,1,0,34.52,30,1.38,1.38,0,0,0,33.15,28.59Zm-18.28,0A1.41,1.41,0,1,0,16.24,30,1.38,1.38,0,0,0,14.87,28.59Zm9.13,0A1.41,1.41,0,1,0,25.37,30,1.38,1.38,0,0,0,24,28.57Zm0-1.84A3.22,3.22,0,0,1,27.21,30,3.26,3.26,0,0,1,26,32.46l-.07.06.17.07a5.6,5.6,0,0,1,2.42,2l0,.06,0,0a5.49,5.49,0,0,1,2.42-2l.17-.07-.07-.06a3.21,3.21,0,1,1,4.09,0l-.07.06.17.07a5.55,5.55,0,0,1,3.36,5.1H36.83a3.66,3.66,0,1,0-7.31,0H27.68v0h0a3.66,3.66,0,1,0-7.31,0h0v0H18.53a3.66,3.66,0,1,0-7.31,0H9.38a5.55,5.55,0,0,1,3.36-5.1l.17-.07-.08-.06a3.21,3.21,0,1,1,4.09,0l-.08.06.17.07a5.6,5.6,0,0,1,2.42,2l0,0,0-.06a5.49,5.49,0,0,1,2.42-2l.17-.07L22,32.46A3.23,3.23,0,0,1,20.83,30,3.17,3.17,0,0,1,24,26.74Zm-7.31-5.48h6.39v.92H16.69Zm0-1.84h6.39v.92H16.69Zm0-1.82h6.39v.92H16.69Zm11.89-1.84a1.84,1.84,0,1,0,1.84,1.84A1.84,1.84,0,0,0,28.57,15.77Zm0-.92a2.74,2.74,0,1,1-2.74,2.74A2.74,2.74,0,0,1,28.57,14.85Zm-6.34-1.71.64.64-2.7,2.7L18.39,14.7l-1.46,1.46-.64-.64,2.1-2.1,1.78,1.78Zm-7.37-1v11H33.15v-11ZM13,10.29H35V24.92H13Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/basiclti/icon.png b/theme/adaptable/pix_plugins/mod/basiclti/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..61596c3de711378327e6c0876457c3279e68dcf2 GIT binary patch literal 2008 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 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/basiclti/icon.svg b/theme/adaptable/pix_plugins/mod/basiclti/icon.svg new file mode 100644 index 0000000..43a2a1d --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/basiclti/icon.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg style="position: absolute; width: 0; height: 0;" width="0" height="0" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <symbol id="icon-Solutions---02" viewBox="0 0 512 512"> + <title>Solutions---02</title> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#0066CC" /> + <path class="path1" d="M5.5910779,1.5819702C4.8241001,1.5830078 3.9370643,1.9550171 2.9290553,2.927002 -1.2530075,7.3959961 5.5171023,9.3969727 5.5171023,9.3969727 6.0431105,9.6259766 6.6341215,10.059998 7.1431008,10.513977L1.9650531,15.75 8.1071025,21.963989C7.7051197,22.286987 7.2760979,22.570984 6.8851014,22.744019 6.8851014,22.744019 0.20202896,24.864014 4.2990686,29.213989 8.7191106,33.445007 10.696127,26.598022 10.696127,26.598022 10.864159,26.200989 11.147183,25.77002 11.464142,25.360962L15.569239,29.513977 20.615266,24.408997C20.350309,24.224976 20.080285,24.067017 19.828267,23.955017 19.828267,23.955017 13.146172,21.835022 17.242234,17.482971 21.662276,13.250977 23.639354,20.10199 23.639354,20.10199 23.749341,20.356995 23.905349,20.627014 24.08632,20.896973L29.173425,15.75 25.033355,11.563965C25.54337,11.115967 26.133344,10.680969 26.656362,10.451965 26.656362,10.451965 33.426471,8.4530029 29.242395,3.9799805 24.94235,-0.16400146 22.844298,6.5980225 22.844298,6.5980225 22.618283,7.1270142 22.190299,7.723999 21.7473,8.2379761L15.569239,1.9879761 10.43117,7.1879883C9.985119,6.6719971 9.5551206,6.0739746 9.3261129,5.5429688 9.3261129,5.5429688 8.0970927,1.5800171 5.5910779,1.5819702z M15.568201,0L21.557294,6.0599976C21.845323,5.1939697 23.341315,1.2310181 26.579333,1.2310181 27.790349,1.2310181 29.008382,1.8150024 30.201392,2.9619751 31.736447,4.6019897 32.278445,6.1879883 31.867429,7.7310181 31.174428,10.315002 27.990363,11.497009 27.190366,11.757019L31.139453,15.75 23.883315,23.093018 22.939332,21.687988C22.693296,21.325012,22.501338,20.979004,22.365289,20.661987L22.330316,20.580017 22.305291,20.494995C22.030324,19.562012 21.060279,17.508972 19.96926,17.508972 19.489274,17.508972 18.877267,17.852966 18.196227,18.505981 17.739251,18.992981 17.167222,19.77301 17.334216,20.424988 17.591239,21.453003 19.408279,22.343018 20.248256,22.617004L20.314298,22.635986 20.381253,22.666992C20.698274,22.804993,21.039283,23,21.399272,23.247986L22.784301,24.205017 15.568201,31.502014 11.794164,27.682983C11.259183,29.085022 9.8101283,32 7.0260945,32 5.80507,32 4.5670773,31.404968 3.343062,30.234985 1.4890334,28.268982 1.4310492,26.615967 1.699058,25.565979 2.2630306,23.362 4.6350714,22.130981 5.8230759,21.640991L0,15.75 4.9900566,10.703979C4.2040354,10.448975 0.99903666,9.2680054 0.30603408,6.6749878 -0.10699617,5.132019 0.43604087,3.5449829 1.9200082,1.960022 3.163067,0.75701904 4.3830539,0.17401123 5.5930921,0.17401123 8.8411218,0.17401123 10.336136,4.1610107 10.616169,5.0100098z" /> + </g> + </symbol> + </defs> +</svg> + diff --git a/theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.png b/theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..da068a2e66a61b8b15e9accd6590ed5d8afd7b7c GIT binary patch literal 978 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tI9PZ!6KjC)UKd3#F-iX5-+6j~|4vm`>~NMqB|rI!w@d2=kXRNd{?-p6U%eD^!< zYkX`becV66t31}%?5$SGfzVc`C9W$?CQO)=lrclENZF9zxHw%#*>=L&CwAxl&nrIv z`B=<#RgI%usk|@#i6o>Rxe&a7=|;m_sa3KS`H>78oh+H3H0T)U@2r>AU|N>F!cT?a zNapiK-a|`;CVbZxJ<pKTx`}DV0kQ4g?;Gc2ub6s);Rx4LodnhZQOT>9Vx4vSq$R!| z*uyE~dcr1Yz4FHGdsvcMH?h7c*k1BNYSp`FCgDRi2l^fywPsM<<(aTgyyc~5>ycL5 zB%{pT|C8dG6?~Qoe{B7lJ=bWj+P3&sAN@10S7*jJAH5f&I(0^u(1AS<3P131@V`Fk zsd-Y8!AxxOjoJQ-*fy$G$WCyGOWP{`l6{8$^u=LI|Cll`O)SaTym?k%6qj_YW{K(o z_E^Ku1!h^AFZMEQatsfCCKSkgg2{(Fki~5F-R`xzs_p;cGh>1>w>WL}*yt2FTP!lf z!hO*ai>j|TqR+&voTxwlamZ}PZC4&8G8L@yeYJL*dGv}UKR*4^{FP;8z@VzbFx8Fw z^N&N<=P>(9t}Kf)F|TuG(o0`(I(^UYKX*5)FzERH;t5}tf7oWmaq)=w)hSa&ZUqUc z&UElRx54O8(AqPqCzhlyeRp^*1H+1~w<6R-*Gzu3_5I2tBIjnOGbaQY7xfwV9SO<a zy3$0?`LpswW{KzS?{pa=yDn>fzZ-7VdwFH(z3^2>UW!D$*S;CVkhOL{^Zk^w2?-2~ zW<-4#|NKYt$G2}knRae%-B?_l&7Gs8*6y~tPPpcJz}t1o+P_vZU)5`G=d6Bx`pw$Y zOQ#>^OXU%*KD*+n`752d=1!NJFI76L^=w$jz+tNObLQONUzF!K`LEX99Jcbd`-F8~ z7FX47%kTf$Wz}xDzSd%*<zMZ%-`d5lHD(4uotj12{THS^JFz5vrhM?+729uFTvhva zGylxv`B(nPAE>?_uz4|qMoU1HkLohERU+rsFXP$F{NtszE}!<hR|gKZSgnh_9<Wxr z+0rnfo@wU&_vv*jSgKSK4J~b(CdSU1_;AaN0*jMp`p>c-Ir7&!OxiZU>9JH6-;J-* z`mG7MWk>IsPrC26-Svm;FGhXu<?e4Z{eRZalj>nuBz!KJ-}~EBFP$aN%_0??m!F%p olQAH+qDs18kBzE?`+tViV>zdLHpD()U|?YIboFyt=akR{07F-~EdT%j literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.svg b/theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.svg new file mode 100644 index 0000000..827cb16 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/bigbluebuttonbn/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#09c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.01" r="24"/><path class="cls-2" d="M13.67,21.52V12c0-.34,0-.67.32-.88a1,1,0,0,1,1.09.09,6.24,6.24,0,0,1,3.06,4.37,24.18,24.18,0,0,1,.21,3.3c0,3.51,0,7,0,10.52a8.79,8.79,0,0,0,.15,1.57,1.2,1.2,0,0,0,1.07,1,8.46,8.46,0,0,0,1.37.13l6.71,0a3.59,3.59,0,0,0,.77-.09,1.09,1.09,0,0,0,.94-.94,4.89,4.89,0,0,0,.09-.9c0-1.91,0-3.82,0-5.74,0-1.14-.39-1.5-1.54-1.58a18.42,18.42,0,0,1-4-.51,6.51,6.51,0,0,1-3.13-1.8,6.33,6.33,0,0,1-1-1.72c-.22-.51,0-.79.56-.83a12.69,12.69,0,0,1,1.46,0c2.21,0,4.42.06,6.64.06a6.13,6.13,0,0,1,5.81,5.27,20,20,0,0,1,.13,2.36c0,1.76,0,3.53,0,5.29a6,6,0,0,1-5.1,5.76,34.36,34.36,0,0,1-10.56,0,5.82,5.82,0,0,1-4.84-5.32c-.11-1.91-.08-3.84-.08-5.78,0-1.35,0-2.7,0-4Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/book/icon.png b/theme/adaptable/pix_plugins/mod/book/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..de7a832ecb102557375d2e8031f5b1e236a89d99 GIT binary patch literal 899 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_owRPZ!6KjC*fqZS<CL6lveTSNJgNQ@t2LCAqj%1%W#ore@lBNPpDQjh&q%`M>c2 zo71f|z6sjb&91gC;hpE3vngdQlhcDE6C50h+z;K<nEmKk%H9>bH&?&P{cy0%*n0o_ zvu9?{G++E<pTAHrPdMu;2i6xK%Em`(3r+vD=~IAZ|Mc7_)Rj0_x&ZnNqf_?Wx4 zav_7$k4-;5eYqaRbYv1^C(i<jJl29=4?b$xF8aMKtUt&?IB*YZPE1pUi?sBjvnjR; zZ9cLy9%bc7ub5-L#E4l(?qXSNMkk+v!XstJIaistG78GKtZ<#h-1TtQ0k0eKr;jF{ zxoNm6tFcCJw#VJi-|Y?V*Y<~8*>3vzjGVwiqe&I-Z`z72@9sS6eXHw2MUJyAt5U08 z_CgoC=}SusTHcvtZ0k>2xlzo^r}z1XZ?}T3GJKV1I9l*^#pAlXRmSsIt>1si#^TT{ z`RPLMa^60!xwHJH9m|B$15=WA?8?vf%jci5N$$(Ot%YBVj3+M+`ufVceiEN*@Ea~G zhC{zEtzKgn>ryVIW4}vw=O35C9r~-cChZ8le=X~-XH4(s>EWMM*ZiDRviH;zj%RNq zo<8d3yKz|Q*_#<#{xgWotI=ZEpOW=!TbZJwn_<jNt?g|`995>-R(xz-6}7;~a+1f6 zH!@shX-W+F>vg4>e&1a@)yH7jvI|%KGbzkZYN%<Oa@+KP=>b!R=TGK`-9E?AE5dAI zr1X12*y`o-4okL|2W!+_-)_2Ix;0CaWd-Zu6;quUUP~O<zLjmmJf)AXoj!NA`dDcP z?wGk(t08%<b7OFzv*Df42bmFXuN@Tp88hwJ>E99Zb!tID6VAU`Tei2JNx>mZRpscW zKWY72j~e}qnKt!!`L9Qp&d$9*QOW<kho$b$)lBmb2P-cQdDr*NwPtf|$L6i_(-vl5 z-1+9iIaUDys|{ue!sRPx`|;fE?P~IF*86wo?H2~eIhuA~Yy=uIVjDk%&G=#b{D5Y$ z)6&Vj`_^PH&$TK)y70U}O8zTfy=T+xUX-r>F10eBf$5RbJn17<X#(5}*q3PS+d1RV z{jS(l-E+?UE)2^LtSAvWs&!%S<+pbGZ@yU<*`UrB7#DNV@i+Gy7lU^isxziAFfcH9 My85}Sb4q9e09@0g4*&oF literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/book/icon.svg b/theme/adaptable/pix_plugins/mod/book/icon.svg new file mode 100644 index 0000000..2348143 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/book/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="23.77" r="24"/><path id="path1" class="cls-2" d="M14.91,16.85V35.69a.9.9,0,0,0,.93.88H32.76a.32.32,0,0,0,.33-.3V17.58H17.28a4.19,4.19,0,0,1-2.36-.72ZM17.28,11a2.36,2.36,0,0,0,0,4.71H27.43V14.29H17.27a.93.93,0,1,1,0-1.86h12v3.29h2.17V11Zm0-1.86h16V11h0v4.71h1.69v1.86h0V36.27a2.15,2.15,0,0,1-2.16,2.13H15.84a2.73,2.73,0,0,1-2.76-2.7V13.58q0-.11,0-.22A4.22,4.22,0,0,1,17.28,9.14Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/booking/icon.png b/theme/adaptable/pix_plugins/mod/booking/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f7ccb4b8849285280c51a09d476587c7bb623a59 GIT binary patch literal 1749 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 Sv@kF*FnGH9xvX<aXaWHDD>1qN literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/booking/icon.svg b/theme/adaptable/pix_plugins/mod/booking/icon.svg new file mode 100644 index 0000000..04c1d48 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/booking/icon.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#15B715" /> + <path id="path1" transform="rotate(0,24,24) translate(9,10.4563647232302) scale(0.937502514577696,0.937502514577696) " fill="#ECECEC" d="M30.574984,14.206012L31.999914,15.598011 19.311893,28.893012 12.976915,23.821014 14.203846,22.250029 19.133914,26.196017z M1.9970007,3.9820013L1.9970007,7.9900098 24.040011,7.9900098 24.040011,3.9820013 19.999988,3.9820013 19.999988,6 17.999988,6 17.999988,3.9820013 7.9999876,3.9820013 7.9999876,6 5.9999876,6 5.9999876,3.9820013z M5.9999876,0L7.9999876,0 7.9999876,1.9789991 17.999988,1.9789991 17.999988,0 19.999988,0 19.999988,1.9789991 26.037012,1.9789991 26.037012,16.207012 24.040011,18.275014 24.040011,9.9930191 1.9970007,9.9930191 1.9970007,24.01602 10.084005,24.01602 12.579006,26.019021 0,26.019021 0,1.9789991 5.9999876,1.9789991z" /> + </g> +</svg> + diff --git a/theme/adaptable/pix_plugins/mod/certificate/icon.png b/theme/adaptable/pix_plugins/mod/certificate/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..45403a9075bb57a0fa455c5f8e404c87c37ff668 GIT binary patch literal 1891 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 RhJk^B!PC{xWt~$(696L^n<fAN literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/certificate/icon.svg b/theme/adaptable/pix_plugins/mod/certificate/icon.svg new file mode 100644 index 0000000..8fc3ce9 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/certificate/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#336;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.09" cy="23.77" r="23.96"/><path id="path1" class="cls-2" d="M25.57,26.3l-.34.24L24.6,27l-.22.17.66,5.6,1-1.63,2.12,1Zm4.68-3.44-.19.15-.64.47,2.79,3.78.26-1.31,1.78-.49Zm-11-2.58-.26.19-3.82,2.71c1.54.43,3.31,1.74,3.84,5.54l3.22-2.3.15-.09v-.06l.11,0,.34-.24.19-.15v-.09a5.52,5.52,0,0,0-3.61-5.41ZM23,17.63l-.36.24c-.86.6-1.7,1.18-2.51,1.76l-.06,0,.13.07A6.44,6.44,0,0,1,24,25v.13l.71-.51,1.63-1.18.15-.11,0-.19c.26-3.09-2.12-4.83-3.37-5.5ZM26.86,15l-.13.09-2.34,1.59-.54.37,0,0a6.89,6.89,0,0,1,3.54,5.47v.11l.52-.37c.6-.45,1.2-.88,1.8-1.31l.15-.11,0-.09c.64-2.81-1.7-4.83-2.92-5.67Zm6.65-3.86a2,2,0,0,0-1,.24c-1.18.67-2.83,1.72-4.68,3l-.19.13.15.11c1.25,1,3.15,2.81,3.11,5.34v.19l.52-.39c1.83-1.37,3.44-2.6,4.59-3.52,1.48-1.18.52-2.86.19-3.35A3.58,3.58,0,0,0,33.51,11.15Zm.06-1.85a5.51,5.51,0,0,1,4.19,2.51c1.4,2.08,1.18,4.4-.56,5.8-1.25,1-3,2.36-5,3.86l-.32.22,6.87,4.44-4.64,1.25-.86,4.27-5.26-7.11-.06,0-.73.52-.07.06,4.68,10.78L26.71,33.5l-2.88,4.74-1.16-9.85-.56.39c-1.2.86-2.32,1.65-3.3,2.34l0,0-1.35,1a6.19,6.19,0,0,1-3.56,1.31A3.87,3.87,0,0,1,12.18,33a5.48,5.48,0,0,1-2.7-4.61,3,3,0,0,1,1-2.81.91.91,0,1,1,1,1.54c-.3.26-.41,2.17.64,3.5s2.38,1.22,4.23-.07c.3-.21.6-.43.94-.66-.34-5.22-2.9-5.17-4.14-5.13h-.26a1,1,0,0,1-1-.71A1,1,0,0,1,12.35,23c.54-.39,14.1-10.22,19.28-13.18A3.79,3.79,0,0,1,33.56,9.3Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/chat/icon.png b/theme/adaptable/pix_plugins/mod/chat/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c4cef5af692fe4b51137536ab8fa4b179c8ae08a GIT binary patch literal 1295 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qXco-U3d8TX#f+Mo9*LgZMz%ZW?}g^6t@g|4O<k(R!fj@(`%8@4iSR)y59r@W7B z4o)h{VB&9ZGT}Ec=gnl(4ZOs2b{5;LjV>v(3J;6@h?>B#QFhXX-R@2tJEm9f`~2Mc z`<>S}Dr}zZ4C)Onb=Vh@kbdxjTmmDLz|ulboAS32Aq?{_u?4UM30~jNKFdK@YsLiz zo1?xD8m^uaO=x2kl5j8$3vJ~3FmL))?*%bZ)7MO8Tybob>w<4R0i8loZkaJzcBTc& z4WC3GJQAIHJo@#aXzN|<t2SowT0F~L8YeZq=Ym{>@KeS<+t?=N4>8;MPWd;lVBu2K zPt=d_|M@pD={&=S6`MYv;*_2?m+P{~Z-W-C>@|B*mY<5@uMqu~eWO0)5?6HV+L`a` zO6nf)-JQ<zY4(~MA6M$1U9fp_)}$KC{-_YoH!9EiB)_`Vtgn5(t?0K<f=~Wqi;oTx zud|o6l`UdSxPK$Um-~2;M5Ho<P*&60L(}-r?JXB<JCGHn|A_S^qw)!!y~`$BpG&-d zBWC*gm)-yF6vpn_a!R+zjqh*F^AC>%K6+kQTN=)JfT6|bQIf#v=Y7Q`uVbZJk2yVE z`lLr@_p^2XEB9~epA;6xxqw69_nGJFht*fA{rvEq{f+c&w}pZ?f*0TG$bV+emei1J zsXysV;Y=R?NZ|*&YGfmR=G6+hy}O_qd}6^&DNj?+Am2w3e0@(J@WuXG)PHy5Vx|ks zj5n@rm;CD1c*splpXcDVeZ6s)<qbo>DeTrdcIj~(|L-L%oQ<)?(*yHOCLfrzHbufS zTm1@)r>W<%368bv7tPIha{A|no~6g8n>;W+^}MfGBQtejt4eN{f7977YQJ{M&2XB! z#cAIhck7FXjINz?n-KWv{FhsgGER#aF+})spPh51bHTNecD>xqzK^f7z1w@bE_MEr zSv*gg92kBr>MzY)xG(%~%e?98L6Tcn{BqUnxgk3{Y-j7Fpn&dIuNXYCZ<(9@+W9W} z$3)fl>JOJLnQ~%kc$M|)^l<J2vb$<7?oYUXBST$Eer>;T*d}W;uPoEntOC;n<Gnkh z7=&CFUszCk%gs|;CQjzp{=YK+zI5OFQZ2%1Z2nI6``ebt1-aV_b=d#L9G8$Xo_7A# zP0xKISC_8Y+A~qex^sczjoS%DVUkfQ^R+cw4_<BfmO1-d=In{4kJc5nPYRm0>6+5X z>7VTiSp>AtJz4Nofs5h$Jy%an4rd#!lJfNOJ-;8zy3bwA$!N1N_JoMhoA7TZ<x&nC z%$UI7H}7$4W$3QZyoV8d)%EWRzg(ZVe#+gA>4#S7|N8fjb3yH`XWNRTXUL>F8~0q| z&R%xRfoIyT=(7AsTkmJn&m`YkQ~p;@xnub<=|7itOqs^(YkbNwV8Yqy$(CDAt(_S# zQDkTM&Cj~}dxf25T{em87IF%#irt-jS-kewqW9OQb_9K9tdRY8HGBTV6T2?0ZO=1Z zFE-b!<|(g>gi_A#OuJP_<QARYZncq1uy6Y1?H2sog;L-D$cue??{eY4xD(dJCf6hy zK7M%h@?zRK>%X?q&QH%ji?!6<voLZoH(x}N_vQlYx4tXZZ~o-}jrrb@&E|Zwms<Y3 zzdvoR*PBDyS3AAcwi}#PX#Q{CFQvnf^dZ0cMlz?w&!Ebq$8-NN{wbJ{CG^tHiGhKE N!PC{xWt~$(69C^}Yr+5k literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/chat/icon.svg b/theme/adaptable/pix_plugins/mod/chat/icon.svg new file mode 100644 index 0000000..e7f9baf --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/chat/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#909;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M28.71,20.21c-2.4,0-5.47.45-6.24,3.92-.84,5,3.84,6.21,4,6.26a16,16,0,0,0,4.93.53l.32-.06.21.24c.26.32.71.77,1.14,1.2l.09-1.63.41-.15a8.69,8.69,0,0,0,2.91-1.61c.62-.81,1.54-4.33-.26-6.26a8.36,8.36,0,0,0-5.49-2.4h0C30.22,20.27,29.51,20.21,28.71,20.21Zm-.08-1.76a17,17,0,0,1,2.32.17l-.09.66.09-.66a11.3,11.3,0,0,1,6.24,3c2.34,2.51,1.28,6.9.32,8.14a8.45,8.45,0,0,1-3.07,1.88l-.21,3.56-1-.94c-.06-.06-1.35-1.24-2.08-2a18.08,18.08,0,0,1-5-.62,6.69,6.69,0,0,1-5-7.86C21.92,20.34,24.66,18.47,28.63,18.45Zm-9.54-4.31a13.56,13.56,0,0,0-2.74.28A7.33,7.33,0,0,0,11,18.53a4,4,0,0,0,.34,3.88,8.55,8.55,0,0,0,4.07,3.39l.3.13.49,2.1.47-1,2.1,0a11.66,11.66,0,0,1,0-5.25,9.08,9.08,0,0,1,5.81-5.92A8.41,8.41,0,0,0,19.09,14.14Zm.11-1.31c5,0,7.09,2.87,7.2,3l.62.88L26,16.88a7.58,7.58,0,0,0-6,5.27,11.49,11.49,0,0,0,.24,5.36l.24.86-3-.06-1.71,3.81-1.2-5.29a9.21,9.21,0,0,1-4.33-3.75A5.31,5.31,0,0,1,9.83,18a8.68,8.68,0,0,1,6.28-4.89A15.64,15.64,0,0,1,19.2,12.83Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/checklist/icon.png b/theme/adaptable/pix_plugins/mod/checklist/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90723a35b63c005a469e1fc22caa2253f938b4b1 GIT binary patch literal 1003 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tK2o-U3d8TX#f+V6KeK;&5c!pJVEtwOOIx)O|%1;0(w^Zf3z#J@q^{cVUQf5UHv zX@@SIntA9HljxgL@m&^z*+z{kUbKW<StWb9F?({$k$r^)kG8pgdK7c}{`Aks@7o@q z8MprUmSwN6Dm{?TKJaSB(&q;xA26+ZZuxG@`Ey596&$1;SSw^c|Ga)c@j{PxS`rh} z4pxyJ@xOT@CJRkeTk!g-=>oo-iEC<E?p4hzF3V>r*&D?;E4amL&IKcz^OikPI}{lA z@yVPKuz3{8{^;gM+Xb($s%<!1lY9QNOmY(c5*C{S+YTuCJM7Toeem1!%2Fo}l{btp zw}c<=v~_Tpa_X|A`kN(JGZ*ha!q*;Pd-TEIS0VB<nIG|5U-j0S9(`on{ROMEJty4| zig;gfV3ydP$5%8&<|W@=KlxjWse^-omJ4GK)B08CfBgJxzV%l75>AmC{gk7e$~jwC zt-FwMWy9OrzCOQqf8I5-uef^e^4mCugjuORn?<-2ci%l~@^*tN+k;<<A)L{Xde1Ar zyil~ZzCGo%K3i?3B;$|r_un$M?%M42g3H0}@)@NMnno{lnu9du8EmbrBrEfdef!k7 zD5LF$XX~uj^A>6N2VNEJI?7v8JJ%^OdENuYbMhKpX`Q(WlP5&P#mzFWXgJfPQ*l41 zdVb#bY^ejxYf??l&Z!8Vb6R&%rq;S@uZmCm7sQ-9?sV;COv3Ho6JKvC%=`EF_<D~I z45v?f=j5&G+@&!4$=UvX|BS6xM}AMPJ3s%x)2E`-r-ye|B|NmtpLSaJQpS}@V#Ox~ zK776KY?&3ep}Bz5^PYTGwq+XL$%hl?%(!eK>Kon9`sVg_?U_Qg^PVmX->IeiEatn& z>8!RUb6;zL;#u45rp}nGP%!Q3vWrm@1dEpj%?o?|?O5Ui#;CP?y=)t86_ojw2^IIc z>8|*Eb(O8Rs#M2LpY_+BudlwJ`t8tPm0NG~80!2cJ-YRIj(VM_)s&rwHqDZeuV<WL z{)1ujf2HhGW}E(~=uQ1J@quWnX29CQdq@8MG&wYjxmGcDy_;I`{>)iYlAk&AH0`VB z1<Onmn&xA4KWxUj5Y^1>k5$W-7b{+!D)H`&?4&;o$x**wRQjdPnNp#Bt2}dSc4Ofd zF(F%b<)bV;4=-MpHB{fVf43&fe(S6!CS^{O7csK8_+%bCm05T&Zq`BfckD-6cNg=` zzA)#{_v347JNF(uD`54}>ik&|H?R4B?dMDNC?<W_Ui(JS>6rbs84sVE)iZ27eJEP> T5a)gd1_lOCS3j3^P6<r_Cimeu literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/checklist/icon.svg b/theme/adaptable/pix_plugins/mod/checklist/icon.svg new file mode 100644 index 0000000..033b320 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/checklist/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#336;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M23.68,33.86H38.63V35.7H23.68Zm-.06-6.21H38.57v1.84H23.63Zm-3.08-2.44,1.31,1.26-7.52,7.86-5-5.19,1.31-1.26,3.66,3.82Zm3.08-3.88H38.57v1.84H23.63Zm0-6.32H38.57v1.82H23.63ZM20.55,12.3l1.31,1.26-7.52,7.86-5-5.19L10.69,15l3.66,3.83Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/choice/icon.png b/theme/adaptable/pix_plugins/mod/choice/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..98a6e6b23fc3f1ca73c1657e3d3d78116c29dbf8 GIT binary patch literal 1030 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tIGo-U3d8TX#fI-h+zP2#w{U(dp&%h>uwrQKLq<&PcWxs^G6#qxP>v1PYbzB+on z>VM-md5z;ggPv}>tYGw;c~#7|(>!{T%|C)>IvwU}l9<XO-BopBb%J%3?B7kVH?Wr& zR6TwE`|59-^S}QDC~`1u^4cDMq2WVNgY3ymf)AMZ9&EXK;N+^^6Q(sVtYy}HQ0MhG z`2gFREh$360ta{#`fHwY7)%oC^j&aUU!#H3CUNT}W~*G&f@0+hZ^fe;rXCErG-KMm zFy`OOTB8jQ%s%i?>Y#FoEZ5za<qK}>M;tJ=TQ9Tas)PR?W@#q52Qv={&2PA|%a_64 z`%q7+)1gHE%NZ-Dn`Wi<{<7^2+UWH4-1hMA%jAz&JZIk1&8R-FwcO5Q@uQcElFthY zEG(LEB=NLr>s@wPsnEHX%4+QGzxEfz$kxA7>-9T(Q($+7Q<~6+th*1E-!-rK{IuKU z0b|xv+qyqh4qx+(GfRw=YaQln=;YI^f0SnQ=F+#?x(tgc+q6C9s+Bgx-DXtQY*?87 zG12OjPttcs^_R)V=J0d4t-r*O5mIXM^y!?<`##J1)v~`&Qg8?h(9)H<`r^R1M5{;J zlT}x~a?dp_=z7#X?;9_lZTI_EZ2yitJ+fJ%>Z0Op+s&*KUx!t#-!j>4D_@LYMyg@+ z{r|Z}e=Eu!@2bBydm>Aa7k7g94F6>|>U=*gO!;(X`mVoC+f-Mr*xC`4=~=6=bWOj* z;#J;re;nCtBp49J8@E=vcw*MQKgVSLr!TvA<<TT2o6S?UY1`Cg1Z%(dJow-6O49Fu zmNz2dtoy3ApM2cD&BUecL9klsN9|2I`EhLbc2~^hu4tJ0=D_#(nL!_%-#6IbFWWTZ z>?_U_i_XU9*FXEQcjoGKcYIgGT~k@<qF??&u~9I4Lh${_qxbu^?RXQqF6Ph~lk~#w z7_+G2Y~f$R)BnBH-zjb{RLHgepYN-v1m-7!5f|ocymu~I&M0rSqbkqhvU{})sv;Lx zE$_51{dkVub2oqCk$JZ4YxZ!Q2;w+itGD)AP*-UwU&p*ZfqFC3&vw|fnm$dP$9gu< z!u`aRsU3PzPgEa^pR97&xJxLUg=O(Q-GY1f-iV|dPJK~ubKmKLguT=5%}()uaEvLr zL{FuvuS$&fZ+V*u_gTf-YvS(y6Pfugo~oOu+Hx&zl2!eqXM&C2Zf6y&ZB?IpA?HE^ z^E98#B`>#BJV=;)@O}h;2m5z><sW}jA55Qhp~>>@J;T*EIHfbzy`1x+(6V*oW1WBh tYqAs+7Zu;O`xfD(wnyaJkDX`L^DoSK@mozchJk^B!PC{xWt~$(696on?+gF{ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/choice/icon.svg b/theme/adaptable/pix_plugins/mod/choice/icon.svg new file mode 100644 index 0000000..db7f0ef --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/choice/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path class="cls-2" d="M20.81,29.27v-1.5a6.72,6.72,0,0,1,.83-3.37,10.62,10.62,0,0,1,3-3,13.27,13.27,0,0,0,2.79-2.46,3.56,3.56,0,0,0,.68-2.12,2.24,2.24,0,0,0-1-2,4.63,4.63,0,0,0-2.7-.69,15.57,15.57,0,0,0-6.9,2l-2.19-4.41A19.12,19.12,0,0,1,24.9,9.15a10.12,10.12,0,0,1,6.6,2A6.54,6.54,0,0,1,34,16.46a7,7,0,0,1-1,3.83,14.69,14.69,0,0,1-3.82,3.64,12.61,12.61,0,0,0-2.46,2.18,3.42,3.42,0,0,0-.51,1.95v1.22Zm-.66,6.9a3.43,3.43,0,0,1,.9-2.57,3.65,3.65,0,0,1,2.64-.86,3.57,3.57,0,0,1,2.59.88,3.37,3.37,0,0,1,.92,2.55,3.45,3.45,0,0,1-.92,2.51,3.49,3.49,0,0,1-2.59.92,3.63,3.63,0,0,1-2.62-.9A3.43,3.43,0,0,1,20.16,36.17Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/choicegroup/icon.png b/theme/adaptable/pix_plugins/mod/choicegroup/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..335c64563680a9667c6f9872c1ab79d2df93a0d5 GIT binary patch literal 1437 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_oAtPZ!6KjC)sS+2=ee6FF`#p32E|H?*Ws<cd$)9+L{sX$nh~!!AEniEt9XP^u|z z6uQdkO5a!0VC~IQbQYv#YDFwI_TFTnY2LPUWf7NfkiVP3GUk6f1^yjj=~MRql=S=i z=ik46-~0UidtF^ob@k~T%QQ|JDD<U1*vz<nAG=lqV~9_%z1;Vk{&HLnfs?cyxNb1b zv-|M#ePgmh`m9}sq72_|pJR~rxpt3fWnkkDwgnm|(+@CiINQF}NWk3p@2RtAi}mK- zXIi)HfsNT_QCH7n>P!xSlO!MX8$SA@_)X-8Z{Vbz8=X$fOPJlCotkjk<ZHcqQi9tY zam7-bAP!A0BU9yb)(UK`!TuaKQ$O);&Rp*kY%jraKyPYzu-}cqx5byQXg52(NH{iE z-0quEf_L-&3Yl+PGhbOY{Op@N)AP)m&y^hQ;T)|gHFo^dr(d7qwfxSVE9#sI%ui(4 zL{xY3{IPwvW|NG=<v1nJfAg|`|85uQUOt)S<J8Po$D10jtXM3}DH<Ak`1$7q2~nQK zPHMu(U&k)Em=UNmFDX*@&_mIb%Fexeb9GjCCWff7D)7m0v#o2(_#t;X?edy+r)t+< z%_>dWD8F&dv;Q^ek6e_puVj^meBJH2>BGr6bN8&98-BI)_|NO>`)}O+JGHxeRdMX) zzFl*~pEIbO?AKve<4njt7w5WjTS3Lqw1qBq^-YN(9x5i!tM;dCU9*h&((~vUDh(e_ zFtjjVeN}qB^5KI=i}vTI3ayC@%bvtA?Ri7`;vNmL^K$}Tl>~&$b8$NQ{BudK=kj=q zpNb9bPMeB*1jD5LofaN>o5532@%h>FZRd3NKmGJ+Rz!~Q`tpU^6IlaXC(hlze}8?4 z%FpU|;>nwCUOKU~Ood^SjKbtj<<s865C1<})OG#!%USC~tdu98+&M3~PUQLKn_E6V zb2;NZY5(iPp{uX1yjmq3x#2>_mJ?~+Pd=-!cx5$x`gId2-$fTeNNJ*}&gz#T>pB@P zY?)rMmpf$j`x|%vw$7eyWjCKa)?e8(_qfU175N+;mu_cl@lY{Ybu~N1^1!`&X1#9R z1s1K#WCK@Uy?L|J(PWqB!h%O$b!T?2{X6N&@{1W0eX`CSx%fhD)1D_5W*T*iFZwck z`YEj5>1z7=OzXF=<*~_XhZ;IAZnMcKV~F0opY!OvO}FxER+eOWcRPgd^i>IA;92an zvgFjcbKFe_8?qUm{+<<*wCU(k|KHR60t?wfbrUB&dA@vp+_#INf(%I;&rTGIVA!#X zzacX$GIr|C8x?+b|80BSgx6nx{EBgI&)oUpnj(H2Y|OD&-9=iGKCXE#T@-&jX<pgp z(icqo0xSwcQuhn3dYx(V_Iu;w?5$Rlp16lzpZiLP>*R~7TWJojh3cABJmq)KT7Fk^ z-*??(N%jI<rP>!F-f)|l%~L+@-DiAQwSwjFiZ30ui3=zAsBC)nWHz_4oXT?R$JP@& z7OZ-08-LAF?bL?UtrxzUv>xI-`bcx(gq~%!&DVDyIO@8!dun1)6oUoBzP}T?Z|s&o z6!h{8r?B8WrUavvyeTUW|1<He&0gevs&J{t&Og(9)Se%!O!vHbbE*Z~zBz8*3)ea| zvotg6tUh_xxAscwfBR)C^}oON@?kyjvvTU^&#!jGcIIg&mha}1cl2MOQ|0t`OF-AR zeOGm&vlh(QT>T;1_|2cy=k{g9n`Y&2+B?hfIsf~zWxJ~um*=*bS-9M3caP$qu}p6M z#3g$C+^?&*N51vlx3^!y?Bl}jD)Zk*zL{nBy?iUXXZhZjweKGtxH{?5zvYRQUW%b? zVRtOv&uX;JX{qelFZG4-lSTbEi9=l$(`G(;Z}y*2L$ma;5NjSg0|Nttr>mdKI;Vst E0N=&AcmMzZ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/choicegroup/icon.svg b/theme/adaptable/pix_plugins/mod/choicegroup/icon.svg new file mode 100644 index 0000000..f189cdb --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/choicegroup/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#336;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.08" cy="24" r="24"/><path id="path1" class="cls-2" d="M15,23.55a3.83,3.83,0,0,0-3.73,3.92.82.82,0,0,0,.77.84h2.89l0-.21a7,7,0,0,1,2.17-4.52l0,0Zm6.81,0a5.1,5.1,0,0,0-5.06,5.12A1.26,1.26,0,0,0,18,29.94h7.37l0-.11A7.08,7.08,0,0,1,25,27.64a6.7,6.7,0,0,1,1.18-3.83l.06-.08h0a4.73,4.73,0,0,0-1.37-.19h-3Zm10-.09a.92.92,0,0,1,.92.92v2.36h2.36a.92.92,0,1,1,0,1.84H32.76v2.36a.92.92,0,1,1-1.84,0V28.57H28.58a.92.92,0,0,1,0-1.84h2.36V24.38A.9.9,0,0,1,31.84,23.46Zm0-.83a5,5,0,1,0,5,5A5,5,0,0,0,31.84,22.63ZM19.58,22l0,.06-.06.08.15-.06h0ZM16.43,17a2.32,2.32,0,0,0-2.25,2.38,2.35,2.35,0,0,0,1.65,2.29,2.24,2.24,0,0,0,1.22,0,2.3,2.3,0,0,0,1.42-1.26l0-.08,0-.11a5,5,0,0,1-.28-1.67,4.57,4.57,0,0,1,0-.51l0-.17,0,0A2.23,2.23,0,0,0,16.43,17Zm6.73-1.65a3.21,3.21,0,0,0-.86,6.28,3,3,0,0,0,1.72,0,3.21,3.21,0,0,0-.86-6.28Zm0-1.84a5,5,0,0,1,5,5,5.08,5.08,0,0,1-1.26,3.34l-.13.15.19.06c.17.06.34.11.51.19l.13.06.17-.13A6.86,6.86,0,1,1,26.57,32l-.17-.22H18a3.1,3.1,0,0,1-2.72-1.63l0,0H12.06a2.64,2.64,0,0,1-2.61-2.66,5.74,5.74,0,0,1,3.66-5.4l.17-.06h0a4.28,4.28,0,0,1-.92-2.66,4.15,4.15,0,0,1,4.09-4.2,4,4,0,0,1,2.38.79l.06,0,0-.06A4.87,4.87,0,0,1,23.16,13.48Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/courseguide/icon.png b/theme/adaptable/pix_plugins/mod/courseguide/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..12e9d3409b7989d1e2f1887cba1f7efdcb969744 GIT binary patch literal 1292 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qXWo-U3d8TXz>Ic6w%iqvKmbSh*Dw4CseV&dE^Uj1Q9v8R4q_@(NTrhC;-Y96{X zEi&xJ&bCbdgO^{KPA@rOne%*;<;g8NZv+$<Se$WKaC2J2rlv1)mvlpq<*hb3w__#~ ze^%+$>iz%fzt)Gg%N#$rM>k}}X@))h90`Y)XG=S9H!vP3(27icw=-S!K~or`2E(<q zIs3UC*aPNH<uPXzui!kuC$~M8N#mnXqMCr#6;}qShFG0#a~U<Prsll9$kD=8%IZ+c z7e4do){y0st}ZX-F>bidFsIJ#L&0;a(&zJPS)E#|L=V{Q%UNwXb<L^0Ox{d#4BujP z-mu(R8qM}Wg|qi$>zxMMr*j>BTo!sTiFh_l=}XKtb#hOw=h%F?Iq%j5UuoTJ1MZ3P z$C+m<J@8t3Qg}m<Ra;P<>~6_Me{E?qujlg<%BuFfQkf`Z{mw~>zam$^w|xEl;%4@S zyFCr%>*qV)_WrzTvRNVP9ocXE(*j!}SdOjEO_{gWu0Z&_>lwfNCu&JbT$-}dJ8$f$ zHnsbrC?9+H9dBf3&HJ}ER2Z98ZD+9TyDh({<e%WJqq>*h9E}lLTGOR;$n(*ql_^gJ z8RcHj-e2S0xIgEfqvgJt78|!sKbEN1xy`hpsK)u{Zh>wG`x{5jh0Re?{QJ%Px>kkZ zf=$0{IQNyMI@)FVv8><c^T1#_3qy{|B8{ilSN?x{PvBFa@K){YFVW9#YWe%DND<K1 z$$HHANoRf7-_N><XU-qr-yaYXy(8yd?>_lwlO|j;6l3CIXo>wkbCt~!*4=*<r#zm0 z;NMkk?p&wW&5xWd_c?C2pJNd|L8+CiwDRcMMa%MnmV`_@XYa>rw%fer$LIJ7+aF~_ z{%6aJDSIEsa&OnW&9em+)E4Q?o-324xWI!cwKi;PpbF!svN=`v%=hW#tqe`5WNX;- z=-P%eEG3uc$;IjmU-))uiqEDc{JS!f|L;DM;m79O`LKa$4p%}LQ)uXs_rD)CpFQ=? z|9W{*&M}939LBEPjA|O?n$IRFb5DAA;m3vaTMJHXyj~O1y<15--!bpPCbk#f+E>Iz zhS)#;mG^Gr-&?1W)!y#Pbk>+9kR<r$bJ(M<*^$!UxjH&EDnd_ZY+B2(`2J^Mg}*aX zg=U_}uUO+Rwfwfck#NRm7V&Po$t)3T?d{Bx7hdMuoj&!<C$rv$868Q@8<rH-wg=fA z-BPsYH*Z6j#CP_!XIQR<DQgIM-erEBfBdDD;`~GF?bjTx*vhIX#yRD|$KL*hl0Ga^ z*BH`yXM1c5+Lv(Th`C5)e!RxKlA7;KieiqM^B28Jb@Mbml$UvJ?yTN<^@|!_vd>Ns zZJ3{>8}fh4pYR_ElQ-E}hF`dl;<$J-OVP{|zGd4UtyuNQrTp`y7n|F7f3A?^*te=H zBs6T_j#S=++J~x7x7_+==yhs&;1&Hx5ld5f-PS2AO}=vSu2WxVjF6Rup242r_R6TA zkHn5n=DWUdUhqk+x|J>yT_?KD^|z|{_{>&yqm6sF{=GfVS?`w`+Fd*x+tkALK;ZP% zQ+}$^H4Rtax21LJSu`|6OWiNIRl8u@nFq_SF~4hms{h8U@%TLd|Ff>fa>_Qe{Pup~ zy50EB{X;Kw=I);t&Rf&5;>Z5IZv-8V{qFeDv;OuU_OtVL1bw?XYX$=Y1B0ilpUXO@ GgeCy=P+DyO literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/courseguide/icon.svg b/theme/adaptable/pix_plugins/mod/courseguide/icon.svg new file mode 100644 index 0000000..bbea3f3 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/courseguide/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>Module Guide</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M24.66,19.5a1.89,1.89,0,0,1,1,.28,1.09,1.09,0,0,1,.37.94,17.87,17.87,0,0,1-.94,3.66,30.22,30.22,0,0,0-1.5,5.91c.09-.09.66-.47,1.88-1.59l.37-.28,1.22,1.22-.38.38a27.77,27.77,0,0,1-2.91,2.44,3.56,3.56,0,0,1-1.69.56A1.55,1.55,0,0,1,21,32.53a1.78,1.78,0,0,1-.47-1.22c0-1.22.75-4.13,2.16-8.91-.19,0-.56.28-1.59,1.31l-.38.28-1.41-1,.38-.38a9.83,9.83,0,0,1,2.72-2.34A4.89,4.89,0,0,1,24.66,19.5Zm0-5.34a2,2,0,1,1-2,2A2,2,0,0,1,24.66,14.16ZM24,11.91A12.09,12.09,0,1,0,36.09,24,12.12,12.12,0,0,0,24,11.91ZM24,9A15,15,0,1,1,9,24,15,15,0,0,1,24,9Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/customcert/icon.png b/theme/adaptable/pix_plugins/mod/customcert/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..45403a9075bb57a0fa455c5f8e404c87c37ff668 GIT binary patch literal 1891 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 RhJk^B!PC{xWt~$(696L^n<fAN literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/customcert/icon.svg b/theme/adaptable/pix_plugins/mod/customcert/icon.svg new file mode 100644 index 0000000..8fc3ce9 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/customcert/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#336;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.09" cy="23.77" r="23.96"/><path id="path1" class="cls-2" d="M25.57,26.3l-.34.24L24.6,27l-.22.17.66,5.6,1-1.63,2.12,1Zm4.68-3.44-.19.15-.64.47,2.79,3.78.26-1.31,1.78-.49Zm-11-2.58-.26.19-3.82,2.71c1.54.43,3.31,1.74,3.84,5.54l3.22-2.3.15-.09v-.06l.11,0,.34-.24.19-.15v-.09a5.52,5.52,0,0,0-3.61-5.41ZM23,17.63l-.36.24c-.86.6-1.7,1.18-2.51,1.76l-.06,0,.13.07A6.44,6.44,0,0,1,24,25v.13l.71-.51,1.63-1.18.15-.11,0-.19c.26-3.09-2.12-4.83-3.37-5.5ZM26.86,15l-.13.09-2.34,1.59-.54.37,0,0a6.89,6.89,0,0,1,3.54,5.47v.11l.52-.37c.6-.45,1.2-.88,1.8-1.31l.15-.11,0-.09c.64-2.81-1.7-4.83-2.92-5.67Zm6.65-3.86a2,2,0,0,0-1,.24c-1.18.67-2.83,1.72-4.68,3l-.19.13.15.11c1.25,1,3.15,2.81,3.11,5.34v.19l.52-.39c1.83-1.37,3.44-2.6,4.59-3.52,1.48-1.18.52-2.86.19-3.35A3.58,3.58,0,0,0,33.51,11.15Zm.06-1.85a5.51,5.51,0,0,1,4.19,2.51c1.4,2.08,1.18,4.4-.56,5.8-1.25,1-3,2.36-5,3.86l-.32.22,6.87,4.44-4.64,1.25-.86,4.27-5.26-7.11-.06,0-.73.52-.07.06,4.68,10.78L26.71,33.5l-2.88,4.74-1.16-9.85-.56.39c-1.2.86-2.32,1.65-3.3,2.34l0,0-1.35,1a6.19,6.19,0,0,1-3.56,1.31A3.87,3.87,0,0,1,12.18,33a5.48,5.48,0,0,1-2.7-4.61,3,3,0,0,1,1-2.81.91.91,0,1,1,1,1.54c-.3.26-.41,2.17.64,3.5s2.38,1.22,4.23-.07c.3-.21.6-.43.94-.66-.34-5.22-2.9-5.17-4.14-5.13h-.26a1,1,0,0,1-1-.71A1,1,0,0,1,12.35,23c.54-.39,14.1-10.22,19.28-13.18A3.79,3.79,0,0,1,33.56,9.3Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/data/icon.png b/theme/adaptable/pix_plugins/mod/data/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9517e8f93f1aec9d00c8edd3c0f0d4eff0f786dc GIT binary patch literal 888 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_ov=PZ!6KjC)sS+4|phkU3VrEN(?hLT>04pUCW!@(S<nt(qBe@|kG|zrzoP(6cXe zBWDIpl}*o7@7|Ec#ib=I6sWR@?}gP9rp}Y6Z`vI{x!GmYxo@AJ{Cl77%;6{{k+*|= z3M2c527m9ooou%p7))C8?Uz*52QjuuPiy4*&|C6@^Ga*JdAQPng2P+^%yx^!%^iGy zl-UY1wn;}duunN5rn^9^X7}1n72XqTM0Z3nRz9&__r&`0`-X>Yr4J_5ToBk7>#5@4 zHr+vSRZ4I6ftliK#byQ+^PW()G)ey1U@}|ib@QYrTkf<h`TQs1i^=;0`Rw{=?ZajH z9$)ut&$&7ALvH>$hQqwvM&9rAs=7YB_4$`}n0LDFOXg`G-oE?MBpDeH?^Y)?e}DNF zf&DgmtMu+pm6;pPl#(fM`d#z6glDDM|8=Wx=Pz4l%QHXYyIko{=gOEWpLY$*PmAnn z=AY7Kefi~9;{zABBu{$X&2X4kcJ9_*w?&^nTr6;Q|50YZw^-DSf9gq_f*p-ae2hLX z_jCsQ{$&@=+9o~C_ol1rp_9|gE8jPz`W?Hn!(#7_%ZHSX%0FCktMS(Q{gvDs&REP0 zw>&5-zWE&U`I$G*95Pt_e7mu6q1XwRhGml+Iiqtdg5EmZt5n$9V0waiMh}};C)dtv zbBe6WYGpbj3=5K(?k-^}X+0_;8+U!mV&S#Cf7Z;4-1xkAZqYF&zb$PSZyygY++Y1) zpIe;6=i%+fhi$cUC3pV&T_^hQ_WYcTSvhjcs*?0QgCDE(%kBF0X!_0x58F<ioAWs4 zg=G`>{jx<%U3|a3J0}0vOz!i{^D&Dj@2rvuF6y$5krPjze(G`2?=O$^JpBz#i~7Zi zL){+lE_IDBniJc5N^(!p<o^aWi>^vuc*mPO?Y48&T*paw0{Z3&pRsumFf-tJL-^#E zJDDc^a6UGp`pTBA3L#+)$E+sD-<o(*y1&S5_Nha678d#GwC_Kwqb9+<$T#vw*@C6& z)A+R*(zO-p*+fE4Xuo$zG3PQ8epU8?ZLvS^u8lvI{J61LVY1iEC+XbgmrVBVI1<d= y(Xp}O=bdE+%QfB@TRFTrb>HD@b%*N@{@l|(XS}(}E;2AMFnGH9xvX<aXaWG1+MQki literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/data/icon.svg b/theme/adaptable/pix_plugins/mod/data/icon.svg new file mode 100644 index 0000000..63591cd --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/data/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="23.91" r="24"/><path id="path1" class="cls-2" d="M21.26,29.39h5.48v.91H21.26Zm-6.4-1.83V34H33.14v-6.4Zm6.4-6.4h5.48v.91H21.26Zm-6.4-1.83v6.4H33.14v-6.4Zm6.4-6.4h5.48v.91H21.26Zm-6.4-1.83v6.4H33.14v-6.4Zm-.91-1.83H34.05a.91.91,0,0,1,.91.91V34.88a.91.91,0,0,1-.91.91H31.31v2.74H29.48V35.79H17.6v2.74H15.77V35.79H13.95a.91.91,0,0,1-.91-.91V10.2A.91.91,0,0,1,13.95,9.28Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/equella/icon.png b/theme/adaptable/pix_plugins/mod/equella/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f96d2ee7d83558ec1e0359731750e9700f897886 GIT binary patch literal 1263 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qXeo-U3d8TYQvip_{ll{j9n?rSBz+Shlf^k(lhmn{E8s5FTz>wS|}kzACxS5P`| z7Vn;nk525(t`aX=H*H=m_(7@lZio13frGO*iLiZLv{BM8W$E0`#eI?2%p#<&=h((Q zvCW+H>~YPKn;&<2p0j*^{D1wt@0ItSSKd?B>}rynsH7<_z{1qPZ*gnM=~4w=mBtpK z&+BK;y05{gIf;uY@qz4_lcEaij{ND|CNM#1rGWse&6coD4k4XCA6Gj$bS<fA;OW`0 z-H6#o!>~TeJ9)O-2jK%X(g&2E%@8l1b-h8v^Jt@?T(6I&Vf_sO#)q+!r1qSOJ6);} za6Z~J%88S^!9Bs_#U{p$Ic^*JdN_Q}a&kPDJX~cIc6x($jziayJxSBw9Go?YxyI|v z{FTcaceEZ<-f%QK%b|sB`q^_23d@B)eJxTCf3SP!)wbN&b9|2y_1CTAV|em%zvmLu zRyRe{FB_)cof7@AtM1<$)^Fb}PKa-tE#xt$s&C>#b*=ZA$_lIJom9J5w;*(5@9D@I z^_^$ieQ*C_n<IMt<?YI|U1Br5r8u@{O>BEC$@j%#!t(c@-m=U6X`i#F>fQHS+)+BR zp_91(znI14v5F<?nc3;-y}t{s<Nuay&%4dP+^D7fE`v+`j>XdtNC-@mn14BUk6gL> z&Fbi|4_=D9Pb@My{b#0f#qVzyttTFP{7y$w*J0m=-p`YNaxMDwf?=;;^<l&FF_F)1 zJ!%tVvfJ&!y0yK1^Xe0qkERxWI=kHa0pl(ivp(DVXSW{ZwRz_^?c83McHb<kXIK5( z3+DVf%F)>1yPhrN1H<9wySwMD&JU84y}%-OVBN-8UEUjK-t1|vog120u=&gXg8#|0 zj#qBT-Lc_9vU##&eogJ<l^-@UtjV=gC@Ea|<xRusg_U!9f8TC1oS`&f?{?P*vuAHL z-=Zad-yr$ozQ(Q74je!JtK$FHMe_6aZaw(&^l64Ye>d$5-MQma{%=knnfK5CyfC(w zJRVTqd7x8#{uR?}y{V5`a@?%hYS-;LA7}aEifP!+bYmxl>?MyM{;L(e{`%+K2X9|E zgj?GPI9y{(>G|^KquaVK@qrJXEVVCpu)V^kHTABEvx2grojB{cZAK22@3$`B@nz!n zmuy?N6wQD9T>Q+D(0GrY^Q;Z4|JXm(^$ZkQ;*oXtMBks(7Yg&|-&ntF$*g8a(}^d= zw^<f_GhZxqLep^n>9DXPmvrC%sxIAq?~~4&(}~JYUYq{<rd^bD(<A6}ZC3BYgL!HB zSNU46r^v5c$CtN0ukgi}^*ubL+b^}AQf2ZwGk==WO39M2$5(n+#~pR#RsVNNl%eb? z>(8c#5*co~`=|Fy<Zd-QuOnhwT$r|W2J^B18m#rNwmNoRTYcu}#<a+dO3StMcZRnM z35%RA51TsYK!mlJkm-*Ft(+{Y&vTZs7JHqU&!VQwQ5Tf3dV-s}N!X_LUGBc6hchoG zo;vkl_A};1kDn;VnLTvlHJK~i?UdE@ZLaK-mV;{1lO}dH?m5tE*07(UVr#?oHH})i z6-WJUWHHZQ^kZe?I+oLCj?2YuT*+csw{3!`ckngtJ0^`<Ms{U(+dp38<SG{77chQg hzVd5zhubIl&C8c3w9FJ|WME)m@O1TaS?83{1OP#zWN82Z literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/equella/icon.svg b/theme/adaptable/pix_plugins/mod/equella/icon.svg new file mode 100644 index 0000000..3e40e4a --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/equella/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="23.91" r="24"/><path class="cls-2" d="M28.61,18.6H25.05a2.6,2.6,0,0,0-2.36,2c-.08.47-.15,1-.24,1.42-.34,1.8-.67,3.58-1,5.38-.28,1.56.41,2.46,2,2.46,2.18,0,4.37,0,6.54,0a2.66,2.66,0,0,0,2.78-2.38c.38-2.21.77-4.42,1.13-6.64.24-1.42-.45-2.23-1.91-2.23C30.82,18.6,29.72,18.6,28.61,18.6ZM17.42,22.35h0c.34,0,.66,0,1,0,.79,0,1.09-.24,1.24-1s.28-1.48.39-2.23c.09-.6-.15-.9-.75-.92-.77,0-1.56,0-2.34,0a1,1,0,0,0-1,.83q-.25,1.27-.45,2.53a.65.65,0,0,0,.67.83C16.56,22.37,17,22.35,17.42,22.35Zm-.34,2.31h0c-.39,0-.79,0-1.16,0a1,1,0,0,0-1.11.9c-.17.79-.3,1.56-.41,2.34-.09.64.17.92.81.94s1.48,0,2.21,0a1.05,1.05,0,0,0,1.14-1c.15-.79.3-1.57.41-2.36.09-.56-.15-.84-.73-.88Zm13.13,6.56H29a1,1,0,0,0-1.05.84c-.17.82-.32,1.65-.43,2.47-.07.56.17.84.75.86.79,0,1.59,0,2.38,0a.94.94,0,0,0,1-.77c.19-.88.34-1.74.47-2.64.07-.49-.21-.77-.71-.77Zm-8.85-16c.39-.06,1,.21,1.18-.47a6.41,6.41,0,0,0,.22-1.31c0-.17-.19-.49-.3-.49a7.37,7.37,0,0,0-1.58,0c-.15,0-.34.24-.41.41a2.69,2.69,0,0,0-.13.67C20.14,15.19,20.14,15.19,21.36,15.19ZM16.91,31.11a1.81,1.81,0,0,0-.64,0,1.09,1.09,0,0,0-.58.43,8,8,0,0,0-.26,1.37q-.08.51.45.51H17.1a.6.6,0,0,0,.66-.58c0-.21.08-.41.11-.64C18.06,31.11,18.06,31.11,16.91,31.11Zm7.18,4.33c1.16,0,1.16,0,1.35-1.16a2.94,2.94,0,0,0,.09-.69c0-.15-.13-.41-.21-.41a6.26,6.26,0,0,0-1.78.06c-.22.06-.34.64-.43,1C22.88,35.44,22.89,35.44,24.09,35.44ZM12.69,19.26c.84,0,1.07-.26,1.07-1,0-.45-.36-.34-.6-.34-.62,0-.88.26-.83,1C12.36,19.07,12.66,19.22,12.69,19.26Zm11-2.68c.82,0,1-.19,1-.9,0-.47-.28-.43-.58-.43-.62,0-.84.22-.79.94C23.33,16.37,23.63,16.52,23.66,16.58Z"/><path class="cls-2" d="M35.66,13.33c-.15.69-.32,1.39-.45,2.1-.17.88-.47,1.14-1.39,1.14-.66,0-1.33,0-2,0s-.92-.28-.82-1,.26-1.58.41-2.34a1,1,0,0,1,1.09-.86c.69,0,1.41,0,2.1,0a1.11,1.11,0,0,1,1.05.49Z"/><path class="cls-2" d="M28.61,18.6H32a1.72,1.72,0,0,1,1.91,2.23l-1.12,6.64A2.64,2.64,0,0,1,30,29.85c-2.18,0-4.37,0-6.54,0-1.54,0-2.25-.92-2-2.46.34-1.8.67-3.58,1-5.38.09-.47.17-1,.24-1.42a2.62,2.62,0,0,1,2.36-2Z"/><path class="cls-2" d="M17.42,22.35c-.43,0-.86,0-1.28,0a.64.64,0,0,1-.67-.83q.2-1.27.45-2.53a1,1,0,0,1,1-.83c.77,0,1.56,0,2.34,0,.6,0,.84.32.75.92s-.24,1.5-.39,2.23-.45,1-1.24,1c-.36,0-.68,0-1,0Z"/><path class="cls-2" d="M17.08,24.66c.39,0,.79,0,1.16,0,.56,0,.81.32.73.88-.13.79-.26,1.57-.41,2.36a1.08,1.08,0,0,1-1.14,1c-.73,0-1.48,0-2.21,0s-.88-.3-.81-.94c.11-.79.24-1.57.41-2.34a1,1,0,0,1,1.11-.9c.39,0,.77,0,1.16,0Z"/><path class="cls-2" d="M30.21,31.22h1.22c.51,0,.79.28.71.77-.11.88-.28,1.76-.47,2.64a1,1,0,0,1-1,.77c-.79,0-1.59,0-2.38,0-.58,0-.82-.3-.75-.86.11-.82.24-1.65.43-2.47A1,1,0,0,1,29,31.22Z"/><path class="cls-2" d="M21.36,15.19c-1.22,0-1.22,0-1-1.18a2.69,2.69,0,0,1,.13-.67c.08-.17.26-.41.41-.41a7.37,7.37,0,0,1,1.58,0c.13,0,.32.34.3.49a6.2,6.2,0,0,1-.22,1.31C22.35,15.39,21.75,15.13,21.36,15.19Z"/><path class="cls-2" d="M16.91,31.11c1.14,0,1.14,0,1,1.12,0,.21-.08.41-.11.64a.6.6,0,0,1-.66.58H15.88q-.53,0-.45-.51a8.4,8.4,0,0,1,.26-1.37,1,1,0,0,1,.58-.43A1.47,1.47,0,0,1,16.91,31.11Z"/><path class="cls-2" d="M24.09,35.44c-1.2,0-1.22,0-1-1.2.08-.36.21-.94.43-1a5.65,5.65,0,0,1,1.78-.06c.07,0,.21.26.21.41a3.08,3.08,0,0,1-.09.69C25.24,35.44,25.24,35.44,24.09,35.44Z"/><path class="cls-2" d="M12.69,19.26s-.34-.17-.36-.34c-.06-.69.21-1,.83-1,.24,0,.58-.09.6.34C13.78,19,13.56,19.26,12.69,19.26Z"/><path class="cls-2" d="M23.66,16.58c-.06-.06-.34-.21-.36-.39-.06-.71.17-.94.79-.94.28,0,.6-.06.58.43C24.64,16.39,24.49,16.58,23.66,16.58Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/feedback/icon.png b/theme/adaptable/pix_plugins/mod/feedback/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..706acc0b0087461da8cfab839e0d9c4b5dd1b9f6 GIT binary patch literal 1651 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 VU*c8LVPIfj@O1TaS?83{1OOIi83zCW literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/feedback/icon.svg b/theme/adaptable/pix_plugins/mod/feedback/icon.svg new file mode 100644 index 0000000..bc1baaf --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/feedback/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#336;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M36.79,22.09l-9.11,4.74V36l9.11-4.69V22.09Zm-25.54-.77v9.13l14.49,5.74V27Zm10.82-4.18a.55.55,0,0,1,.17,0l4.52,2a.46.46,0,0,1-.17.88.83.83,0,0,1-.19,0l-4.52-2a.47.47,0,0,1-.24-.6A.49.49,0,0,1,22.07,17.14Zm-2-1-7.73,3.47,14.27,5.66,9.26-4.69-7.24-2.87v4.46l.41.17a.44.44,0,0,1,.24.62.54.54,0,0,1-.49.26.58.58,0,0,1-.24-.06l-9.52-4a.44.44,0,0,1-.24-.62.58.58,0,0,1,.71-.21l.54.22Zm2-1.2a.55.55,0,0,1,.17,0l4.52,2a.46.46,0,0,1-.17.88.83.83,0,0,1-.19,0l-4.52-2a.47.47,0,0,1-.24-.6A.46.46,0,0,1,22.07,14.91Zm0-2.38a.55.55,0,0,1,.17,0l4.52,2a.46.46,0,0,1-.17.88.83.83,0,0,1-.19,0l-4.52-2a.47.47,0,0,1-.24-.6A.49.49,0,0,1,22.07,12.53Zm-1-1.69V19l6.43,2.72V13.46ZM20,9.38l8.59,3.51V16l10,4V32.4l-12,6.23-17.27-7V19.31L20,14.57Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/flashcard/icon.png b/theme/adaptable/pix_plugins/mod/flashcard/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..df2519baf98b3250895b4d81a8f5a91d20289869 GIT binary patch literal 1178 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l-%PZ!6KjC)UK*=I<XiyXI~)ZEQ_OiP@JSImk-BX~oXTJ=URYqN;RJ-@6veL^ox z^K?F$Rud;>@^nkpM3?l3pOgHVI(e3|1xreL`SyFTsN`s^Yks$GQqs$Rp3|J<?kE(# zue<+#@AumM_uv23TkzVdPItmOHO9|EjW1?I>o>A3U|KcLf5pDz-@Fbm=rU?1oPGQw zcL9q=+{qnk2@K~MZ~I*LYnT(Q)W^wkDO7MmqikAss>0&u!o4d6IhL%}boe+?D=x_U z(&x@ecY7V#AEYj*=zm~%OiL=cy5B(|HnhR^SvITf{0TAc362Mt-$+Kx5jJqz&hdcV z^F&|J34xr6N4JPCJ|w4<Fy+)Om4nBW6`$NK-Ilz1o<L02_LRO^Q?hi|U)8jWNOBMM z*tXiNOQnj9UqE!4=Dr#y8=+58bDvl%C!E=EeDWUG#F_T@ZI~Y{cVd1TQfyjfBjj}U zSVVN>tMd_G#ZIk!rYd!Rrt!A~mZGw_MIWyIpIE>tx8T~Ffb6n0_pPN9yY%N@sQBD- z=6gR!`?NWBTIN%nj@`b$&);;~YhAANSyRK<cMICd90^pJxTLl#PkP2$4v+7#epzSr zCS7{)I@~l))^z8Uyx(7of*)8&J5O}k?e5Xc67+dh_gX*xokhJre%b2Ziwjt{T}SEC zG=r0-tO6!Z8;>zXth}c5^>wRgujARfp%a;2W*KF?S(KKgcf?X?ZvET5nYN6X-VY8t z&bjmXc;7q!qlaBupVlq4zAPgskk0w<?z|UYa--}RT8(?dmZwMGY!)_q5X>BJae0FF z^=)>GHk>)<cH^v#Fz>IkZ&Px<PL`2p)z{>0Um_p%TJ`In#me&WERS9*_H8!2@+P>u zCHw=!r8N86C#UZB)yn-)yl^2eqr5t2&H4pt^B2weclco55#fsIkLLQZNZrys`L`IP zLGFmPEYmZlvnmfo|17AO+fh5^z|$=rGGXgto8?9F|E{v$5>>j%zRF>Lxa3^6&Ys-| zYdf!>d8F{y?e(2ABKf;-f8Mk)@VVask^Gg{id5se8#bC|pJ(19|B)%zc7DKFt&myg zZ-3W`<KApJ(el+H(=D?XzH9mI`?u@r#f%K|Cv`P%LieaWTez;}RZ9GtwB&uav>4vz z^`8lxTwbm8Hjn?~*Xr#*>lZy2K6!De%ei%HnI=?KH|~*kG+Fld;yTwaeg9vF$7WmJ zef90c-*4hOpWpu>n4cGuzOLmwb5Ban)HD43I)$_H_k?6ChOb*`YVG`YX@33BySe|S zf2~n6?Q7`p&A#Z{I&qtvQZAQs;L7+?&h<KiqSGub|9ceuzZ3e+@KV&C>Ce_}QB}z0 za=x`PzSQ&G|A%I^YUbta9w!c*-@M}Y%8DCNM!Q++S3DP<>A#`RdST=OX1*B(UdCmX zxfUW~l6&6P9VoDwqx|Er=L7X+7i15G{gCO8?Dd$^VDZ=aMP&3%jj4Ut(&Nu9XG`wz osL7MB_EAWVQ~Lg6=h=Gccy*=QA=@u8FfcH9y85}Sb4q9e0BE-=K>z>% literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/flashcard/icon.svg b/theme/adaptable/pix_plugins/mod/flashcard/icon.svg new file mode 100644 index 0000000..4c8f0d4 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/flashcard/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M25.82,19a.93.93,0,0,1,.64.26.92.92,0,0,1,0,1.29l-7.16,7.16-3.43-3.43A.91.91,0,1,1,17.16,23l2.14,2.14,5.87-5.87A1,1,0,0,1,25.82,19Zm4.48-5V33a2.75,2.75,0,0,1-2.78,2.74H24.41l4.28,1.76a.89.89,0,0,0,.69,0,.93.93,0,0,0,.51-.53L37.5,18A1,1,0,0,0,37,16.73ZM14.76,12a.94.94,0,0,0-1,.92V33a.94.94,0,0,0,1,.92H27.52a.94.94,0,0,0,1-.92V12.86a.94.94,0,0,0-1-.92H14.76Zm0-1.84H27.52a2.77,2.77,0,0,1,2.55,1.67l0,.11L37.69,15a2.8,2.8,0,0,1,1.52,3.62l-7.63,19a2.71,2.71,0,0,1-1.5,1.52,2.6,2.6,0,0,1-1.07.22A2.91,2.91,0,0,1,28,39.17l-8.38-3.45H14.76A2.76,2.76,0,0,1,12,33V12.86A2.76,2.76,0,0,1,14.76,10.13Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/folder/icon.png b/theme/adaptable/pix_plugins/mod/folder/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2d7d838341ad2ee0c5607c044d29cc4dc9eea9c7 GIT binary patch literal 819 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_q|5o-U3d8Ta1K+L(RYLF8C{*&Yj%8y6;NDz-IU=d#)A9rB4iBzBYA?3W?V{}`R0 z2c&A<dt%z1Yt^pdZ6s-uY9JyLW5wc<%VgHVR{B9xW1a5nH#a6;s#)NA=jA^C@9*yJ z{;v0Mm)>Wkz?@YL-=8$B<@D8WG)rKd7Ofdk+;6*@!7+;6M8Wv+58DNNGd6ck6jSJ! z-k2*Vw%j2y%p-%bGs&1;;sCo?H(NlFcI&rhP7jgUQVHi0wKmU~_AZR;YbNhx#S_vF zxt&i0ldm4RvH85iq8lcSwsPX3+k-AFO;=EQ!uXA4`Uj>XS6mzS#J32ZYB|!hR?xRb zCicd67LM>t{mO%<s??9ZPjdG?(P2<|-dZv_-Qmp!^&`yh_sUv7t&Ze-mH5i?+r51} zU%z@kXM1~_|BYXLAk!(O0-19Mw(QS;llQ4IJ@#A#`}P#?Z->$k&Stb-v2@c+xo;L3 ziCfn(@1GSL`|x{st4|fH@5!L;ySs%JFqbo#%$#@XnbxdHd%v%{{lB@e#&vuB>CD%& z-u{u=>c4*FJnJoUr8Uz^cS|4pW706iKwk5F^4sSpmxLdiefy*6(#7-3a}TYPw)e7r zy@Gvhrjk)-Qm}7jw_{erV!mG-8{a(;`+SG-z`HMj`(hUCuWp@HulV)to2)K#>CB$H zyjPm9ZF#nt)8M?!8DsSgzPEQS{PTDEm;2$Lv*)~d?riSgWt<Z;(VI=?z~hFsDu%~5 zH>5M3XKH)7Z^t8}lLq0E;aev22(K+{6|HA{lyR+F@#nl~&4_v8tBwaa+^ttL<Mc?m zIhXNvQQ)IH8?2jGI)2OuJEWxWT)%k5eDSNr?00!e`j+P{U0~O_<<wn2oj8*uhkXy% zKk8`{TG~;)WZnY{J24?kciTfuUoKv{Y=3ahiGOl#6aGBhG3oLVF5ea|8G(SMGbQ}G z`L1eAo?j&=Aj>a*Gl%K=lgs*>e~E3|w4`>?!3|u(T6Vwd*Gc&ZEE3*+T8=SkkNR<; d^|$}9Ubpd)uhaaV&A`CG;OXk;vd$@?2>^xFcP;<` literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/folder/icon.svg b/theme/adaptable/pix_plugins/mod/folder/icon.svg new file mode 100644 index 0000000..d34de12 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/folder/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M13.95,16.69h4.57a.91.91,0,0,1,0,1.83H13.95a.91.91,0,0,1,0-1.83ZM11.2,14.86V33.14H36.8V18.52H24.5a3.37,3.37,0,0,1-3.29-2.61l-.24-1ZM11,13H21.18a1.55,1.55,0,0,1,1.51,1.2L23,15.49a1.55,1.55,0,0,0,1.51,1.2H37a1.62,1.62,0,0,1,1.62,1.62v15A1.62,1.62,0,0,1,37,35H11a1.62,1.62,0,0,1-1.62-1.62V14.65A1.62,1.62,0,0,1,11,13Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/forum/icon.png b/theme/adaptable/pix_plugins/mod/forum/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..91e00181841936724d4ce73d72411269ef644e5a GIT binary patch literal 1342 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qW7o-U3d8TXz>?a#mECG)>QDSe};N_k`J>@z|l-aaRV_G@}C-jTIjuTXq_|9A10 zf2^g)Dm33XnkHpUxAL}L=<YJjU?!`Yw?f3DR`UnD(}mS<sQI3(mgiKwd-MM9x_A5b zzdJYcXH8#eVww*t!(Ty$qwI3B3<V4~7;<XY-G1FS*VFqzcog#jrW>A?|5zGW7tEXH zlf<;|ktsuaMN|yq3RP91*$p!dIT@G_aMtNYF|7I(Q$DY!VWwdy!&g%u)!xj^UoqRm zY-jW?+{5_bw6PX{{Tls$H(rJxI5VS^Vfl}}883=sz4tIlFS22n^J~`;^#}TQ)imT) z5|0=u-cQ`0VEi%d=0<0hKLIyu)7pKd<~~X2h&5A8Zl1dQ;>nH}M*G8c$4?)SH%ZQC zmCE+FY;s@`nA&i<aE4W|#Wd#pH?~uM%!;?r3sZRcMtG{jC6%1#+a5;n<-Ojn_*BLB zP-7uOUVC!xy(7-inTOx*TC;Y6h(e|ii;~N!&dpp?9Nx@`Ul|%;xw_@noow;krM&rX z7A_F^@auQ>wRaJGUQwJi6Wh%{UYIg_hE=e~ES9I6g(ewGpDECcKc;%|!t-5jb0d1Q z#X>6cA06BqcV^iR^+ijjGBE7E8q3gdx#+@_*)OhG?U<tfFmCsQGK(pzQyC6479M!g zx6X}$Vc&`NTj36@4Qul4tP43=&MsWLMUr!oP?{J+LGqnH*>=hNwu`qbXm@L9u%^X* zW0%fd%Den&%drmOiD!CNrmQqpVcdS$af&O4O9Hds+{_pAPZusM-&TB^p<(w&*>}gv z73~)+2~v=3TxfeF#~^yo&&Y!tYtN)@ihr;F=3e<FueFCxh)!YB+S>ZkavJmY*PEtJ z_EX*S_qXok;^_UpE(*ns+|MW4zOn7t)S>*Eqh?}y?%aOak}O%X&+q4cU&tX+@@?6j zl%grSm<s(<{+d5G{}K3iV#@)A*)Ek?T+Y=JL0PYSW}d8g?7C!OpXbH=FBijZDejlJ zxj2A@>r`iF(laLUjo+rk^c|j9+VUsSZd1M4jEAd#iRN8zbzpfi)rLjMVT$XHSo0}o zPF*$nX8(5m6ya=d$>|3+)}ER2-u%~@XReVhJEPd*pM3nhe!f6Q!(IDw<<yGFt|meq z3@<|ty~t{rcd^<0n^V!GC(RKo8J9XwEh{PQ@CsNrZKm&K6|pn-4(^+GF?q7Ul+Wva z^VLjj|9f!Xb@|q{JC-u{ngn{k?zNq=Md*mF;JqV<&A&#U@Ml;M{JLR9i=|%RNn6&; zeK-DZk-s@VD_Fkg;X%pRxP^Zb=dHc4>61v`Qv-K3brqv;mpK<DA5%1l*lx1qspI(< zYfJ35zg~3zMf<5fQ-c$~(x<LG_|zip_Li6k)>+0UI3uSwO#aFr75sSWpDxR^J3%w1 zN&7QYpH?prd!8Ak)}Hn`x!p2Biskfs6P8W?uH`gMZ|}6bo-5_H_PCkJK_P`Kw!&xc zMYE<JiQ;*yc&2A&TZ8Bs=Ir^0KDz$9=l528@xB_ZMCGG1_dBq7%$gF#Ej3Z-k=Oj! zURPGSJW8Ir(%5(PkzLD$9{RkoE`H5wk*{6Ry*b7!#N0;7w?=kxw&MRYfwvQwZhg9X zd2iA=`@gpL9iE<Fyl#%%o`p9TaPyUQOkFYOTYN^&rcc`6ng4Y^-+wbl?(50_|4g$F zY<!x2N?2DXFL8|p|NnYB(~5?Uw&~wC9#>9z(f>fwe)~`M-zCisD-Y?<V_;xl@O1Ta JS?83{1OSGhdjbFe literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/forum/icon.svg b/theme/adaptable/pix_plugins/mod/forum/icon.svg new file mode 100644 index 0000000..bcbe26d --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/forum/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#909;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M24.45,24.26A5.77,5.77,0,0,0,18.69,30v2.55H30.22V30A5.8,5.8,0,0,0,24.45,24.26Zm0-1.76A7.54,7.54,0,0,1,32,30v4.31H16.91V30A7.54,7.54,0,0,1,24.45,22.5Zm8.85-1.16a5,5,0,0,1,5.32,5.32V32H32.79v-1.8h4.05V26.64A3.29,3.29,0,0,0,33.3,23.1a4,4,0,0,0-2.87,1.2l-1.26-1.24A5.74,5.74,0,0,1,33.3,21.34Zm-18.11,0a6.22,6.22,0,0,1,4.33,1.72L18.26,24.3a4.44,4.44,0,0,0-3.06-1.2c-2,0-4,1.22-4,3.54v3.54h4.93V32H9.38V26.64C9.38,23.63,11.87,21.34,15.19,21.34ZM33.3,16.22a1.61,1.61,0,1,0,1.61,1.61A1.6,1.6,0,0,0,33.3,16.22Zm-18.11,0a1.61,1.61,0,1,0,1.61,1.61A1.62,1.62,0,0,0,15.19,16.22Zm9.23-.79a2.4,2.4,0,1,0,2.4,2.4A2.39,2.39,0,0,0,24.41,15.43Zm8.89-1a3.38,3.38,0,1,1-3.37,3.38A3.38,3.38,0,0,1,33.3,14.44Zm-18.11,0a3.38,3.38,0,1,1-3.37,3.38A3.38,3.38,0,0,1,15.19,14.44Zm9.23-.79a4.18,4.18,0,1,1-4.18,4.18A4.19,4.19,0,0,1,24.41,13.65Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/glossary/icon.png b/theme/adaptable/pix_plugins/mod/glossary/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7d46d5675c47523de7d96bd7de37246c2c5b8b2f GIT binary patch literal 1123 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;UPZ!6KjC)UK*=Ia16FF|b&6#hu6n9LNkdTsw#)YG7{^i}FdA=RH??~mgMe9A- zDj>eA&C0vm{O&DPzP+X{kyVoq#&L1VXk58dcwACu-H{^#R}R{%Zu{eJ|1`;ZFMCu= z-KFoVzt{S||31&D?{Psm_tVu@59G8qm_52we1J{oK+!45*XPdjc{wzgHONfh`0<B( z0n-e}lODnX2f7`mozGTx2%OfD&$O^6l-28i*!S%A1AW2gZp`m>(1<_F_)K<*kV{ls z@VOARScz|p`&eXr1Z^HgvOl_c&{p77xNpMjo434|&oNwLYoPvsaT}}Hf0mZl&lqd; zJ16OLb~e~1*u8bnPxvk<uyT{x(}t;M{a)S^c|A2Cv+v;Un=4IB*p7&Y{ZdY~@k|mm z=~4I0G|4IwjO|{mCZ0MimvarXoCaH^YSt&gNZr0!H7a|z?bvyy&~fpOojQBB^fx3- znY&G{D6_Tuo9WIiO1n*7R~`J?V95QUGlS*u7Ec#tM-Pd;EJd3Y0`j-4yb?9X+}e@j z3B#d7qMeZf2LFF?I4r%!<)k2ZD*Wq)8?I+v?DpG5AJt5+`1ob^x;ambXYMo5W_o^> zudOorQ~11k^*xuSs4egl-Xqj~hhghEySqXww%(|l<I?Gx5gYgW+@ptv(jTg)_W#`V z-*<O@oRQcyv1PjQufGV}znL5O`}wB-e<Hu{xw~i5uKQOwZ=B^T+iW~1rhmzws2S{e z{PP<0IbTM})Y|$jy(V?x-IAWopT9)Ed3M#IY75t`3#QLrFALG)y`1~KSABzAdU-PE z>ID<ydVlUJtTJ_byZrslyAg*f=EyAiUjLg<b(=$2cXIuk6BZ9@8uQi{<!I^3?p2>2 z=CRggBbU^#2|ABh^F>XBE^>&!%`W?w!E^0!{l2%m>&lZm|1Rd=)R@!$(k$wD)ay@D zweP1?E%sHCl6=jn|HCtd(Z_k|y^P?RuVGf&zm*KSTDF&QGVS}4Gxds>$l)!i`wp11 zIQ2aiI29gUryZm!c2H~0<fu-jfc!0<Yh5hYY`?J8qBCPjwA}xHX-XBb-Ni8%e*C<; z+s^an-3QiBdmb&j*mXa2?>vUQj+~{tRGs!-RQr4SzVJcqjVIQYJUMtjp^S5ek@jAd znAPUDUQ{pNw<O?BM4Il-8{reZX5G9l&h>Qlv^V>1)OJT}x^nH&mMwRGwsvYw$|B+A zb1v?c-=tKa^Sbj+e!fPYUa(cwduxuX>%}bv6-wV~`>!owP5-xB=-#=Q*5C38cdqiy zEmAXIC3Z{j?e5C0+>M15hU!&)$7F;J_AFlBm=#h||NXhZpMM)BUOvHjR)*Ii;mnk@ zCv^@w6_)28sOM!IIrLZZbXtzjzyHVA*f!2xX5F&yyN!Q2*W=Z8zl-WjB^-}DSorsi kVOvl23ZIYVv;Q;rtodWWbvv?+fq{X+)78&qol`;+0NP>>Z2$lO literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/glossary/icon.svg b/theme/adaptable/pix_plugins/mod/glossary/icon.svg new file mode 100644 index 0000000..7d36a5b --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/glossary/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M30.07,26.38H36.3v.73l-5.1,7.46v0h5.18v1H29.64v-.69l5.12-7.48v-.06H30.08v-1Zm2.38-12.52c-.13.54-.28,1.11-.47,1.67l-.9,2.66h2.78L33,15.53c-.21-.6-.34-1.14-.49-1.69h0Zm-.68-1h1.44L36.38,22H35.1l-1-2.91H30.84l-1,2.91H28.65Zm-15.11-.41h1.82V32.14l5.76-5.61a.91.91,0,1,1,1.27,1.31l-7.93,7.71L9.66,27.84a.91.91,0,0,1,1.28-1.31l5.74,5.59Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/helixmedia/icon.png b/theme/adaptable/pix_plugins/mod/helixmedia/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3b61fd1a1e16336ad1ab968c68fe9f716204a2f6 GIT binary patch literal 1093 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tKuo-U3d8TX#fI-M;YE^@5CO2OGNVj<ghGmm5qPqDAfDz{c}d#_%1>(qs*n>TE_ zwCPp+gp>6K_d-{n=uQdUaA(b)PZ^J0RmHWJJ+L{D+&o1}L`#t););*>tcmVU)7(E# zc=D~G%=mlV{BxG~ZJ*!yxMEdT>}fIELWlpH4o_ye`!ljmV6>DCP2F8xy+-JQ!}10d zgWSUZY!ldv!cOw|Ixy=ot+vQ!bXk;!y(kvoIR1NJ*xWfdm(t?Ei<+2rakbmAt5 zsP@9CUhY@_`0`izHcW4LE7;_IJ3IRJ?Op7fT>bfOB<$HXwQtqRg|-HY4;al%be?f$ zteeg9!A)q=N`Xs4WwTFi*?RBDJEaAlPak#&mqnCD8G7a9b9km5Jbp;%<q9vWRg)r4 z2+k35Tk01S&!3_GX5P%ureLPm-s)TQq*-dhYx>WbYlgZ+O)uTvsq#hm_6x>~Z*HIM z-o`%vzo3&{rPJn8t^<$W_}j#VRhZ}teQ3R-qEOa4;oJM@Rk`=B6z$RBel!2?osTCD z?_B=p$5+MgX+e2{5myb$Z4#=NFD~vBPi<^|WgIn~x2ciiW9#!Z`r2n$ALmW*kmOXE z8?mu8a(Rg0&#&i2?_Jw=>!(o1hr54m_wCoW_Eq70@bhW8kNWzHmjb8eTr5uBbo`Kz z(~DINwb8d1g@{^LrkKy%8zug<%T!;a?7nJc>6{17n;8z=oymJ`x?e}Ds=AQvJ1xe$ zt9)nP?|b}aUwm=Y#z%2$+hsd7>!1JDy7oBr=&B^ndyKq0vahX<V7y=AzOlvH`K9NP z(=&X+*NCSs^xnAdZ`SN1PUpT@B`YVWY$~7h=;>?LDZl*GFN&udZ*OkM6aRl@_mZ!V zmpcc)|9PC(<^lJ{FRpDzZ~81Z`150>&s#Hr9p;lCJ>27AAaL=}%7Yho9(lcA<+AA7 z`QLWrFY!{j<0-`LE&IUC??tfv+sLOL9A{V`+sqRGX;cyVL+h*TxAR6}-rPt1t1s+& zrS_rD^kREjW032TTT|ZZ=)Pu|w`=Nx`z@;P&gDp4S$gJQ<T1Su+DtE3Xr*`0Yn$9# zW1pJ2&Cha0$DWl>=iFL0v2WF;bxxJbpE-1ImR##F`(4nqAlH&x2P+r%JbNSbVya6+ z^Q+AZRSG>$mvUqzhHRL0rud-6#tlcF&)403b<3B{O?Fd1d@uRsT$<)DcV+i6Ei=`N zze1e8Grc{{8ocdy-CA#<cv<xuwbNx-f5<-8?OS^>>413BDS0W@7k!^D7)R~-dN?+J zex|eBjX4e<X8&C`XWia~GZ%34NfdZLmNoe9$rwGE=ijkDpX$QmGl|9pF(2>mPcv2d zcFN#+i|pL%ON{cLJ^X)uIoovs&%)bw-y|H|{&V(xD4t!<c&O)7K+^vm^BEWz7(8A5 KT-G@yGywqb`Vi&- literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/helixmedia/icon.svg b/theme/adaptable/pix_plugins/mod/helixmedia/icon.svg new file mode 100644 index 0000000..5adef0b --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/helixmedia/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#98ca00;}</style></defs><title>Artboard 1</title><path id="path1" class="cls-1" d="M36.8,22.1l-9.1,4.7V36l9.1-4.7Zm-25.5-.8v9.1l14.5,5.7V27Zm10.8-4.2h.2l4.5,2a.48.48,0,0,1,.2.6c-.1.2-.2.3-.4.3h-.2l-4.5-2a.48.48,0,0,1-.2-.6A.45.45,0,0,1,22.1,17.1Zm-2-1-7.7,3.5,14.3,5.7L36,20.6l-7.2-2.9v4.5l.4.2a.6.6,0,0,1,.3.6v.1a.55.55,0,0,1-.5.3c-.1,0-.2,0-.2-.1l-9.5-4a.6.6,0,0,1-.3-.6v-.1a.66.66,0,0,1,.7-.2l.5.2Zm2-1.2h.2l4.5,2a.48.48,0,0,1,.2.6c-.1.2-.2.3-.4.3h-.2l-4.5-2a.48.48,0,0,1-.2-.6c0-.2.2-.3.4-.3Zm0-2.3h.2l4.5,2a.48.48,0,0,1,.2.6c-.1.2-.2.3-.4.3h-.2l-4.5-2a.48.48,0,0,1-.2-.6c0-.2.2-.4.4-.3Zm-1-1.7V19l6.4,2.7V13.4ZM20,9.4l8.6,3.5V16l10,4V32.4l-12,6.2-17.3-7V19.3L20,14.6Z"/><circle class="cls-2" cx="24" cy="24" r="24"/><path id="path1-2" data-name="path1" class="cls-1" d="M15.5,12.6a.46.46,0,0,0-.5.4V35c0,.1,0,.1.1.2a.47.47,0,0,0,.7.1c2.9-1.6,5.8-3.1,8.7-4.7,3.8-2.1,7.6-4.1,11.4-6.2l.2-.2c.1-.1,0-.2,0-.3a.22.22,0,0,0-.2-.2l-7.2-3.9c-4.3-2.4-8.7-4.7-13-7.1h0A.35.35,0,0,0,15.5,12.6ZM16,11a2.28,2.28,0,0,1,1.1.3c4.3,2.4,8.6,4.7,13,7.1l7.2,3.9a2,2,0,0,1,1,1.2,2.26,2.26,0,0,1-.2,1.6,2.34,2.34,0,0,1-.7.7l-.1.1C33.5,28,29.6,30,25.8,32.1c-2.9,1.6-5.8,3.1-8.7,4.7a1.93,1.93,0,0,1-1,.3,2.23,2.23,0,0,1-1.8-1,1.93,1.93,0,0,1-.3-1V13A1.94,1.94,0,0,1,16,11Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/hotpot/icon.png b/theme/adaptable/pix_plugins/mod/hotpot/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..689fa9d74b7f45b7b64c4f28751eb82a46138e2a GIT binary patch literal 1435 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_oANPZ!6KjC)sS9n8O6CUV?9Z&S}Aw~y0S1~qx<8a9SBN<E96apTdu1BpUeD*qTR zd2hRYsC1UgI%`uIiwUPMaku++lzlnE>OT2_!J?&Fw_Hrj%I`+4o$yd&DeL-!)>jNY zCzouTJMEpeYp?9@>)-$GeP6x*d)>B#S!!_;wYj)1{O~xCG2MH$17|>Eke6Bd-hcTA zbsJ*>I~X$+7+Y*^7%)Ex-n8-^H)HlTV~08ak~lrAzj~c#DROmW*>u45n;5IY%t^j$ zr+2f<oY}RF|5$S6wkg_NFZT&-VB7UL@VTEL)3Rb$#rK|0*BHbr_i8Y7PRV)IXH($N zlkeeGs-oy^#(wYz+p*mzSNb}I9k}-};Zcy6+2&Zzh?PONpEs0tUDmv*Q#v`|SntQK zDe9`TV<fZ%KXfw0NN7)-<ohIJ*?ktPYK3qSl?Ud_7kef(6(7&Lb$pgx`?B+kJmQ%X zuB?9IeS66w$5!#uxu@JtYIm-jlJku5k9FF8!$pp*SECNv)oqye^vQ?SrWy~p1$ni2 zSXE|Usi|z5I_p)d1KWw)LYva=hhD4P6!>WR{`H%8OiN9TRTT=k@Fh}fHupEvi3^%k z)S~)qlT5-zgwD!&R7<`+d@(!sdEJlC+=twcF>>%9Tahzu!jY48$DD*_<gyeVI?HkV zuxGTG^~ajkN%iY<wq^gjQZQ>u&LWS>?JR1=u8v=#OaHEzEodikvg~iJ$`{VhPdSe` zC-1SX=z1RAf4fOup~m9bQEtxtS6z2H9T3ktX?MHL_g4LE)#CU4-n!qf@$_;Ou9x(b zWRUScxbmUN@xz{SYhp7eo_y=xIj#6?#m1$#8)I*31R9(C-pzjGPZ<Aei|_7Dg<kcw zt%2)>KD16yo<31|dim7bcb{&l{qN<QdN<<H{OPY*YLwh!%R`la?%mRs{JzF*=cMOP zK0LQQxz_SQcH6PTUh}>y#eX_>K%hH2GTiJ(Z03FaTW+Ra?QCJ$tcTi@bw6I@Sscfj z@i8pf#$2MHIJlx<Pjc6lFP3>zMMHEm=SQ1Qd&6*FZNX&y&O6cju1r6Az_9T}Uua0R zRrYkDNfsx6KF;#aH4%}>z4$#KVG4J$>bV}D+s|tbEj06gC)ud(8+mv|c;FJ7{&{uD z=ZjP%-fwsi`{tEn!`kW6xx$;%S$cNmNlSld?O=I4$F^$f)mdGud^hIlSW5X{er_T3 z=Z0`&)8*MAx|!_B%;|2+ECv0F$}M|W`EC^{zN^0Y)O!E@szN=&ay>iM7VP}!RJ>r{ zDx=49tji{UUo3gb|5MHP56W_DVqd<Pdg>X|{r~?o)BEGTPU;kE$^P!ie0klqLe2Zj z-p@6z`F*kM?B_-6gk^&`ez?ln@AiLjwtV8^nbS&UYjgSe&JbL8SDo$8i*;f`mi11# z+^6QMWVN|f6x^8>y8U&mzziXu%PQd_Cfh|b^;N2l_RjaWUfX*wD@F18?=Nn#&yV?U zT<1FHveaqDXF;1*@|V2&a`(ZzqFFybxkQ&0MLo4TuzKE&Tc_rI^h#Q~tf)5dm1b*M z*VO2z^R727JyzVpxPG_LU6s&vU+m<ko^8#2wD-0B<!wUIaXXymFUUGMk84{|YFjkF zsQO=rg&Wnc<fgB?r7zT-9T}3l>T&++txTVva%P8?UpceH`rR4Rr%A1L`S05Jk54$l z^6;R<YEx0)`CFsC+HcerZ|`h*eYKL`aPp+H+kP#5Ej)|)O9OL)qUvlz_orKqmOW#T zh`*8h`nkRQ=6z|FrBiwrw;kh})mfLeTrOZMliAJbeP^n89d4<6iZ<Lhm-9HvNmga) z2J`h&It(*ytFJC#p7{UyX}R4MOFvG$ar}DjKh~eE{+s0PHSc0zU|{fc^>bP0l+XkK Df_Sfp literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/hotpot/icon.svg b/theme/adaptable/pix_plugins/mod/hotpot/icon.svg new file mode 100644 index 0000000..2e09e08 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/hotpot/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.09" cy="24" r="24"/><path class="cls-2" d="M29.38,14.44A1.52,1.52,0,0,0,27.2,13c-.66.11-.69.69-.9,1.16s.15.39.32.56.09.38.09.58a32.69,32.69,0,0,0,.09,3.32,1.14,1.14,0,0,0,.73,1.07c1.13.53,1.91,0,1.95-1.2S29.45,15.79,29.38,14.44Z"/><path class="cls-2" d="M35.25,32.32a2.28,2.28,0,0,1-.36-1.26c-.07-1.24-.15-2.49-.26-3.73A7.86,7.86,0,0,0,34,24.86c-.17.56-.32,1-.45,1.43-.09.15-.17.32-.26.47-.94,1.65-2.08,3.09-4.11,3.45A7.2,7.2,0,0,1,25.91,30a1.19,1.19,0,0,0-.77-.13c.22.45.43.88.64,1.31h0c-.19.32-.39.3-.56,0s-.36-.66-.53-1a1.36,1.36,0,0,0-1-1c-1.11-.43-2.19-.88-3.26-1.37a8.5,8.5,0,0,1-3.51-2.55c-.34-.45-.56-.41-.81,0-.3.15-.54.49-1,.22,0-.24.13-.36.32-.45a1.08,1.08,0,0,0,.62-.9,5.31,5.31,0,0,0-1.33-4.33.88.88,0,0,0-1.07-.47c-.47.13-.32.58-.34.86a14.25,14.25,0,0,1-.56,4.44,1.9,1.9,0,0,0,.56,1.65c.94,1.22,1.76,2.49,2.64,3.75a6.17,6.17,0,0,0,1.48,1.48,48.35,48.35,0,0,1,7.86,6.49,1.55,1.55,0,0,0,1.91.45h0a12.84,12.84,0,0,0,5.44-2.29,9.43,9.43,0,0,0,2.4-2C36.07,33.17,36,33.54,35.25,32.32Z"/><path class="cls-2" d="M20.53,12.24a5.75,5.75,0,0,1-.66-.52c-1.07-1-1.74-1-2.61.21a2.75,2.75,0,0,0-.77,1.14,1.44,1.44,0,0,0,.19,1.48c.21.21.6-.26.92-.39,0,0,0,0,0-.08-.21-.19-.43-.39-.64-.6s-.21-.28-.06-.43.3-.06.41.08a1.24,1.24,0,0,0,1.11.49l-.17-.08a.67.67,0,0,1,.17.08,2.54,2.54,0,0,0,2-.17A.64.64,0,0,0,20.53,12.24Z"/><path class="cls-2" d="M32.81,16.78A1.11,1.11,0,0,0,31.42,16a2,2,0,0,0-.94.71c-.07.11-.24.26-.11.36.51.39.15.84.13,1.28a4.59,4.59,0,0,0,.06,2.36,1,1,0,0,0,1.44,0A4.91,4.91,0,0,0,32.81,16.78Z"/><path class="cls-2" d="M25.59,15.86a25,25,0,0,0-.9-3.67c-.41-1.2-1.42-1.44-2.44-.62a2.17,2.17,0,0,0-.86,1.67l-.37.54c.69.24.45-.28.51-.54.36-.15.45.13.56.36A13.89,13.89,0,0,1,22.87,16a1.35,1.35,0,0,0,2.19,1h0A1,1,0,0,0,25.59,15.86Z"/><path class="cls-2" d="M33.49,22.28h0a1.53,1.53,0,0,0-.11-.64,1.87,1.87,0,0,0-.47-1.11,5,5,0,0,1-.77,1.22h0a2,2,0,0,1-2-.08l-.22-.39-.11-.11h0a3,3,0,0,0-.07-1,2.18,2.18,0,0,1-3.09.06h0c-.11-.19-.22-.36-.32-.54s-.09-.49-.13-.73,0-.22,0-.56-.11-.73-.17-1.11a2.42,2.42,0,0,1-3.32.51c-.13-.32-.24-.66-.37-1h0a2.4,2.4,0,0,0-.24-1c-.19-1.09-.26-1.12-1.33-1.22-.51,0-1,.09-1.5.09a3.65,3.65,0,0,0-2.62,1A3.14,3.14,0,0,0,15.43,17a1.21,1.21,0,0,0-.34.73c-.19.43-.41.86.13,1.2h0l.62.84.34.62h0c.08.21.17.41.24.6h0c0,.17.09.32.13.49h0l.11.49h0V22h0l.11,1.13c0,.3,0,.6.24.83h0a5.14,5.14,0,0,0,1.22,1.33h0a2.81,2.81,0,0,0,1.2.84h0a7.11,7.11,0,0,0,1.69.84h0a4.27,4.27,0,0,0,1.33.6h0a5.21,5.21,0,0,0,1.58.62,6.74,6.74,0,0,0,2.06.71,5.1,5.1,0,0,0,3.86,0h0a2.35,2.35,0,0,0,1.2-.73h0a1.88,1.88,0,0,0,.73-.84,2.66,2.66,0,0,0,.84-1.22,3.88,3.88,0,0,0,.6-1.33A4.33,4.33,0,0,0,33.49,22.28ZM18,19.37a1,1,0,0,1-.24-.62,1,1,0,0,1,1.2-.39l1,.28c.28.24.56.49.84.71.13.3.19.6-.11.84a2,2,0,0,1-1.58-.11C18.6,20,18.35,19.61,18,19.37ZM22,24a1.28,1.28,0,0,1-1.76.6.92.92,0,0,1-.45-.26,7.33,7.33,0,0,1-.49-.83.62.62,0,0,1,.34-.71,1.27,1.27,0,0,1,1.58-.13c.21.24.41.49.6.73A.54.54,0,0,1,22,24Zm.66-4.71c.47-.49.77-.49,1.33,0l.11.49C23.42,20.27,23,20.14,22.7,19.26Zm3,5.44a.85.85,0,0,1-.84-.11.8.8,0,0,1-.36-.84l.24-.36c.28,0,.56.08.84.13a2,2,0,0,1,.38.6A.73.73,0,0,1,25.74,24.69Zm4-.26a2.27,2.27,0,0,1-.58-.17c-.11-.07-.19-.21-.32-.26a.8.8,0,0,1-.36-1.11,1.55,1.55,0,0,1,.13-.24,2.09,2.09,0,0,1,1.82.24c.43,0,.73.17.79.64C31,24.36,30.6,24.6,29.75,24.43Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/hotquestion/icon.png b/theme/adaptable/pix_plugins/mod/hotquestion/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7ecc2e99b9fc92e3684199a3b6c725ef03b56 GIT binary patch literal 1413 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_oAhPZ!6KjC)sSozK2qCUMNZuh+@TCnm-yCrs&u#{!ch&fDWw#qJ7T>0bV7>A9oV zE$SV&>|gM%a9_yIHzqT@{a!CUrMlN=!2_ce3w_j%E?dYXJ$;H)Q`v?uxk9%$rdW6W zZcr?2`LyT#?>+YW){7LzPA?2JQr3D<pOTPyV5#nbPj+r`g{L3?dw*gD+mtgNY#I!a z35h&>w;Se#$2<<=`LJg#<M$alSq_RhAxzQ%M#`KEIBXs!ZRCFPsVkd(XN6kJ^|#&% z{o;|UQ{1?O4G-+TxZ1E(PK^7&<w=qkk58IV-mD&@`;F0b{f@H%f-{6ZF=RIgdO6L@ zS=PA6AZfy!DwQXyZ?u;Q&#pYG%V%;xd+IgIxg}B`lH&^3ItpD{HpeDT|5dNU$(|Dj z>czjtPrh!%VBvmEC2#w@@EBQzvSYu6+vl7p`kpqia;5Z*bA{ZS*9BVY^c6?*UT|g6 zJN3Ir>Ga>|`mcX)zivKT*c)+Pd)>R|4iA;~)PGN#zTj?6V_xj?6t@*tw;41tc{OLf zn0T}K=>g}=Zz)3QhbK*5#5rxU<&-ULb54A_x<_C7@VSeYd0q4EOS3#KvR*3O+Fqko z$FC~uahBgZO-1nYvtI#I)%8x^`M=PoD9^X%ddB-r!GFCnu6@p1yjT0g?~MA5GpA4U zh&!ENe!6Lo$lJbXuPpA}k$jUr+?0GTBFeRD&Ez@Ji;M3px_FOO;)Uh@Uvs+RbQF0e zF5(P{bnnu;V43%EW$^1$?|(b|TzHOQ<_5MUkM3?hbGkgTY?W5pV{?W}pW|x-^QBAr zqCGCMPUL1deR596oX<BO9qdw^v#~rtU{m$$7mh1Z()Jj?@mR5)*I&&oKL2;<vvZHw z`tzb~cI7TSb)K=J_{W<NmGV_u+9&5sh&7$Tn62!(p!g0`Lg~f_f;wMvgQoJk-jlI= zEoHu4%_hEd+r&kjAAjF-Y}}#JePUMc16|EezwLJ{o^sr}TiW*i{+jRHZ~J;DU78@w zf3*DEL|3hKM$*0s=BG6>d!IfLUaz$_xU@uWeVOp?S2@Wyb{>dHJ2b^N^xDbPSxX+R zTJgNjg{OLM+3{xwHz}U@&8dG>C`5Mdsl<s-r7a!*eLbia>hA8Dea72+k$1bJj>gaU z{Ai_vlZvxVZn5nuohyCaNJ(67{Vk7+taHx)o>9uHx_086=*Q2aQ~&)jZ=Q2v(UNT~ zJzDLqk0$9XiLlhU^zO!F^90qI!g8x*!~%n&??m2NaL;VR>{LUuAJ^aP34JMJUf8#* z(^_A5ZqWhHnI+brnC4q;y>Rcl>J_Otv(Sf%o7W|p$Jb14zrNuQSBm-RMTJ|fw`Jtn z{dBATKIPcsNixfFzL_Ru?4E6Fym^ic`?*JJSmr2mACCL}>YrWx{?kUDNA}d7-&>Hn z<)pmm%#xV5lES|hzIt&aJylD-t8pFw=8YU8jfuTF9A|c!Wqx9~cBWFhJD<T#XZ?<_ zlRa0Oe3odRytA_M`0;zc4~0qwd^^2@FNb$`Ra}(N)h8-brnG&&xhv<B{->_nJShk6 z{dV$Kwd(A57h8Q_^oY)T^VQwo_GP409^LAlmQsDeEYE3ni{~P<3~j?tRvUi>iYI^7 z$f)Nub;@WqD|SstKBav=FRt+FXS3q1n&t}(3g&z(`><0}ZGUBv{;lse4{UYs%yIg3 zJKg-Ut!05w@RtVW1pD<nW(KHl+Vkky3%kR6i#|PHTi0fn_I2Jho4rmqpB%FiuHN-q z^TfgQri@BX>n#gdtgcjd>c&r6Qr76RzeU#5sa9p_C*6Hn7miQJ|D4)iyy#JVO56Ut hWQJ=haoc~&_nr<nzOw$EBm)BjgQu&X%Q~loCIAl^taJbX literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/hotquestion/icon.svg b/theme/adaptable/pix_plugins/mod/hotquestion/icon.svg new file mode 100644 index 0000000..6c0437b --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/hotquestion/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.11" cy="24" r="24"/><path id="path1" class="cls-2" d="M13.38,23.12l2.68,3.73a.91.91,0,1,1-1.48,1.07l-2-2.83a1.66,1.66,0,0,0-1.22,1.61v4.69h5.51a.92.92,0,1,1,0,1.84H9.48V26.72a3.52,3.52,0,0,1,3.38-3.53Zm3-4.61a1.72,1.72,0,0,0-1.54,1.86,1.72,1.72,0,0,0,1.54,1.86,1.72,1.72,0,0,0,1.54-1.86A1.73,1.73,0,0,0,16.38,18.51Zm19.76-.41L26.67,28.52l-3.79-4.58a3,3,0,0,0-2.4,2.61v6.68H32.41V25.76l4.11-7.12ZM16.38,16.67a3.55,3.55,0,0,1,3.38,3.69,3.55,3.55,0,0,1-3.37,3.69A3.55,3.55,0,0,1,13,20.36,3.55,3.55,0,0,1,16.38,16.67Zm19.91-1.48,2.44,3.34-4.46,7.72v8.81H18.67V26.49a4.79,4.79,0,0,1,4.5-4.44l.49-.06,3.08,3.71Zm-9.22-.41a2.64,2.64,0,0,0-2.4,2.83,2.64,2.64,0,0,0,2.4,2.83,2.64,2.64,0,0,0,2.4-2.83A2.64,2.64,0,0,0,27.07,14.78Zm0-1.84a4.47,4.47,0,0,1,4.24,4.67,4.47,4.47,0,0,1-4.24,4.67,4.47,4.47,0,0,1-4.24-4.67A4.49,4.49,0,0,1,27.07,12.94Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/hsuforum/icon.png b/theme/adaptable/pix_plugins/mod/hsuforum/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..91e00181841936724d4ce73d72411269ef644e5a GIT binary patch literal 1342 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qW7o-U3d8TXz>?a#mECG)>QDSe};N_k`J>@z|l-aaRV_G@}C-jTIjuTXq_|9A10 zf2^g)Dm33XnkHpUxAL}L=<YJjU?!`Yw?f3DR`UnD(}mS<sQI3(mgiKwd-MM9x_A5b zzdJYcXH8#eVww*t!(Ty$qwI3B3<V4~7;<XY-G1FS*VFqzcog#jrW>A?|5zGW7tEXH zlf<;|ktsuaMN|yq3RP91*$p!dIT@G_aMtNYF|7I(Q$DY!VWwdy!&g%u)!xj^UoqRm zY-jW?+{5_bw6PX{{Tls$H(rJxI5VS^Vfl}}883=sz4tIlFS22n^J~`;^#}TQ)imT) z5|0=u-cQ`0VEi%d=0<0hKLIyu)7pKd<~~X2h&5A8Zl1dQ;>nH}M*G8c$4?)SH%ZQC zmCE+FY;s@`nA&i<aE4W|#Wd#pH?~uM%!;?r3sZRcMtG{jC6%1#+a5;n<-Ojn_*BLB zP-7uOUVC!xy(7-inTOx*TC;Y6h(e|ii;~N!&dpp?9Nx@`Ul|%;xw_@noow;krM&rX z7A_F^@auQ>wRaJGUQwJi6Wh%{UYIg_hE=e~ES9I6g(ewGpDECcKc;%|!t-5jb0d1Q z#X>6cA06BqcV^iR^+ijjGBE7E8q3gdx#+@_*)OhG?U<tfFmCsQGK(pzQyC6479M!g zx6X}$Vc&`NTj36@4Qul4tP43=&MsWLMUr!oP?{J+LGqnH*>=hNwu`qbXm@L9u%^X* zW0%fd%Den&%drmOiD!CNrmQqpVcdS$af&O4O9Hds+{_pAPZusM-&TB^p<(w&*>}gv z73~)+2~v=3TxfeF#~^yo&&Y!tYtN)@ihr;F=3e<FueFCxh)!YB+S>ZkavJmY*PEtJ z_EX*S_qXok;^_UpE(*ns+|MW4zOn7t)S>*Eqh?}y?%aOak}O%X&+q4cU&tX+@@?6j zl%grSm<s(<{+d5G{}K3iV#@)A*)Ek?T+Y=JL0PYSW}d8g?7C!OpXbH=FBijZDejlJ zxj2A@>r`iF(laLUjo+rk^c|j9+VUsSZd1M4jEAd#iRN8zbzpfi)rLjMVT$XHSo0}o zPF*$nX8(5m6ya=d$>|3+)}ER2-u%~@XReVhJEPd*pM3nhe!f6Q!(IDw<<yGFt|meq z3@<|ty~t{rcd^<0n^V!GC(RKo8J9XwEh{PQ@CsNrZKm&K6|pn-4(^+GF?q7Ul+Wva z^VLjj|9f!Xb@|q{JC-u{ngn{k?zNq=Md*mF;JqV<&A&#U@Ml;M{JLR9i=|%RNn6&; zeK-DZk-s@VD_Fkg;X%pRxP^Zb=dHc4>61v`Qv-K3brqv;mpK<DA5%1l*lx1qspI(< zYfJ35zg~3zMf<5fQ-c$~(x<LG_|zip_Li6k)>+0UI3uSwO#aFr75sSWpDxR^J3%w1 zN&7QYpH?prd!8Ak)}Hn`x!p2Biskfs6P8W?uH`gMZ|}6bo-5_H_PCkJK_P`Kw!&xc zMYE<JiQ;*yc&2A&TZ8Bs=Ir^0KDz$9=l528@xB_ZMCGG1_dBq7%$gF#Ej3Z-k=Oj! zURPGSJW8Ir(%5(PkzLD$9{RkoE`H5wk*{6Ry*b7!#N0;7w?=kxw&MRYfwvQwZhg9X zd2iA=`@gpL9iE<Fyl#%%o`p9TaPyUQOkFYOTYN^&rcc`6ng4Y^-+wbl?(50_|4g$F zY<!x2N?2DXFL8|p|NnYB(~5?Uw&~wC9#>9z(f>fwe)~`M-zCisD-Y?<V_;xl@O1Ta JS?83{1OSGhdjbFe literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/hsuforum/icon.svg b/theme/adaptable/pix_plugins/mod/hsuforum/icon.svg new file mode 100644 index 0000000..bcbe26d --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/hsuforum/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#909;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M24.45,24.26A5.77,5.77,0,0,0,18.69,30v2.55H30.22V30A5.8,5.8,0,0,0,24.45,24.26Zm0-1.76A7.54,7.54,0,0,1,32,30v4.31H16.91V30A7.54,7.54,0,0,1,24.45,22.5Zm8.85-1.16a5,5,0,0,1,5.32,5.32V32H32.79v-1.8h4.05V26.64A3.29,3.29,0,0,0,33.3,23.1a4,4,0,0,0-2.87,1.2l-1.26-1.24A5.74,5.74,0,0,1,33.3,21.34Zm-18.11,0a6.22,6.22,0,0,1,4.33,1.72L18.26,24.3a4.44,4.44,0,0,0-3.06-1.2c-2,0-4,1.22-4,3.54v3.54h4.93V32H9.38V26.64C9.38,23.63,11.87,21.34,15.19,21.34ZM33.3,16.22a1.61,1.61,0,1,0,1.61,1.61A1.6,1.6,0,0,0,33.3,16.22Zm-18.11,0a1.61,1.61,0,1,0,1.61,1.61A1.62,1.62,0,0,0,15.19,16.22Zm9.23-.79a2.4,2.4,0,1,0,2.4,2.4A2.39,2.39,0,0,0,24.41,15.43Zm8.89-1a3.38,3.38,0,1,1-3.37,3.38A3.38,3.38,0,0,1,33.3,14.44Zm-18.11,0a3.38,3.38,0,1,1-3.37,3.38A3.38,3.38,0,0,1,15.19,14.44Zm9.23-.79a4.18,4.18,0,1,1-4.18,4.18A4.19,4.19,0,0,1,24.41,13.65Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/hvp/icon.png b/theme/adaptable/pix_plugins/mod/hvp/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa7c799700df036779346e40ddf71eddea106e4 GIT binary patch literal 2113 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 b<ceqd>K<6-b0DS_RP%ef`njxgN@xNAoDsT3 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/hvp/icon.svg b/theme/adaptable/pix_plugins/mod/hvp/icon.svg new file mode 100644 index 0000000..f743ce1 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/hvp/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><title>h5picon</title><image width="48" height="48" xlink:href="data:image/png;base64,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"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/imscp/icon.png b/theme/adaptable/pix_plugins/mod/imscp/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..13f587b1494e0151b6a3a54d07eec465a4d5f6f5 GIT binary patch literal 1240 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qWcPZ!6KjC)UK?azLkCUM-}tINyThsRDRi9?X(iszLFde>D?P0~ER^KokDri!)y znyu;&zC4y<S$Xn`GT%wL)SV}rwA*;1{FWV6V`r0YpT^Z=?Glz6|HxxgvFAGeiivSQ z|JHr|Qy0hiw{E`Y(*t552_GgmT#cS~jZu69!|ECDqbI-9RZ)G=e64|L!-RtWd;yGS zmZT&Je`uWEP`XU}xr3(O#1kxhIgU&`4^+QvA8&{|^Zo8hL5mHoObLDoTAn`M+RuJZ z-7R*`g(088Jzl`(U!42DdnZi`61dei96s!|bH@AZjiw(w4luv@5wS@4L+E#j1MQv? z6MasU-&pKf?$ass$+&llMxwb_iEX)*OPyTl8%3tN6@Lsm4=;1+tl8EwM>1@|Wi9_^ z5B3wS&Tm$5N_rkle^#_&(n;;7$qfHat-kzb9<xGvS;f+tl;X8cW=s8q=JrUNK1jc@ zdB^`5zAK+h_tshd&-={Tca~fI+46Q9mG$1$x%fhDm4X?Yq_xk1u3$M8M$y)sE1MS2 zc)yyl(IP+oT*jXnx`z7e_-<~$Y^1#Z?Cd{JPOT2kol+2$rlZSK<Iw%7MQi4YOSct2 zOxxZ4G;5>J!q+<^*0nu-$9DbM(a?~Wx~oKHHi%twuwM1q?=a7@C&F`+_RSajw%WyP ze(q7;D|2R^Ua0eG;~9r@85-VJGvC+kxcG<dS!KjzrP&iV#4ly}Bb8ceSpW7z+<ND? zlO5k)dT{&Zm$~0+X5YWjEA;=%Ee&t0`{`RA{A<*8QQe^aCcIOomW_qY$jc@2(nMpa zM{k@8?!Vr8oGD!FYeKEYLbpD*Lv^C%KK#|njN5C^Y~C5eaOQPsL6q87@5gQlfgNVM z#DadM#xRv8?)Q2!ugKxSm75ZqZWiWCbfje6oN{Vfjo5UC`#wQ4EOdpJr}7C-G`2o5 zQ+7_+%<uIv!ap*P&M<j)pT99sDQH*2rWKp#M1A+1;V5h3cx~>Qr9H}dyAO3PW@C+s zn02?<Po{L$gXMk?6rZeC2&g~w<F)a>HJ?^?Ss(to;Ook+uU|NStc_GZ7d$&#bc$<j zmc|s<xo5ZDc(_$KFGI;TcjxYy%F-h@S?8_alNMb#@lxxN14~O9bB{j#c`EzJP1Zee z=_^j|bk=%kAf>t7T6gk&y`mduZTG~rR~Ak^yHz>vMA#ZnXS1cBoVMMFu}+GMIr_SE z!|ZCAYfCJiwn_?oIQzopRB+t--n$w<XHR8}@LRiCb^byxHO)Y&+uW-sot#!9R+8+u z`poWbnX<(#*RzjjzFNDoCzNf$p|=aKzg9S>SMVx_VT0;?*6W9s9{p`!8dYCABV&z` zu+~)Fb^PK<p4W>l3q$<o@<!GF=lc-ZEVQre=~c^8GmFA2OCIzeUT(2?X^Wh>67Q0W zTPw;Za?8|)tlMKXZTIO!UoWdOQaxf{tL5Ky)m=ATVY9TP@zzqo<HsiRmOHEPJecl2 zZOc88&wtNPI=@!kr2j<&^RYRX&h)&jH(l^&#=-kH`G4^5t4!D|Y5ncbul~qd-;jow zNj4Kz&K1{k39q#K{l6xwfLpM4`L~V7C5)Qq9})j^=UII7@4anz6#us|FfcH9y85}S Ib4q9e0A6fYrT_o{ literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/imscp/icon.svg b/theme/adaptable/pix_plugins/mod/imscp/icon.svg new file mode 100644 index 0000000..892fb87 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/imscp/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M35,20.06,26.75,24V35.52L35,31Zm-21.91,0V31l8.21,4.56V24Zm23.74-2.89V32L24.92,38.62V22.8Zm-25.57,0L23.09,22.8V38.62L11.22,32ZM24,11.4l-8.56,4.05L24,19.5l8.5-4.05Zm0-2,12.75,6.08L24,21.53,11.21,15.45Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/jclic/icon.png b/theme/adaptable/pix_plugins/mod/jclic/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0d2fa016b5c39d670fff0d4c701fb6d8f519be08 GIT binary patch literal 1026 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tIWo-U3d8TX#fI-m78OypSoq}~<nhJqYcCI&rWaZ~up3eDfD&12W#{lfU@W`*@R z=@tG0=daB!ymufq&HmG}yDkcsZ>r4J+Hzw;kE3Iz53ghFi&Hx~-@fSopK*&>X+})^ z{P*+TTYtY-tnzc8e^hpK5HE+kY{j;O7orK>$8$pd=H0qKZ4y&P^dvrs19lrGA8Jot zaCybAXKoA~SIru1!?K?{80Up3F=s?i;@Z-{d@b9S@kpE`!^T&4r-XE7xA(5PUzIUI z=HO4Y4|RvnG0SY#T{&5cDXEEhTXyY&Yb$=`t>H*HQrTdXz{Tsvd|}&?Dz`cFSub|E zC@S9go^tse=kIt1ml<zVtuw{myuK+?J>ALe*j)A(XL=hfr2fd2o2f4Uboa8ts=nK) z4`%x0-7RgdxVhq&Y{Q?qSNn7Czmb}<#o_(K`xkfgKQ5E3Qe!?J!_J`G)AHnZ-J`4Q zKR$X(uMLR%@&Dvo(Z1V9OI~GnI<07Q*{p3@y7KeE8M0ot7<V7NwTkgdy6YSc2btw| z_ikkLYiU@PhBAD+HEqdr)>@%dx!Ak?CV3|B7O(z&<%{{{^UmkzblJ6D{q#uesFm`A z9kKK5GyUwdTaBU?o(Z47pDE$RS0;n&YKx_<#TYC#8x}449MLJZ<Me*NX<1LZAIaUf zvwie?lH~Lx(a1ZA$+N$$5UO0G^Hf=_^@jSlO*|o!7&y$<tmgRg({ftaqr9e~OWmiQ zTw9vFdXIJW!S?R!J;(m;eB3rq(p$SwcfJ~{hUL^rVeTha&Z&J<V7cvw&hmw;f4>pS z{!{z9sIpinQu{EAq^4-Y&E%GK{}>K@eKMypy~xO*;7(r3ua+%$G#J(v#M|{xO`o!b znemP9lbvmG`V;w$9Jl7Yp4ReTk-?gqf$e%)62rM|bNA1ibNp`Oa?$P(!4GQZ!d^1p zFmgPq8+jwVB+^p0bW+{bDbk_$6*bnVMeWqs{n7cRx?(L8!)Luyg=LTL>qV_(`12{< z_+@x?@2dApm>3dxds>fow7<D-&v$uEU3!(Wu!)tQ)#cllqjChUeOPXC(VBm?P|>Yb zE*?v_FE_P|+0!@0=>FWj2c#r2XQba*&E0t`fV*O9+OtKAKL7C)UK=1-{y^64vE5>6 z>EmygdCt6i=l+UcQ(YPC-d<X?;^1}TH-8@9y>@x;r(G|^_MKVq`S|B3zvAD_P7jYu z7VYCV5tn@y!LVqapn!L@4}Y1a>*V(t-CH78o%~-nyVUK*uQrXV!Yhp~nf?7=c{PDg pV6Oki)PB`KPWidK`|>{X+fLZEsWOG%kb!}L!PC{xWt~$(696J6>A(O0 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/jclic/icon.svg b/theme/adaptable/pix_plugins/mod/jclic/icon.svg new file mode 100644 index 0000000..9167d31 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/jclic/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.12" cy="24" r="24"/><path class="cls-2" d="M24.55,25.29c.53-1.93,1-3.79,1.52-5.66.19-.73.39-1.44.58-2.17.13-.58.47-.75,1-.6,2.61.71,5.19,1.41,7.8,2.1a.69.69,0,0,1,.58.94c-.9,3.28-1.67,6.58-2.74,9.81a9.93,9.93,0,0,1-6.11,6.56,8.86,8.86,0,0,1-11.72-6.94,12.93,12.93,0,0,1,.17-5.23c.21-.92.36-1,1.26-.79l6.79,1.82C23.95,25.2,24.23,25.22,24.55,25.29Z"/><path class="cls-2" d="M26.22,15.32c-.24.9-.53,1.91-.79,2.92-.45,1.67-.9,3.34-1.33,5a.72.72,0,0,1-1,.6l-7.65-2.06c-.83-.22-.9-.39-.69-1.2.67-2.53,1.37-5.06,2-7.59.15-.58.39-.83,1-.66,2.57.71,5.16,1.41,7.72,2.1C25.92,14.53,26.27,14.66,26.22,15.32Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/journal/icon.png b/theme/adaptable/pix_plugins/mod/journal/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..31b90a9fa71afbec72161eec1b89ad060e57f36d GIT binary patch literal 1133 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;+PZ!6KjC)UK?azLkCUM+;Q?Iv^PfUr3UV%%&g4s?nSC+;udTG^Kl{@`~*xJ(n z&0p*b!u2;!kDfi@WnR#<-6yZ8#U^p63!gaHxT4v)Z-!Lq1?{VaIj;6Hx0l9$NLX|I zSKa*YmGiz=%6S&WPT#V^bg57PTZsds+47hw)-6YlGwB2uS7*M{RTT}$_Tma)<Vmn) zZC759m9@@Pobk}D(1iAzi@4v+o)uj$!nk!=h{%!e6)|%dM1zZ?jfDfUy{rmmTnc^k zNIbZB^-9(&vnDahY>+THlJ`Z3bA{>BUotVz6t^(e&gwWbX)^l_HXXs{i!2=5BiTMJ zR$4VN#$(Az`v|{3Sy}J?g)k<~j#5;AY4qUrMUmOkzYG5>ep;3D;p{QH?4Mtx*0aan zn=`#IFRF0b)y7MEWd)WVY~L`ob<d&xX!*3PbFUqzUtjKZCw2WFVQcfWzX2=!c^KZ@ z6U;w<dS%DzO^xehRPLUv-6$L<CUmWTPU)QUYp+auZ}Ht*ZfS(td|kK5eVRt86Wn@T z-Ir-e@2UA`ZJ&Gd#OkFQ-qYM0-up59d}FnHm8k0xzqOj0k`L{;eP`QM?k+!)P$BBx zGEXq}%At#eQhRLmi|<@~+;PEpv1RGKyvL7||FfL_w$Ivpt-|4_n_t)b|5NajrRSH; zfy;;Zye_K8KDnJ9chguR?xUJbY<`-Hu$_6$_cc-z-dpg_IiD-YUK(=q!=?+T%$gsH z9+&-lFxblcJi|AScdSozZ!65#-SqpAQ`}9y7FSjITl{C5_qcC6{=3_&AXes6v9Lj} zO~an3M@u=b?0E8ADdF{MFNZprPg$L@SC!_+`n%i{zfkMLSXcJ3Idtmx3sZmjocQgS zW1prVzVE}NJHNZRvMX6*EghzQfA{9p>)_Ud1~*#_m8~|oH^=LJ&(D2xPf>Px;rm%j zo;2p^bvB4^I(Y2X6Rsy`R<bYs<mR|v_=G>VwX$lZbY-gb+S)ZT_oi3~vDJTPTJ4wZ zqPPD4U+c=HCC|5fwcc=g-_hOOTy8Jde_`@DyEWT)rL-#Fxj9y^gq1hM9M5FA7MA)r zZrjJd|C9f->@Gj?bn8@&fWs^)Zc<O%%M)%q{^7%K)D@&tZ2hM2=Jpr-Z#tV^uhqAF z_hL@ns&ljcbgtZ4ro;GHNlvSB?(DEx482BkKkyj^7w7JA%qkMpF4oQ3{VzmGI9*Fn z%}Mj0K)2(OXSO+7hJ{AqtaW|fN7k$TZIC_dlBm9A|FUaS9^KiLo0XS2tHEXQ(L%w( z56_q1uP_hlJtnJrE@wyd3GIUqw6%V1|90eO0LP*B-O5TlV&6m3q@<QTlbU!cQ}AK0 zs?z;q)k~!J$_r#kdpp_vQ(h)0xL)j~S>luB#}2g2xDa{i_?n4PULU0zjMS%F39j5E w_4LvI^Q+l}TQvTN$A9CvIkk09-e-PaM)4^|4vNzm7#J8lUHx3vIVCg!0112(i2wiq literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/journal/icon.svg b/theme/adaptable/pix_plugins/mod/journal/icon.svg new file mode 100644 index 0000000..8d08c17 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/journal/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.13" cy="24" r="24"/><path id="path1" class="cls-2" d="M14.68,31.14l4,4.11V31.14Zm11.85-4.89-.21.21-.56,3.17,3.13-.9.11-.11ZM31.91,21l-4.07,4,2.47,2.36,4-3.94Zm3.88-3.83-2.59,2.55,2.4,2.44,2.63-2.57ZM14.21,12.88a.82.82,0,0,0-.58.24.74.74,0,0,0-.22.53l0,15.67h7v7.31H30.3a.78.78,0,0,0,.81-.77V29.1l-1.28,1.26-6.39,1.84,1.18-6.64,6.49-6.37V13.65a.78.78,0,0,0-.81-.77Zm0-1.84H30.3a2.62,2.62,0,0,1,2.64,2.59v3.75l2.87-2.81,5,5L32.94,27.3v8.57a2.62,2.62,0,0,1-2.64,2.59H19.2L11.61,30.6l0-17a2.54,2.54,0,0,1,.75-1.82A2.67,2.67,0,0,1,14.21,11Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/label/icon.png b/theme/adaptable/pix_plugins/mod/label/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9ced79d77c1b24e1f14260c3cb1be2926ce7ee61 GIT binary patch literal 1119 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;EPZ!6KjC)UK?a#j*CUD$-N{dKP)|A#R23D;IKZbG<ZaqJ-)fvZ`u3ZWJlC&^( zkwWxzt$GKBQf>9itEZo;cL=m}-ZVv{*Y9Hx=O>{hToFqq2$`~3zvx_A7op^ODeMyS zmX7zf_rBM@e|Pup;wKh-iLR?=a0M`RImkuqPk0gEpjzj<;N^=|vajx*W}2XtCbxn~ z>VqSPY#+nni&u{=Wf0gM%f78M?3TmXi=oP53_(YyG)fgruXrl4!fIERE1Scr8LkIr zB;PJPVe20LdLg5SXfX4)ol=$pIXAo+Cul8WoU${@lCev?(c?_uzI{xmm_-_veQ>O2 zXt{YOwRDL>O@SeUpxXCH+nW=$(sI}FbIe-f-Dl?b)HuJx*Vksv&(M84cCt0h@m%!8 zh(TkgNs(3O9M8ZXD!d2YIdVTPH1}d)Ti|1;5Y%ihb~dl3x_fSu%!^*bh8wbTZ>;RR z8?RAh_4EGC`?42a?PFU|`OBPP?Yr!#dOn5=?@~1HpW76Fj(@wJmB7}C!OLxSzt>JV zHB<4~dHH#lY-%mb-@K^gQMk6WYo+C`te!;yC(CVwx8;66zkJ@)&DT$)ukZc4TvC^B z9?#T!`p+-EOIe)c_<7a@v65|9U-nv-d_66_E?;Ku4T=8UCOWrS1(}6!2OT}5-ttrA z&lw3<>Ge}YX2!qE@3Z{o=lE=SeqLbElN~cPR9!?}1=#b-+27S))z9M*p0?Re`u^W@ z?ca6$&i!Jzvj5!CT)(4J%$MnvJy;^086$VJGU5BZ3ql9p+?%~{x$h5Wo@Yx`niyuJ zI>nhS<9Z<3Cub=9S~IYPeNx~Vc7u5(KmV=nzj-g_@WrbiozL&u5!c_<=`vxD`^lxt z`8NMLe?Dfmy!WH?ze4z}KR-@j@lgr<o>j-5pS$P$wX?5t?(XsXeUxGK<>y!J`Rwhg zKpfAe<>%uT+E@8~-Ozm3I(|pm;Y~Mxys(`T(>6(I_v>GJuWtS}4TwH;;nS4-bmjAf z(_+m9m;>KLTzRb)_`QtrXD;hRm%G}!!kuBPE>0l{r&EJJs%GumUAd6q2&2*h<7m#d z*rYJVBSI5bXj=9)hZySam&}Y&EIYYVn$v&bnNY)xOFkX%Ter=PK`O8^G(bQx!uZay zX^U6uw)I}|W)*b$`E7;vM2=fiKWJY)+&BBlStdav)s?<m#aOxd7cnJ#Kc{I|V<qh3 z@zj6Mc8iVM#h&_1diK}<Zr-tvFTJ;SMH@-VWb69f<?nd*E<bSAWt9$rQ}zWVTg_&A z^>jLHmsV}?ERG8Lx!Ch*y3vEgMSe%hS&Gy=3yX8}R#r~DTYOOW?C$ELDrY>qZ_Qyk zb<pOiI7roruDH!=f7U#Ckrn)<zitQ1(SFwzYko`q`Tz4u0^0<ei?we8#dX-PKhFJs b=UM!QtkM%bjuD&;3=9mOu6{1-oD!M<O27pu literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/label/icon.svg b/theme/adaptable/pix_plugins/mod/label/icon.svg new file mode 100644 index 0000000..b710f7e --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/label/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.14" cy="24" r="24"/><path id="path1" class="cls-2" d="M20,17.7a2,2,0,0,0-1.42,3.43,2,2,0,0,0,2.85,0,2,2,0,0,0,0-2.85A2,2,0,0,0,20,17.7Zm0-1.84a3.84,3.84,0,0,1,2.72,6.56,3.85,3.85,0,0,1-5.44,0A3.84,3.84,0,0,1,20,15.86Zm3-2.89-8.87.88-.88,8.87,15,15L38.07,28Zm.68-1.91L40.64,28,28.31,40.31,11.39,23.38l1.13-11.21Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/lesson/icon.png b/theme/adaptable/pix_plugins/mod/lesson/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e90253c7ee08079c0d1970fc0c50149fc530143c GIT binary patch literal 929 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tI}PZ!6KjC*fqo%fOs6ggIJ?6u_RgoaB(j&r8QayCY8oqeRX`y=O^&~3i+5By*& zycu~^-Qd!?U50tLdYJ{w8=aQ8W*V9(9=&kEAok2$f#f-7=1Uk~W8TcMv-teaXLCN^ ztv<hVQsyic(Yq5aHT=~$a7x$te1lW~+o{N_I_y_<f3hm{Tx;MF_`GsI=ahy;@doo4 zISzkz==yj{aKd-4Bc=>@Cx|Ry$SK%f#PDR(sz_;RhPxAt91I;+{NU8pT|H}QAaknt zG4Y1KoKpQ4w(6Js{@#%1;hNC>d+OdbSy}UJ4OEXY=Ws^fVHQ|7o9*#$N0FG;2_858 z7G~6L&N6)T{JrI?@TrA9J1uA2_$07Or|<P<)kT^@7e$JVKNk8%ikN*%DNArT*qIxr zD;`lX>C2k0Pm3DQ-EVvSQGQjXdYM+o)&Er-ouc0gLOfI!t+eK^xI4j(H*$OUn>zj{ zceQ?edMo?2EcH#>{y5tbYZHC5J~4)mUt{ZURqZ}hxw>ZlnkkofHRpz&-8A=z)I;I5 z^IMG+Stc+}aQ$(_i9=zTbWqf`$%;!p88B!lvMo5l$Ns_KM3>Krr-BER8~)24&<Wd_ z|Hx;{t_6Z~H%siPid=Ga<<FVVe2%RO^@|akuNFDWf3wMaHKW(RkAzLWCqJ2e+g!Jm z-j!F5d~;!x61SFkuH3I-Z4|cDVolIApUW=hOWZEVW~Qk1P5Z-o;P@-6=WC53ufF-a zZ_lbP|Neg|ogB4T<jRk<Fw@6o+^q9*-&XQ9+@HI7;qtA1+a0&=yYBtK&|~F%H|FU< zaU6^LWFLL|vOIKl&?9LV<;L$5j><6<9<u+mSG6a7M*o@%FU@3~7uSU@yl1y@&jyJT z-0o$ePTzv3N*sI1u+^>8@A}Sv#lH@JI&^Dui~z$=Y3+>;B@arMWw<zY=Eragx<AUB z@}gw6OIlK0@xulCyN!2DJ&|ZCZlj*RxAxJhe>Y#6RV6MvVtZX^)}dqB(VV~b1YG9z zJnUR}ov$<TO!37{mtLKBh`7l;MY^ANS`b^bzq7Q=qA7pNPKm9(yC&<N$DC+uli({4 z7~1vDuDzZ4@z6aJY5Pw$3UPP#KHpHdWL5k9G|_n)jGp$dTjb}lPs-~lf7{5Izl8H~ pXmfN!M$6kx?aUIt17|!aon0TMpLDZ#LjeN_c)I$ztaD0e0suH_v#tOD literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/lesson/icon.svg b/theme/adaptable/pix_plugins/mod/lesson/icon.svg new file mode 100644 index 0000000..84f8116 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/lesson/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M30.49,27.66h2.74v.92H30.49ZM13.18,13.14v16H35.06v-16Zm10-3.77H25v2H36.86V29.16h1.52V31H28.89l2.85,6.94-1.69.69L26.93,31H25v7.59H23.19V31H21.28l-3.13,7.65-1.69-.69L19.31,31H9.6V29.18h1.74V11.33H23.19Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/lightboxgallery/icon.png b/theme/adaptable/pix_plugins/mod/lightboxgallery/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..009b50bc495093f6928dd94c16cb76c61dceb930 GIT binary patch literal 1164 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;8PZ!6KjC*IJ>^p9U$^1)^7MGPV(^&GwqPVH4T~91ymrmJ^KF-2v(QAF0{J7?N zi{3K55*xJT<ZIpK1>0wx@Xi-{G0nk;iHlFXC}Bf^MR0Q<yRD|*`xN`XA4B(_ym7wj z{`cSitNnYPS9)mo<Zv?FVqjsgX>dDoshEL}LAv2#=AyY(@0YM}WVAA@SGjYaHG$!U znyT<@2IXj72CW9QO;Z^eu3mO2R%Vg7xr*rmqs^hTr>YDmFCXDN`-n;PhuO!wzRm16 z4=YwLQ$KKG8rK7h*7@oaxf+r;l^2O8rFgSnIQU=N)?aw(4Y3oP6$~!~biS}i+<f(n z<&ROP-c;_14=mm6o^D*;>88RsNo#I2-;Euu{qcSq|CcJ-9)HmMX36~w#-PObi*}vQ zPfd7t_ohTp`XBx;`;Q0O-{;pV%Y5l=dXCNJJ8xa>hr7o(c-k(9vV<_KoxqTJW>%r; zmp5OMpB<=N*K^KpdtT(^iMj_byI5~%5W0V)c!j$7OyP{H+cP;>W-M(uaBAP;>WJom zWOKQNYo8ysIDMNTK<VIaqt6_sVrPGqy)zP0j$Ruiz+>|1O9o4|$!VFBtLMM&>=o;7 zu$^^K#xMByj=vANCa-o?iwjpW&RMnQVbAjtt{+d?P0Av7Y-YWse{X3R8^b<^1pTwZ zC7tfkD}pljZG5fX#1-B0>G541oekk0uldgYGArq1KR@;Qwcb?2`##2<^PQsJvP<#W ztzRYG|G%pJU_n><a`}bxdyN$XyneaAlxw$L$5Xz|p;dQL<o&jy0`a^5QlHfGR5Kl( zmon?1P~?JLp)TJyJQLsfFZD*ypTdadfZMyaUHkF!_g1l8q1+nm_ZJ-!dw5Rl#W(xJ zqOA_mD;8}$?KHiDWB=Z7-b({o^**lHa8!TV<+*1#cNC^E%+q$Z{9xQsWWx2i^7ql( z2EMO^`d@_!_Dj{3RoTx=&R^+ydO}NdL%pA;?yJMHAvcun^*@i_y8HCm*!20_2mjyP z-}2%2>kDVCe#cCEmm<zE<I1i5pIy68U3^h>`TG7Nr$2kH`1<>N`@*SLCUbxBcy@9^ zOJrAaqe21u;mj1Xi6!i>nq^lk7r%b1^+2iB@0calGY@M@++4KH;pAnP=83wiEI671 zZ9C;s)WW-^*KY1|3Htxt<RZtV6^^w&WoCYN4}0wTJk?rNB-yg}m{n`A{JRu`E$0s_ z&DbKy!C5gY!)TM~f|=dHvurodw0{-y=vcDo-yELeAC~@?XGoPAr7QiOJ>55BzvxwN z&m~6BLt~mgK1(}y*+u=N;A!11Cj+<g#q-o&F;rQy+*iJFZTsaXZcC;ce+XJrso<_M zDc105%Mq^+A;OZC2PQ|K^V4fke|;c+HM6$(DKY<3-<Q2U!tl@4{OXm-UqYMp7RA5t z(0Z=oJ8R>IB^5UpyLNfqx_w5D!B+3X6_#loArI%T-}YdCp7NR*x-mu$H&cX6GVgbG cOsZ!r(Ks6TC{KF{0|Nttr>mdKI;Vst07}6jxBvhE literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/lightboxgallery/icon.svg b/theme/adaptable/pix_plugins/mod/lightboxgallery/icon.svg new file mode 100644 index 0000000..b61e1ae --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/lightboxgallery/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path id="path1" class="cls-2" d="M16.93,24.39l-5.1,9.24h20l-6.07-6.11-4.54,3Zm11.75-4.47a1.83,1.83,0,1,1-1.83,1.83A1.83,1.83,0,0,1,28.68,19.93ZM11.2,18.1V31l5.54-10.06,5,7.1L26,25.18l7.12,7.16V18.1ZM9.38,16.27H35v19.2H9.38Zm5.48-3.66H38.63V30H36.8V14.44H14.86Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/lti/icon.png b/theme/adaptable/pix_plugins/mod/lti/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a52c070ca17264281e1949d1368625ed1f9a15cb GIT binary patch literal 895 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_owBPZ!6KjC)sS?a#XvAkuEnyvR$1MJRYx*vBb70+m_fclL(<WizPMt!(<lws7@y zt9*y6?+p19I(t%^TKzm+LZ&u#-D%`jeRzK>?>0;2T?PB)pWi7yzw^7+yyt?~L{956 zTLrL%EZCHoA^n0;_P|G<wZc{J7BGpN4sEnbV0g~>{5`Yof|oO+^o1Ci_ArLrTz{8| zcfZpTR*h-V0uSa}tf>`ra;p|z5V1}#X0EzaH3QH2D|2F*gM~hnKG1WjI1*Ey^zMON zfX;P0hc&<Br01=?T2rl{_<<qxppw4B4n58fr<Gm>F&$~z+u60IEO3f!mGTWUZh@7X zM4!e-@776rz3R<}X$f~;G^QQ7nI<MLeo5(5Q(<UBfqzcnduNwY%PG@rFLcx{J=L9I zBdD<Iq}Q60`>ekxFH)LuvV;HCCgG{w%T4a@yCq$*vx2MPuIwY8V>MSpS1B1^owK$y zR(8c%R*6l*VsT4W&UwoBh&8&xF{p)cf*|*y@@Fsq&rA3(^Nx3)dEFOnrS%c(<_XRX zS->g%?t#L*jIVAc>cO9GnzThZoK1b%tNyx;O?-m~`z>#~?DT~T@Be-Ai07Edrp!6~ z)1Pe4lv4}-^8T*x2Zk!IFB>{HSk|np*wpZz=YP)cu34NH&#zl@u=3BZjn8IZpVFo7 z_<-@8@$cvs;W-CyeE;q?dH?34t5^FUa$|~Hr`)}Um78I{2BYHxVPyr48YPw<x#q*? zvlq5oR5iFdKmH=&yR32U^rSLB$@%O}hnY))8V-M)CA^#6DN6n6%&6B9FKvUf&u+f( zR`An(z2$+wIaqISGd>d8QSb8M@6VH^uOj;^b;9Crb{<X2JAM6~<d^A|S7OYvetau> zR2k=Y-6iVoPUGoa>UZ|;@7R+g)U!tP!OSTCjd$3h`O4k<gKzCWCO!KT^X*fzw{CIw zvE1dg@tply&&urH1&wL11wLoq-Mr)a-)WcG3g$EXsNN>C=k1=QJ(hE}-Rt)>HF?Co zF1ca<Jy!j@6DyZpV_Em-jd0>4*&RZ~@3tuT^Jbkn_x${Ko_pe%m%?Mko^5PbG@IJ5 zcyK>s*@aDUoWJk5#q5&&Q=b<+>4cW-Mb2$0_g!)})yrH=SL?i~?8U&qz~JfX=d#Wz Gp$P!a@1HyX literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/lti/icon.svg b/theme/adaptable/pix_plugins/mod/lti/icon.svg new file mode 100644 index 0000000..a2fcf49 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/lti/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#09c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.09" r="24"/><path id="path1" class="cls-2" d="M34.2,13.95v3.39H14.7v19.5h1.86V19.2H34.2v3.39l4.33-4.31ZM32.34,9.47l8.81,8.81-8.81,8.81v-6H18.41V38.72H12.84V15.49H32.36v-6Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/mapleta/icon.png b/theme/adaptable/pix_plugins/mod/mapleta/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9c353873d28ffc1534f710374acd883910fa48f7 GIT binary patch literal 1311 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qXko-U3d8Tana+Mh3#E^*x6p{g%-zK=^P`;rsaj|Q$*2##$tldY+DP&O|46!5t9 z>JNPl$=!w}n%x^iN>{r)(TuI)DHUIwuyj?*s}=1!4>fc}S-PHNzbJih&hW;AYW`P? z>?Wn&EWh*n@28si^6!h+pWme8wl(LXIJ3!bcgAV#ccK`19N51s$$GhX-P_Mz3ejIQ z9k@D}yXCIuGgK-(pW0)<C|w~Hk-jI4;nE_P7~vCQwT{w1)c&s%ZEy+N+Ott`gXuhZ zN3FOeQ@vJ&y;~*ySMPv&!}9AKatVAl&WY9ToM<ya_m|Oi1xfQsUFxB$^`10xGV*Uo zS|0mdXj7^&=L6YjIhEzyLYtVsX->|J_PlzEM<rqTsY|BpYgW`w2w6Mn^YPw&-75nR zY9F)^j8ry}-CeNu)+Ni#S0S^NO9do%r@s8!5Sv*4^^=RHfXc23N>=loxXu(_(h11@ z+BAoYh4Zx3!k4L0X{~>=W)<l^pL(P%SovJF!rQOX*Qe-BNHRT}sM~&XCI6oIX(H3^ zc`nereTP@I;**Wtmw$}Y*oxBSKKy#C-@41;)K=MVeZfu+PR1LJAKm{a^Z5<yapuDH z4@#2{PZr5~E9Bo7w$t{vpkU<U%B%|}yc6AzG|qY4cxi&%zb4m|UjJ$Po*#d#J8$oq z2c~&)_pWmKmQOde<}EudwYALiiQ)E|__xYB^S7=z`2MZXecS$i);-B-%N^H=h)kOJ zzs9RbTUJxR#at+v_5NNn^<Nfe`8ixK{Pi&ZTX{mp`5^P3ujSnrdIaXo5MYc9nlV+( z@{7Qh@J4fM-n!q#^Iz0>t#JQzdv-y%*fi5Ke$FNrVjA!3W{14$?>ynu^{ma~wSvFq zm0PNEcTN`ETA93WUHr_}J5B4idi$_8o$=~E#M1KSS@h>Slg+ESE-h#fW8^Q$IzClL zb6s{*Jj)(_`|WxAu1}wO?Y!12jR%Zpu0QHLs6WlgUi!oTIH9-$)<Uc;TAS|AW7x52 zeq5bpOU(3^@DB`|CpF#7<fuMS>Qz}<b)@?mzq98WS6=@IbI*m9tbefFdUZ+a)n(`2 z-t5=h`y<KY+RV=^aa=5cO{*oo?&0{?HG%z%-`t|y_SWt8F6L$h|97sI-ul9cYx8A+ zw>jQ*)sLRt>bh!@SlK!)bBEGHwwesJHmQGel0qx|r<UK!bY8N+cE{vI%_$FNr+4jD zzWt5o9%og*^z8DvCR$In?|m%dd-=h`SlPw%_FoZJdXVaw_&q?FHQr;@w1=Gx1=}7( zUD=_aZvH@d)3N36%Wt>e-Slkf?z3P0Rf{;z{aNZT;rB$7@6XIFGsS*9?O$J6T4hpF zt>Mj;rE)eXWA5a%gRhtEn-rp(nv*=WW6i?0xewL_<i0ZTZI0UAa&x8dtUg75P21$D zdmm59IJKu_%_aMp(wPgiz3(pIJ1rI5sIYM6jhk~$Najd>-BVzFFP+`<YS-U<w{F|B zb8MUaeGGoMKR+0MtM``v&24G|e;zN%S{l3c@;8p^^ri#yJVD0}{B;Q0DkSXmh~Yv@ zyy0Bsk1zI5IcxAieA5Mm(_*!SyFZ<cEj|!;^KR$97v~sPUtjqBUyiX+W3h|LuFpTG z8ZxiHV4J0IWqNPX4dr9A56cxRzp|MgqU&GVn(uQw_tCeGpw0FC${`z8@AdnB!nbzH c*8hwh*Sh{^g*)$JU|?YIboFyt=akR{01jPn8UO$Q literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/mapleta/icon.svg b/theme/adaptable/pix_plugins/mod/mapleta/icon.svg new file mode 100644 index 0000000..d87dff0 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/mapleta/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24.13" cy="24.04" r="24"/><path class="cls-2" d="M11.26,25c.81-.84,1.59-1.72,2.44-2.53a1.2,1.2,0,0,1,1.74.11c1.46,1.48,2.94,2.94,4.41,4.42a4.25,4.25,0,0,1,.39.53c.24-.22.39-.36.54-.51,3.73-3.75,7.48-7.48,11.19-11.25a1.38,1.38,0,0,1,2.27,0c.53.6,1.14,1.14,1.71,1.72a1.24,1.24,0,0,1,0,1.88q-7.34,7.34-14.68,14.66a1.23,1.23,0,0,1-1.91,0q-3.85-3.83-7.69-7.69c-.17-.17-.28-.37-.43-.54C11.26,25.56,11.26,25.28,11.26,25Z"/><path class="cls-2" d="M11.68,24.1h0c.26-.28.54-.56.81-.86V24A11.76,11.76,0,0,1,24.24,12.26a10.41,10.41,0,0,1,7.82,2.94,1.86,1.86,0,0,1,.88-.51,11.47,11.47,0,0,0-8.68-3.41A12.75,12.75,0,0,0,11.51,24c0,.38,0-.22,0,.15Z"/><path class="cls-2" d="M36.16,19.54l-.77.77A11.73,11.73,0,0,1,24.26,35.8c-5.38,0-9.75-3-11.31-7.69l-1.29-1.29c1.29,6.08,6.23,10,12.6,10A12.68,12.68,0,0,0,36.16,19.54Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/mediagallery/icon.png b/theme/adaptable/pix_plugins/mod/mediagallery/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6a8ac2b7033e9fa2c902748dfeb7680d2870eb GIT binary patch literal 1117 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;>PZ!6KjC)sS9n6-BlsRtiCMeQ$XiDp}mNO@#JG<43FD|`4ZR_;67f+si?eMQ5 zN@MT3y*d@IDtz<{J=ZOq>|G@EWm9TvYPz9H&dtQBB1=<Kzdg9z<9V#-T+i!&4h0`9 z-~X=N|9#*7`|tOPZ_?pjFQ#RB$$`;>*=WPt3tLzv9E4XyU-h|Me7{>rU_mIO&BlcP zwhQ<|#8i#V8v|BOWIyG=SO1npBl>EO90y2%#pFSC)7JTnM^~lo%9C2ZlO<lD*gWGy zhaJzw<>gEZgBJ71RLpqZ9l>NFP^)j(>a?_tP2^|(+O$yPQ+MPd1V1piHXgNY3|JW$ zDfppGNHjdyLuVdy*yVZASAFhIY?$Dce3HBD-U0R7s%Axt8=tYn)qdhVb&h$Pc~YZN z&RqTre*YI0{W<A5*F^B|a$5zZH|@LsEq&v$NBfkw?5n%88q2b*{{FdZk#yVk!HS(; zVY7PrcHQR`sd#!+?Y6Yxlr<*Z4w5JQIXaHKxp&9y?)wGX*E6Xucvt>o!}MM2BlSYE zmaNTMW;rcz>i_vGzgYx)c)j-JS&Nrve;9FyFeoK`RQtoU{{2#qBZsb>^SSa}cGus; zgr7kxuAErkE8eWn{h^&#XyXTsk~GyG^ZvlHB?@~V=r?+%UfTDF|Fd>y#Ef!R$y)tZ z9am=M2fGivetUjN{QpDIf;~kmOY7=C&-k?2y{=q!J!7;ZkMDMSm)iReZ(o<soXgKp z@$<{V`@vb+x*01o%6^6P{onJgGVAX3@9s@3`rd&iu4?KI-`Sj(%cOthm@?n?(bLQA zlb>%tK3_vV_J`i;Mu82?`E&ULOq$b{%b#%g>9xRmhuqzJ2_8Q`9Qv+VEZ#UTe&UXr z3d_I+pB}Qd{WSf+q<$!4zR~mPk3{Nz<<8`cHD-FzEi_@N#;J47uNl6*4t?<Q+u0eG z-x^*gEx!Ff{nMNt_hnu_s@jP&;%yYYjwyexJg_S5)xw~|SFLuem%BG<Q4Z_m&vz{U zugzL!w=BARGk=J+@p7?cI<A^&OIND+<=20c`Sa%eoW#=IJz04i8Oxttk?}uvIJk6m zV%^6zHb1ZIie|dCL`himU*~Mo2|ex0h0ONWn*OX{o!`^Ga?g#DsT-}|dfnN%cG29K zPl8_O<RAI*`}mh2wH;+?=Xfk1>rc)pnI^s^cyhS=^e=3OAA~<kII(*1>Bwj;^>4v* zW=D5#ns7DrUX)Mt)t<uB%!jy-b>(kdQTAaw2ZxYUK(ylr)5hMJEG(R<M?Ea&U9y-q zMcXB5qw+FQMML2~ds%W+%37Ipf^*B>GuL~&rJKoku4((P{$+c~yo-+8A97u93g^|b zh<_1rCCW+d>&_1qJKjuW7uhpE?HWUF+=F?XH#$@#->!cq8N1!{e8&@8!I!fa30=8+ fud`rNJ>&XyYtMa%l|IhEz`)??>gTe~DWM4f7=jr0 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/mediagallery/icon.svg b/theme/adaptable/pix_plugins/mod/mediagallery/icon.svg new file mode 100644 index 0000000..ad4ca86 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/mediagallery/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path id="path1" class="cls-2" d="M11.2,32.27v1.83a.91.91,0,0,0,.91.91H35.88a.91.91,0,0,0,.91-.91V32.27H19.43v.91H15.77v-.91ZM21.28,19v6.51L23,24.4l3.5-2.18-1.27-.79Zm-.49-2.23a1.31,1.31,0,0,1,.71.21l4.7,2.93L28.08,21a1.37,1.37,0,0,1,.48,1.82,1.58,1.58,0,0,1-.35.43l-.13.1L24,26,21.48,27.5a1.27,1.27,0,0,1-.69.2,1.31,1.31,0,0,1-.33,0,1.34,1.34,0,0,1-.85-.68,1.4,1.4,0,0,1-.16-.65V18.1A1.36,1.36,0,0,1,20.79,16.73Zm-8.67-3.66a.91.91,0,0,0-.91.91V30.44h4.57v-.91h3.66v.91H36.8V14a.91.91,0,0,0-.91-.91Zm0-1.83H35.88A2.75,2.75,0,0,1,38.63,14V34.09a2.75,2.75,0,0,1-2.74,2.74H12.12a2.75,2.75,0,0,1-2.74-2.74V14A2.75,2.75,0,0,1,12.12,11.24Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/page/icon.png b/theme/adaptable/pix_plugins/mod/page/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a1902faed860a5da8d2f98cc154955684c55e1db GIT binary patch literal 801 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_q|fo-U3d8Ta1K+M9hlK;&rsCSL`%<mPs#%nmhnuBB|gXN6kNEq!&#^wpB94|YCS zIs2g8R?Y0`8ugA-md<5~d#%bhm)EROgvm|zi%!UK`AM>tL9>_-HLKjSe1EUJ)?TG@ z-tlD<c*0qiP3L)|U-3msf`RWr#j-~^d%o>rV#$=f(C}&2>wZS*4cA14qZ<_NG4)kM z#4xSh;iSYpfhU~J>VW&Cqm~yw_@sVFU{X1Jl_8rsJo5O~lJg;NLiwL5O?c0c9<9<J z@!Dt2-hHephqpEe%ZV?axAMhC-3daU7}T4*OBfH8WGcvuUF>|~^yt96iGDS+Y;XMM z5Lmcr`%`9VbE~U|70a%*T4}1;_xjD<DJJlLgZSrrMYBx%vX-tj+WY4If^EOM^W^>i zZ#*oPF;SK!hI`@N_?fq5w*R*HE6woa6<_m1S%#@eat#;XKDoDdM?ki!-mI;m{j#2I ztE-kh^3h#zJkrH}P0`o4yuT8sdKva#wmFa~7ohoU<$KYWwY9xVPkTl?^6MxBJvx!G zr(W<+U!MA&+O5)W=0#}--aEVas<w2b5|c?oQ^EX*iSD`o7k#emKlc58Po8>><=4ft z?n)<qcVmdKZ}*(UuHoIbTFgaM^k(T5W_u~#w(>^RrpaF)F0B{)=X2qe=$0x~Gwq$b zgtPUNIV~!~eWOi+xEHncP7_mmFLvVUTnlMc@o6Ftz9;;ZU`sUFsJnLRBaR#ACl=ap zSBQPkIgtN7RrI>$pSaX8!xIn9qc(G?|7LQ@G&`s8vv9|Pjm8=W_}T-to0--uQ2Ok1 zbRy69qR?2Gg<*H2E<ZY1qMBx1Y<800!0hPGbgf&9LKaQ@)L|kf^ZdEfqUS4?M>N=3 zSM8d9rt@9CdFzwipWXbp)lD>|3Jy%tJbS1lYEQwev`^Zv89yd{pLjRz<;<F2$=5C% zT=$rNp>Y4_AHg#`e%}25dF4SSMXSi)H<CMNyj0zM^nC6=rfn4l0r!uco5aAtz~JfX K=d#Wzp$Pz&iE7;d literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/page/icon.svg b/theme/adaptable/pix_plugins/mod/page/icon.svg new file mode 100644 index 0000000..0fc66e0 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/page/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M11.2,20.34V34.05H36.8V20.34Zm10.51-5.48a1.37,1.37,0,1,1-1.37,1.37A1.37,1.37,0,0,1,21.71,14.86Zm-3.66,0a1.37,1.37,0,1,1-1.37,1.37A1.37,1.37,0,0,1,18.06,14.86Zm-3.66,0A1.37,1.37,0,1,1,13,16.23,1.37,1.37,0,0,1,14.4,14.86Zm-3.2-.91v4.57H36.8V13.95ZM9.38,12.12H38.63V35.88H9.38Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/pearson/icon.png b/theme/adaptable/pix_plugins/mod/pearson/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..71434e482a7d930ad6e83db073a9e53c3b37a3b5 GIT binary patch literal 1142 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l-lPZ!6KjC*IJY%^|0iTwNHe6UHNQE{oNVp~(YUg*UsdZ8z8eY&$Wd&<d@RSTkD zF1WNaM0TrZ_Vg7?R<GQ2WO5}J|GfvEM-^Fv8-2QbnB8P+-g^A=EM33UCEa+J-S^)m z=WOeDwHokjTqQC~meGN^fnfpLgM*Wnsx!ne@G)p#aZP`^H<DA}0vF@Hb36VsG_X1( zt2+BGxbVh<A%vlS;yM||0O!~(#)<(MWfcr;4fPY&)-Wox?|Pvg8RNsT&#}fl`-4LD zj+FCPIYnm4iXJ$~Rd-J5H3Q#?dvEkYGRk-us&_;hI>$~~sXIaV1H%UQ4JzsZ8D(4M zH1topAhc7!(uV!SavT2Ai&0(;EneSe@$2M1TJ7qbQ<>JdQL7@c+u8X+;Ocu$Ypb-s zo%yPlP<}qq((X^f5vjR6lm0#yQ;_^7-5oYh+wtaJ-pb`&ZzV5yJA2=5uBu?WzB}RO zr;{ql?wmJto;GgU&G_|Z?3!>dAF=B1U)ir5Hh#59`HE|%SA$5}|4aKy-yRkFR<^46 z{Jv`Og1z@Q?B1`mF0^sON!=|IZhljc*3<25U#Mp_*?7T)f48eD`TJjIPhXyG(RA(A zVT*s2OQSu$nyxti?#RF0+k;y#b1&HXE94lvg1^p#%QA7P<;?Q&IkC4kSM%L0So!DX zglxmtylu)??)`Bu-od_5?~`j$#Y!cA=LgAlI<+s(H9p@dwB(mmMc&7o6L`hl?|wf0 zAxQJ+7qw20)--o5<_Q^hk7raD?B$N=<3IiJd4qNSd;i+MPf9o*{{7GWHDhhvnW-(l zd=2i*trHJyWPe=o(QWg>F9*|7SISu@HJ)@_F0#_2>c*9p=ci6yH)^}raixo4TmC9N zfo(f(<S#d061p+=2yfS>-Gb@`QUR|u?y3B^X=5ZUFv;WRuEx(XoUbnLt}y-l?doCU zWlZmHad><+HDQtawD?ub-IQ&%w{&XuvN*-Y{CX3-n*V8We`NVg6V8>U2J60CM_S&r zZLj)~tS5fK%p$et`IMEuH}A~<I4Q|M=HN@#%WcPAvTl%>zm<Q6=yIL|ZoiTbdF1db z)V@etdQVS?J4NkZ!IY&{%RP4eam<qPXP%Lp|H_W1m&?~O*LTr`sn=vq@E=>SHLU*C zZ^=&4vq#*-W|nQc<{8<e$$06Z{L*DJOQOHMt_oA>Qw#3Q&|R`DG|?~W=vS}KrN{i0 zUd0$rpC&pduEBq;9ou2SZ@%T3k0&3y-G8?AM9vDgLs|3OyPZ~azdKYk%O+q#eBh!7 z;@_AT>Pa!2k!dpL+_vA`<VknOOydtmq3+X^g>vT4C~|t_^`WyWX;Fgx*(%M%EjRy5 z|JZ3z_4H6-hv_Drz3PQ>kNNL*u5!4xnoVk2{ELLC!a{1fS~l7--zG4e&<xqA#?UPG z;}naMOGVG~)k4Rv=eB4Y{cK37YA<OjyJRhR`U!sy>xHcbysc&o3=9mOu6{1-oD!M< D&BqgW literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/pearson/icon.svg b/theme/adaptable/pix_plugins/mod/pearson/icon.svg new file mode 100644 index 0000000..e3a0294 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/pearson/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path class="cls-2" d="M24.71,28.73c-.06.6-.09,1.14-.13,1.69a1.48,1.48,0,0,1-.94,1.31,4.83,4.83,0,0,1-1.44.37,1.18,1.18,0,0,1-1.31-1.2c-.08-2-.15-3.94-.21-5.91-.06-1.8-.06-3.62-.11-5.42a1.43,1.43,0,0,1,.94-1.44A4.59,4.59,0,0,1,24,17.89a1,1,0,0,1,.92.92A20.34,20.34,0,0,1,25,20.95c0,1.63-.11,3.28-.15,4.91,0,.07,0,.17,0,.34.51-.07,1-.13,1.43-.22a9.62,9.62,0,0,0,3.62-1.65,5.26,5.26,0,0,0-.53-8.79,8,8,0,0,0-3.92-1.09,12.65,12.65,0,0,0-6.39,1.11,32,32,0,0,0-3.28,2.27c-.26.19-.47.43-.71.64a1.25,1.25,0,0,1-1.41.09,1,1,0,0,1-.36-1.29,6.22,6.22,0,0,1,1-1.63,13.09,13.09,0,0,1,7.07-3.92,13.8,13.8,0,0,1,9.21.88,8,8,0,0,1,4.84,6.21,8.34,8.34,0,0,1-5.08,8.85,14.57,14.57,0,0,1-5.12,1.11Z"/><path class="cls-2" d="M22.74,36.77a1.65,1.65,0,0,1-1.8-1.61,1.84,1.84,0,0,1,1.72-1.86,1.88,1.88,0,0,1,1.84,1.86A1.69,1.69,0,0,1,22.74,36.77Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/peerwork/icon.png b/theme/adaptable/pix_plugins/mod/peerwork/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0ad379a8710b4b282689b21c266a7847a6ca2c7c GIT binary patch literal 1837 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 GgeCx=&tqEv literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/peerwork/icon.svg b/theme/adaptable/pix_plugins/mod/peerwork/icon.svg new file mode 100644 index 0000000..05e6c91 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/peerwork/icon.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#F79F1C" /> + <path id="path1" transform="rotate(0,24,24) translate(11.030774978041,11) scale(0.80933746424431,0.80933746424431) " fill="#FFFFFF" d="M19.886933,22.000057L19.922878,22.126386C20.005204,22.445684 20.049003,22.780313 20.049003,23.125001 20.049003,24.503751 19.348222,25.721563 18.284062,26.441036L18.170931,26.513624 18.382488,26.597173C19.458902,27.053149,20.37553,27.814669,21.023056,28.772413L21.048829,28.812582 21.074942,28.772418C21.722468,27.814676,22.639098,27.053169,23.715512,26.597202L23.927103,26.513642 23.813944,26.441036C22.749784,25.721563 22.049003,24.503751 22.049003,23.125001 22.049003,22.780313 22.092802,22.445684 22.175128,22.126386L22.211073,22.000057z M26.049003,21.125001C24.946003,21.125001 24.049003,22.022001 24.049003,23.125001 24.049003,24.228001 24.946003,25.125001 26.049003,25.125001 27.152003,25.125001 28.049003,24.228001 28.049003,23.125001 28.049003,22.022001 27.152003,21.125001 26.049003,21.125001z M16.049003,21.125001C15.704316,21.125001,15.379745,21.212599,15.09638,21.366706L15.028553,21.40795 15.043833,21.699182C15.047268,21.798165,15.049003,21.898438,15.049003,22.000001L14.396575,22.000001 14.390985,22.007472C14.175144,22.32672 14.049003,22.711376 14.049003,23.125001 14.049003,24.228001 14.946003,25.125001 16.049003,25.125001 17.152003,25.125001 18.049003,24.228001 18.049003,23.125001 18.049003,22.022001 17.152003,21.125001 16.049003,21.125001z M9.0490026,10.500001C7.946003,10.500001 7.0490026,11.397 7.0490026,12.500001 7.0490026,13.603001 7.946003,14.500001 9.0490026,14.500001 10.152003,14.500001 11.049003,13.603001 11.049003,12.500001 11.049003,11.397 10.152003,10.500001 9.0490026,10.500001z M2,2L2,20.000001 3.3089714,20.000001 3.3191547,19.960938C3.9043083,17.929688,5.3888025,16.679688,7.2665348,16.210938L7.455574,16.168118 7.316206,16.105021C5.9757862,15.458286 5.0490026,14.085563 5.0490026,12.500001 5.0490026,10.294001 6.8430033,8.5 9.0490026,8.5 11.255003,8.5 13.049003,10.294001 13.049003,12.500001 13.049003,14.085563 12.12222,15.458286 10.7818,16.105021L10.642432,16.168118 10.831471,16.210938C12.509694,16.629884,13.873803,17.672883,14.562483,19.339936L14.586886,19.402841 14.674926,19.368072C15.103593,19.210846 15.566441,19.125001 16.049003,19.125001 16.945191,19.125001 17.773382,19.421081 18.440918,19.920582L18.542004,20.000057 23.556002,20.000057 23.657088,19.920582C24.324624,19.421081 25.152816,19.125001 26.049003,19.125001 26.94519,19.125001 27.773382,19.421081 28.440918,19.920582L28.541933,20.000001 30.000001,20.000001 30.000001,2z M1,0L31.000001,0C31.553,0,32.000001,0.4470005,32.000001,1L32.000001,21.000001C32.000001,21.553001,31.553,22.000001,31.000001,22.000001L29.886916,22.000001 29.922878,22.126386C30.005204,22.445684 30.049003,22.780313 30.049003,23.125001 30.049003,24.503751 29.348222,25.721563 28.284061,26.441036L28.170899,26.513644 28.382485,26.597202C30.535316,27.509136,32.048992,29.64323,32.048992,32.125042L30.048992,32.125042C30.048992,29.918987 28.255016,28.125042 26.048992,28.125042 23.911936,28.125042 22.16152,29.808618 22.054206,31.919474L22.048998,32.124804 20.048992,32.125042 20.043789,31.919438C19.936476,29.808628 18.186066,28.125001 16.049003,28.125001 13.843003,28.125001 12.049003,29.919001 12.049003,32.125001L10.049003,32.125001C10.049003,29.643251,11.562691,27.509126,13.715518,26.597173L13.927075,26.513624 13.813945,26.441036C12.749784,25.721563 12.049003,24.503751 12.049003,23.125001 12.049003,22.228813 12.345083,21.400622 12.844584,20.733086L12.903605,20.658016 12.868902,20.516847C12.361769,18.660157 10.841378,18.000001 9.0490026,18.000001 6.8430033,18.000001 5.0490026,19.000001 5.0490026,22.000001L5.000001,22.000001 3.0490036,22.000001 1,22.000001C0.4470005,22.000001,0,21.553001,0,21.000001L0,1C0,0.4470005,0.4470005,0,1,0z" /> + </g> +</svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/questionnaire/icon.png b/theme/adaptable/pix_plugins/mod/questionnaire/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d53c56e07e5eb2f940c3959a4e74832b6fde8920 GIT binary patch literal 847 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_owEPZ!6KjC*&ZHs;+n5UAy85o64<a9Ae6u}CwUr{I)GYJT8%?ngRv{<4SeUodZH z$Umml8Q;9*9S%soJ~`P~a>YX(Uak$zFGIX^XHIdN{qlB-=QG>8AMeU~DlK)1EpWWW zV8w8Yp@t!ywf`Q|3<e3t4#}yDcO_3=#WLlhZED5lM?Z=g3>)e~jgGPGP+)8jIKVnj zT(e<9;LHfc1FV(|0*n>>yMJ&kIKAS}%FiF>E&C`dx_eu1yHxU`V{7~yxP_P`u62CY z%3zXMwQ+U3fVc`%LF?V511sK0RUZ(1%=p0T<|6+F?n~uIe&}@W3*%UM(m3Y(huxQp zT7MgM8_aVx>#Ru#Id)Ti57Ww+?!|69zxclwzCEvFheBOg(%$9#FIJqbFgx%l@l(a! zD&ZWJ?>(<XR=%09zQAbBv~&IGg3G$kT+eH5Wsxw|58a`#w6cj?XpK=>nNWL$B}cJb zK<mt@5;s1Wu71?SeJL%ZFHG@@apmuV{d0Hc?fF#xuvM<%U6;f0CucuDKCjmj$j)G6 zTDkSroZYTbRrMdUH~BL8>Ulh1<S4oS!;CLx_L(%6sNRId?3;cjMzd^5e{lQq`=#HH z-c|2-8Fux?eddCu>D|KXPeq$QJ{_aBY^}hT7mxiUDz>q22=JQm{fOS-UF<qjg}!pk zk^lbj!|(U2+#he<AIyKoH(ZAA`}vs81v}fHS}hGztuo$MvT_PT!$fw4i>ec{lkPZd z`qUsMlkw{RSBAw#&zdsMGPBS674yef>+w{sqXrS|w-STryUv+<@kCEUU2uMJ=hKM{ zi;W&OEiwC*<h|78SN&SA8C3_DtlS>pwd7{(oaIYh!cS&hzPPMzT~16qx1pAL%mldx z{XVm=wr;6;6Shjr@V(_t*;uwXdA)<>E(W%B(($)0Shy^@B{o@E#jE#vmgZIY7;V-Y z%WlQw8|^C)5`VFC{iW@1XDJ>nS{&_tX_cJmi^V@2+(YkZ%{0-<d@b{hBP3|0_I755 zi7Ov&<xJ_2N!YnhDxvbuve-!{pKD$24^f}-VqT}lA^Ek(?S=l|Q;%U_U|{fc^>bP0 Hl+XkKjKP8L literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/questionnaire/icon.svg b/theme/adaptable/pix_plugins/mod/questionnaire/icon.svg new file mode 100644 index 0000000..91a4d35 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/questionnaire/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path id="path1" class="cls-2" d="M24,34.09H36.81v1.84H24ZM13,32.27v4.57h4.58V32.27Zm-1.82-1.84h8.23v8.23H11.21ZM24,23.12H36.81V25H24ZM13,21.75v4.57h4.58V21.75Zm-1.82-1.82h8.23v8.23H11.21ZM24,12.15H36.81V14H24Zm-11-.9v4.57h4.58V11.25ZM11.21,9.41h8.23v8.23H11.21Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/quiz/icon.png b/theme/adaptable/pix_plugins/mod/quiz/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9416510d04e26d97e42b55dce324c566145fc079 GIT binary patch literal 973 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tJOPZ!6KjC*Hi*#|!g5II_JHO1mql5*&+)F)q4s^^w^=N@5;PJ6)ofwAORn0CgJ zDYAA4+WtLE;1r#@#pu%RqrPSj<2@AAERA`L6;+<^`F(Ha_kAhvU%&32yD)@t2LqRb z+>edV&X_0g@HFs6@t5uWdhZ%jOUYHWx*bpdE_YzBxDn=4!RR&9fmuUPx5|Ne?b`Q( z3$6%WV5$4K_XqQoi@%gloczsrGX6&K_uVIx?<eQnH)dk=GHsYH`|PKg1xwtU>h}h% zQ?i(@t8|M+O+W0eAp3-IU9O%Yi^kUpLZ9LU`429epfgYWQs@1QyOH`1Yl>t(H}TH? z@hPtRtj?SZUsUVs_;$+ps3k;x_$j%)!Fl%MqeUz#hyTU?+}_MwQNwb$p#N-IK`HC| z_nt0~mQUWeS1{uJ&gs+je%|ES6#4T?)~And>RLW(7v8S>y_IpsItI-%UUt*8?k^U+ zbC2ugW@C-1&z4_*nOVXV7yp-KO2DdyYs~6QR~^J{?WTEDEPj|^(Ni`lXJ4GChBecl z*L`{6ZsxP6Zhc_;?!iC1cclf2ojzjkAG$I6`*R=7&;J>5r*FxWnT2A9-E3?fck5nG zH}X7HD<<f~(DDBJ?@X_pv}5lQn-3bOsMpkWDXaF^uvu`HWGz;__HygaDBaGSH%C3y zE@VD`{`rYx%*rs~#CtDXS{nlS^bUBYe6_lBmoxG5=9*VuUkFDo6`I%(s{D9u_ax60 zMV5$ZJ?j!Z{7$6ZeSA`NlfZ7F^XDh+IG}1TrRr9-Z=t4`J(IaXn_}zTf*I3l4DSBc z64PCDKed~CZ|woDuhWwZeVNwqYzme+<CSe%&b#GyW&|^HrsmX7@$Abq@9kf8pjhkD z58<04Yx`xS-~M>VY}uJ(R(#vz@Pv6c1O@7qURUI9d0B0kp7Oi)r-kxkpXDhRV(v_h zc<(l~%UWu3=j}b;tMY>7H~(I+b?1x|*WP<=x&3a1^^^3?Kff8pnN0iMYUcXzfrD7F zp8AvBcb9m*{J(g=m&3hnS55MD#V%Cs*(Ekfe$^83Q2n1*kIuQ~yI`u*C*9f64Ot=c zI<^G4?UPwI$FILbSKe@`bBq4Z1m5152gk}9Ebcx_6aJYh{k1tF$GGj@!giY%a_p~9 zY^%Qdpz*7INlv%<Z8hhw>bv*6Nn}bbtDkj~X;xzAd%XjqMJ;dE&pPqijCH<`-B#nj k2i~!)UGUyz&0+h)j0xr~f8FvJ7#J8lUHx3vIVCg!05K=Zvj6}9 literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/quiz/icon.svg b/theme/adaptable/pix_plugins/mod/quiz/icon.svg new file mode 100644 index 0000000..24d9938 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/quiz/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#f33;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path id="path1" class="cls-2" d="M18.49,32H38.63v1.84H18.49Zm-5.89-.15A1.41,1.41,0,1,0,14,33.23,1.4,1.4,0,0,0,12.6,31.82Zm0-1.84a3.22,3.22,0,1,1-3.22,3.24A3.22,3.22,0,0,1,12.6,30Zm5.89-7.16H38.63v1.84H18.49Zm-5.89-.19A1.41,1.41,0,1,0,14,24,1.4,1.4,0,0,0,12.6,22.63Zm0-1.82A3.22,3.22,0,1,1,9.38,24,3.22,3.22,0,0,1,12.6,20.81Zm5.89-7.14H38.63v1.84H18.49Zm-5.89-.21A1.41,1.41,0,1,0,14,14.87,1.4,1.4,0,0,0,12.6,13.46Zm0-1.84a3.22,3.22,0,1,1-3.22,3.23A3.22,3.22,0,0,1,12.6,11.63Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/recordingsbn/icon.png b/theme/adaptable/pix_plugins/mod/recordingsbn/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a7ca1edc1843ed7b831812cae568db1666aab370 GIT binary patch literal 6491 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 NgQu&X%Q~loCIBM|A%_3} literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/recordingsbn/icon.svg b/theme/adaptable/pix_plugins/mod/recordingsbn/icon.svg new file mode 100644 index 0000000..827cb16 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/recordingsbn/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#09c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.01" r="24"/><path class="cls-2" d="M13.67,21.52V12c0-.34,0-.67.32-.88a1,1,0,0,1,1.09.09,6.24,6.24,0,0,1,3.06,4.37,24.18,24.18,0,0,1,.21,3.3c0,3.51,0,7,0,10.52a8.79,8.79,0,0,0,.15,1.57,1.2,1.2,0,0,0,1.07,1,8.46,8.46,0,0,0,1.37.13l6.71,0a3.59,3.59,0,0,0,.77-.09,1.09,1.09,0,0,0,.94-.94,4.89,4.89,0,0,0,.09-.9c0-1.91,0-3.82,0-5.74,0-1.14-.39-1.5-1.54-1.58a18.42,18.42,0,0,1-4-.51,6.51,6.51,0,0,1-3.13-1.8,6.33,6.33,0,0,1-1-1.72c-.22-.51,0-.79.56-.83a12.69,12.69,0,0,1,1.46,0c2.21,0,4.42.06,6.64.06a6.13,6.13,0,0,1,5.81,5.27,20,20,0,0,1,.13,2.36c0,1.76,0,3.53,0,5.29a6,6,0,0,1-5.1,5.76,34.36,34.36,0,0,1-10.56,0,5.82,5.82,0,0,1-4.84-5.32c-.11-1.91-.08-3.84-.08-5.78,0-1.35,0-2.7,0-4Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/resource/icon.png b/theme/adaptable/pix_plugins/mod/resource/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..788cd31ab16590311d8135bebeb803bc544ebc4b GIT binary patch literal 5435 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4mJh`hM1xiX$%YuY)RhkE)4%caKYZ?lNlI9 zK6tt~hE&{oJD0m8^y<vx<{JDfFK=8Bv}AQ(e_7X*Q1>nV=@AiY!@{R#UB0HpGviM6 z=Lt+llU!3)tY<Y|dhM!}u~ig{djJb-e720pnj{6w=XWL~G%-q6wwp5^QhrjH_P%&B zmypw>>i3rE??3-u|26)I7}L*l|LV%;@7@15S8>8UsmOnpg4^3Y`X}fK6uzz#+?;-D z{rsu%`?dDR>Fr-PDenFw-%Hj04}FjS-M8w>{8F**7F+GFwoku@)w>_swe;!xH;0)W zG;Yt@_Qq%8bDl|@)0Kq_SNWW>yd;0(|GJ4~%w@goT{CQ>>;5*h9QJ>*KK}E+vm6Wk zR&Dr{Gt=ezQoG!_l}0nQmb`YJ@M7-PU*^-7*RiE<>WI8vWiRdY!a_7cyt_r%zjjr1 zaQ^jz_228lz8XKD8gqof;bPF8cZ#+#Y6p9!`InnNic^i3U)CM*(m>ygLvqF!W|uRa zCy#EK{`5M#$FJb3{WV`BHR{g_w=RuOd7w}`^>vIE`)3o&8QPw2^mcDjZJTbeAmB%6 z$R+=}Q_oK`?he^Ck>%WsRGqckmkKLw-=WXU+xm8{o%_Da`id|AHZ_XBmf0p1IH9w? zb9;s8_U#j=G*(8RHhv(=_;ZO(`iePS`wlFxU2*3^kZ~a^vq);3<>&Jgj#@5$!cgq7 zZGv^;?R%Sp&hsq%B7A@6<iAzEuel$6(`R6OwdcyCN#S$T^HnBp`Ofw}a#KQw)x;mt z9x{*ra4I;wUHj_(kq~>O^=zEa+1PBdVz2(+CL^Nl*>j?DrH9IXM<$ohRo84vnbvov zbgY@Z$8f^-P4=9EGr!kt-(B;<RadsagK5p4+uFurdFDb#QdB2}Z_D8~d{G#!_nB+& zgha2eXRB>O4Uah-U%pZ+<mu7H)4!?}o{4<&dqQKc^-Hda`L&Z-E`?f7wT-s>I$QG0 zj_$l~$0kU5dnh0E+V|>ei*ZAtTTY}>;5Shr@9rntkDqKWo8j1P{DwJBRgr7u6;;ud z_rCCi&9OcHX8F;UN1Hu%1k}Eo&Nq#J`7Fk++D}sN7m10q?X7*UCS~<D>3=+D(zRTb zgn*#syCR+*cYH6jVisc+A4|Xu;mzWP422$LoMJ_RtKv`j#%VO~o#oi>9Bv`9eZNmr z+Sl6qtDi3C*5}Kgo^(zqC3mC$UC;RA3WpWHJ4Wh%P!=*<e7okF2j8cJ-Ws2}gu8x^ z(v}=JE_vR|{#r?Uf(+-JxXAY*Y(m#$n7&Uq5$pZ$C67Xm$IlNXLc4-5*S+`3VB<66 zWD53t!>79cuL3ihbd-TfFH3&?!F;1;7U_5QC%nk@|E+FlQQ%@TvBf^|;CV^k`Z}4F zzd5cf-F(9PpZw%M7EO6yRz)kX4LKfntNh`LUjZf7{LB5mUNzt-c)~B>`cCBGi%rE( z_+w8VK7M2KuI-QV7f<|i?{3Yv*ES_R{u&}tPoA==?vrpAW8$AV_s>eUd2`+z{xPM! z{mHr+Gf&Rnb0&M*0q0{oY~Ks-JhwDW_5Y2j^QYV2aNlJ==l$6qCu);!`h8#g*8R?0 zcBwnZmZgc$T~lh^w~JYQ>f|#ozlxizIGJ&1-qO#^J2O6Iec$>i%1D~yM`gha&)|E% z-CzDuX#2bF*4^0a4sOhh|JT+=8<s9!b?p7$tW)(*B7|qoX)}AbvRr?fv4+qt?{%U- zQ|p(%-uX}NQ$_yDbqD|d(La4BckA&J3297@ziVyt<-Bg}ziRoRe@~?IGp5D~d$?`{ zmsmeBzLMR$Ci6hGm{s1}hi;puU2oj5Bko>9?M}7my4O{lrL~)mG4B7iachdUaic*V z@3#vJuY~^&yZ2q^hR-r1VUC5Vr)_vw{e4x+|3&`Zy^9MQW46BS$j-MA@yx#wd*0_j zz5LYm*P9PjpVCiUZ=iU=f8X}i&dn7sltU-V_I!M9W+2RwCiY>SK;Cu(u|H2{38gSR z;Ad!Xn9kke)|z|%f6U9h44`5>e$VCXRl;%%7WF2Y2N({-GcYvj?dAGor!)Vb^B%tC zU^Vu46|$Mx+7~{0FMGL(o1ux3LEuFR`$F@kxF5SO9y!3&aE3AA{X(~G9GiD5RKz;m zt~-5h%KwHe1_p*(!58oJzB}~9E{*Bt8x0u`<G|9b6BjzNGR$Od_*I+!#QNQxOSTRQ z*_#=jr#sgCTF=bDut1q1V81*^%GIlJe!s%s=if~&a$t0L#(06N@ws<{!?ZL;yI8YX z!8e2dwMsKEFuc%VXmAMIdv1QD!y(D!Dg#D`Zwwc>8vn;}vKZ~xeQ{^vUuOHZb$^%{ z{xdMJY<gyT^R8e=q`AA|1O|oQ3>mEliX9X#mol2CZG3b67dHa~!%QXyffHW;50q~T z{rrFJ3Kl_zAM6YY6GY=D_WJ)9XJBAB!Nw4!Xpnw1Pho<j{rs2y{~4{yf3h+>WM>dK zQ6T<s&(y!hoDJ-YL>w3dct9$pIZ_@ic~C2<^=IPtNk`uLGB7awU}R93VE8{iZ5vDS zxvzoTats_|3~L1{wn=a-*{(d{Po3JWT3rSPh6DKw8s+{s92E9$OZ<Gv`=8;-&n=9# z{}>zQF)%P?{_Z%T`)>9*UHiw2BrF&vSTek^y76G${ZbR%)hW+DP1M`1Fn90&BnAeC zKW{ia`VaQ(XWMgo>yz`oaT@y_jTsmite6=%ZulzIUp=ty&0qhXR}a`2(ij~wZ)M+J z^hfH$>2-VlPY1<5Lr(?okA|1_3=9kx=Q5UjGIj8kRN7O-#lX<exb$G_+?oIOgW3ZO z2CqyL3Xe~!clsFliC+fPHeir=YGqg!yS~rof4#t0Auk37hMtmn#lG&#{u?tgFkCol z|Ld|9!-bF68zg0CGKMgLZ0jj;{e5zNd|lm??f)4V7!2;5U9@6JeD(F(|1AoP3=9cd z+_#kfcy7qV!objQ(%0!tefIjF?wXEF3=9V{7}jK1|K!#_KW+cQMu_6j^7nS?%nok_ zg=82Q7?c9UVyxDz{r=yRg`I(+!J{H6`uG3y!V(~<Kmn8Gx(sX73>X*~Tn?qq-}z_p zrsW+MuEvA-cifj&%Y-vb`NQVO$iScw#NF83&)iU~4(53=Y^bY~VR+-t>1Dw1B6dni z{kJPs<);>{efQ=6%FY(`{hN-;eta*#+m>6FL12!&jU0o%kidV&g15=pcW3=QerJW( z*4O{OeKBQFm?4%}tjWkHz;J&e^ZyeY)y!Yrs$X((@0)l#7KVM%aleZ1h&}LQdZf<~ zwq|{CpqIY-cSeT)vBiHR8JHd^GyJ+A{LajL@{hGa#tV%<%Q8GD&u4h=<FM~*Y12b} zhPFT3CawA3^UE)SmElj>JYx^v|M^N1|Ck=k`IwrQEF9br_SX8!i&$nphCg;vZ|*E$ znBZ>kC!RTCPnP`5`-{}4y-nd&kYMNuWca~*@Pj;q_`UFldHo;65>{V(XL0N4OM3>7 z-vJlaw_aE3NMH$KU$AaXT+mkgNuBHOHHcYM-~Ft<&P%(EL1ArF!^>6t2j+9SZCHAM zS$*p6!|7qwt&V{oHrx;0{e0=n9}Ei0|MVZ;m2^|E;64!9pz)2pCh{)JmN^Lw3=9IE z4113II)EbNx+oXJYfcsh28M}WqF5WwvM_<-vuZKZ0X`;11_lR}y|Ii7rh$|Sc)ky1 zu<%h}U|{H2!mr6-BE$iT?w4*10tyUQB!6&zHs8H^yIdK++|||A7cRD?m7m_19JfYq z!qgrH2XFHaTmk|NA<k#EYn`=VU%+R&^zt=s2A1Dk2b2;R7y|AV`c9g4v}EgQeg=** z!3ntz^BFIA*Zo?ZHe=aKtI7-P3@@&UPjFJW&uHM`H{JJ&Ew>pn121=@otnTurU%-= zVbYuph5C6+3{umWiX~2@GpzVmu2#mp>(xb528Y9k9o`Fb{1<0De(rWk$fx}~cTJ0B z_#4%<cl9qu9VLN(%nN4EVg2K2cG$#m>HI4@pa07eWw>zen$uOm7m`eZF{%r)CYq)n z&D-vxHgWI631M|x_gw3>-+u6I-~*O#YZx4&cpc{jG0bo@&|qCAVlj!KBE*z+t_vvj zcPvpqwe3NeULykogF;iE<>H-EJPZsCC-yO|Sbb3F<Y{oTKX4A?l$y{E&ZFs_S3(c6 zfO6xD9iBb0*S#x(_8#N_g>m4gu#e$K-%bTfYWV(t+UhW$>A+0)08mtfh(0Ku&*UJ= zA~3~);lgf>m;b)3{Q7Q+YxG_<MaSP0&9m=x>gSfR`>$qUNc(;8F+;*#KZif;4ZBp! zU+?=ckE`b5t=HdnT5~d-DLJ>Cp<^nan!-P(2VL!}?^y9lHSUWG<Yw?GY5e+qK2yT2 zO~z0ESF{}X!QN1QP3FS0E%$l**RwLD{hV-OW__E5W1+QY!XHM5{hX`sEK95H`)#M# zuglD^_!q;JuE|q2qzg*aGkhrBd0^S(xT0@N44bzx%(s(dc$Fe>@FOe3^B(<TY13Iu ze_lu5W^9NwZuqFqtPu7qmg(_*sRNHfe{b=qJG%0;{3<4fw4c%H%nnysHw(OwX3*QS z-l_Z8f>nAAu?&aB7#^4LUtAVaslfPYE#HGjCs*H94Sl#rF2_H%IohOLxz*uWD8oP3 zIYC?4xtEAkEZ7rM{Nqg4diw;4nx%hsHtl%E&G6$Y+nbQCNONJ8b4&$d2Qu!LWHCIM zKHondT%eY2*3f#Xek=5j9y^FHk?B4s%&^)D#6EE1@BemdhK5UtARYtr-2B@2Ul=@O zc~-H41SFo?s>?H2J+*4sW6T2*NVxRve)pooUAHSiJch{(4|-l&Fue1MQwX^Y%ApJm zoOvRZ+P}N~G}H8%L24v6{@U+S5@ons0K`i;^2PpMCBq8M=2>9-8A{5UzwBJ(-F4d= zT!lpa*1ec|^ewm|Xt;LnwH3oLmGhJDFOUOiUa+*k<IbafO1eki>;&-{cx5-IcW)8h z-VU~D%a^M!nH_fAePX@}T=!%=^_pmSbgd^?>DsLS^-~=gZZI$D2V1nIp!T_Uz5jj* z3s4h+VS($*_r^+(yKXzXxcp*eVqjRX@v7p!^_^=p(*Em%3{bcdU7z2vM)O$=KZwh* z@$;N)moU-or$Ic1g<F(2>0ONk6-NvLhhFY4cK^n>K#Kd=TT_s9L%_}$pW@xz@l!VB zfn%WM<>mXke1E99N;T&TFflMV7*DTM{k_jikey*d3xfjB{D0=$|DVbVc`-3e5@2B2 z^*&ySDfrMXL9pxzXZvfVN&!db9%W!)Zg9x@C*E;Dbi4jUJ1zzTh7UXMhR2<BRTAm4 zHfCU8W7t>s%U;K6@$PB=d;hrSGBZdp{CL!Rsn%u1lnq6UAbFiXy+RA8>}p|PNMM+7 zcIE#h!KkNd`cwX&Xx_#Cv){YCBqLyIP0(|_mACqtOKvW6maPi=zIW4676zFK|2A=K zee})cE_3bwMQinpychrb)5+;-|Jdcl)45_>bmlVGzOJwFIVQ1ewOs7t-&g)K|M&a7 zbhS<TiVHis3+C^h{BGedwN1waa_>dIFZ0g0_tZSBYUc&7!spkmgul;xy8DET^OFBe z3ykA3)Zd@j@O9fd$N!i1iss!7Ok>=Y9sSnu`<soPa~VJE`tM$Ay!1~f^Hcr}(bpf` z|8(SdUa+y}lGS|j*_UcpZ*zMxeZKydusd=KKYv+W@Xl>JBUgs{*0$o?_3N%%{+ylq z|EzS2px2GfC2a>x_pdLrzf(11%_WzkuV3G(ukiS;r2E=4vFF&j*Q(-1UZr&kcZFBJ zJ^1rmTzPpcbEMdz^1tcb<-4!j@4MF0H2?e3YL3NAv}Qc|rWfoLP%W-{SHDO9uwub^ zr{haLrIoJ8YcL4veDeJ%|8);u>tngQ9IJL1L~&Mg@k>26yZ>9A^YPMG%tGI?ZE|}2 zKWP7&@@v^@&ZFs-ThH2*^aOX;EuQ{WY+_KLr~S|7@XsF#kNll@Lie4v(n_Y+Ns<k+ zJWMiy`TYLL=NE`*@AYDPB+$(0ex|f2{Eb(m%WKo*9R5cya=xpsiT3y-yp4Tsbo4hz zNrf9<_Wl!8skwc%>A%k1BMJxGc6pvFbD#9$t<!;Bvm2XwOU~%29JFR@%;b^CiTLEN zy3u;8fkx)ab1ude8{S?1w<FfUshpF^ug8>eP35QO6HjEm<%`(9*}B2>i{Z`(-;0GT zj7yo68yT)?{k+O9ek$YWgL2<2ji%nOevuEq%L<uEpDNXR${@)1_29f6wVa)^d{+jy zaQ?PsljghmRQA2w_mKJbK?68DeRp&?wmV-snZEGPR{qYHtDod*zS<Vj`(ELQn$9=N z!Xm>9xrd(ES3X}Q*>fZQ`U&rX{}Wkwnm>ird9LGLlMs+Jf7zd-_qLX6xCFgVDEuAC zrhcx-_y6TMe{YdnpB}z;eO1KDrhRV7cCY^pESDW$xn14yRjXMn{Y7y7M2X2Z{YNh4 zHC~W<`ZX?D;PJYj3(VUloLkSbv^i4YNbHX*@p|Du*(W+IyTY?1jd{1^ogXG)$tvx0 z3<F!2#!pG9?{w_*4?Jh>o~QX;J@CwZ%^l5inQG>kpEmp$ETrreRK1h)cU=C$c9xD+ z{1s21SWdO|>^PApd8$j#^y}jP2~+O8vHiSzlK2FM6;HPw__a_*LR6*wW1Ooae-=N> z^;1ep-7bcCjq9&eZ{sMHVcF*VDC1pA?&KdUvQN#dZ?kz{tG=G?mfzR<hqlfwm%{Gs zi>jNo=U7tWinVz&=DoMoZrQZRCVfSWa)o2T+{zg`(Z{SmKUp7c;4sl%YsHVI>wH>y z(<e7D`hC7?9B`g#VM98re^AJi%bd2N*`mcQPD?{hf2y(cnEt72!prjN*SRlQ)^49D zCly=PSI%U(wIIS@sQlK~xHlm@p*n(>D%cYn$}blls=K+`UVW$k)4#g5-#+h|Jexz3 zqgn3K;is#<^1R}X68n_&t>os8)EVN^ElNudo!T6o#q%z=GWJGt$!6aCsP}UZ?YZ^) zrl8aOnTk)M?@uvqSu|~K>(p8A^kjUtCI#C!|A|am8vc>9KOo%X?Av#Dt!5YGO}+ed z<;usW&#~WoYm&G_(f#ZB?wgyRUgtltwNHL|-ifKXHw2s_r|r@(&Euaarx>eXU-*9d zNekmr&#s)_b(4(q!adhdzo)d<eCv&C*O=yf|DT)rb8qNM``0?t=0DjSbU(N*|7*F* jn&_y1etJi)#Qf(^N{?<m(w5l@D)T*E{an^LB{Ts5!9u6S literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/resource/icon.svg b/theme/adaptable/pix_plugins/mod/resource/icon.svg new file mode 100644 index 0000000..4f2c966 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/resource/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#06c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M18.52,27.66h6.4a.91.91,0,0,1,0,1.83h-6.4a.91.91,0,0,1,0-1.83Zm0-3.66h8.23a.91.91,0,1,1,0,1.83H18.52a.91.91,0,0,1,0-1.83Zm0-3.66h11a.91.91,0,1,1,0,1.83h-11a.91.91,0,0,1,0-1.83ZM28.57,12.5V15.8h3.31ZM14.86,11.2V36.8H33.14V17.63h-6.4V11.2Zm0-1.83H28L35,16.31V36.8a1.83,1.83,0,0,1-1.83,1.83H14.86A1.83,1.83,0,0,1,13,36.8V11.2A1.83,1.83,0,0,1,14.86,9.38Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/scheduler/icon.png b/theme/adaptable/pix_plugins/mod/scheduler/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b497d953ef353566b271c9b1a749e2c4408a4584 GIT binary patch literal 1080 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tI=o-U3d8TYQv+M6%sDRHdc(PUeX%t_IpMFF|eF3V1u>Uc?)>{C-;&Y2mpR9Uh9 zfY<e=C-1g|T-ar(zWjVptnr$Hmun~F91UHv*<nWsR~xTgb>dC_rulp`=2pr*`#Wc@ zyScf!ORIpJ+zs|m4ty2wjK7FTFflLCUTWI0>)YZDOd>C5DeaRk`m4QwTc%BP^CkvG z9$AA%rH_l)X9Pu+DKWJe$T8<WD4%rH-oe%D^?rrsV8)a0wQu{*vdQXQ7-hVRL6Jw; zqA%fj;s$P+)msnGo*>{RU2%N9tGn0gO`rI3I4T>;ySxh+og~_&o$BI_Bq|68ohv?* z_K06|?Yi|0qFU|QWj8{F<MZr}{JY<BOWLPSZ0@b^?;1{Rh!3CXdwJfZlR1nJc8T}6 zO?Eh+YIHl}mN0|c;_R63HKB@2Baiu=ugzR%b^7`Fd<_v32A;*9``@#P{9`%2iur+s z-qoz)t4n<yzWaEHrn$=Tb#-mt&b&2s*`=I1h1qvM%O*>t=9^DDSXlQiWZ?qErKPXG zpFaKm%IkW4<F!%&Gc7+J+I86A6!*#Fy*Xw4hc)!2xA9L1JGXoHjhEZrZT%d!`rY5x z(@KSIu*dDWP*a=m?RL&tS3yq>XaBQv9yU7py9-{(kh#mX*zw^maS6A}+=~NQpB%E2 zRjSjzH2J`@Z7e%QKD=R^ePMHn%;prM+fz>KU;Jv*+SK-Vhl!N?J-$?<+cGkHoH%+n zZ!Vs7cJU37C68ylxV+qa$2-=?A5B}E+*XIWU(eQZy1Mt0V&0NRGg3@u1!&bid3$@m z!P(5UTQ`3;=DGFlK=G{|g17lnjG{wUuh*Pj&-~!@^!n$Yj(R5f3CwQDDeG@;4!`$2 z|Ng%9N4tYpd3N;F-Cr8=>&4&CS6^kDyv=;X>(}+tQnm1BQ|7jpU;Xl@DsU!EV0*J- z>6UfRGfnI&j&vV=-gEqVOq^PtTbWg#tVFMxMW6RZ3)?C0zWQxkGugWCfrVar`ty(P z8m5GWb3E4a(!cYb{q-uRk~r?S@m{N~Pcz?Tk8mq_emKMWgk#v=f9j7vssx5vTi<qU z-)74g6tzvj?fm(fOG`9Gwj4RdxZhNK!Siyh!uK1!R+l#A@?ZG+C+fuQZwZkr^X^%S z2Q3$#==@6R!sJVr{&wy3ikWG-^2}<Bd-1(qnY`0wH~vYwBI5qPD9UR!Z;9E28u!BJ zd;BZRmFDs)$49PQcvC^}VVq%ZM@Q=u=B-n*n!acE`Gp_&o%-bclSWs8Lnl^#nOidN zg<<VQTOK2eH=)hWfAmXQj;`dI{7sABb?2LftCt9U%}ry^Snp-CsligQf9AaPuYA_u sej@%wEPPd?)C#`~d{yd)`0W`q%;uWz(mvtGz`(%Z>FVdQ&MBb@0B*PVCIA2c literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/scheduler/icon.svg b/theme/adaptable/pix_plugins/mod/scheduler/icon.svg new file mode 100644 index 0000000..f68ed04 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/scheduler/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#336;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path id="path1" class="cls-2" d="M27.22,22.35h1.31v8.32H27v-6.9h0l-1.52.77-.26-1.2Zm-6.9-.15c1.72,0,2.61,1,2.61,2a2,2,0,0,1-1.54,1.95v0a2.12,2.12,0,0,1,1.84,2.08c0,1.41-1.18,2.53-3.24,2.53a4.46,4.46,0,0,1-2.33-.58L18,29a4.2,4.2,0,0,0,1.89.51c1.2,0,1.69-.68,1.67-1.33,0-1-.92-1.41-1.87-1.41H19V25.67h.71c.73,0,1.65-.34,1.65-1.16,0-.56-.43-1-1.33-1A3.46,3.46,0,0,0,18.3,24l-.36-1.14A4.24,4.24,0,0,1,20.33,22.2Zm-1.8-7.07h11V17h-11Zm-3.21-2.29h2.74v6.39H15.32V17H11.21V33.41H36.81V17H32.7v2.29H30V12.85H32.7v2.29h4.11A1.84,1.84,0,0,1,38.64,17V33.41a1.84,1.84,0,0,1-1.84,1.84H11.21a1.84,1.84,0,0,1-1.84-1.84V17a1.84,1.84,0,0,1,1.84-1.84h4.11Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/scorm/icon.png b/theme/adaptable/pix_plugins/mod/scorm/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd192d2ccf24a317e3fc7af7311def8311301c48 GIT binary patch literal 1288 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qX0o-U3d8TYQvI-jkSE^*vGYom&bh)yGiB6H)Esl0|~G^<a(QQi9{NWA*i=~>;^ zZ|q<2Oh2M}&2+za+J<j*r}s?%c1ck<NJxs~%%%k<3L0$8mkewz__B;{{#4z1KJZxO z{`=1>=Rf~x_kNG!VS^9ZqNY<AH#x8gH1N)LKhGe0p&`oA#AJVV`yL?!M{(Z=h5fI~ z8>9lhc^xxt?2?+)Ahf}ELm0ak>&1X&P74;b2{rK4oG*FHaYEc>@!Ka4H#mQYEPn4N zZSzcO*Un}2OroYCOnkL(iuJ|V`Oa?Lo4>G2YEonQpY3G<0=;`)bDZI*Xed_`d(NVj zwZcaEfxG7vSwVxz>F3{fo&3pTz1sBJq@N2s-q=pNk!+bT&FgOdp>_IVg2AebCLYY5 zb=RtE-tybm|4w?ra44g5ZcoD-_8OB(3F%vpUHeqMXrjg1-Np>ll5Rh_B=x%csKTnr ze==Ck@Krzhde%v)Tjj*G>sO!IWi56ovTIg3q4M0Uspr_~#N%?k4kuS%kJp)gGtyQ? zs<Zg^f0wUC!dne1VplH@+<Rhy+4triw@e+TWd_z?C#_g~Z*P|DhuhDU{{G&!_<5~c z@@568TLw(7`kk|?%e;#6n*Y_dKYnelQFTx3kI14xnO=w6nV;O0Irl}c-6(SV+tZ^A zi!N$*xG8-(EpOp>FCs)?`ZYO~Rr12Ysz;X2XE@`LmRTaJ{c3umL(#7<x6CWKYhJ(W z`Mm%1zTK1Wt&4nWCe^uFWb&Oqu|=*4IoxM*Ka|C~d!*D>*3WR4^W~atyvBRIY~k(y zE+#_h>0c*^pH5vgvE}jq<w-(2x1{CHP1}Ah^!0(@-<-_adH=8Oy8NSHqGP^1<A<8U z{_FR(RGu66RUKQsRd{vP-+w>d?czM+ykC0V$bR$ddv4$Q%^JV-{{H4Wxs$i(ykD+& zl9t*>=cva&AI*FE^_6SeXX*M%k#DnG!gXi9ay{mCWbM=Md*XsSTwB@W;yc!{Jp7u# z$x!lpS8nsHw(IV1S1T;%+Zwp&X6OW#nXNClqEEkH`)i$<S0fX@3De?hFSKfQoVf7j z+S)q7E!WjKOPcC3|L^mC9JWy^RG|8<pI?jQoZx@)zTC`xt?vKlH(v75^;jo0UAXt@ z#>Lv}loo8y4x3uJd)?%s<<W0aE2B0)Ia0uTTu`IK?QEu8$#1Kxr+vNM_C#41>MFZE zEzw)sd-nX*)4ox*$N#+%aanA(#;GrJdlj>!;T1i<)FZ)4e!1`XXC<BwwYuhZdc~V< zr!=bOJ$`N8mvUsu^>(Ga7p48itvP}Q>VI~L>&DBj{OK(D@y0Z+6R(|D^ZTnZS%=M> zvnI;6cj*nYs^1+qt~asnDJ^eX^n5Q<eZd-|N~7fu9-lt_WRCT|w^noRPpR!+u<X04 zwdTa{muE)X_V(9%{;B-@`Nn%^L2-s3eVrdaZVoi&s(cW>Uz@X2=XJO1moi-s*)7om zygxme)Hr9}w2EV&%`e-(D)95c=1BJcOXkc|6uM_;c$TA6{h+l}#u@Q9e#v=1oVMQl zD^zh?rS6PYmWRRZ@86~zye?VuqRfP~a&K0^v42kO;nvJu!Mn2(oK@r<cG@zeY(G#d zETK3lp<thu;f}n?2mh_um&_M1%aT*e@4d^7oBPk#H#qIuD`?5Uz`)??>gTe~DWM4f DpOI%K literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/scorm/icon.svg b/theme/adaptable/pix_plugins/mod/scorm/icon.svg new file mode 100644 index 0000000..74f79be --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/scorm/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24.04" r="24"/><path id="path1" class="cls-2" d="M13.48,20V29.5L22.61,36V26.51Zm21-.13L24.47,26.53v9.53l10.05-6.64ZM23.68,11.44l-9.53,6.79,9.41,6.71,10.22-6.79Zm0-2a1.06,1.06,0,0,1,.52.15l11.78,7.82a.9.9,0,0,1,.41.77v.11l0,.09v0a.58.58,0,0,1,0,.19v11a.64.64,0,0,1,0,.19h0v0a.94.94,0,0,1-.39.94L24.07,38.51h0a.68.68,0,0,1-.32.13H23.4a1,1,0,0,1-.32-.13l0,0-11-7.78a.93.93,0,0,1-.36-.94v-.06a.58.58,0,0,1,0-.19V18.47l0-.08,0-.06v-.11a1,1,0,0,1,.38-.75L23.19,9.58A.58.58,0,0,1,23.64,9.41Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/structlabel/icon.png b/theme/adaptable/pix_plugins/mod/structlabel/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e23704309e9d9f30d6085267c1e7aa3908b221df GIT binary patch literal 1881 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 BcnJUi literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/structlabel/icon.svg b/theme/adaptable/pix_plugins/mod/structlabel/icon.svg new file mode 100644 index 0000000..6f0c755 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/structlabel/icon.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"> + <g> + <ellipse cy="24" cx="24" ry="24" rx="24" fill="#F79F1C" /> + <path id="path1" transform="rotate(90,24,24) translate(9,9.92668054059196) scale(0.937469826119774,0.937469826119774) " fill="#F9F9F9" d="M12.523432,6.5932371C12.062422,6.5932371 11.627413,6.7782275 11.300406,7.1132114 10.610391,7.8171765 10.610391,8.9641196 11.300406,9.6690852 11.95542,10.337052 13.090444,10.338052 13.747459,9.6680848 14.436473,8.9631202 14.436473,7.8171765 13.746459,7.1132114 13.418452,6.7782275 12.984442,6.5932371 12.523432,6.5932371z M12.523432,4.6300842C13.495453,4.6300838 14.467474,4.9908158 15.175489,5.7122805 16.621521,7.1892073 16.621521,9.591089 15.175489,11.067016L15.174489,11.068016C14.467474,11.789981 13.526453,12.186961 12.523432,12.186961 11.520411,12.186961 10.579391,11.789981 9.872375,11.068016 8.425344,9.591089 8.425344,7.1892073 9.8713751,5.7122805 10.57939,4.9908158 11.551411,4.6300838 12.523432,4.6300842z M5.0143417,4.438823L2.895002,4.6530449 2.0820014,12.965046 15.991009,27.167047 17.586607,25.537159 4.2790283,11.949961z M15.282026,2.0849626L7.1730118,2.9050317 6.3610606,11.218027 20.270066,25.420065 29.202016,16.298963z M16.037031,0L32.001029,16.298963 20.270066,28.277002 18.986857,26.966831 15.991009,30.024048 0,13.697046 1.065001,2.8270449 5.2131304,2.4082195 5.343055,1.0810556z" /> + </g> +</svg> + diff --git a/theme/adaptable/pix_plugins/mod/survey/icon.png b/theme/adaptable/pix_plugins/mod/survey/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e80ef103425c536e3683f5b3d7b1b6072f85024e GIT binary patch literal 800 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_q`}o-U3d8Ta1K+L(7MK%~unx0vX}HA;q!QLUQUx|OG!;uV_AJJi>IU|X91V9mTa z>Kq#S3Cnz@y}04yGI18Crbggum$i2qxr-RA6L^kg)}P#z68rnl{_`JqexH}D$-!jS z`q-Pzz&7hZ;*kr|2^<v-=`S0%uguk4$>4a4W!8cAmT$)$#0w^OMMgI$++%#Tq}%R7 zWcZ0wOfvDS7|kvu?0&(R>&@S`lCz*@CFc(57OzPcjC5^Z>@y39Yuv*d^Jaq0p=+%h z=H6lM)8BbO^X8*Fmzaxl`QLC>HUu9`EoD4Z`rE-y=TMwy!;ggMhuO#H=lJ#6oMMlf zrWDl_y@g?-miV1ey{wQcR`2gm-Sn+R+rq8tz`eq4o|ERjS;};T=XS#Xivls?HWw7_ zXB37y<=b!F>ig3n`qy5YzdO^v6i;aD-1XSIZ7KUaCA}G$`{q6FHxn)WSbN9z%g#M! zmyD8`j2X}7{;2%&TKfF{_nI22r;eV_wBN_6(EZ`2*R!3wa*Hc-3rcU_^KvVyIXU5| zp4R#bX{Ibov4F|RW)CV4Ec7`yv6|6q(rlgDpL*x^|L*#~`15_{=<g00tlSN8Zx@8` zTT>GsYPw>@>CNAF$%V|A^1EzSr`9S#%?e*ebFRrzscI{~eEiwgzI>s;)C@zLDKkZ) zj1PXFc>3wqjVHOTo(^%~`j8UnBl~>9pBtB$^Jls3wiLO*d!9Luk9CJJqf*Z=OQrks zzX{%R3r%{(dVG4-kt=PdTyqoMSOd7X-rTuLN@U8V?vIy?9W?_(ds{!Ao+lZ6@I~ux z3;k2C7v-4GxUW24rqWY=){%}C2|bLuclHK83!CYDe7cVsZ*M?p;)c%>of|UGZJ#&I z%)&);_tx~n`m~n^ohp?#Id697<?DEP>4f2IF=31Q>7Ul;WgO{Emu&BqG=K1Rdt|Hn zu7+bvIQQm$aak3US^Mq%pDY2zBP#j!)kivJ%9)n^*nKwsOqA-ny5sX&85kHCJYD@< J);T3K0RUshaex2- literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/survey/icon.svg b/theme/adaptable/pix_plugins/mod/survey/icon.svg new file mode 100644 index 0000000..17933ae --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/survey/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#9c0;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M31.31,28.57v8.23h2.74V28.57ZM14.87,25.84v11h2.74v-11Zm8.21-9.15v20.1h2.74V16.69ZM9.38,9.38h1.84V36.79H13V24h6.39V36.79h1.84V14.85h6.39V36.79h1.84v-10h6.39V36.79h2.72v1.84H9.38Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/turnitintool/icon.png b/theme/adaptable/pix_plugins/mod/turnitintool/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ba3fbf4b51cd2bd9185ada7b415c4d954170bb02 GIT binary patch literal 1070 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tH_o-U3d8TXz>?GJwBAoA}_#H^1|8B4g<D!+WlKVhQw(urErp7(rdi<8vZc*xDG z@Du-$y2N!;y`(eVvsJIJs8nO}EHiJq^CQj3V8hM7`)cjKt48PN=bzBo7Rz`bobf>1 znG@m+=NW1k>{>o-*4w+(x52@j;R?f>tabmz4=_0RPw%l}WO~P>@woV@V8d!Jmw64j z;tVAWGKpJ1F+AXLvZ<G2yuFU`MXG_Q?By(n4#!&#&P+Az2mVSORE~IU7h&DZmb07v z0ZY90c7a6;*EODH;Ac>06k}ytvdfo2UiIQcW5uEy)j7-l%qf4<&!Q3XFK+YURc_1w zW^~-0Cw(E1slIqIzbnVw-{s6bTV0u^Bwn3#<)EV(Q&kbm?R5pm`mzp$&N^mt`2Ab6 z4_{Qu<5#j+_=N=*H6JomaM-;*>~=)hkGr26Zu-n~2$5l*;n#SQMWR3Tx#nguiypHr z-<|3%R@l$-X{eEVQO0=v?~1z>nJ%9ld@B?0o%|Rv-{N$EN7S0@FU!wQRt!{TO0j#D zXgr@^JY4(uOJl$6@^X{sKP#(THofgScy58t8|F>tPH);gd7b5XW<LJZw96_9vlJOa zg*WLu_-cAH|HuBy_I|~H0JQ*4i+;D)UuU~&N^r7&C}CQ4az(_H-Ti;Ft$b$}p1)u; zzbG<aJ=HWoYvN2_wUxnn+1B|nNnWS8ZOk-_W~yc0m~MT0wXg)A=hmGv1(iF0{D1kf zs9~C`poZ>8S^3Kro=k1iUKbn+oR~G$ZgRs+wX?Q!*UdPP<7U0qsY%wJQ{w$Tr<u78 zw`!}D!Z%O&`J`ye=E5bq-CVmbb_mU!vqxe<+|peZe2q*RNlRwRFnsy_QM$0y_45v| z(7&I}R$N>a=JovZl01fttr~YqcQ0J)t{l8kqV2B5=a*%f#<P4^UXT==&l#g3nD8O6 z)O_}q>OxbV>1Vm!g{PlcesxmW9SugSE4RPNFYUVW`H*b4-YLU7n+~lxZEYhT<vY19 z{llj+dASd*KMqzVDBNXFYLA&VA<fuQVVbMi{L?R+($Bn4+_GG>vcxZ`&)Kxz?8EyR zy$g1Ha{ODlQ)YqkRjtOS#sz!lvHnpmSLJ!S?ns49n=fC#(E)wEjO)K^_xkL+F*V}W zOlgm=Z8GXPy<2Xl2nGJu6Vsd$ruC3B$0X|AnOC0}6tiwdbzkI|wCc_A4|8@mT}#^b zEN`!0p}kkjY1aG8wY^WL1_sUAt=P%e5Pw5wXPZF!XZvIJ3vbS0mvJ~0JnKi%th^m} zQ$D<W&baI2ukN$UCYn5uwn<a<<zaZ!Ea$X7O0Frua)RWa-Z;|;za@?T-=+3f7}bbw fICeevALD%Y?dwxxlPnn+7#KWV{an^LB{Ts52XE)e literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/turnitintool/icon.svg b/theme/adaptable/pix_plugins/mod/turnitintool/icon.svg new file mode 100644 index 0000000..ab73a53 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/turnitintool/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#f33;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path class="cls-2" d="M32,14.64a21.65,21.65,0,0,1,3.34.41c3,.84,4.35,3.77,3.43,7.42a17.86,17.86,0,0,1-4.12,7.46c-3.81,4.33-8.33,7.58-14.08,8.78a9.4,9.4,0,0,1-3.62,0c-3.06-.56-4.67-3.47-3.86-6.81a11.45,11.45,0,0,1,1-2.57,25.67,25.67,0,0,1,1.69-2.55c-.83-.75-1.74-1.54-2.59-2.4a1.3,1.3,0,0,1-.32-1.11,32.07,32.07,0,0,0,2.23-11.17,4.35,4.35,0,0,1,.13-.73h15.3c-.09,1.63-.11,3.28-.28,4.89a48.57,48.57,0,0,1-3.68,14,1.69,1.69,0,0,1-1.71,1.16c-3.11.24-6,0-7.59-3.17-1.91,1.43-3,4.82-2.21,6.81a3.47,3.47,0,0,0,3.9,2.06,19.21,19.21,0,0,0,5.32-1.28C29.92,33.51,34.42,29.74,37,24a13.29,13.29,0,0,0,.94-3.37c.49-3.21-1.35-5.29-4.82-5.61-.36,0-.73-.06-1.09-.08C32,14.81,32,14.72,32,14.64ZM20.25,29.72c1.42,0,2.68,0,3.92,0a1.08,1.08,0,0,0,1.22-.82,46.81,46.81,0,0,0,3.06-11.34c.19-1.42.24-2.89.36-4.33H17.17c-.79,3.6-1.54,7.07-2.31,10.61h5.38Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.png b/theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ba3fbf4b51cd2bd9185ada7b415c4d954170bb02 GIT binary patch literal 1070 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tH_o-U3d8TXz>?GJwBAoA}_#H^1|8B4g<D!+WlKVhQw(urErp7(rdi<8vZc*xDG z@Du-$y2N!;y`(eVvsJIJs8nO}EHiJq^CQj3V8hM7`)cjKt48PN=bzBo7Rz`bobf>1 znG@m+=NW1k>{>o-*4w+(x52@j;R?f>tabmz4=_0RPw%l}WO~P>@woV@V8d!Jmw64j z;tVAWGKpJ1F+AXLvZ<G2yuFU`MXG_Q?By(n4#!&#&P+Az2mVSORE~IU7h&DZmb07v z0ZY90c7a6;*EODH;Ac>06k}ytvdfo2UiIQcW5uEy)j7-l%qf4<&!Q3XFK+YURc_1w zW^~-0Cw(E1slIqIzbnVw-{s6bTV0u^Bwn3#<)EV(Q&kbm?R5pm`mzp$&N^mt`2Ab6 z4_{Qu<5#j+_=N=*H6JomaM-;*>~=)hkGr26Zu-n~2$5l*;n#SQMWR3Tx#nguiypHr z-<|3%R@l$-X{eEVQO0=v?~1z>nJ%9ld@B?0o%|Rv-{N$EN7S0@FU!wQRt!{TO0j#D zXgr@^JY4(uOJl$6@^X{sKP#(THofgScy58t8|F>tPH);gd7b5XW<LJZw96_9vlJOa zg*WLu_-cAH|HuBy_I|~H0JQ*4i+;D)UuU~&N^r7&C}CQ4az(_H-Ti;Ft$b$}p1)u; zzbG<aJ=HWoYvN2_wUxnn+1B|nNnWS8ZOk-_W~yc0m~MT0wXg)A=hmGv1(iF0{D1kf zs9~C`poZ>8S^3Kro=k1iUKbn+oR~G$ZgRs+wX?Q!*UdPP<7U0qsY%wJQ{w$Tr<u78 zw`!}D!Z%O&`J`ye=E5bq-CVmbb_mU!vqxe<+|peZe2q*RNlRwRFnsy_QM$0y_45v| z(7&I}R$N>a=JovZl01fttr~YqcQ0J)t{l8kqV2B5=a*%f#<P4^UXT==&l#g3nD8O6 z)O_}q>OxbV>1Vm!g{PlcesxmW9SugSE4RPNFYUVW`H*b4-YLU7n+~lxZEYhT<vY19 z{llj+dASd*KMqzVDBNXFYLA&VA<fuQVVbMi{L?R+($Bn4+_GG>vcxZ`&)Kxz?8EyR zy$g1Ha{ODlQ)YqkRjtOS#sz!lvHnpmSLJ!S?ns49n=fC#(E)wEjO)K^_xkL+F*V}W zOlgm=Z8GXPy<2Xl2nGJu6Vsd$ruC3B$0X|AnOC0}6tiwdbzkI|wCc_A4|8@mT}#^b zEN`!0p}kkjY1aG8wY^WL1_sUAt=P%e5Pw5wXPZF!XZvIJ3vbS0mvJ~0JnKi%th^m} zQ$D<W&baI2ukN$UCYn5uwn<a<<zaZ!Ea$X7O0Frua)RWa-Z;|;za@?T-=+3f7}bbw fICeevALD%Y?dwxxlPnn+7#KWV{an^LB{Ts52XE)e literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.svg b/theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.svg new file mode 100644 index 0000000..ab73a53 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/turnitintooltwo/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#f33;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path class="cls-2" d="M32,14.64a21.65,21.65,0,0,1,3.34.41c3,.84,4.35,3.77,3.43,7.42a17.86,17.86,0,0,1-4.12,7.46c-3.81,4.33-8.33,7.58-14.08,8.78a9.4,9.4,0,0,1-3.62,0c-3.06-.56-4.67-3.47-3.86-6.81a11.45,11.45,0,0,1,1-2.57,25.67,25.67,0,0,1,1.69-2.55c-.83-.75-1.74-1.54-2.59-2.4a1.3,1.3,0,0,1-.32-1.11,32.07,32.07,0,0,0,2.23-11.17,4.35,4.35,0,0,1,.13-.73h15.3c-.09,1.63-.11,3.28-.28,4.89a48.57,48.57,0,0,1-3.68,14,1.69,1.69,0,0,1-1.71,1.16c-3.11.24-6,0-7.59-3.17-1.91,1.43-3,4.82-2.21,6.81a3.47,3.47,0,0,0,3.9,2.06,19.21,19.21,0,0,0,5.32-1.28C29.92,33.51,34.42,29.74,37,24a13.29,13.29,0,0,0,.94-3.37c.49-3.21-1.35-5.29-4.82-5.61-.36,0-.73-.06-1.09-.08C32,14.81,32,14.72,32,14.64ZM20.25,29.72c1.42,0,2.68,0,3.92,0a1.08,1.08,0,0,0,1.22-.82,46.81,46.81,0,0,0,3.06-11.34c.19-1.42.24-2.89.36-4.33H17.17c-.79,3.6-1.54,7.07-2.31,10.61h5.38Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/url/icon.png b/theme/adaptable/pix_plugins/mod/url/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..71802e323237ef121f70339c852c1483f083b19b GIT binary patch literal 1087 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_tJjo-U3d8TX#fx|sDSL*#gUmS^TllL<==n7xZr6K4b++`V_@Zw4dDXN<3=ZvQG+ z!&1F7&eHz^kKKV~=G9x9Qok*Wb7iv%P+Jh8BkZ<Gyzb<(u*n%cyLPh|O`mYz{&~%F z>-(0TmVM4K-ln%)59|$jP;qXp?Ew~^0|i>F%HMWR7gbnrx`9nb@8>_p1<YqtkLu_+ zG+b{e4$C%o$PHAS$ht%-S9AsQdyi{&jG@be48qkEu4ZIA7`v^|5spe-9(3Vwo{xLO zdgkldE$(|>NA0Pu<X<B7R;}P*+S|p!np4ET9hi22Ifi4}3#OLW+ZgZZFOu_Yh}baQ zQ}|?1Tan*Eji(yVr)c!YaBcgU?XcvO-z9Fd^>31=6v=qatiRM}yX?lROB>x1Z)(2x z-*{PW#d_g=-!qComwLRv_$=U5!jz&XQ~z#w6EgoL<Hp1P_UZkIe%+b$x^JJY+cYDI zrNZZ?>|HVM$JM(V|H`e%Ws+Nv-><pt-HxsEZ2Vgy-bQkCuCL$6S#do^Fwde@=S}g! zH<b(acvkH1csAFR(Tek|PwIY#+{+*0vP2)Oo4|0H;nE?|&Xjpk53f3XaC_PEn7Omj z*3JC)!mmNAF6u2is;MPdz5Eozi?sRsY}MAk3I6{y{mL2>;d}F2n~wa_iDcY<ZF0w3 z`8bQ%eYQ?#-?1HPUveRxsqBcPnc(WJ?=OZ(KK^$xnwz=0$MVlUt_p@y^#=@cw@Wtn zTW*<uP&_*J%)-4~6%CK)mgdIkrF{I#I7@5s%A%MGh9vHQ%|cJ__-Cy0IW86W|Bu6Q zsk1HX=gAd5yuIMjq2po(v7z%ct0#AT_<WfAN1y4UYx};>yL{H}>^t)_3uV{qAIYz` zDtwr~R(+yY{ho*oh5HMX<YsI?e2#0j%EP-si?(xBm};Nx`8na2-MX)r7KKah-WIrd zxBjNHu6_%vs-`SqFh0BFqSS5!<M#VIf4x4)EnQxy_J&b;QiDe5Qp+tX|9x!jGj*C~ zbcJjE-m^M^jE^$RICibBs(LavadFtxcWPJP)OoM@`yzy^ty=EFVKZ%WAF;;r(``$% zME^G~liU%+(lYC=>$j~=haL#~E%fzo;8;9+!K$K!eoMOv243nDU;S)(m@oFqYlide zq}8Vn$Q3VpbMfbgW2g0`8|sf)-JN-vQzF6V{TA8mmRWbd_BCcV{eKd|b!*SL7*h@9 z(lryGcRFhx{<-?iu7~VRg=P6yuHV_q_crs1h)JlM`R{u(%Xz=mRb?3$A5e_CIX!23 ztmRavudJ{4aGn+8wNOa#GA?&gmiw{e?1x6C8l`n+GG>jr=l}nk5nAEI{>i89Lg({J zwO|R`Q)mCrkLP{Hq4_!g_f2Jop6ZorK9<kk�k#I4Wc1*Fpvc1_n=8KbLh*2~7Ze Cj0%?k literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/url/icon.svg b/theme/adaptable/pix_plugins/mod/url/icon.svg new file mode 100644 index 0000000..83f7fca --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/url/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#09c;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M18.62,23.21a3.14,3.14,0,0,1,2.19.9l-1.29,1.29a1.27,1.27,0,0,0-1.78,0l-6.15,6.15a1.27,1.27,0,0,0,0,1.78l3.07,3.07a1.3,1.3,0,0,0,1.78,0l6.15-6.15a1.27,1.27,0,0,0,0-1.8l1.29-1.29a3.06,3.06,0,0,1,.9,2.19,3.09,3.09,0,0,1-.9,2.19l-6.15,6.15a3.09,3.09,0,0,1-2.19.9,3.12,3.12,0,0,1-2.19-.9l-3.08-3.07a3.1,3.1,0,0,1,0-4.37l6.15-6.15A3.11,3.11,0,0,1,18.62,23.21Zm10-4.74a.93.93,0,0,1,.64.26.92.92,0,0,1,0,1.29L20,29.25a.88.88,0,0,1-.64.26.93.93,0,0,1-.64-.26.92.92,0,0,1,0-1.29L28,18.73A.93.93,0,0,1,28.61,18.47Zm3.84-9.09a3.14,3.14,0,0,1,2.19.9l3.07,3.07a3.1,3.1,0,0,1,0,4.37l-6.15,6.15a3.09,3.09,0,0,1-2.19.9,3.12,3.12,0,0,1-2.19-.9l1.29-1.29a1.3,1.3,0,0,0,1.78,0l6.15-6.15a1.27,1.27,0,0,0,0-1.8l-3.07-3.08a1.27,1.27,0,0,0-1.78,0L25.41,17.7a1.27,1.27,0,0,0,0,1.78l-1.29,1.29a3.1,3.1,0,0,1,0-4.37l6.15-6.15A3.21,3.21,0,0,1,32.46,9.38Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/wiki/icon.png b/theme/adaptable/pix_plugins/mod/wiki/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c2f28da568d7adf17c04fe6333fc427cacaa32 GIT binary patch literal 1179 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_l;iPZ!6KjC)UK?a#jzC34(;!HL+8rYV{_6-ixRl`Mm&P1qYaGxCvM+{gNW@AeAG zwi8veJ8$hh=OnK4m2;ZG%%wAx8WTP}GEayXJjCCuKjr(qi4qRtci(@%Q{1=zc})5t z1C_OwlFNCH$ct*|MciVqIlvlYY;t!?b-J0VK%-d$SHYHt|C|?anwY6>Hf!YA!5gD= zU5>FjR;iQILeib})dBWx+58QuZOim~8zvWOIViiX$PtQalbm%nlzTJhf$NRyty%oH zT)&?2{;dCjlNP)W+U&E-(=w#8=O38Yz`mn%gNnL<>|MKt`D%gwE(%^ZqHf7@KV{mr zcJUL@NR=$XZAUEyrk;v768d&)@3SYLS`s8KtStYK;~!RPHCu1vX7zySjQdiiSnRZ6 zbg&G%VE;1Yua@d!mBm(9pU-pn((+gFuy5#ouXQsPmtRkQ(tkmR_i2WZ*W-T@*9(5W z{pYv&Xy~6K^XwR-*?vq4R0=;M$io~J#(n<Vj_~&%?W$Q0H^dko|1JOi^1QacS}jWN zmtQmUihFp+j(tJ!djTFMh9<{`^y)8Cdry8(Y`FjBa!#paqL_NZi4XhIGM{u=GTeK5 zf3Ko=_1(?3j(V+<mszxo_N<Sb^`kDzx_0W0=ie+(O+2*2ep&GQ6Cc7^6+XA>2Tcm! zUi!t~>hZrB%a2`NeWdp1@7Blr`xp!CcPUL#xe;piYSEI){khv+AC=|rW?=fW!+sj4 z+yb4}1FnCaTG~CX99+UxyZ@KowViF<Dcb3a<;s$+L-;lCPHFl6d}?UJ^W)FAY^|TS z@}??p#iJYjk88|qxSlpCsC#;yi87r1?Edle<4h0yUq3&hDjLeMW5%6nd*-iEju!oW znPKW4r=5XrYgdSzVqMrC6_%ZC^;@AgrjPO1!-bJj2Nf1ve^K~0j`_mw*B?K<PW{hu z<l#g6ABTR=T6%F))C$qBygT<Q8NB^#@lF4g_@UpS`?dDie{zpjosequE_(eBvs3FC zjxD`nddEDg<n*=kMl(a#=}upM*m-;D#1o&dybwEUpJ0^XtFtt@;a<9bOM7~?MkVXr zYpvf^Ik$yAOH*d*)cv+EEmLWt`#+D8IO{KV4YN593#dQlne^>YT1MBaU=48&HpblD zXBSp!TzRp#YCrb}XJeDkg`!#Otl3{(+r6W$J1x`cjP51p=uOYQOx`ik&Clm`-N~ZL znWij_D-PGkc(a9ttf~|Jdf4SmS^n-C?fToIKaN*u6&hTAss8SaU-ojN<0{hrs%h%Q zzF{#5M?7V!|E~P_Wm%(tGN;VPgtoNQiBmQ#;>*o+o%OBi4)<vz^~Y<ZbfdVpPLQ0n zZ*n1znz+>G)#r1kZ@Vj2TKA;CqORs%=nIL>;r_+?7K;MQdjD>UHa1y%Q(pYug*O*j z*>!d<J*npz#=PTp`UmakQ!o0r*IVTDq;miJeSOVEPm@MtwdogbZ(~>6Zg5tm`@j8q tDIJBR5Bb$MlR0PpSiI@*_1u4q92I(+to%M(7#J8BJYD@<);T3K0RW^+E|UNN literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/wiki/icon.svg b/theme/adaptable/pix_plugins/mod/wiki/icon.svg new file mode 100644 index 0000000..c84d283 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/wiki/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#909;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M16.61,36.79h19.2v1.84H16.61ZM14,31v4.18l3.49-2.34Zm6.62-8.32.81.43-3.3,6.19-.81-.43ZM21,17,14.1,30l4.2,2.21,6.88-12.94Zm2.74-5.18-1.89,3.54,1.35.73L26,17.64,27.9,14.1ZM23,9.38l7.39,4-3.6,6.77-7.18,13.5-7.41,5,0-9,7.18-13.52Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/pix_plugins/mod/workshop/icon.png b/theme/adaptable/pix_plugins/mod/workshop/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..55877c3e5685faf2f29b14060a2add56d527b303 GIT binary patch literal 1354 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIoCO|{#S9GGLLkg|>2BR0 z1_qWto-U3d8TXz>?a#g)A@l!+<(a@$Y6=|b`jw3f{1o~0?rinGvh+?s;N7yhi*z1s zEB@bfIahY>#wacQn@%z|Q^dSPx%gz6_|66Se@c>H$RsW8WfEfU^68}gmoF-}bKkw= zmg~9q`FG8izx&SruG@cJ^LnY_>X(|aED!2A8n#YYsLLS6Al$HW*DA}`w^lA;vAMv- zAbFtZ(AR1PEry;YDM`W~8nPM64C1XB7aUbR<m2G~lEs1ZfUwTBK1Pwy@U73>9Q<Dz zF(kJIcnU>zy;?POMPnx02UQ05ctM*zTl@BGom}nU|B|gCwkBG8^}1C@z8&CY<Y(9| zCsxKZ?{yy24~0&lV2(=ZZy6IaEDkj)-}z8||JCCBuaV3C8SUHX&QTE*8GohiY^SBj z7N$+sH#^hYZ?|uFZPsn|v{}en&S|N}2bqTxgo{tCNl9uH7h&wbZ#$3c-W}t}1HWH| z%zDN6zNJ>&Z1VZf6#-QSOba6O7KN=j_|oc1Xs6GI7cVYeNq;u4bJ@k7*Js-Qb;^Ee z5HqvdJgG6oOE};EhN!~Kpe5!m&U4g%2(Ec~;5Pr++;^GZCq({ZUs9X*xZt;d-+9LU zM}s{%x(tl2Z}0xw9q^$^NkRQt21}XE8E1ZtEf;I%$^YGVwE6t!6<t?7gc2Rr?FwJ` z;OKOvO-W{_xV<JcoOxva`_8eKn=ilKP@!owk>ix=+odcAFL$u}ur+LX&b8`OjfoxK z!7XRa&e$4@b*_~D%_(_P+VR7hpOfZYI=Us?HE?aFN#gw5Cv@WT({|?>PhN0x@dj;& z($E#_tQhA7hTY^~a0p$arj!`EMn$W6+s!mJhX^;TSarcp|F8Zp=9DBgZa<$J_DIE7 z^MJFb$?3BZD(AKsTiCYe_ReDtPF$yT{Ze7<>yq{235pNB-~GK*yJzD4gbPwGP3yZt zul-<W(D9tGMa3{HRHxvzOjv!)(PNi+ugt$*`17HVi<*Mb!Rf2@BmQQJG8DXSkAASd za?h`yqI2doTfZ)Mo04@wYQe_)2X4pTPfFl@RH_>H;HXcp;za?)H(ZCeAAkOHWzh{C z^T#c0o0q2lZTwg;tE@nm>rV`az{~}KSv;O1|1^9W){AB<7)5e4zTZ=JKWfs+e=lFQ z_t#tWHc$8A{3B-X{Y^4$?Uw7SeMElOt@-`%akY|!+3cgfAEN&REM#$=q0X4?+2j)8 z|M+9CwNm<h{lv#F+3x-SCO2JPuW;F1Mh2@i_B&SQKKD9&F64hr_EcT_bd!o>m(TLW z2l^+kuwVc9bNqaD6>|@z?*{CB1ria>C%XEQ>X*e_Z=H8(w@#}KZ_Nd^wTyfx7%%@h z=b65v{?Ru1(D2xgp&FamBQJ3IJeWJL`TD2V0ViesH8)vD=7^b37FfZOdZk0EHhiab z(CJ^TXLnrfGR>;-uMWGwwXl`{XpbV}`a|{$d-v@vt6pN4F-5Ag>q};5Y?`HT)Q|QX zo@>^%%zwmoKf+O4<(0vfvu1oJ7}q3cPYbwo$f{VhB_`p5*S^c?%Y$tX6?JvDS-gp| zuw_5ym0}{ldYxALQX$v%wdPyb?76XjhIB=IT=TsZqS-oYk3RA_dp9B3%q@7@%7>>v zM1Ogf*Wki=;QfxsZR>Ys6)uwF*u8M4qqq#i(`jlZbHAxg$v66OqUylx9shd|x6NAq z_xt)tTmKM-Cxx|~do!$$ebx18P5toy`Fb`*7m+`=|GjZkd|c15@6r9-e{5!7PAp^k TbUTlMfq}u()z4*}Q$iB}3Z8#f literal 0 HcmV?d00001 diff --git a/theme/adaptable/pix_plugins/mod/workshop/icon.svg b/theme/adaptable/pix_plugins/mod/workshop/icon.svg new file mode 100644 index 0000000..9c4edc6 --- /dev/null +++ b/theme/adaptable/pix_plugins/mod/workshop/icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#909;}.cls-2{fill:#fff;}</style></defs><title>48px_sourcefile_20170718_multi</title><circle class="cls-1" cx="24" cy="24" r="24"/><path id="path1" class="cls-2" d="M13.91,33a1.68,1.68,0,0,0-1.09.46A1.55,1.55,0,1,0,15,35.66a1.49,1.49,0,0,0,0-2.19A1.9,1.9,0,0,0,13.91,33Zm12.31-6.38,3.65,3.65,2.19.46L36,36.66l-1.46,1.46L28.59,34.2,28.14,32l-3.65-3.65Zm-9.39-.09,5,5c-2.37,2.55-4.29,4.65-5.2,5.56a3.77,3.77,0,0,1-5.38,0,3.85,3.85,0,0,1,0-5.38ZM12.09,16a.7.7,0,0,0-.46.18.72.72,0,0,0,0,1l8.48,8.48a.71.71,0,0,0,1-1l-8.48-8.48A1.29,1.29,0,0,0,12.09,16Zm2.1-2.1a.69.69,0,0,0-.46.18.68.68,0,0,0,0,1.09l8.48,8.48a.71.71,0,0,0,1-1L14.73,14A1.29,1.29,0,0,0,14.18,13.86Zm-.82-1.91A3.86,3.86,0,0,1,16.1,13L27,23.89l-5.47,5.47-11-10.76a3.8,3.8,0,0,1,0-5.47A3.94,3.94,0,0,1,13.36,11.94ZM31.51,9.76a6.67,6.67,0,0,1,2.74.55l-3.83,3.83a1.66,1.66,0,0,0,0,2.46l1.46,1.46a1.66,1.66,0,0,0,2.46,0l3.83-3.74a7.92,7.92,0,0,1,.46,2.55,7.11,7.11,0,1,1-7.11-7.11Z"/></svg> \ No newline at end of file diff --git a/theme/adaptable/renderers.php b/theme/adaptable/renderers.php new file mode 100644 index 0000000..fbe5424 --- /dev/null +++ b/theme/adaptable/renderers.php @@ -0,0 +1,3833 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015-2017 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Load libraries. +require_once($CFG->dirroot.'/course/renderer.php'); +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->dirroot.'/message/lib.php'); +require_once($CFG->dirroot.'/course/format/topics/renderer.php'); +require_once($CFG->dirroot.'/course/format/weeks/renderer.php'); + +use \theme_adaptable\traits\single_section_page; + +/** + * Class for implementing topics format rendering. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * @copyright 2017 Gareth J Barnard + * + */ +class theme_adaptable_format_topics_renderer extends format_topics_renderer { + use single_section_page; +} + +/** + * Class for implementing weeks format rendering. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * @copyright 2017 Gareth J Barnard + * + */ +class theme_adaptable_format_weeks_renderer extends format_weeks_renderer { + use single_section_page; +} + +/****************************************************************************************** + * @copyright 2017 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * @copyright 2017 Gareth J Barnard + * + * Grid format renderer for the Adaptable theme. + */ + +// Check if GRID is installed before trying to override it. +if (file_exists("$CFG->dirroot/course/format/grid/renderer.php")) { + include_once($CFG->dirroot."/course/format/grid/renderer.php"); + + /** + * Class for implementing grid format rendering. + * @copyright 2017 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * + */ + class theme_adaptable_format_grid_renderer extends format_grid_renderer { + use single_section_page; + + /** + * Generate the html for the 'Jump to' menu on a single section page. + * + * @param stdClass $course The course entry from DB. + * @param array $sections The course_sections entries from the DB. + * @param bool $displaysection the current displayed section number. + * + * @return string HTML to output. + */ + protected function section_nav_selection($course, $sections, $displaysection) { + $settings = $this->courseformat->get_settings(); + if (!$this->section0attop) { + $section = 0; + } else if ($settings['setsection0ownpagenogridonesection'] == 2) { + $section = 0; + } else { + $section = 1; + } + return $this->section_nav_selection_content($course, $sections, $displaysection, $section); + } + + /** + * Generate next/previous section links for navigation. + * + * @param stdClass $course The course entry from DB. + * @param array $sections The course_sections entries from the DB. + * @param int $sectionno The section number in the coruse which is being displayed. + * @return array associative array with previous and next section link. + */ + public function get_nav_links($course, $sections, $sectionno) { + $settings = $this->courseformat->get_settings(); + if (!$this->section0attop) { + $buffer = -1; + } else if ($settings['setsection0ownpagenogridonesection'] == 2) { + $buffer = -1; + } else { + $buffer = 0; + } + return $this->get_nav_links_content($course, $sections, $sectionno, $buffer); + } + + /** + * Output the html for a single section page. + * + * @param stdClass $course The course entry from DB. + * @param array $sections (argument not used). + * @param array $mods (argument not used). + * @param array $modnames (argument not used). + * @param array $modnamesused (argument not used). + * @param int $displaysection The section number in the course which is being displayed. + */ + public function print_single_section_page($course, $sections, $mods, $modnames, $modnamesused, $displaysection) { + $settings = $this->courseformat->get_settings(); + if (!$this->section0attop) { + $section0attop = 0; + } else if ($settings['setsection0ownpagenogridonesection'] == 2) { + $section0attop = 0; + } else { + $section0attop = 1; + } + $this->print_single_section_page_content($course, $sections, $mods, $modnames, $modnamesused, $displaysection, + $section0attop); + } + } +} + +// Check if Flexible is installed before trying to override it. +if (file_exists("$CFG->dirroot/course/format/flexible/renderer.php")) { + include_once($CFG->dirroot."/course/format/flexible/renderer.php"); + + /** + * @copyright 2019 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * + * Flexible format renderer for the Adaptable theme. + */ + class theme_adaptable_format_flexible_renderer extends format_flexible_renderer { + use single_section_page; + + /** + * Generate the html for the 'Jump to' menu on a single section page. + * + * @param stdClass $course The course entry from DB. + * @param array $sections The course_sections entries from the DB. + * @param bool $displaysection the current displayed section number. + * + * @return string HTML to output. + */ + protected function section_nav_selection($course, $sections, $displaysection) { + if ($this->settings['section0attop'] == 2) { // One is 'Top' and two is 'Grid'. + $section = 0; + } else { + $section = 1; + } + return $this->section_nav_selection_content($course, $sections, $displaysection, $section); + } + + /** + * Generate next/previous section links for navigation. + * + * @param stdClass $course The course entry from DB. + * @param array $sections The course_sections entries from the DB. + * @param int $sectionno The section number in the coruse which is being displayed. + * @return array associative array with previous and next section link. + */ + public function get_nav_links($course, $sections, $sectionno) { + if ($this->settings['section0attop'] == 2) { // One is 'Top' and two is 'Grid'. + $buffer = -1; + } else { + $buffer = 0; + } + return $this->get_nav_links_content($course, $sections, $sectionno, $buffer); + } + + /** + * Output the html for a single section page. + * + * @param stdClass $course The course entry from DB. + * @param array $sections (argument not used). + * @param array $mods (argument not used). + * @param array $modnames (argument not used). + * @param array $modnamesused (argument not used). + * @param int $displaysection The section number in the course which is being displayed. + */ + public function print_single_section_page($course, $sections, $mods, $modnames, $modnamesused, $displaysection) { + $this->print_single_section_page_content($course, $sections, $mods, $modnames, $modnamesused, $displaysection, + false); + } + } +} + +/****************************************************************************************** + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * + * Collapsed Topics format renderer for the Adaptable theme. + */ + +// Check if Collapsed Topics is installed before trying to override it. +if (file_exists("$CFG->dirroot/course/format/topcoll/renderer.php")) { + include_once($CFG->dirroot."/course/format/topcoll/renderer.php"); + + /** + * Constructor + * + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + class theme_adaptable_format_topcoll_renderer extends format_topcoll_renderer { + /** + * Constructor method. + * + * @param moodle_page $page + * @param string $target one of rendering target constants. + */ + public function __construct(moodle_page $page, $target) { + parent::__construct($page, $target); + $this->courserenderer = $this->page->get_renderer('theme_adaptable', 'topcoll_course'); + } + } +} + +define('ADAPTABLE_COURSE_STARRED', 'starred'); +define('ADAPTABLE_COURSE_IN_PROGRESS', 'inprogress'); +define('ADAPTABLE_COURSE_PAST', 'past'); +define('ADAPTABLE_COURSE_FUTURE', 'future'); +define('ADAPTABLE_COURSE_HIDDEN', 'hidden'); + +/** + * Class for core renderer. + * + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * Core renderers for Adaptable theme + */ +class theme_adaptable_core_renderer extends core_renderer { + /** @var custom_menu_item language The language menu if created */ + protected $language = null; + + /** + * Returns HTML attributes to use within the body tag. This includes an ID and classes. + * + * @since Moodle 2.5.1 2.6 + * @param string|array $additionalclasses Any additional classes to give the body tag, + * @return string + */ + public function body_attributes($additionalclasses = array()) { + if (\core_useragent::is_safari()) { + if (is_array($additionalclasses)) { + $additionalclasses[] = 'safari'; + } else { + $additionalclasses .= ' safari'; + } + } + return parent::body_attributes($additionalclasses); + } + + /** + * Outputs the opening section of a box. + * + * @param string $classes A space-separated list of CSS classes + * @param string $id An optional ID + * @param array $attributes An array of other + * attributes to give the box. + * @return string the HTML to output. + */ + public function box_start($classes = 'generalbox', $id = null, $attributes = array()) { + $this->opencontainers->push('box', html_writer::end_tag('div')); + $attributes['id'] = $id; + $attributes['class'] = 'box ' . renderer_base::prepare_classes($classes); + return html_writer::start_tag('div', $attributes); + } + + /** + * Renders an action menu component. + * + * @param action_menu $menu + * @return string HTML + */ + public function render_action_menu(action_menu $menu) { + global $CFG; + + if ($CFG->branch < 37) { + // We don't want the class icon there! + foreach ($menu->get_secondary_actions() as $action) { + if ($action instanceof \action_menu_link && $action->has_class('icon')) { + $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']); + } + } + + if ($menu->is_empty()) { + return ''; + } + $context = $menu->export_for_template($this); + + return $this->render_from_template('core/action_menu', $context); + } else { + return parent::render_action_menu($menu); + } + } + + /** + * Return list of the user's courses + * + * @param string $overridetype The override type, if null because being called from the course renderer, + * then will be retrieved. + * + * @return array list of courses + */ + public function render_mycourses($overridetype = null) { + if ((empty($overridetype)) && (!empty($this->page->theme->settings->mysitessortoverride))) { + $overridetype = $this->page->theme->settings->mysitessortoverride; + } + + // Set limit of courses to show in dropdown from setting. + $coursedisplaylimit = '20'; + if (isset($this->page->theme->settings->mycoursesmenulimit)) { + $coursedisplaylimit = $this->page->theme->settings->mycoursesmenulimit; + } + + $courses = enrol_get_my_courses( + join(',', array_keys(\core_course\external\course_summary_exporter::define_properties())) + ); + + /* Add timeaccess and timestart to the courses for all override types to use in some shape or form. + Get the last accessed information for the user and populate. */ + global $DB, $USER; + $lastaccess = $DB->get_records('user_lastaccess', array('userid' => $USER->id), '', 'courseid, timeaccess'); + if ($lastaccess) { + foreach ($courses as $course) { + if (!empty($lastaccess[$course->id])) { + $course->timeaccess = $lastaccess[$course->id]->timeaccess; + } + } + } + // Determine if we need to query the enrolment and user enrolment tables. + $enrolquery = false; + foreach ($courses as $course) { + if (empty($course->timeaccess)) { + $enrolquery = true; + break; + } + } + if ($enrolquery) { + // We do. + $params = array('userid' => $USER->id); + $sql = "SELECT ue.id, e.courseid, ue.timestart + FROM {enrol} e + JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)"; + $enrolments = $DB->get_records_sql($sql, $params, 0, 0); + if ($enrolments) { + // Sort out any multiple enrolments on the same course. + $userenrolments = array(); + foreach ($enrolments as $enrolment) { + if (!empty($userenrolments[$enrolment->courseid])) { + if ($userenrolments[$enrolment->courseid] < $enrolment->timestart) { + // Replace. + $userenrolments[$enrolment->courseid] = $enrolment->timestart; + } + } else { + $userenrolments[$enrolment->courseid] = $enrolment->timestart; + } + } + // We don't need to worry about timeend etc. as our course list will be valid for the user from above. + foreach ($courses as $course) { + if (empty($course->timeaccess)) { + $course->timestart = $userenrolments[$course->id]; + } + } + } + } + + if ($overridetype == 'last') { + uasort($courses, array($this, 'timeaccesscompare')); + } + + // Get courses in sort order into list. + if ($coursedisplaylimit != 0) { + $sortedcourses = array(); + $counter = 0; + foreach ($courses as $course) { + if ($counter >= $coursedisplaylimit) { + break; + } + $sortedcourses[] = $course; + $counter++; + } + } else { + $sortedcourses = $courses; + } + + return $sortedcourses; + } + + + + /** + * Returns the URL for the favicon. + * + * @return moodle_url The favicon Moodle URL. + */ + public function favicon() { + if (!empty($this->page->theme->settings->favicon)) { + return \theme_adaptable\toolbox::get_setting_moodle_url('favicon', $this->page->theme); + } + return parent::favicon(); + } + + /** + * Returns settings as formatted text + * + * @param string $setting + * @param string $format = false + * @param string $theme = null + * @return string + */ + public function get_setting($setting, $format = false, $theme = null) { + if (empty($theme)) { + $theme = theme_config::load('adaptable'); + } + + if (empty($theme->settings->$setting)) { + return false; + } else if (!$format) { + return $theme->settings->$setting; + } else if ($format === 'format_text') { + return format_text($theme->settings->$setting, FORMAT_PLAIN); + } else if ($format === 'format_html') { + return format_text($theme->settings->$setting, FORMAT_HTML, array('trusted' => true)); + } else { + return format_string($theme->settings->$setting); + } + } + + /** + * Returns user profile menu + */ + public function user_profile_menu() { + global $CFG, $COURSE; + $retval = ''; + + // False or theme setting name to first array param (not all links have settings). + // False or Moodle version number to second param (only some links check version). + // URL for link in third param. + // Link text in fourth parameter. + // Icon in fifth param. + $usermenuitems = array(); + $usermenuitems[] = array('enablemy', false, $CFG->wwwroot.'/my', get_string('myhome'), + \theme_adaptable\toolbox::getfontawesomemarkup('dashboard')); + $usermenuitems[] = array('enableprofile', false, $CFG->wwwroot.'/user/profile.php', get_string('viewprofile'), + \theme_adaptable\toolbox::getfontawesomemarkup('user')); + $usermenuitems[] = array('enableeditprofile', false, $CFG->wwwroot.'/user/edit.php', get_string('editmyprofile'), + \theme_adaptable\toolbox::getfontawesomemarkup('cog')); + $usermenuitems[] = array('enableaccesstool', false, $CFG->wwwroot.'/local/accessibilitytool/manage.php', + get_string('enableaccesstool', 'theme_adaptable'), \theme_adaptable\toolbox::getfontawesomemarkup('low-vision')); + $usermenuitems[] = array('enableprivatefiles', false, $CFG->wwwroot.'/user/files.php', + get_string('privatefiles', 'block_private_files'), \theme_adaptable\toolbox::getfontawesomemarkup('file')); + if (\theme_adaptable\toolbox::kalturaplugininstalled()) { + $usermenuitems[] = array(false, false, $CFG->wwwroot.'/local/mymedia/mymedia.php', + get_string('nav_mymedia', 'local_mymedia'), $this->pix_icon('my-media', '', 'local_mymedia')); + } + $usermenuitems[] = array('enablegrades', false, $CFG->wwwroot.'/grade/report/overview/index.php', get_string('grades'), + \theme_adaptable\toolbox::getfontawesomemarkup('list-alt')); + $usermenuitems[] = array('enablebadges', false, $CFG->wwwroot.'/badges/mybadges.php', get_string('badges'), + \theme_adaptable\toolbox::getfontawesomemarkup('certificate')); + $usermenuitems[] = array('enablepref', '2015051100', $CFG->wwwroot.'/user/preferences.php', get_string('preferences'), + \theme_adaptable\toolbox::getfontawesomemarkup('cog')); + $usermenuitems[] = array('enablenote', false, $CFG->wwwroot.'/message/edit.php', get_string('notifications'), + \theme_adaptable\toolbox::getfontawesomemarkup('paper-plane')); + $usermenuitems[] = array('enableblog', false, $CFG->wwwroot.'/blog/index.php', get_string('enableblog', 'theme_adaptable'), + \theme_adaptable\toolbox::getfontawesomemarkup('rss')); + $usermenuitems[] = array('enableposts', false, $CFG->wwwroot.'/mod/forum/user.php', + get_string('enableposts', 'theme_adaptable'), \theme_adaptable\toolbox::getfontawesomemarkup('commenting')); + $usermenuitems[] = array('enablefeed', false, $CFG->wwwroot.'/report/myfeedback/index.php', + get_string('enablefeed', 'theme_adaptable'), \theme_adaptable\toolbox::getfontawesomemarkup('bullhorn')); + $usermenuitems[] = array('enablecalendar', false, $CFG->wwwroot.'/calendar/view.php', + get_string('pluginname', 'block_calendar_month'), \theme_adaptable\toolbox::getfontawesomemarkup('calendar')); + + $returnurl = $this->get_current_page_url(true); + $context = context_course::instance($COURSE->id); + + if ((!is_role_switched($COURSE->id)) && (has_capability('moodle/role:switchroles', $context))) { + // TBR $returnurl = str_replace(). + $url = $CFG->wwwroot.'/course/switchrole.php?id='.$COURSE->id.'&switchrole=-1&returnurl='.$returnurl; + $usermenuitems[] = array(false, false, $url, get_string('switchroleto'), + \theme_adaptable\toolbox::getfontawesomemarkup('user-o')); + } + + if (is_role_switched($COURSE->id)) { + $url = $CFG->wwwroot.'/course/switchrole.php?id='.$COURSE->id.'&sesskey='.sesskey(). + '&switchrole=0&returnurl='.$returnurl; + + $usermenuitems[] = array(false, false, $url, get_string('switchrolereturn'), + \theme_adaptable\toolbox::getfontawesomemarkup('user-o')); + } + + $usermenuitems[] = array(false, false, $CFG->wwwroot.'/login/logout.php?sesskey='.sesskey(), get_string('logout'), + \theme_adaptable\toolbox::getfontawesomemarkup('sign-out')); + + for ($i = 0; $i < count($usermenuitems); $i++) { + $additem = true; + + // If theme setting is specified in array but not enabled in theme settings do not add to menu. + $usermenuitem = $usermenuitems[$i][0]; + if (empty($this->page->theme->settings->$usermenuitem) && $usermenuitems[$i][0]) { + $additem = false; + } + + // If item requires version number and moodle is below that version to not add to menu. + if ($usermenuitems[$i][1] && $CFG->version < $usermenuitems[$i][1]) { + $additem = false; + } + + if ($additem) { + $retval .= '<a class="dropdown-item" href="' . $usermenuitems[$i][2] . '" title="' . $usermenuitems[$i][3] . '">'; + $retval .= $usermenuitems[$i][4].$usermenuitems[$i][3].'</a>'; + } + } + return $retval; + } + + + + /** + * Returns current url minus the value of $CFG->wwwroot + * + * @param bool $stripwwwroot + * + * Should be replaced with inbuilt Moodle function if one can be found + */ + public function get_current_page_url($stripwwwroot = false) { + global $CFG; + $pageurl = 'http'; + + if ( isset( $_SERVER["HTTPS"] ) && strtolower( $_SERVER["HTTPS"] ) == "on" ) { + $pageurl .= "s"; + } + + $pageurl .= "://"; + + if ($_SERVER["SERVER_PORT"] != "80") { + $pageurl .= $_SERVER["SERVER_NAME"].":".$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"]; + } else { + $pageurl .= $_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"]; + } + + if ($stripwwwroot) { + $pageurl = str_replace($CFG->wwwroot, '', $pageurl); + } + return $pageurl; + } + + /** + * Returns the user menu + * + * @param string $user = null + * @param string $withlinks = null + * @return the user menu + */ + public function user_menu($user = null, $withlinks = null) { + $usermenu = new custom_menu('', current_language()); + return $this->render_user_menu($usermenu); + } + + /** + * Prints a nice side block with an optional header. + * + * The content is described by a core_renderer::block_contents object. + * + * <div id="inst{$instanceid}" class="block_{$blockname} block"> + * <div class="header"></div> + * <div class="content"> + * ...CONTENT... + * <div class="footer"> + * </div> + * </div> + * <div class="annotation"> + * </div> + * </div> + * + * @param block_contents $bc HTML for the content + * @param string $region the region the block is appearing in. + * @return string the HTML to be output. + */ + public function block(block_contents $bc, $region) { + $bc = clone($bc); // Avoid messing up the object passed in. + if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) { + $bc->collapsible = block_contents::NOT_HIDEABLE; + } + if (!empty($bc->blockinstanceid)) { + $bc->attributes['data-instanceid'] = $bc->blockinstanceid; + } + $skiptitle = strip_tags($bc->title); + if ($bc->blockinstanceid && !empty($skiptitle)) { + $bc->attributes['aria-labelledby'] = 'instance-'.$bc->blockinstanceid.'-header'; + } else if (!empty($bc->arialabel)) { + $bc->attributes['aria-label'] = $bc->arialabel; + } + if ($bc->dockable) { + $bc->attributes['data-dockable'] = 1; + } + if ($bc->collapsible == block_contents::HIDDEN) { + $bc->add_class('hidden'); + } + if (!empty($bc->controls)) { + $bc->add_class('block_with_controls'); + } + $bc->add_class('mb-3'); + + if (empty($skiptitle)) { + $output = ''; + $skipdest = ''; + } else { + $output = html_writer::link('#sb-'.$bc->skipid, get_string('skipa', 'access', $skiptitle), + array('class' => 'skip skip-block', 'id' => 'fsb-' . $bc->skipid)); + $skipdest = html_writer::span('', 'skip-block-to', + array('id' => 'sb-' . $bc->skipid)); + } + + $output .= html_writer::start_tag('section', $bc->attributes); + + $output .= $this->block_header($bc); + $output .= $this->block_content($bc); + + $output .= html_writer::end_tag('section'); + + $output .= $this->block_annotation($bc); + + $output .= $skipdest; + + $this->init_block_hider_js($bc); + return $output; + } + + /** + * Produces a header for a block + * + * @param block_contents $bc + * @return string + */ + protected function block_header(block_contents $bc) { + + $title = ''; + if ($bc->title) { + $attributes = array(); + $attributes['class'] = 'd-inline'; + if ($bc->blockinstanceid) { + $attributes['id'] = 'instance-'.$bc->blockinstanceid.'-header'; + } + $title = html_writer::tag('h2', $bc->title, $attributes); + } + + $blockid = null; + if (isset($bc->attributes['id'])) { + $blockid = $bc->attributes['id']; + } + $controlshtml = $this->block_controls($bc->controls, $blockid); + + $output = ''; + if ($title || $controlshtml) { + $output .= + html_writer::tag('div', + html_writer::tag('div', + html_writer::tag('div', '', array('class' => 'block_action')).$title. + html_writer::tag('div', $controlshtml, array('class' => 'block-controls float-right')), + array('class' => 'title')), + array('class' => 'header') + ); + } + return $output; + } + + /** + * Produces the content area for a block + * + * @param block_contents $bc + * @return string + */ + protected function block_content(block_contents $bc) { + $output = html_writer::start_tag('div', array('class' => 'content')); + if (!$bc->title && !$this->block_controls($bc->controls)) { + $output .= html_writer::tag('div', '', array('class' => 'block_action notitle')); + } + $output .= $bc->content; + $output .= $this->block_footer($bc); + $output .= html_writer::end_tag('div'); + + return $output; + } + + /** + * Produces the footer for a block + * + * @param block_contents $bc + * @return string + */ + protected function block_footer(block_contents $bc) { + $output = ''; + if ($bc->footer) { + $output .= html_writer::tag('div', $bc->footer, array('class' => 'footer')); + } + return $output; + } + + /** + * Produces the annotation for a block + * + * @param block_contents $bc + * @return string + */ + protected function block_annotation(block_contents $bc) { + $output = ''; + if ($bc->annotation) { + $output .= html_writer::tag('div', $bc->annotation, array('class' => 'blockannotation')); + } + return $output; + } + + /** + * Calls the JS require function to hide a block. + * + * @param block_contents $bc A block_contents object + */ + protected function init_block_hider_js(block_contents $bc) { + if (!empty($bc->attributes['id']) and $bc->collapsible != block_contents::NOT_HIDEABLE) { + $config = new stdClass; + $config->id = $bc->attributes['id']; + $config->title = strip_tags($bc->title); + $config->preference = 'block' . $bc->blockinstanceid . 'hidden'; + $config->tooltipVisible = get_string('hideblocka', 'access', $config->title); + $config->tooltipHidden = get_string('showblocka', 'access', $config->title); + + $this->page->requires->js_init_call('M.util.init_block_hider', array($config)); + user_preference_allow_ajax_update($config->preference, PARAM_BOOL); + } + } + + /** + * Renders preferences groups. + * + * @param preferences_groups $renderable The renderable + * @return string The output. + */ + public function render_preferences_groups(preferences_groups $renderable) { + return $this->render_from_template('core/preferences_groups', $renderable); + } + + /** + * Returns list of alert messages for the user + * + * @return string + */ + public function get_alert_messages() { + global $CFG, $COURSE; + $alerts = ''; + + $alertcount = $this->page->theme->settings->alertcount; + + if (core\session\manager::is_loggedinas()) { + $alertindex = $alertcount + 1; + $alertkey = "undismissable"; + $logininfo = $this->login_info(); + $logininfo = str_replace('<div class="logininfo">', '', $logininfo); + $logininfo = str_replace('</div>', '', $logininfo); + $alerts = $this->get_alert_message($logininfo, 'warning', $alertindex, $alertkey) . $alerts; + } + + if (empty($this->page->theme->settings->enablealerts)) { + return $alerts; + } + + for ($i = 1; $i <= $alertcount; $i++) { + $enablealert = 'enablealert' . $i; + $alerttext = 'alerttext' . $i; + $alertsession = 'alert' . $i; + + if (isset($this->page->theme->settings->$enablealert)) { + $enablealert = $this->page->theme->settings->$enablealert; + } else { + $enablealert = false; + } + + if (isset($this->page->theme->settings->$alerttext)) { + $alerttext = $this->page->theme->settings->$alerttext; + } else { + $alerttext = ''; + } + + if ($enablealert && !empty($alerttext)) { + $alertprofilefield = 'alertprofilefield' . $i; + $profilevals = array('', ''); + + if (!empty($this->page->theme->settings->$alertprofilefield)) { + $profilevals = explode('=', $this->page->theme->settings->$alertprofilefield); + } + + if (!empty($this->page->theme->settings->enablealertstriptags)) { + $alerttext = strip_tags($alerttext); + } + + $alerttype = 'alerttype' . $i; + $alertaccess = 'alertaccess' . $i; + $alertkey = 'alertkey' . $i; + + $alerttype = $this->page->theme->settings->$alerttype; + $alertaccess = $this->page->theme->settings->$alertaccess; + $alertkey = $this->page->theme->settings->$alertkey; + + if ($this->get_alert_access($alertaccess, $profilevals[0], $profilevals[1], $alertsession)) { + $alerts .= $this->get_alert_message($alerttext, $alerttype, $i, $alertkey); + } + } + } + + if (is_role_switched($COURSE->id)) { + $alertindex = $alertcount + 1; + $alertkey = "undismissable"; + + $returnurl = $this->get_current_page_url(true); + $url = $CFG->wwwroot.'/course/switchrole.php?id='.$COURSE->id.'&sesskey='.sesskey(). + '&switchrole=0&returnurl='.$returnurl; + + $message = get_string('actingasrole', 'theme_adaptable') . '. '; + $message .= '<a href="' . $url . '">' . get_string('switchrolereturn') . '</a>'; + $alerts = $this->get_alert_message($message, 'warning', $alertindex, $alertkey) . $alerts; + } + + return $alerts; + } + + /** + * Returns formatted alert message + * + * @param string $text message text + * @param string $type alert type + * @param int $alertindex + * @param int $alertkey + */ + public function get_alert_message($text, $type, $alertindex, $alertkey) { + if ($alertkey == '' || theme_adaptable_get_alertkey($alertindex) == $alertkey) { + return ''; + } + + $retval = '<div class="customalert alert alert-dismissable adaptable-alert-' . $type . ' fade in">'; + $retval .= '<button type="button" class="close" data-dismiss="alert" aria-label="Close" data-alertkey="' . $alertkey. + '" data-alertindex="' . $alertindex . '">'; + + if ($alertkey != 'undismissable') { + $retval .= '<span aria-hidden="true">×</span>'; + } + + $retval .= '</button>'; + $retval .= '<i class="fa fa-' . $this->alert_icon($type) . ' fa-lg"></i> '; + $retval .= $text; + $retval .= '</div>'; + return $retval; + } + + /** + * Displays notices to alert teachers of problems with course such as being hidden. + */ + public function get_course_alerts() { + $retval = ''; + $alerttype = $this->page->theme->settings->alerthiddencourse; + if ($alerttype != 'disabled') { + if ($this->page->course->visible == 0) { + global $CFG, $COURSE; + $alerttext = get_string('alerthiddencoursetext-1', 'theme_adaptable'). + '<a href="'.$CFG->wwwroot.'/course/edit.php?id='.$COURSE->id.'">'. + get_string('alerthiddencoursetext-2', 'theme_adaptable').'</a>'; + + $alertindexkey = 'hiddencoursealert-'.$COURSE->id; + + $retval = $this->get_alert_message($alerttext, $alerttype, $alertindexkey, $alertindexkey); + } + } + + return $retval; + } + + /** + * Checks the users access to alerts + * @param string $access the kind of access rule applied + * @param string $profilefield the custom profile filed to check + * @param string $profilevalue the expected value to be found in users profile + * @param string $alertsession a token to be used to store access in session + * @return boolean + */ + public function get_alert_access($access, $profilefield, $profilevalue, $alertsession) { + $retval = false; + switch ($access) { + case "global": + $retval = true; + break; + case "user": + if (isloggedin()) { + $retval = true; + } + break; + case "admin": + if (is_siteadmin()) { + $retval = true; + } + break; + case "profile": + /* Check if user is logged in and then check menu access for profile field. */ + if ( (isloggedin()) && ($this->check_menu_access($profilefield, $profilevalue, $alertsession)) ) { + $retval = true; + } + break; + } + return $retval; + } + + /** + * Returns FA icon depending on the type of alert selected + * + * @param string $alertclassglobal * + * @return string + */ + public function alert_icon($alertclassglobal) { + switch ($alertclassglobal) { + case "success": + $alerticonglobal = $this->page->theme->settings->alerticonsuccess; + break; + case "info": + $alerticonglobal = $this->page->theme->settings->alerticoninfo; + break; + case "warning": + $alerticonglobal = $this->page->theme->settings->alerticonwarning; + break; + } + return $alerticonglobal; + } + + /** + * Returns html to render Development version alert message in the header + * + * @return string + */ + public function get_dev_alert() { + global $CFG; + $output = ''; + + // Development version. + if (get_config('theme_adaptable', 'version') < '2019051300') { + $output .= '<div id="beta"><h3>'; + $output .= get_string('beta', 'theme_adaptable'); + $output .= '</h3></div>'; + } + + // Deprecated moodle version (< 3.6). + if ($CFG->version < 2018120300) { + $output .= '<div id="beta"><center><h3>'; + $output .= get_string('deprecated', 'theme_adaptable'); + $output .= '</h3></center></div>'; + } + + return $output; + } + + /** + * Returns Google Analytics code if analytics are enabled + * + * @return string + */ + public function get_analytics() { + $analytics = ''; + $analyticscount = $this->page->theme->settings->enableanalytics; + $anonymize = true; + + // Anonymize IP. + if (($this->page->theme->settings->anonymizega = 1) || (empty($this->page->theme->settings->anonymizega))) { + $anonymize = true; + } else { + $anonymize = false; + } + + // Load settings. + if (isset($this->page->theme->settings->enableanalytics)) { + for ($i = 1; $i <= $analyticscount; $i++) { + $analyticstext = 'analyticstext' . $i; + $analyticsprofilefield = 'analyticsprofilefield' . $i; + $analyticssession = 'analytics' . $i; + $access = true; + + if (!empty($this->page->theme->settings->$analyticsprofilefield)) { + $profilevals = explode('=', $this->page->theme->settings->$analyticsprofilefield); + $profilefield = $profilevals[0]; + $profilevalue = $profilevals[1]; + if (!$this->check_menu_access($profilefield, $profilevalue, $analyticssession)) { + $access = false; + } + } + + if (!empty($this->page->theme->settings->$analyticstext) && $access) { + // The closing tag of PHP heredoc doesn't like being indented so do not meddle with indentation of 'EOT;' below! + $analytics .= <<<EOT + + <script type="text/javascript"> + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + + ga('create', '$analyticstext', 'auto'); + ga('send', 'pageview'); + ga('set', 'anonymizeIp', $anonymize); + </script> +EOT; + } + } + } + return $analytics; + } + + /** + * Returns Piwik code if enabled + * + * @copyright 2016 COMETE-UPO (Universit\E9 Paris Ouest) + * + * @return string + */ + public function get_piwik() { + global $DB; + + $enabled = $this->page->theme->settings->piwikenabled; + $imagetrack = $this->page->theme->settings->piwikimagetrack; + $siteurl = $this->page->theme->settings->piwiksiteurl; + $siteid = $this->page->theme->settings->piwiksiteid; + $trackadmin = $this->page->theme->settings->piwiktrackadmin; + + $enabled = $this->page->theme->settings->piwikenabled; + $imagetrack = $this->page->theme->settings->piwikimagetrack; + $siteurl = $this->page->theme->settings->piwiksiteurl; + $siteid = $this->page->theme->settings->piwiksiteid; + $trackadmin = $this->page->theme->settings->piwiktrackadmin; + + $analytics = ''; + if ($enabled && !empty($siteurl) && !empty($siteid) && (!is_siteadmin() || $trackadmin)) { + if ($imagetrack) { + $addition = '<noscript><p> + <img src="//'.$siteurl.'/piwik.php?idsite='.$siteid.' style="border:0;" alt="" /></p></noscript>'; + } else { + $addition = ''; + } + // Cleanurl. + $pageinfo = get_context_info_array($this->page->context->id); + $trackurl = ''; + // Adds course category name. + if (isset($pageinfo[1]->category)) { + if ($category = $DB->get_record('course_categories', array('id' => $pageinfo[1]->category))) { + $cats = explode("/", $category->path); + foreach (array_filter($cats) as $cat) { + if ($categorydepth = $DB->get_record("course_categories", array("id" => $cat))) { + $trackurl .= $categorydepth->name.'/'; + } + } + } + } + // Adds course full name. + if (isset($pageinfo[1]->fullname)) { + if (isset($pageinfo[2]->name)) { + $trackurl .= $pageinfo[1]->fullname.'/'; + } else if ($this->page->user_is_editing()) { + $trackurl .= $pageinfo[1]->fullname.'/'.get_string('edit', 'local_analytics'); + } else { + $trackurl .= $pageinfo[1]->fullname.'/'.get_string('view', 'local_analytics'); + } + } + // Adds activity name. + if (isset($pageinfo[2]->name)) { + $trackurl .= $pageinfo[2]->modname.'/'.$pageinfo[2]->name; + } + $trackurl = '"'.str_replace('"', '\"', $trackurl).'"'; + // Here we go. + $analytics .= '<!-- Start Piwik Code -->'."\n". + '<script type="text/javascript">'."\n". + ' var _paq = _paq || [];'."\n". + ' _paq.push(["setDocumentTitle", '.$trackurl.']);'."\n". + ' _paq.push(["trackPageView"]);'."\n". + ' _paq.push(["enableLinkTracking"]);'."\n". + ' (function() {'."\n". + ' var u="//'.$siteurl.'/";'."\n". + ' _paq.push(["setTrackerUrl", u+"piwik.php"]);'."\n". + ' _paq.push(["setSiteId", '.$siteid.']);'."\n". + ' var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0];'."\n". + ' g.type="text/javascript"; g.async=true; g.defer=true; g.src=u+"piwik.js";s.parentNode.insertBefore(g,s);'."\n". + ' })();'."\n". + '</script>'.$addition."\n". + '<!-- End Piwik Code -->'."\n". + ''; + } + return $analytics; + } + + /** + * Returns all tracking methods (Analytics and Piwik) + * + * @return string + */ + public function get_all_tracking_methods() { + $analytics = ''; + $analytics .= $this->get_analytics(); + $analytics .= $this->get_piwik(); + return $analytics; + } + + /** + * Returns the upper user menu + * + * @param custom_menu $menu + * @return string + */ + protected function render_user_menu(custom_menu $menu) { + // Add the custom usermenus. + $content = html_writer::start_tag('ul', array('class' => 'navbar-nav mr-auto')); + foreach ($menu->get_children() as $item) { + $content .= $this->render_custom_menu_item($item, 1, 'usermenu'); + } + + return $content.html_writer::end_tag('ul'); + } + + /** + * Process user messages + * + * @param array $message + * @return array + */ + protected function process_message($message) { + global $DB, $USER; + + $messagecontent = new stdClass(); + if ($message->notification || $message->useridfrom < 1) { + $messagecontent->text = $message->smallmessage; + $messagecontent->type = 'notification'; + + if (empty($message->contexturl)) { + $messagecontent->url = new moodle_url('/message/index.php', + array('user1' => $USER->id, 'viewing' => 'recentnotifications')); + } else { + $messagecontent->url = new moodle_url($message->contexturl); + } + + } else { + $messagecontent->type = 'message'; + if ($message->fullmessageformat == FORMAT_HTML) { + $message->smallmessage = html_to_text($message->smallmessage); + } + if (strlen($message->smallmessage) > 18) { + $messagecontent->text = core_text::substr($message->smallmessage, 0, 15) . '...'; + } else { + $messagecontent->text = $message->smallmessage; + } + $messagecontent->from = $DB->get_record('user', array('id' => $message->useridfrom)); + $messagecontent->url = new moodle_url('/message/index.php', + array('user1' => $USER->id, 'user2' => $message->useridfrom)); + } + $messagecontent->date = userdate($message->timecreated, get_string('strftimetime', 'langconfig')); + $messagecontent->unread = empty($message->timeread); + return $messagecontent; + } + + /** + * This renders a notification message. + * Uses bootstrap compatible html. + * + * @param string $message + * @param string $classes for css + */ + public function notification($message, $classes = 'notifyproblem') { + $message = clean_text($message); + $type = ''; + + if ($classes == 'notifyproblem') { + $type = 'alert alert-error'; + } + if ($classes == 'notifysuccess') { + $type = 'alert alert-success'; + } + if ($classes == 'notifymessage') { + $type = 'alert alert-info'; + } + if ($classes == 'redirectmessage') { + $type = 'alert alert-block alert-info'; + } + return '<div class="' . $type . '">' . $message . '</div>'; + } + + /** + * Returns html to render socialicons + * + * @return string + */ + public function socialicons() { + if (!isset($this->page->theme->settings->socialiconlist)) { + return ''; + } + + $target = '_blank'; + if (isset($this->page->theme->settings->socialtarget)) { + $target = $this->page->theme->settings->socialtarget; + } + + $retval = ''; + + $socialiconlist = $this->page->theme->settings->socialiconlist; + $lines = explode("\n", $socialiconlist); + + foreach ($lines as $line) { + if (strstr($line, '|')) { + $fields = explode('|', $line); + $retval .= '<a target="'.$target.'" title="'.$fields[1].'" href="'.$fields[0].'">'; + $retval .= '<i class="fa '.$fields[2].'"></i>'; + $retval .= '</a>'; + } + } + + return $retval; + } + + /** + * Returns html to render news ticker + * + * @return string + */ + public function get_news_ticker() { + $retval = ''; + + if (!isset($this->page->theme->settings->enabletickermy)) { + $this->page->theme->settings->enabletickermy = 0; + } + + // Display ticker if possible. + if ((!empty($this->page->theme->settings->enableticker) && + $this->page->theme->settings->enableticker && + $this->page->bodyid == "page-site-index") || + ($this->page->theme->settings->enabletickermy && $this->page->bodyid == "page-my-index")) { + $msg = ''; + $tickercount = $this->page->theme->settings->newstickercount; + + for ($i = 1; $i <= $tickercount; $i++) { + $textfield = 'tickertext' . $i; + $profilefield = 'tickertext' . $i . 'profilefield'; + + $access = true; + if (!empty($this->page->theme->settings->$profilefield)) { + $profilevals = explode('=', $this->page->theme->settings->$profilefield); + if (!$this->check_menu_access($profilevals[0], $profilevals[1], $textfield)) { + $access = false; + } + } + + if (($access) && (!empty($this->page->theme->settings->$textfield))) { + $msg .= format_text($this->page->theme->settings->$textfield, FORMAT_HTML, array('trusted' => true)); + } + } + + $msg = preg_replace('#\<[\/]{0,1}(li|ul|div|pre|blockquote)\>#', '', $msg); + if ($msg == '') { + $msg = '<p>' . get_string('tickerdefault', 'theme_adaptable') . '</p>'; + } + + $retval .= '<div id="ticker-wrap" class="clearfix container ' . $this->page->theme->settings->responsiveticker . '">'; + $retval .= '<div class="pull-left" id="ticker-announce">'; + $retval .= get_string('ticker', 'theme_adaptable'); + $retval .= '</div>'; + $retval .= '<ul id="ticker">'; + $retval .= $msg; + $retval .= '</ul>'; + $retval .= '</div>'; + } + + return $retval; + } + + + /** + * Renders block regions on front page (or any other page + * if specifying a different value for $settingsname). Used for various block region rendering. + * + * @param string $settingsname Setting name to retrieve from theme settings containing actual layout (e.g. 4-4-4-4) + * @param string $classnamebeginswith Used when building the blockname to retrieve for display + * @param string $customrowsetting If $settingsname value set to 'customrowsetting', then set this to + * the layout required to display a one row layout. + * When using this, ensure the appropriate number of block regions are defined in config.php. + * E.g. if $classnamebeginswith = 'my-block' and $customrowsetting = '4-4-0-0', 2 regions called + * 'my-block-a' and 'my-block-a' are expected to exist. + * @return string HTML output + */ + public function get_block_regions($settingsname = 'blocklayoutlayoutrow', $classnamebeginswith = 'frnt-market-', + $customrowsetting = null) { + global $COURSE, $USER; + $fields = array(); + $retval = ''; + $blockcount = 0; + $style = ''; + $adminediting = false; + + // Check if user has capability to edit block on homepage. This is used as part of checking if + // blocks should display the dotted borders and labels for editing. (Issue #809). + $context = context_course::instance($COURSE->id); + + // Check if front page and if has capability to edit blocks. The $pageallowed variable will store + // the correct state of whether user can edit that page. + $caneditblock = has_capability('moodle/block:edit', $context); + if ( ($this->page->pagelayout == "frontpage") && ($caneditblock !== true) ) { + $pageallowed = false; + } else { + $pageallowed = true; + } + + if ( (isset($USER->editing) && $USER->editing == 1) && ($pageallowed == true) ) { + $style = '" style="display: block; background: #EEEEEE; min-height: 50px; border: 2px dashed #BFBDBD; margin-top: 5px'; + $adminediting = true; + } + + if ($settingsname == 'customrowsetting') { + $fields[] = $customrowsetting; + } else { + for ($i = 1; $i <= 8; $i++) { + $marketrow = $settingsname . $i; + + // Need to check if the setting exists as this function is now + // called for variable row numbers in block regions (e.g. course page + // which is a single row of block regions). + + if (isset($this->page->theme->settings->$marketrow)) { + $marketrow = $this->page->theme->settings->$marketrow; + } else { + $marketrow = '0-0-0-0'; + } + + if ($marketrow != '0-0-0-0') { + $fields[] = $marketrow; + } + } + } + + foreach ($fields as $field) { + $vals = explode('-', $field); + foreach ($vals as $val) { + if ($val > 0) { + $retval .= '<div class="my-1 col-md-' . $val . $style . '">'; + + // Moodle does not seem to like numbers in region names so using letter instead. + $blockcount ++; + $block = $classnamebeginswith. chr(96 + $blockcount); + + if ($adminediting) { + $retval .= '<span style="padding-left: 10px;"> ' . get_string('region-' . $block, 'theme_adaptable') . + '' . '</span>'; + } + + $retval .= $this->blocks($block, 'block-region-front'); + $retval .= '</div>'; + } + } + } + return $retval; + } + + /** + * Renders block regions for potentially hidden blocks. For example, 4-4-4-4 to 6-6-0-0 + * would mean the last two blocks get inadvertently hidden. This function can recover and + * display those blocks. An override option also available to display blocks for the region, regardless. + * + * @param array $blocksarray Settings names containing the actual layout(s) (i.e. 4-4-4-4) + * @param array $classes Used when building the blockname to retrieve for display + * @param bool $displayall An override setting to simply display all blocks from the region + * @return string HTML output + */ + public function get_missing_block_regions($blocksarray, $classes = array(), $displayall = false) { + global $USER; + $retval = ''; + $adminediting = false; + + if (isset($USER->editing) && $USER->editing == 1) { + $adminediting = true; + } + + if (!empty($blocksarray)) { + + $classes = (array)$classes; + $missingblocks = ''; + + foreach ($blocksarray as $block) { + + // Do this for up to 8 rows (allows for expansion. Be careful + // of losing blocks if this value changes from a high to low number!). + for ($i = 1; $i <= 8; $i++) { + + // For each block region in a row, analyse the current layout (e.g. 6-6-0-0, 3-3-3-3). Check if less than + // 4 blocks (meaning a change in settings from say 4-4-4-4 to 6-6. Meaning missing blocks, + // i.e. 6-6-0-0 means the two end ones may have content that is inadvertantly lost. + $rowsetting = $block['settingsname'] . $i; + + if (isset($this->page->theme->settings->$rowsetting)) { + $rowvalue = $this->page->theme->settings->$rowsetting; + + $spannumbers = explode('-', $rowvalue); + $y = 0; + foreach ($spannumbers as $spannumber) { + $y++; + + // Here's the crucial bit. Check if span number is 0, + // or $displayall is true (override) and if so, print it out. + if ($spannumber == 0 || $displayall) { + + $blockclass = $block['classnamebeginswith'] . chr(96 + $y); + $missingblock = $this->blocks($blockclass, 'block'); + + // Check if the block actually has content to display before displaying. + if (strip_tags($missingblock)) { + if ($adminediting) { + $missingblocks .= '<em>ORPHANED BLOCK - Originally displays in: <strong>' . + get_string('region-' . $blockclass, 'theme_adaptable') .'</strong></em>'; + + } + $missingblocks .= $missingblock; + } + + } + } // End foreach. + } + } + } + + if (!empty($missingblocks)) { + $retval .= '<aside class="' . join(' ', $classes) . '">'; + $retval .= $missingblocks; + $retval .= '</aside>'; + } + } + + return $retval; + } + + /** + * Renders marketing blocks on front page + * + * @param string $layoutrow + * @param string $settingname + */ + public function get_marketing_blocks($layoutrow = 'marketlayoutrow', $settingname = 'market') { + $fields = array(); + $blockcount = 0; + + $extramarketclass = $this->page->theme->settings->frontpagemarketoption; + + $retval = '<div id="marketblocks" class="container '. $extramarketclass .'">'; + + for ($i = 1; $i <= 5; $i++) { + $marketrow = $layoutrow . $i; + $marketrow = $this->page->theme->settings->$marketrow; + if ($marketrow != '0-0-0-0') { + $fields[] = $marketrow; + } + } + + foreach ($fields as $field) { + $retval .= '<div class="row marketrow">'; + $vals = explode('-', $field); + foreach ($vals as $val) { + if ($val > 0) { + $retval .= '<div class="my-1 col-md-' . $val . ' ' . $extramarketclass . '">'; + $blockcount ++; + $fieldname = $settingname . $blockcount; + if (isset($this->page->theme->settings->$fieldname)) { + // Add HTML format. + $retval .= $this->get_setting($fieldname, 'format_html'); + } + $retval .= '</div>'; + } + } + $retval .= '</div>'; + } + $retval .= '</div>'; + if ($blockcount == 0 ) { + $retval = ''; + } + return $retval; + } + + /** + * Returns footer visibility setting + * + */ + public function get_footer_visibility() { + global $COURSE; + $value = $this->page->theme->settings->footerblocksplacement; + + if ($value == 1) { + return true; + } + + if ($value == 2 && $COURSE->id != 1) { + return false; + } + + if ($value == 3) { + return false; + } + return true; + } + + /** + * Renders footer blocks + * + * @param string $layoutrow + */ + public function get_footer_blocks($layoutrow = 'footerlayoutrow') { + $fields = array(); + $blockcount = 0; + + if (!$this->get_footer_visibility()) { + return ''; + } + + $output = '<div id="course-footer">' . $this->course_footer() . '</div> + <div class="container blockplace1">'; + + for ($i = 1; $i <= 3; $i++) { + $footerrow = $layoutrow . $i; + $footerrow = (!empty($this->page->theme->settings->$footerrow)) ? $this->page->theme->settings->$footerrow : '3-3-3-3'; + if ($footerrow != '0-0-0-0') { + $fields[] = $footerrow; + } + } + + foreach ($fields as $field) { + $output .= '<div class="row">'; + $vals = explode('-', $field); + foreach ($vals as $val) { + if ($val > 0) { + $blockcount ++; + $footerheader = 'footer' . $blockcount . 'header'; + $footercontent = 'footer' . $blockcount . 'content'; + if (!empty($this->page->theme->settings->$footercontent)) { + $output .= '<div class="left-col col-' . $val . '">'; + if (!empty($this->page->theme->settings->$footerheader)) { + $output .= '<h3>'; + $output .= $this->get_setting($footerheader, 'format_html'); + $output .= '</h3>'; + } + $output .= $this->get_setting($footercontent, 'format_html'); + $output .= '</div>'; + } + } + } + $output .= '</div>'; + } + $output .= '</div>'; + return $output; + } + + /** + * Renders frontpage slider + * + */ + public function get_frontpage_slider() { + $noslides = $this->page->theme->settings->slidercount; + $responsiveslider = $this->page->theme->settings->responsiveslider; + + $retval = ''; + + // Will we have any slides? + $haveslides = false; + for ($i = 1; $i <= $noslides; $i++) { + $sliderimage = 'p' . $i; + if (!empty($this->page->theme->settings->$sliderimage)) { + $haveslides = true; + break; + } + } + + if (!$haveslides) { + return ''; + } + + if (!empty($this->page->theme->settings->sliderfullscreen)) { + $retval .= '<div class="slidewrap'; + } else { + $retval .= '<div class="container slidewrap'; + } + + if ($this->page->theme->settings->slideroption2 == 'slider2') { + $retval .= " slidestyle2"; + } + + $retval .= ' ' . $responsiveslider . '"> + <div id="main-slider" class="flexslider"> + <ul class="slides">'; + + for ($i = 1; $i <= $noslides; $i++) { + $sliderimage = 'p' . $i; + $sliderurl = 'p' . $i . 'url'; + + if (!empty($this->page->theme->settings->$sliderimage)) { + $slidercaption = 'p' . $i .'cap'; + } + + $closelink = ''; + if (!empty($this->page->theme->settings->$sliderimage)) { + $retval .= '<li>'; + + if (!empty($this->page->theme->settings->$sliderurl)) { + $retval .= '<a href="' . $this->page->theme->settings->$sliderurl . '">'; + $closelink = '</a>'; + } + + $retval .= '<img src="' . $this->page->theme->setting_file_url($sliderimage, $sliderimage) + . '" alt="' . $sliderimage . '"/>'; + + if (!empty($this->page->theme->settings->$slidercaption)) { + $retval .= '<div class="flex-caption">'; + $retval .= $this->get_setting($slidercaption, 'format_html'); + $retval .= '</div>'; + } + $retval .= $closelink . '</li>'; + } + } + $retval .= '</ul></div></div>'; + return $retval; + } + + /** + * Renders the breadcrumb navbar. + * + * @return string Markup or empty string if 'nonavbar' for tge given page layout in the config.php file is true. + */ + public function page_navbar() { + $retval = ''; + if (empty($this->page->layout_options['nonavbar'])) { // Not disabled by 'nonavbar' in config.php. + if (!isset($this->page->theme->settings->enabletickermy)) { + $this->page->theme->settings->enabletickermy = 0; + } + + // Do not show navbar on dashboard / my home if news ticker is rendering. + if (!($this->page->theme->settings->enabletickermy && $this->page->bodyid == "page-my-index")) { + $retval = '<div class="row">'; + if (($this->page->theme->settings->breadcrumbdisplay != 'breadcrumb') + && (($this->page->pagelayout == 'course') + || ($this->page->pagelayout == 'incourse'))) { + global $COURSE; + $retval .= '<div id="page-coursetitle" class="col-12">'; + switch ($this->page->theme->settings->breadcrumbdisplay) { + case 'fullname': + // Full Course Name. + $coursetitle = $COURSE->fullname; + break; + case 'shortname': + // Short Course Name. + $coursetitle = $COURSE->shortname; + break; + } + + $coursetitlemaxwidth = (!empty($this->page->theme->settings->coursetitlemaxwidth) + ? $this->page->theme->settings->coursetitlemaxwidth : 0); + // Check max width of course title and trim if appropriate. + if (($coursetitlemaxwidth > 0) && ($coursetitle <> '')) { + if (strlen($coursetitle) > $coursetitlemaxwidth) { + $coursetitle = core_text::substr($coursetitle, 0, $coursetitlemaxwidth) . " ..."; + } + } + + switch ($this->page->theme->settings->breadcrumbdisplay) { + case 'fullname': + case 'shortname': + // Full / Short Course Name. + $courseurl = new moodle_url('/course/view.php', array('id' => $COURSE->id)); + $retval .= '<div id="coursetitle" class="p-2 bd-highlight"><h1><a href ="' + .$courseurl->out(true).'">'.format_string($coursetitle).'</a></h1></div>'; + break; + } + $retval .= '</div>'; + } else { + if ($this->page->include_region_main_settings_in_header_actions() && + !$this->page->blocks->is_block_present('settings')) { + $this->page->add_header_action(html_writer::div( + $this->region_main_settings_menu(), + 'd-print-none', + ['id' => 'region-main-settings-menu'] + )); + } + + $header = new stdClass(); + $header->navbar = $this->navbar(); + $header->headeractions = $this->page->get_header_actions(); + $header->headerclasses = $this->page->theme->settings->responsivebreadcrumb; + $retval .= $this->render_from_template('theme_adaptable/header', $header); + } + $retval .= '</div>'; + } + } + + return $retval; + } + + /** + * Render the navbar + * + * @return string + */ + public function navbar() { + $items = $this->page->navbar->get_items(); + $breadcrumbseparator = $this->page->theme->settings->breadcrumbseparator; + + $breadcrumbs = ""; + + if (empty($items)) { + return ''; + } + + $i = 0; + + foreach ($items as $item) { + $item->hideicon = true; + + // Text / Icon home. + if ($i++ == 0) { + $breadcrumbs .= '<li>'; + + if (get_config('theme_adaptable', 'enablehome') && get_config('theme_adaptable', 'enablemyhome')) { + $breadcrumbs = html_writer::tag('i', '', array( + 'title' => get_string('home', 'theme_adaptable'), + 'class' => 'fa fa-folder-open fa-lg' + ) + ); + } else if (get_config('theme_adaptable', 'breadcrumbhome') == 'icon') { + $breadcrumbs .= html_writer::link(new moodle_url('/'), + // Adds in a title for accessibility purposes. + html_writer::tag('i', '', array( + 'title' => get_string('home', 'theme_adaptable'), + 'class' => 'fa fa-home fa-lg') + ) + ); + $breadcrumbs .= '</li>'; + } else { + $breadcrumbs .= html_writer::link(new moodle_url('/'), get_string('home', 'theme_adaptable')); + $breadcrumbs .= '</li>'; + } + continue; + } + + $breadcrumbs .= '<span class="separator"><i class="fa-'.$breadcrumbseparator.' fa"></i> + </span><li>'.$this->render($item).'</li>'; + + } // End loop. + + $classes = $this->page->theme->settings->responsivebreadcrumb; + + return '<nav role="navigation" aria-label="'. get_string("breadcrumb", "theme_adaptable") .'"> + <ol class="breadcrumb ' . $classes . '">'.$breadcrumbs.'</ol> + </nav>'; + } + + /** + * Renders a navigation node object. + * + * @param navigation_node $item The navigation node to render. + * @return string HTML fragment + */ + protected function render_navigation_node(navigation_node $item) { + if ($item->action instanceof action_link) { + $action = clone($item->action); + $item = clone($item); + $item->action = $action; + } + return parent::render_navigation_node($item); + } + + /** + * Compares two course entries against their access time for a user to see which is first. + * + * @param stdClass $a A course. + * @param stdClass $b A course. + * + * @return int -1 'a' is first, 1 'b' is first or 0 they are equal. + */ + protected static function timeaccesscompare($a, $b) { + // The timeaccess is lastaccess entry and timestart an enrol entry. + if ((!empty($a->timeaccess)) && (!empty($b->timeaccess))) { + // Both last access. + if ($a->timeaccess == $b->timeaccess) { + return 0; + } + return ($a->timeaccess > $b->timeaccess) ? -1 : 1; + } else if ((!empty($a->timestart)) && (!empty($b->timestart))) { + // Both enrol. + if ($a->timestart == $b->timestart) { + return 0; + } + return ($a->timestart > $b->timestart) ? -1 : 1; + } + + /* Must be comparing an enrol with a last access. + -1 is to say that 'a' comes before 'b'. */ + if (!empty($a->timestart)) { + // If 'a' is the enrol entry. + return -1; + } + // Then 'b' must be the enrol entry. + return 1; + } + + /** + * Returns menu object containing main navigation. + * + * @return menu boject + */ + public function navigation_menu_content() { + global $COURSE; + $menu = new custom_menu(); + + $access = true; + $overridelist = false; + $overridetype = 'off'; + + if (!empty($this->page->theme->settings->navbardisplayicons)) { + $navbardisplayicons = true; + } else { + $navbardisplayicons = false; + } + + if (!empty($this->page->theme->settings->enablemysites)) { + $mysitesvisibility = $this->page->theme->settings->enablemysites; + } + + $mysitesmaxlength = '30'; + if (!empty($this->page->theme->settings->mysitesmaxlength)) { + $mysitesmaxlength = $this->page->theme->settings->mysitesmaxlength; + } + + $mysitesmaxlengthhidden = $mysitesmaxlength - 3; + + if (isloggedin() && !isguestuser()) { + if (!empty($this->page->theme->settings->enablehome)) { + $branchtitle = get_string('home', 'theme_adaptable'); + $branchlabel = ''; + if ($navbardisplayicons) { + $branchlabel .= '<i class="fa fa-home fa-lg"></i>'; + } + $branchlabel .= ' ' . $branchtitle; + + if (!empty($this->page->theme->settings->enablehomeredirect)) { + $branchurl = new moodle_url('/?redirect=0'); + } else { + $branchurl = new moodle_url('/'); + } + $branchsort = 9998; + $branch = $menu->add($branchlabel, $branchurl, '', $branchsort); + } + + if (!empty($this->page->theme->settings->enablemyhome)) { + $branchtitle = get_string('myhome'); + + $branchlabel = ''; + if ($navbardisplayicons) { + $branchlabel .= '<i class="fa fa-dashboard fa-lg"></i> '; + } + $branchlabel .= ' ' . $branchtitle; + $branchurl = new moodle_url('/my/index.php'); + $branchsort = 9999; + $branch = $menu->add($branchlabel, $branchurl, '', $branchsort); + } + + if (!empty($this->page->theme->settings->enableevents)) { + $branchtitle = get_string('events', 'theme_adaptable'); + $branchlabel = ''; + if ($navbardisplayicons) { + $branchlabel .= '<i class="fa fa-calendar fa-lg"></i>'; + } + $branchlabel .= ' ' . $branchtitle; + + $branchurl = new moodle_url('/calendar/view.php'); + $branchsort = 10000; + $branch = $menu->add($branchlabel, $branchurl, '', $branchsort); + } + + $overridetype = null; + $overridelist = null; + + if (!empty($this->page->theme->settings->mysitessortoverride)) { + $overridetype = $this->page->theme->settings->mysitessortoverride; + } + + if (!empty($this->page->theme->settings->mysitessortoverridefield)) { + $overridelist = $this->page->theme->settings->mysitessortoverridefield; + } + + if (($overridetype == 'profilefields' || $overridetype == 'profilefieldscohort') && (isset($overridelist))) { + $overridelist = $this->get_profile_field_contents($overridelist); + + if ($overridetype == 'profilefieldscohort') { + $overridelist = array_merge($this->get_cohort_enrollments(), $overridelist); + } + } + + if ($overridetype == 'strings' && isset($overridelist)) { + $overridelist = explode(',', $overridelist); + } + + if ($mysitesvisibility != 'disabled') { + $showmysites = true; + + // Check custom profile field to restrict display of menu. + if (!empty($this->page->theme->settings->enablemysitesrestriction)) { + $fields = explode('=', $this->page->theme->settings->enablemysitesrestriction); + $ftype = $fields[0]; + $setvalue = $fields[1]; + + if (!$this->check_menu_access($ftype, $setvalue, 'mysitesrestriction')) { + $showmysites = false; + } + + } + + if ($showmysites) { + $branchtitle = get_string('mysites', 'theme_adaptable'); + $branchlabel = ''; + + if ($navbardisplayicons) { + $branchlabel .= '<i class="fa fa-briefcase fa-lg"></i>'; + } + + $branchlabel .= ' ' . $branchtitle; + + $branchurl = new moodle_url('#'); + $branchsort = 10001; + + $menudisplayoption = ''; + + // Check menu hover settings. + if (isset($this->page->theme->settings->mysitesmenudisplay)) { + $menudisplayoption = $this->page->theme->settings->mysitesmenudisplay; + } else { + $menudisplayoption = 'shortcodehover'; + } + + // The two variables below will control the 4 options available from the settings above for mysitesmenuhover. + $showshortcode = true; // If false, then display full course name. + $showhover = true; + + switch ($menudisplayoption) { + case 'shortcodenohover': + $showhover = false; + break; + case 'fullnamenohover': + $showshortcode = false; + $showhover = false; + case 'fullnamehover': + $showshortcode = false; + break; + } + + // Calls a local method (render_mycourses) to get list of a user's current courses that they are enrolled on. + $sortedcourses = $this->render_mycourses($overridetype); + + /* After finding out if there will be at least one course to display, check + for the option of displaying a sub-menu arrow symbol. */ + if (!empty($this->page->theme->settings->navbardisplaysubmenuarrow)) { + $branchlabel .= ' <i class="fa fa-caret-down"></i>'; + } + + /* Add top level menu option here after finding out if there will be at least one course to display. This is + for the option of displaying a sub-menu arrow symbol above, if configured in the theme settings. */ + $branch = $menu->add($branchlabel, $branchurl, '', $branchsort); + $icon = ''; + + if ($sortedcourses) { + if ($overridetype == 'myoverview') { + $myoverviewcourses = $this->parsemyoverview($sortedcourses); + + if (!empty($myoverviewcourses[ADAPTABLE_COURSE_STARRED])) { + $icon = \theme_adaptable\toolbox::getfontawesomemarkup('star-o'); + $this->addcoursestomenu($branch, $myoverviewcourses[ADAPTABLE_COURSE_STARRED], + $showshortcode, $showhover, $mysitesmaxlength, $icon); + } + + if (!empty($myoverviewcourses[ADAPTABLE_COURSE_IN_PROGRESS])) { + $icon = \theme_adaptable\toolbox::getfontawesomemarkup('tasks'); + $child = $branch->add($icon . rtrim( + mb_strimwidth(format_string(get_string('inprogress', 'theme_adaptable')), + 0, $mysitesmaxlengthhidden)) . '...', $this->page->url, '', 1000); + $this->addcoursestomenu($child, $myoverviewcourses[ADAPTABLE_COURSE_IN_PROGRESS], + $showshortcode, $showhover, $mysitesmaxlength); + } + + if (!empty($myoverviewcourses[ADAPTABLE_COURSE_PAST])) { + $icon = \theme_adaptable\toolbox::getfontawesomemarkup('history'); + $child = $branch->add($icon . rtrim( + mb_strimwidth(format_string(get_string('past', 'theme_adaptable')), + 0, $mysitesmaxlengthhidden)) . '...', $this->page->url, '', 1000); + $this->addcoursestomenu($child, $myoverviewcourses[ADAPTABLE_COURSE_PAST], + $showshortcode, $showhover, $mysitesmaxlength); + } + + if (!empty($myoverviewcourses[ADAPTABLE_COURSE_FUTURE])) { + $icon = \theme_adaptable\toolbox::getfontawesomemarkup('clock-o'); + $child = $branch->add($icon . rtrim( + mb_strimwidth(format_string(get_string('future', 'theme_adaptable')), + 0, $mysitesmaxlengthhidden)) . '...', $this->page->url, '', 1000); + $this->addcoursestomenu($child, $myoverviewcourses[ADAPTABLE_COURSE_FUTURE], + $showshortcode, $showhover, $mysitesmaxlength); + } + + if (!empty($myoverviewcourses[ADAPTABLE_COURSE_HIDDEN])) { + $faicon = (!empty($this->page->theme->settings->chiddenicon)) ? + $this->page->theme->settings->chiddenicon : ''; + $hiddenicon = \theme_adaptable\toolbox::getfontawesomemarkup($faicon); + $child = $branch->add($hiddenicon . rtrim( + mb_strimwidth(format_string(get_string('hiddenfromview', 'theme_adaptable')), + 0, $mysitesmaxlengthhidden)) . '...', $this->page->url, '', 1000); + $this->addcoursestomenu($child, $myoverviewcourses[ADAPTABLE_COURSE_HIDDEN], + $showshortcode, $showhover, $mysitesmaxlength); + } + } else { + foreach ($sortedcourses as $course) { + if ($course->visible) { + $coursename = ''; + $rawcoursename = ''; // Untrimmed course name. + + if ($showshortcode) { + $coursename = mb_strimwidth(format_string($course->shortname), 0, + $mysitesmaxlength, '...', 'utf-8'); + } else { + $coursename = mb_strimwidth(format_string($course->fullname), 0, + $mysitesmaxlength, '...', 'utf-8'); + } + + if ($showhover) { + $alttext = $course->fullname; + } else { + $alttext = ''; + } + + if (!$overridelist) { // Feature not in use, add to menu as normal. + $icon = $this->getcoursemenuicons($course); + $branch->add($icon.$coursename, + new moodle_url('/course/view.php?id='.$course->id), $alttext); + } else { + // We want to check against array from profile field. + if ((($overridetype == 'profilefields' || + $overridetype == 'profilefieldscohort') && + in_array($course->shortname, $overridelist)) || + ($overridetype == 'strings' && + $this->check_if_in_array_string($overridelist, $course->shortname))) { + + $icon = $this->getcoursemenuicons($course); + $branch->add($icon.$coursename, + new moodle_url('/course/view.php?id='.$course->id), $alttext, 100); + } else { + // If not in array add to sub menu item. + if (!isset($child)) { + $icon = '<i class="fa fa-history"></i> '; + $child = $branch->add($icon . rtrim( + mb_strimwidth(format_string(get_string('pastcourses', 'theme_adaptable')), + 0, $mysitesmaxlengthhidden)) . '...', $this->page->url, $alttext, 1000); + } + if ($showshortcode) { + $rawcoursename = $course->shortname; + } else { + $rawcoursename = $course->fullname; + } + + $icon = $this->getcoursemenuicons($course); + $child->add($icon.rtrim(mb_strimwidth(format_string($rawcoursename), + 0, $mysitesmaxlengthhidden)) . '...', + new moodle_url('/course/view.php?id='.$course->id), + format_string($rawcoursename)); + } + } + } + } + + $faicon = (!empty($this->page->theme->settings->chiddenicon)) ? + $this->page->theme->settings->chiddenicon : 'eye-slash'; + $hiddenicon = \theme_adaptable\toolbox::getfontawesomemarkup($faicon); + $child = null; + foreach ($sortedcourses as $course) { + $icon = $this->getcoursemenuicons($course, $hiddenicon); + if (!$course->visible && $mysitesvisibility == 'includehidden') { + if (empty($child)) { + $child = $branch->add($hiddenicon. + rtrim(mb_strimwidth(format_string(get_string('hiddencourses', 'theme_adaptable')), + 0, $mysitesmaxlengthhidden)) . '...', $this->page->url, '', 2000); + } + + $child->add($icon.rtrim(mb_strimwidth(format_string($course->fullname), + 0, $mysitesmaxlengthhidden)) . '...', + new moodle_url('/course/view.php?id='.$course->id), format_string($course->shortname)); + } + } + } + } else { + $noenrolments = get_string('noenrolments', 'theme_adaptable'); + $branch->add('<em>'.$noenrolments.'</em>', new moodle_url('/'), $noenrolments); + } + } + } + + if (!empty($this->page->theme->settings->enablethiscourse)) { + if (ISSET($COURSE->id) && $COURSE->id > 1) { + $branchtitle = get_string('thiscourse', 'theme_adaptable'); + $branchlabel = ''; + if ($navbardisplayicons) { + $branchlabel .= '<i class="fa fa-sitemap fa-lg"></i><span class="menutitle">'; + } + + $branchlabel .= $branchtitle . '</span>'; + + $data = theme_adaptable_get_course_activities(); + + // Check the option of displaying a sub-menu arrow symbol. + if (!empty($this->page->theme->settings->navbardisplaysubmenuarrow)) { + $branchlabel .= ' <i class="fa fa-caret-down"></i>'; + } + + $branchurl = $this->page->url; + $branch = $menu->add($branchlabel, $branchurl, '', 10002); + + // Course sections. + if ($this->page->theme->settings->enablecoursesections) { + $this->create_course_sections_menu($branch); + } + + // Display Participants. + if ($this->page->theme->settings->displayparticipants) { + $branchtitle = get_string('people', 'theme_adaptable'); + $branchlabel = '<i class="icon fa fa-users fa-lg"></i>'.$branchtitle; + $branchurl = new moodle_url('/user/index.php', array('id' => $this->page->course->id)); + $branch->add($branchlabel, $branchurl, '', 100004); + } + + // Display Grades. + if ($this->page->theme->settings->displaygrades) { + $branchtitle = get_string('grades'); + $branchlabel = $this->pix_icon('i/grades', '', '', array('class' => 'icon')).$branchtitle; + $branchurl = new moodle_url('/grade/report/index.php', array('id' => $this->page->course->id)); + $branch->add($branchlabel, $branchurl, '', 100005); + } + + // Kaltura video gallery. + if (\theme_adaptable\toolbox::kalturaplugininstalled()) { + $branchtitle = get_string('nav_mediagallery', 'local_kalturamediagallery'); + $branchlabel = $this->pix_icon('media-gallery', '', 'local_kalturamediagallery').$branchtitle; + $branchurl = new moodle_url('/local/kalturamediagallery/index.php', + array('courseid' => $this->page->course->id)); + $branch->add($branchlabel, $branchurl, '', 100006); + } + + // Display Competencies. + if (get_config('core_competency', 'enabled')) { + if ($this->page->theme->settings->enablecompetencieslink) { + $branchtitle = get_string('competencies', 'competency'); + $branchlabel = $this->pix_icon('i/competencies', '', '', array('class' => 'icon')).$branchtitle; + $branchurl = new moodle_url('/admin/tool/lp/coursecompetencies.php', + array('courseid' => $this->page->course->id)); + $branch->add($branchlabel, $branchurl, '', 100007); + } + } + + // Display activities. + foreach ($data as $modname => $modfullname) { + if ($modname === 'resources') { + $icon = $this->pix_icon('icon', '', 'mod_page', array('class' => 'icon')); + $branch->add($icon.$modfullname, new moodle_url('/course/resources.php', + array('id' => $this->page->course->id))); + } else { + $icon = $this->pix_icon('icon', '', $modname, array('class' => 'icon')); + $branch->add($icon.$modfullname, new moodle_url('/mod/'.$modname.'/index.php', + array('id' => $this->page->course->id))); + } + } + } + } + } + + if ($navbardisplayicons) { + $helpicon = '<i class="fa fa-life-ring fa-lg"></i>'; + } else { + $helpicon = ''; + } + + if (!empty($this->page->theme->settings->helplinkscount)) { + for ($helpcount = 1; $helpcount <= $this->page->theme->settings->helplinkscount; $helpcount++) { + $enablehelpsetting = 'enablehelp'.$helpcount; + if (!empty($this->page->theme->settings->$enablehelpsetting)) { + $access = true; + $helpprofilefieldsetting = 'helpprofilefield'.$helpcount; + if (!empty($this->page->theme->settings->$helpprofilefieldsetting)) { + $fields = explode('=', $this->page->theme->settings->$helpprofilefieldsetting); + $ftype = $fields[0]; + $setvalue = $fields[1]; + if (!$this->check_menu_access($ftype, $setvalue, 'help'.$helpcount)) { + $access = false; + } + } + + if ($access && !$this->hideinforum()) { + $helplinktitlesetting = 'helplinktitle'.$helpcount; + if (empty($this->page->theme->settings->$helplinktitlesetting)) { + $branchtitle = get_string('helptitle', 'theme_adaptable', array('number' => $helpcount)); + } else { + $branchtitle = $this->page->theme->settings->$helplinktitlesetting; + } + $branchlabel = $helpicon.$branchtitle; + $branchurl = new moodle_url($this->page->theme->settings->$enablehelpsetting, + array('helptarget' => $this->page->theme->settings->helptarget)); + + $branchsort = 10003; + $branch = $menu->add($branchlabel, $branchurl, '', $branchsort); + } + } + } + } + + return $menu; + } + + /** + * Get the icon markup of the icon(s) for the course that will be used in its menu item. + * + * @param stdClass $course Course. + * @param string $existingicon Existing icon markup if any. + * + * @return string Icon markup(s). + */ + protected function getcoursemenuicons($course, $existingicon = '') { + global $CFG; + $icon = $existingicon; + + if (!empty($course->timestart)) { + $faicon = (!empty($this->page->theme->settings->cneveraccessedicon)) ? + $this->page->theme->settings->cneveraccessedicon : ''; + $icon .= \theme_adaptable\toolbox::getfontawesomemarkup($faicon); + } + + if (!empty($CFG->contextlocking)) { + $context = context_course::instance($course->id); + if ($context->locked) { + $faicon = (!empty($this->page->theme->settings->cfrozenicon)) ? + $this->page->theme->settings->cfrozenicon : ''; + $icon .= \theme_adaptable\toolbox::getfontawesomemarkup($faicon); + } + } + + if (empty($icon)) { + $faicon = (!empty($this->page->theme->settings->cdefaulticon)) ? + $this->page->theme->settings->cdefaulticon : ''; + $icon = \theme_adaptable\toolbox::getfontawesomemarkup($faicon); + } + + return $icon; + } + + /** + * Classify the courses in the same way that the My Overview block does non the dashboard. + * + * @param array $sortedcourses Array of courses - must contain the fields by 'define_properties' in 'course_summary_exporter'. + * + * @return array array of arrays that classify the courses. + */ + protected function parsemyoverview(&$sortedcourses) { + global $USER; + + $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($USER->id)); + $starred = $ufservice->find_favourites_by_type('core_course', 'courses'); + $starredids = array(); + + if ($starred) { + $starredids = array_map( + function($favourite) { + return $favourite->itemid; + }, $starred); + } + + $hiddenids = get_hidden_courses_on_timeline($USER); + + $myoverviewcourses = array( + ADAPTABLE_COURSE_STARRED => array(), + ADAPTABLE_COURSE_IN_PROGRESS => array(), + ADAPTABLE_COURSE_PAST => array(), + ADAPTABLE_COURSE_FUTURE => array(), + ADAPTABLE_COURSE_HIDDEN => array() + ); + + foreach ($sortedcourses as $course) { + if (in_array($course->id, $starredids)) { + $myoverviewcourses[ADAPTABLE_COURSE_STARRED][] = $course; + } // Starred can also appear in the respective sub-menu. + if (in_array($course->id, $hiddenids)) { + $myoverviewcourses[ADAPTABLE_COURSE_HIDDEN][] = $course; + } else { + switch (course_classify_for_timeline($course, $USER)) { + case COURSE_TIMELINE_PAST: + $myoverviewcourses[ADAPTABLE_COURSE_PAST][] = $course; + break; + case COURSE_TIMELINE_FUTURE: + $myoverviewcourses[ADAPTABLE_COURSE_FUTURE][] = $course; + break; + case COURSE_TIMELINE_INPROGRESS: + $myoverviewcourses[ADAPTABLE_COURSE_IN_PROGRESS][] = $course; + break; + } + } + } + + return $myoverviewcourses; + } + + /** + * Adds the given array of courses to the supplied menu. + * + * @param custom_menu_item $menu The menu to add to. + * @param array $courses Array of courses. + * @param bool $showshortcode Use the course shortname instead of full. + * @param bool $showhover Put the course full name in the alternative text. + * @param int $mysitesmaxlength Max lenghth of the course name string displayed. + * @param string $icon Prefix an icon (HTML markup) if any. + */ + protected function addcoursestomenu(&$menu, $courses, $showshortcode, $showhover, $mysitesmaxlength, $icon = '') { + foreach ($courses as $course) { + if (($course->visible) || (has_capability('moodle/course:viewhiddencourses', $this->page->context))) { + if ($showshortcode) { + $coursename = mb_strimwidth(format_string($course->shortname), 0, + $mysitesmaxlength, '...', 'utf-8'); + } else { + $coursename = mb_strimwidth(format_string($course->fullname), 0, + $mysitesmaxlength, '...', 'utf-8'); + } + + if ($showhover) { + $alttext = $course->fullname; + } else { + $alttext = ''; + } + + $courseicon = $this->getcoursemenuicons($course, $icon); + $menu->add($courseicon.$coursename, new moodle_url('/course/view.php?id='.$course->id), $alttext); + } + } + } + + /** + * Adds the course sections to the 'This course' menu. + * + * @param custom_menu_item $menu The menu to add to. + */ + protected function create_course_sections_menu($menu) { + global $COURSE; + + $courseformat = course_get_format($COURSE); + $modinfo = get_fast_modinfo($COURSE); + $numsections = $courseformat->get_last_section_number(); + $sectionsformnenu = array(); + foreach ($modinfo->get_section_info_all() as $section => $thissection) { + if ($section > $numsections) { + // Don't link to stealth sections. + continue; + } + /* Show the section if the user is permitted to access it, OR if it's not available + but there is some available info text which explains the reason & should display. */ + $showsection = $thissection->uservisible || + ($thissection->visible && !$thissection->available && !empty($thissection->availableinfo)); + + if (($showsection) || ($section == 0)) { + $sectionsformnenu[$section] = array( + 'sectionname' => $courseformat->get_section_name($section), + 'url' => $courseformat->get_view_url($section) + ); + } + } + + if (!empty($sectionsformnenu)) { // Rare but possible! + $branchtitle = get_string('sections', 'theme_adaptable'); + $branchlabel = '<i class="icon fa fa-list-ol fa-lg"></i>'.$branchtitle; + $branch = $menu->add($branchlabel, null, '', 100003); + + foreach ($sectionsformnenu as $sectionformenu) { + $branch->add($sectionformenu['sectionname'], $sectionformenu['url']); + } + } + + return $sectionsformnenu; + } + + /** + * Returns html to render main navigation menu + * + * @param string $menuid The id to use when creating menu. Used so this can be called for a nav drawer style display. + * + * @return string + */ + public function navigation_menu($menuid) { + + $sessttl = 0; + $cache = cache::make('theme_adaptable', 'userdata'); + + if ($sessttl > 0 && time() <= $cache->get('usernavbarttl')) { + return $cache->get('mysitesvisibility'); + } + static $builtmenu = null; + + if ($builtmenu != null) { + $menu = $builtmenu; + } else { + $menu = $this->navigation_menu_content(); + $builtmenu = $menu; + } + + if ($sessttl > 0) { + $cache->set('usernavbarttl', $sessttl); + $cache->set('usernavbar', $this->render_custom_menu($menu, '', '', $menuid)); + } + + return $this->render_custom_menu($menu, '', '', $menuid); + } + + /** + * Returns true if needs from array found in haystack + * @param array $needles a list of strings to check + * @param string $haystack value which may contain string + * @return boolean + */ + public function check_if_in_array_string($needles, $haystack) { + foreach ($needles as $needle) { + $needle = trim($needle); + if (strstr($haystack, $needle)) { + return true; + } + } + return false; + } + + /** + * Returns html to render tools menu in main navigation bar + * + * @param string $menuid The id to use when creating menu. Used so this can be called for a nav drawer style display. + * + * + * @return string + */ + public function tools_menu($menuid = '') { + $custommenuitems = ''; + $access = true; + $retval = ''; + + if (!isset($this->page->theme->settings->toolsmenuscount)) { + return ''; + } + $toolsmenuscount = $this->page->theme->settings->toolsmenuscount; + + $class = ''; + if (!empty($this->page->theme->settings->navbardisplayicons)) { + $class .= "<i class='fa fa-wrench fa-lg'></i>"; + } + $class .= "<span class='menutitle'>"; + + for ($i = 1; $i <= $toolsmenuscount; $i++) { + $menunumber = 'toolsmenu' . $i; + $menutitle = $menunumber . 'title'; + $accessrules = $menunumber . 'field'; + $access = true; + + if (!empty($this->page->theme->settings->$accessrules)) { + $fields = explode ('=', $this->page->theme->settings->$accessrules); + $ftype = $fields[0]; + $setvalue = $fields[1]; + if (!$this->check_menu_access($ftype, $setvalue, $menunumber)) { + $access = false; + } + } + + if (!empty($this->page->theme->settings->$menunumber) && $access == true && !$this->hideinforum()) { + $menu = ($this->page->theme->settings->$menunumber); + + /****************************************************************************************** + * @copyright 2018 Mathieu Domingo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + * + * Parse the end of each line to look for capabilities. + */ + + // Explode the content of the toolmenu in an "array of lines". + $linesmenu = explode("\n", $menu); + + // For each line we take the "$key" to be able to remove it from the "array of lines". + foreach ($linesmenu as $key => $line) { + // Explode each line in an "array of cells". + $cells = explode("|", $line); + + // If there is more than 3 cells, the user have add some "|text" to the line. + if (count($cells) > 3) { + // We look each cells added to the line for capabilities. + for ($i = 3; $i < count($cells); $i++) { + // Check if the current cell contain a valid capability or not. + if (!get_capability_info(trim($cells[$i]))) { + + // Should we say to the user that the capability is not valid ? + // It should be better to print this when the "admin" fill the toolmenu, not when we print it. + + // If it's not valid, check the next cell (here we could change the behaviour from "do nothing" + // to "delete the line"). + continue; + } + + // Check if the current user have the capability contained in the current cell. + if (!has_capability(trim($cells[$i]), context_course::instance($this->page->course->id))) { + // We remove the current line from the array. + unset($linesmenu[$key]); + + // We have removed the line, we don't need to check nexts cells. + break; + + // NOTE: The behaviour here is "the user need to have ALL capabilities written on the line" + // I.E: AND logic only, it needs a more complex traitement if we want to take in + // account some logics mixing OR and AND. + } + } + } + } + + // Once we have finish to check all lines, we recreate the menu + // (without the lines that the user don't have the capabilities needed) to continue the original process. + $menu = implode("\n", $linesmenu); + + $label = $this->page->theme->settings->$menutitle; + + // Check the option of displaying a sub-menu arrow symbol. + if (!empty($this->page->theme->settings->navbardisplaysubmenuarrow)) { + $label .= ' <i class="fa fa-caret-down"></i>'; + } + + $custommenuitems = $this->parse_custom_menu($menu, $label, $class, '</span>'); + $custommenu = new custom_menu($custommenuitems); + $retval .= $this->render_custom_menu($custommenu, '', '', $menuid); + } + } + return $retval; + } + + /** + * Returns html to render logo / title area. + * @param bool/int $currenttopcat The id of the current top category or false if none. + * + * @return string Markup. + */ + public function get_logo($currenttopcat) { + global $CFG, $SITE; + $logomarkup = ''; + + $responsivelogo = $this->page->theme->settings->responsivelogo; + + $logosetarea = ''; + if (!empty($currenttopcat)) { + $categoryheaderlogoset = 'categoryheaderlogo'.$currenttopcat; + if (!empty($this->page->theme->settings->$categoryheaderlogoset)) { + $logosetarea = $categoryheaderlogoset; + } + } + if ((empty($logosetarea)) && (!empty($this->page->theme->settings->logo))) { + $logosetarea = 'logo'; + } + + if (!empty($logosetarea)) { + // Logo. + $logomarkup = '<div class="bd-highlight ' . $responsivelogo . '">'; + $logo = '<img src=' . $this->page->theme->setting_file_url($logosetarea, $logosetarea) . ' id="logo" alt="" />'; + + // Exception - logo is not a link to site homepage. + if (!empty($this->page->layout_options['nonavbar'])) { + $logomarkup .= $logo; + } else { + // Standard - Output the logo as a link to site homepage. + $logomarkup .= '<a href=' . $CFG->wwwroot . ' aria-label="home" title="' . format_string($SITE->fullname). '">'; + $logomarkup .= $logo; + $logomarkup .= '</a>'; + } + $logomarkup .= '</div>'; + } + + return $logomarkup; + } + + /** + * Returns html to render logo / title area. + * @param bool/int $currenttopcat The id of the current top category or false if none. + * + * @return string Markup. + */ + public function get_title($currenttopcat) { + global $COURSE, $SITE; + $retval = ''; + + $responsivecoursetitle = $this->page->theme->settings->responsivecoursetitle; + $coursetitlemaxwidth = + (!empty($this->page->theme->settings->coursetitlemaxwidth) ? $this->page->theme->settings->coursetitlemaxwidth : 0); + + // If it is a mobile and the site title/course is not hidden or it is a desktop then we display the site title / course. + $usedefault = false; + $categoryheadercustomtitle = ''; + if (!empty($currenttopcat)) { + $categoryheadercustomtitleset = 'categoryheadercustomtitle'.$currenttopcat; + if (!empty($this->page->theme->settings->$categoryheadercustomtitleset)) { + $categoryheadercustomtitle = $this->page->theme->settings->$categoryheadercustomtitleset; + } + } + + // If course id is greater than 1 we display course title. + if ($COURSE->id > 1) { + // Select title. + $coursetitle = ''; + + switch ($this->page->theme->settings->enableheading) { + case 'fullname': + // Full Course Name. + $coursetitle = $COURSE->fullname; + break; + + case 'shortname': + // Short Course Name. + $coursetitle = $COURSE->shortname; + break; + } + + // Pre-process to avoid any filter issue. + $coursetitle = format_string($coursetitle); + + // Check max width of course title and trim if appropriate. + if (($coursetitlemaxwidth > 0) && ($coursetitle <> '')) { + if (strlen($coursetitle) > $coursetitlemaxwidth) { + $coursetitle = core_text::substr($coursetitle, 0, $coursetitlemaxwidth) . " ..."; + } + } + + switch ($this->page->theme->settings->enableheading) { + // Full / Short Course Name. + case 'fullname': + case 'shortname': + $retval .= '<div id="sitetitle" class="pb-2 bd-highlight ' . $responsivecoursetitle . '">'; + if (!empty($categoryheadercustomtitle)) { + $retval .= '<h1>'. format_string($categoryheadercustomtitle) . '</h1>'; + } + $retval .= '<h1 id="coursetitle">'.$coursetitle.'</h1>'; + $retval .= '</div>'; + break; + default: + // Default 'off'. + $usedefault = true; + break; + } + } + + // If course id is one or 'enableheading' was 'off' above then we display the site title. + if (($COURSE->id == 1) || ($usedefault)) { + if (!empty($categoryheadercustomtitle)) { + $retval .= '<div id="sitetitle" class="pb-2 bd-highlight ' . $responsivecoursetitle . '">'; + $retval .= '<h1>'. format_string($categoryheadercustomtitle) . '</h1>'; + $retval .= '</div>'; + } else { + switch ($this->page->theme->settings->sitetitle) { + case 'default': + $sitetitle = $SITE->fullname; + $retval .= '<div id="sitetitle" class="pb-2 bd-highlight ' . $responsivecoursetitle . '"><h1>' + . format_string($sitetitle) . '</h1></div>'; + break; + + case 'custom': + // Custom site title. + if (!empty($this->page->theme->settings->sitetitletext)) { + $header = theme_adaptable_remove_site_fullname($this->page->theme->settings->sitetitletext); + $sitetitlehtml = $this->page->theme->settings->sitetitletext; + $header = format_string($header); + $this->page->set_heading($header); + + $retval .= '<div id="sitetitle" class="pb-2 bd-highlight ' . $responsivecoursetitle . '">' + . format_text($sitetitlehtml, FORMAT_HTML) . '</div>'; + } + } + } + } + + return $retval; + } + + /** + * Returns html to render top menu items + * + * @param bool $showlinktext + * + * @return string + */ + public function get_top_menus($showlinktext = false) { + global $COURSE; + $template = new stdClass(); + $menus = array(); + $visibility = true; + $nummenus = 0; + + if (!empty($this->page->theme->settings->menuuseroverride)) { + $visibility = $this->check_menu_user_visibility(); + } + + $template->showright = false; + if (!empty($this->page->theme->settings->menuslinkright)) { + $template->showright = true; + } + + if (!empty($this->page->theme->settings->menuslinkicon)) { + $template->menuslinkicon = $this->page->theme->settings->menuslinkicon; + } else { + $template->menuslinkicon = 'fa-link'; + } + + if ($visibility) { + if (!empty($this->page->theme->settings->topmenuscount) && !empty($this->page->theme->settings->enablemenus) + && (!$this->page->theme->settings->disablemenuscoursepages || $COURSE->id == 1)) { + $topmenuscount = $this->page->theme->settings->topmenuscount; + + for ($i = 1; $i <= $topmenuscount; $i++) { + $menunumber = 'menu' . $i; + $newmenu = 'newmenu' . $i; + $fieldsetting = 'newmenu' . $i . 'field'; + $newmenutitle = 'newmenu' . $i . 'title'; + $requirelogin = 'newmenu' . $i . 'requirelogin'; + $custommenuitems = ''; + $access = true; + + if (empty($this->page->theme->settings->$requirelogin) || isloggedin()) { + if (!empty($this->page->theme->settings->$fieldsetting)) { + $fields = explode('=', $this->page->theme->settings->$fieldsetting); + $ftype = $fields[0]; + $setvalue = $fields[1]; + if (!$this->check_menu_access($ftype, $setvalue, $menunumber)) { + $access = false; + } + } + + if (!empty($this->page->theme->settings->$newmenu) && $access == true) { + $nummenus++; + $menu = ($this->page->theme->settings->$newmenu); + $title = ($this->page->theme->settings->$newmenutitle); + $custommenuitems = $this->parse_custom_menu($menu, format_string($title)); + $custommenu = new custom_menu($custommenuitems, current_language()); + $menus[] = $this->render_overlay_menu($custommenu); + } + } + } + } + } + + if ($nummenus == 0) { + return ''; + } + + $template->rows = array(); + + $grid = array( + '5' => '3', + '6' => '3', + '7' => '4', + '8' => '4', + '9' => '3', + '10' => '4', + '11' => '4', + '12' => '4' + ); + + if ($nummenus <= 4) { + $row = new stdClass(); + $row->span = (12 / $nummenus); + $row->menus = $menus; + $template->rows[] = $row; + } else { + $numperrow = $grid[$nummenus]; + $chunks = array_chunk($menus, $numperrow); + $menucount = 0; + for ($i = 0; $i < $nummenus; $i++) { + if ($i % $numperrow == 0) { + $row = new stdClass(); + $row->span = (12 / $numperrow); + $row->menus = $chunks[$menucount++]; + $template->rows[] = $row; + } + } + } + + if ($showlinktext == false) { + $template->showlinktext = false; + } else { + $template->showlinktext = true; + } + + return $this->render_from_template('theme_adaptable/overlaymenu', $template); + } + + /** + * Render the menu items for the overlay menu + * + * @param custom_menu $menu + * @return array of menus + */ + private function render_overlay_menu(custom_menu $menu) { + $template = new stdClass(); + if (!$menu->has_children()) { + return ''; + } + $template->menuitems = array(); + foreach ($menu->get_children() as $item) { + $this->render_overlay_menu_item($item, $template->menuitems); + } + return $template; + } + + /** + * Render the overlay menu items. + * + * @param custom_menu_item $item + * @param array $menuitems + * @param int $level + */ + private function render_overlay_menu_item(custom_menu_item $item, &$menuitems, $level = 0) { + if ($item->has_children()) { + $node = new stdClass; + $node->title = $item->get_title(); + $node->text = $item->get_text(); + $node->class = 'level-' . $level; + $menuitems[] = $node; + + /* Top level menu. Check if URL contains a valid URL, if not + then use standard javascript:void(0). Done to fix current + jquery / Bootstrap incompatibility with using # in target URLS. + Ref: Issue 617 on Adaptable theme issues on Bitbucket. */ + if (empty($item->get_url())) { + $node->url = "javascript:void(0)"; + } else { + $node->url = $item->get_url(); + } + + $level++; + foreach ($item->get_children() as $subitem) { + $menuitems[] = $this->render_overlay_menu_item($subitem, $menuitems, $level); + } + } else { + $node = new stdClass; + $node->title = $item->get_title(); + $node->text = $item->get_text(); + $node->class = 'level-' . $level; + $node->url = $item->get_url(); + $menuitems[] = $node; + } + } + + /** + * Checks menu visibility where setup to allow users to control via custom profile setting + * + * @return boolean + */ + public function check_menu_user_visibility() { + global $COURSE, $USER; + $uservalue = ''; + + if (empty($this->page->theme->settings->menuuseroverride)) { + return true; + } + + if (isset($USER->theme_adaptable_menus['menuvisibility'])) { + $uservalue = $USER->theme_adaptable_menus['menuvisibility']; + } else { + $profilefield = $this->page->theme->settings->menuoverrideprofilefield; + $profilefield = 'profile_field_' . $profilefield; + $uservalue = $this->get_user_visibility($profilefield); + } + + if ($uservalue == 0) { + return true; + } + + if ($uservalue == 1 && $COURSE->id != 1) { + return false; + } + + if ($uservalue == 2) { + return false; + } + + // Default to true means we dont have to evaluate sitewide setting and guarantees return value. + return true; + } + + /** + * Check users menu visibility settings, will store in session to avaoid repeated loading of profile data + * @param string $profilefield + * @return boolean + */ + public function get_user_visibility($profilefield) { + global $CFG, $USER; + $uservisibility = ''; + + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + profile_load_data($USER); + + $uservisibility = $USER->$profilefield; + $USER->theme_adaptable_menus['menuvisibility'] = $uservisibility; + return $uservisibility; + } + + /** + * Checks menu access based on admin settings and a users custom profile fields + * + * @param string $ftype the custom profile field + * @param string $setvalue the expected value a user must have in their profile field + * @param string $menu a token to identify the menu used to store access in session + * @return boolean + */ + public function check_menu_access($ftype, $setvalue, $menu) { + global $CFG, $USER; + $usersvalue = 'default-zz'; // Just want a value that will not be matched by accident. + $sessttl = (time() + ($this->page->theme->settings->menusessionttl * 60)); + $menuttl = $menu . 'ttl'; + + if ($this->page->theme->settings->menusession) { + if (isset($USER->theme_adaptable_menus[$menu])) { + + // If cache hasn't yet expired. + if ($USER->theme_adaptable_menus[$menuttl] >= time()) { + if ($USER->theme_adaptable_menus[$menu] == true) { + return true; + } else if ($USER->theme_adaptable_menus[$menu] == false) { + return false; + } + } + } + } + + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + profile_load_data($USER); + $ftype = "profile_field_$ftype"; + if (isset($USER->$ftype)) { + $usersvalue = $USER->$ftype; + } + + if ($usersvalue == $setvalue) { + $USER->theme_adaptable_menus[$menu] = true; + $USER->theme_adaptable_menus[$menuttl] = $sessttl; + return true; + } + + $USER->theme_adaptable_menus[$menu] = false; + $USER->theme_adaptable_menus[$menuttl] = $sessttl; + return false; + } + + /** + * Returns list of cohort enrollments + * + * @return array + */ + public function get_cohort_enrollments() { + global $DB, $USER; + $userscohorts = $DB->get_records('cohort_members', array('userid' => $USER->id)); + $courses = array(); + if ($userscohorts) { + $cohortedcourseslist = $DB->get_records_sql('select ' + . 'courseid ' + . 'from {enrol} ' + . 'where enrol = "cohort" ' + . 'and customint1 in (?)', array_keys($userscohorts)); + $cohortedcourses = $DB->get_records_list('course', 'id', array_keys($cohortedcourseslist), null, 'shortname'); + foreach ($cohortedcourses as $course) { + $courses[] = $course->shortname; + } + } + return($courses); + } + + /** + * Returns contents of multiple comma delimited custom profile fields + * + * @param string $profilefields delimited list of fields + * @return array + */ + public function get_profile_field_contents($profilefields) { + global $CFG, $USER; + $timestamp = 'currentcoursestime'; + $list = 'currentcourseslist'; + + if (isset($USER->theme_adaptable_menus[$timestamp])) { + if ($USER->theme_adaptable_menus[$timestamp] >= time()) { + if (isset($USER->theme_adaptable_menus[$list])) { + return $USER->theme_adaptable_menus[$list]; + } + } + } + + $sessttl = 1000 * 60 * 3; + $sessttl = 0; + $sessttl = time() + $sessttl; + $retval = array(); + + require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->dirroot.'/user/lib.php'); + profile_load_data($USER); + + $fields = explode(',', $profilefields); + + foreach ($fields as $field) { + $field = trim($field); + $field = "profile_field_$field"; + if (isset($USER->$field)) { + $vals = explode(',', $USER->$field); + foreach ($vals as $value) { + $retval[] = trim($value); + } + } + } + + $USER->theme_adaptable_menus[$list] = $retval; + $USER->theme_adaptable_menus[$timestamp] = $sessttl; + return $retval; + } + + /** + * Parses / wraps custom menus in HTML + * + * @param string $menu + * @param string $label + * @param string $class + * @param string $close + * + * @return string + */ + public function parse_custom_menu($menu, $label, $class = '', $close = '') { + + // Top level menu option. No URL added after $close (previously was #). + // Done to fix current jquery / Bootstrap version incompatibility with using # + // in target URLS. Ref: Issue 617 on Adaptable theme issues on Bitbucket. + $custommenuitems = $class . $label. $close . "||".$label."\n"; + $arr = explode("\n", $menu); + + // We want to force everything inputted under this menu. + foreach ($arr as $key => $value) { + $arr[$key] = '-' . $arr[$key]; + } + + $custommenuitems .= implode("\n", $arr); + return $custommenuitems; + } + + /** + * Hide tools menu in forum to make room for forum search optoin + * + * @return boolean + */ + public function hideinforum() { + $hidelinks = false; + if (!empty($this->page->theme->settings->hideinforum)) { + if (strstr($_SERVER['REQUEST_URI'], '/mod/forum/')) { + $hidelinks = true; + } + } + return $hidelinks; + } + + /** + * Wrap html round custom menu + * + * @param string $custommenu + * @param string $classno + * + * @return string + */ + public function wrap_custom_menu_top($custommenu, $classno) { + $retval = '<div class="dropdown pull-right newmenus newmenu$classno">'; + $retval .= $custommenu; + $retval .= '</div>'; + return $retval; + } + + /** + * Returns language menu + * + * @param bool $showtext + * + * @return string + */ + public function lang_menu($showtext = true) { + global $CFG; + $langmenu = new custom_menu(); + + $addlangmenu = true; + $langs = get_string_manager()->get_list_of_translations(); + if (count($langs) < 2 || empty($CFG->langmenu) || ($this->page->course != SITEID && !empty($this->page->course->lang))) { + $addlangmenu = false; + } + + if ($addlangmenu) { + $strlang = get_string('language'); + $currentlang = current_language(); + + if (isset($langs[$currentlang])) { + $currentlang = $langs[$currentlang]; + } else { + $currentlang = $strlang; + } + + if ($showtext != true) { + $currentlang = ''; + } + + $this->language = $langmenu->add('<i class="fa fa-globe fa-lg"></i><span class="langdesc">'.$currentlang.'</span>', + new moodle_url($this->page->url), $strlang, 10000); + + foreach ($langs as $langtype => $langname) { + $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname); + } + } + return $this->render_custom_menu($langmenu, '', '', 'langmenu'); + } + + /** + * Display custom menu in the format required for the nav drawer. Slight cludge here to make this work. + * The calling function can't call the default custom_menu() method as there is no way to know to + * render custom menu items in the format required for the drawer (which is different from displaying on the normal navbar). + * + * @return Custom menu html + */ + public function custom_menu_drawer() { + global $CFG; + + if (!empty($CFG->custommenuitems)) { + $custommenuitems = $CFG->custommenuitems; + } else { + return ''; + } + + $custommenu = new custom_menu($custommenuitems, current_language()); + return $this->render_custom_menu($custommenu, '', '', 'custom-menu-drawer'); + } + + /** + * This renders the bootstrap top menu. + * This renderer is needed to enable the Bootstrap style navigation. + * + * @param custom_menu $menu + * @param string $wrappre + * @param string $wrappost + * @param string $menuid + * + * @return string + */ + protected function render_custom_menu(custom_menu $menu, $wrappre = '', $wrappost = '', $menuid = '') { + global $CFG; + + // TODO: eliminate this duplicated logic, it belongs in core, not + // here. See MDL-39565. + $addlangmenu = true; + $langs = get_string_manager()->get_list_of_translations(); + if (count($langs) < 2 + or empty($CFG->langmenu) + or ($this->page->course != SITEID and !empty($this->page->course->lang))) { + $addlangmenu = false; + } + + if (!$menu->has_children() && $addlangmenu === false) { + return ''; + } + + $content = ''; + foreach ($menu->get_children() as $item) { + if (stristr($menuid, 'drawer')) { + $content .= $this->render_custom_menu_item_drawer($item, 0, $menuid, false); + } else { + $content .= $this->render_custom_menu_item($item, 0, $menuid); + } + } + $content = $wrappre . $content . $wrappost; + return $content; + } + + /** + * This code renders the custom menu items for the bootstrap dropdown menu. + * + * @param custom_menu_item $menunode + * @param int $level = 0 + * @param int $menuid + * + * @return string + */ + protected function render_custom_menu_item(custom_menu_item $menunode, $level = 0, $menuid = '') { + static $submenucount = 0; + + // If the node has a url, then use it, even if it has children as the URL could be that of an overview page. + if ($menunode->get_url() !== null) { + $url = $menunode->get_url(); + } else { + $url = '#'; + } + if ($menunode->has_children()) { + $content = '<li class="nav-item dropdown my-auto">'; + $content .= html_writer::start_tag('a', array('href' => $url, + 'class' => 'nav-link dropdown-toggle my-auto', 'role' => 'button', + 'id' => $menuid . $submenucount, + 'aria-haspopup' => 'true', + 'aria-expanded' => 'false', + 'aria-controls' => 'dropdown' . $menuid . $submenucount, + 'data-target' => $url, + 'data-toggle' => 'dropdown', + 'title' => $menunode->get_title()) + ); + $content .= $menunode->get_text(); + $content .= '</a>'; + $content .= '<ul role="menu" class="dropdown-menu" id="dropdown' . $menuid . $submenucount . '" aria-labelledby="' + .$menuid . $submenucount . '">'; + + foreach ($menunode->get_children() as $menunode) { + $content .= $this->render_custom_menu_item($menunode, 1, $menuid . $submenucount); + } + $content .= '</ul></li>'; + + } else { + if ($level == 0) { + $content = '<li class="nav-item">'; + $linkclass = 'nav-link'; + } else { + $content = '<li>'; + $linkclass = 'dropdown-item'; + } + + /* This is a bit of a cludge, but allows us to pass url, of type moodle_url with a param of + * "helptarget", which when equal to "_blank", will create a link with target="_blank" to allow the link to open + * in a new window. This param is removed once checked. + */ + if (is_object($url) && (get_class($url) == 'moodle_url') && ($url->get_param('helptarget') != null)) { + $helptarget = $url->get_param('helptarget'); + $url->remove_params('helptarget'); + $content .= html_writer::link($url, $menunode->get_text(), array('title' => $menunode->get_title(), + 'target' => $helptarget, 'class' => $linkclass)); + } else { + $content .= html_writer::link($url, $menunode->get_text(), + array('title' => $menunode->get_title(), 'class' => $linkclass)); + } + + $content .= "</li>"; + } + return $content; + } + + /** + * This code renders the custom menu items for the bootstrap dropdown menu. + * + * @param custom_menu_item $menunode + * @param int $level = 0 + * @param int $menuid + * @param bool $indent + * + * @return string + */ + protected function render_custom_menu_item_drawer(custom_menu_item $menunode, $level = 0, $menuid = '', $indent = false) { + static $submenucount = 0; + + if ($menunode->has_children()) { + + $submenucount++; + $content = '<li class="m-l-0">'; + $content .= html_writer::start_tag('a', array('href' => '#' . $menuid . $submenucount, + 'class' => 'list-group-item dropdown-toggle', + 'aria-haspopup' => 'true', 'data-target' => '#', 'data-toggle' => 'collapse', + 'title' => $menunode->get_title())); + $content .= $menunode->get_text(); + $content .= '</a>'; + + $content .= '<ul class="collapse" id="'.$menuid . $submenucount . '">'; + $indent = true; + foreach ($menunode->get_children() as $menunode) { + $content .= $this->render_custom_menu_item_drawer($menunode, 1, $menuid . $submenucount, $indent); + } + $content .= '</ul></li>'; + } else { + + // The node doesn't have children so produce a final menuitem. + if ($menunode->get_url() !== null) { + $url = $menunode->get_url(); + } else { + $url = '#'; + } + + if ($indent) { + $dataindent = 1; + $marginclass = 'm-l-1'; + } else { + $dataindent = 0; + $marginclass = 'm-l-0'; + } + + $content = '<li class="'.$marginclass.'">'; + $content .= '<a class="list-group-item list-group-item-action" href="'.$url.'"'; + $content .= 'data-key="" data-isexpandable="0" data-indent="'.$dataindent; + $content .= '" data-showdivider="0" data-type="1" data-nodetype="1"'; + $content .= 'data-collapse="0" data-forceopen="1" data-isactive="1" data-hidden="0" '; + $content .= 'data-preceedwithhr="0" data-parent-key="'.$menuid.'">'; + $content .= '<div class="'. $marginclass .'">'; + $content .= $menunode->get_text(); + $content .= '</div></a></li>'; + + } + return $content; + } + + + /** + * Renders tabtree + * + * @param tabtree $tabtree + * @return string + */ + protected function render_tabtree(tabtree $tabtree) { + if (empty($tabtree->subtree)) { + return ''; + } + $firstrow = $secondrow = ''; + foreach ($tabtree->subtree as $tab) { + $firstrow .= $this->render($tab); + if (($tab->selected || $tab->activated) && !empty($tab->subtree) && $tab->subtree !== array()) { + $secondrow = $this->tabtree($tab->subtree); + } + } + return html_writer::tag('ul', $firstrow, array('class' => 'nav nav-tabs mb-3')) . $secondrow; + } + + /** + * Renders tabobject (part of tabtree) + * + * This function is called from core_renderer::render_tabtree() + * and also it calls itself when printing the $tabobject subtree recursively. + * + * @param tabobject $tab + * @return string HTML fragment + */ + protected function render_tabobject(tabobject $tab) { + if ($tab->selected or $tab->activated) { + return html_writer::tag('li', html_writer::tag('a', $tab->text, + array('class' => 'nav-link active')), array('class' => 'nav-item')); + } else if ($tab->inactive) { + return html_writer::tag('li', html_writer::tag('a', $tab->text, array('class' => 'nav-link disabled')), + array('class' => 'nav-link')); + } else { + if (!($tab->link instanceof moodle_url)) { + // Backward compatibility when link was passed as quoted string. + $link = "<a class=\"nav-link\" href=\"$tab->link\" title=\"$tab->title\">$tab->text</a>"; + } else { + $link = html_writer::link($tab->link, $tab->text, array('title' => $tab->title, 'class' => 'nav-link')); + } + return html_writer::tag('li', $link, array('class' => 'nav-item')); + } + } + + /** + * Returns empty string + * + * @return string + */ + protected function theme_switch_links() { + // We're just going to return nothing and fail nicely, whats the point in bootstrap if not for responsive? + return ''; + } + + /** + * Output all the blocks in a particular region. + * + * @param string $region the name of a region on this page. + * @return string the HTML to be output. + */ + public function blocks_for_region($region) { + /* If 'shownavigationblockoncoursepage' is false and we are in a 'course' or 'incourse' page then + the navigation block will not be shown. */ + if ((!empty($this->page->theme->settings->shownavigationblockoncoursepage)) || + (($this->page->pagelayout != 'course') && ($this->page->pagelayout != 'incourse'))) { + return parent::blocks_for_region($region); + } + $blockcontents = $this->page->blocks->get_content_for_region($region, $this); + $blocks = $this->page->blocks->get_blocks_for_region($region); + + $lastblock = null; + $zones = array(); + foreach ($blocks as $block) { + if ($block->instance->blockname == 'navigation') { + continue; + } + $zones[] = $block->title; + } + $output = ''; + + foreach ($blockcontents as $bc) { + if ($bc->attributes['data-block'] == 'navigation') { + continue; + } + if ($bc instanceof block_contents) { + $output .= $this->block($bc, $region); + $lastblock = $bc->title; + } else if ($bc instanceof block_move_target) { + $output .= $this->block_move_target($bc, $zones, $lastblock, $region); + } else { + throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.'); + } + } + return $output; + } + + /** + * Render blocks + * @param string $region + * @param array $classes + * @param string $tag + * @return string + */ + public function blocks($region, $classes = array(), $tag = 'aside') { + $output = parent::blocks($region, $classes, $tag); + + if ((!empty($output)) && ($region == 'side-post')) { + $output .= html_writer::tag('div', + html_writer::tag('i', '', array('class' => 'fa fa-3x fa-angle-left', 'aria-hidden' => 'true')), + array('id' => 'showsidebaricon', 'title' => get_string('sidebaricon', 'theme_adaptable'))); + $this->page->requires->js_call_amd('theme_adaptable/showsidebar', 'init'); + } + + return $output; + } + + /** + * This is an optional menu that can be added to a layout by a theme. It contains the + * menu for the course administration, only on the course main page. Lifted from Boost theme + * to use for the course actions menu. + * + * @return string + */ + public function context_header_settings_menu() { + $context = $this->page->context; + $menu = new action_menu(); + + $items = $this->page->navbar->get_items(); + $currentnode = end($items); + + $showcoursemenu = false; + $showfrontpagemenu = false; + $showusermenu = false; + + // We are on the course home page. + if (($context->contextlevel == CONTEXT_COURSE) && + !empty($currentnode) && + ($currentnode->type == navigation_node::TYPE_COURSE || + $currentnode->type == navigation_node::TYPE_SECTION || + $currentnode->type == navigation_node::TYPE_SETTING)) { // Show cog on grade report page. + $showcoursemenu = true; + } + + $courseformat = course_get_format($this->page->course); + // This is a single activity course format, always show the course menu on the activity main page. + if ($context->contextlevel == CONTEXT_MODULE && + !$courseformat->has_view_page()) { + + $this->page->navigation->initialise(); + $activenode = $this->page->navigation->find_active_node(); + // If the settings menu has been forced then show the menu. + if ($this->page->is_settings_menu_forced()) { + $showcoursemenu = true; + } else if (!empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY || + $activenode->type == navigation_node::TYPE_RESOURCE)) { + + // We only want to show the menu on the first page of the activity. This means + // the breadcrumb has no additional nodes. + if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) { + $showcoursemenu = true; + } + } + } + + // This is the site front page. + if ($context->contextlevel == CONTEXT_COURSE && + !empty($currentnode) && + $currentnode->key === 'home') { + $showfrontpagemenu = true; + } + + // This is the user profile page. + if ($context->contextlevel == CONTEXT_USER && + !empty($currentnode) && + ($currentnode->key === 'myprofile')) { + $showusermenu = true; + } + + if ($showfrontpagemenu) { + $settingsnode = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING); + if ($settingsnode) { + // Build an action menu based on the visible nodes from this navigation tree. + $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true); + + // We only add a list to the full settings menu if we didn't include every node in the short menu. + if ($skipped) { + $text = get_string('morenavigationlinks'); + $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id)); + $link = new action_link($url, $text, null, null, new pix_icon('t/edit', '')); + $menu->add_secondary_action($link); + } + } + return $this->render($menu); + } else if ($showcoursemenu) { + $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); + if ($settingsnode) { + // Build an action menu based on the visible nodes from this navigation tree. + $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true); + + // We only add a list to the full settings menu if we didn't include every node in the short menu. + if ($skipped) { + $text = get_string('morenavigationlinks'); + $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id)); + $link = new action_link($url, $text, null, null, new pix_icon('t/edit', '')); + $menu->add_secondary_action($link); + } + } + return $this->render($menu); + } else if ($showusermenu) { + // Get the course admin node from the settings navigation. + $settingsnode = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER); + if ($settingsnode) { + // Build an action menu based on the visible nodes from this navigation tree. + $this->build_action_menu_from_navigation($menu, $settingsnode); + } + return $this->render($menu); + } + + return ''; + } + + /** + * Mobile settings menu. + * + * TODO: Possibly make a Mustache template for all of the menu? + * + * @return string Markup. + */ + public function context_mobile_settings_menu() { + $output = ''; + + $showcourseitems = false; + $context = $this->page->context; + $items = $this->page->navbar->get_items(); + $currentnode = end($items); + + // We are on the course home page. + if (($context->contextlevel == CONTEXT_COURSE) && + !empty($currentnode) && + ($currentnode->type == navigation_node::TYPE_COURSE || $currentnode->type == navigation_node::TYPE_SECTION)) { + $showcourseitems = true; + } + + $courseformat = course_get_format($this->page->course); + // This is a single activity course format, always show the course menu on the activity main page. + if ($context->contextlevel == CONTEXT_MODULE && + !$courseformat->has_view_page()) { + + $this->page->navigation->initialise(); + $activenode = $this->page->navigation->find_active_node(); + // If the settings menu has been forced then show the menu. + if ($this->page->is_settings_menu_forced()) { + $showcourseitems = true; + } else if (!empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY || + $activenode->type == navigation_node::TYPE_RESOURCE)) { + + /* We only want to show the menu on the first page of the activity. This means + the breadcrumb has no additional nodes. */ + if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) { + $showcourseitems = true; + } + } + } + + if ($showcourseitems) { + $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); + if ($settingsnode) { + $displaykeys = array('turneditingonoff', 'editsettings'); // In the order we want. + $displaykeyscount = count($displaykeys); + $displaynodes = array(); + foreach ($settingsnode->children as $node) { + if ($node->display) { + if (in_array($node->key, $displaykeys)) { + $displaynodes[$node->key] = $node; + } + if (count($displaynodes) == $displaykeyscount) { + break; + } + } + } + + foreach ($displaykeys as $displaykey) { // Ensure order. + if (!empty($displaynodes[$displaykey])) { + $currentnode = $displaynodes[$displaykey]; + $output .= '<a class="list-group-item list-group-item-action " href="'.$currentnode->action.'">'; + $output .= '<div class="m-l-0">'; + $output .= '<div class="media">'; + $output .= '<span class="media-left">'; + $output .= $this->render($currentnode->icon); + $output .= '</span>'; + $output .= '<span class="media-body ">'.$currentnode->text.'</span>'; + $output .= '</div>'; + $output .= '</div>'; + $output .= '</a >'; + } + } + } + } + + return $output; + } + + /** + * This is an optional menu that can be added to a layout by a theme. It contains the + * menu for the most specific thing from the settings block. E.g. Module administration. Lifted from Boost. + * + * @return string + */ + public function region_main_settings_menu() { + $context = $this->page->context; + $menu = new action_menu(); + + if ($context->contextlevel == CONTEXT_MODULE) { + + $this->page->navigation->initialise(); + $node = $this->page->navigation->find_active_node(); + $buildmenu = true; + // If the settings menu has been forced then show the menu. + if ($this->page->is_settings_menu_forced()) { + $buildmenu = true; + } else if (!empty($node) && ($node->type == navigation_node::TYPE_ACTIVITY || + $node->type == navigation_node::TYPE_RESOURCE)) { + + $items = $this->page->navbar->get_items(); + $navbarnode = end($items); + // We only want to show the menu on the first page of the activity. This means + // the breadcrumb has no additional nodes. + if ($navbarnode && ($navbarnode->key === $node->key && $navbarnode->type == $node->type)) { + $buildmenu = true; + } + } + if ($buildmenu) { + // Get the course admin node from the settings navigation. + $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING); + if ($node) { + // Build an action menu based on the visible nodes from this navigation tree. + $this->build_action_menu_from_navigation($menu, $node); + } + } + + } else if ($context->contextlevel == CONTEXT_COURSECAT) { + // For course category context, show category settings menu, if we're on the course category page. + if ($this->page->pagetype === 'course-index-category') { + $node = $this->page->settingsnav->find('categorysettings', navigation_node::TYPE_CONTAINER); + if ($node) { + // Build an action menu based on the visible nodes from this navigation tree. + $this->build_action_menu_from_navigation($menu, $node); + } + } + + } else { + return ''; + } + return $this->render($menu); + } + + /** + * Take a node in the nav tree and make an action menu out of it. + * The links are injected in the action menu. Lifted from Boost theme. + * + * @param action_menu $menu + * @param navigation_node $node + * @param boolean $indent + * @param boolean $onlytopleafnodes + * @return boolean nodesskipped - True if nodes were skipped in building the menu + */ + protected function build_action_menu_from_navigation(action_menu $menu, + navigation_node $node, $indent = false, $onlytopleafnodes = false) { + $skipped = false; + + // Build an action menu based on the visible nodes from this navigation tree. + foreach ($node->children as $menuitem) { + + if ($menuitem->display) { + if ($onlytopleafnodes && $menuitem->children->count()) { + $skipped = true; + continue; + } + if ($menuitem->action) { + if ($menuitem->action instanceof action_link) { + $link = $menuitem->action; + // Give preference to setting icon over action icon. + if (!empty($menuitem->icon)) { + $link->icon = $menuitem->icon; + } + } else { + $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon); + } + } else { + if ($onlytopleafnodes) { + $skipped = true; + continue; + } + $link = new action_link(new moodle_url('#'), $menuitem->text, null, ['disabled' => true], $menuitem->icon); + } + if ($indent) { + $link->add_class('ml-4'); + } + if (!empty($menuitem->classes)) { + $link->add_class(implode(" ", $menuitem->classes)); + } + + $menu->add_secondary_action($link); + $skipped = $skipped || $this->build_action_menu_from_navigation($menu, $menuitem, true); + } + } + return $skipped; + } + + /** + * Redirects the user by any means possible given the current state + * + * This function should not be called directly, it should always be called using + * the redirect function in lib/weblib.php + * + * The redirect function should really only be called before page output has started + * however it will allow itself to be called during the state STATE_IN_BODY + * + * @param string $encodedurl The URL to send to encoded if required + * @return string The HTML with javascript refresh... + */ + public function adaptable_redirect($encodedurl) { + $url = str_replace('&', '&', $encodedurl); + $this->page->requires->js_function_call('document.location.replace', array($url), false, '0'); + $output = $this->opencontainers->pop_all_but_last(); + $output .= $this->footer(); + return $output; + } + + /** + * Returns a search box. + * + * @param string $id The search box wrapper div id, defaults to an autogenerated one. + * @return string HTML with the search form hidden by default. + */ + public function search_box($id = false) { + global $CFG; + + // Accessing $CFG directly as using \core_search::is_global_search_enabled would + // result in an extra included file for each site, even the ones where global search + // is disabled. + if (empty($CFG->enableglobalsearch) || !has_capability('moodle/search:query', context_system::instance())) { + return ''; + } + + $header2searchbox = 'expandable'; + if (!empty($this->page->theme->settings->header2searchbox)) { + $header2searchbox = $this->page->theme->settings->header2searchbox; + } + + if ($header2searchbox == 'disabled') { + return ''; + } else if ($header2searchbox == 'static') { + $expandable = false; + } else { + $expandable = true; + } + + if ($id == false) { + $id = uniqid(); + } else { + // Needs to be cleaned, we use it for the input id. + $id = clean_param($id, PARAM_ALPHANUMEXT); + } + + // JS to animate the form. + $this->page->requires->js_call_amd('theme_adaptable/search-input', + 'init', array('data' => array('id' => $id, 'expandable' => $expandable))); + + $searchicon = html_writer::tag('div', $this->pix_icon('a/search', get_string('search', 'search'), 'moodle'), + array('role' => 'button', 'tabindex' => 0)); + $formclass = 'search-input-form'; + if (!$expandable) { + $formclass .= ' expanded'; + } + $formattrs = array('class' => $formclass, 'action' => $CFG->wwwroot . '/search/index.php'); + $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'), + 'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id, 'class' => 'form-control'); + + $contents = html_writer::tag('label', get_string('enteryoursearchquery', 'search'), + array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs); + if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) { + $contents .= html_writer::empty_tag('input', ['type' => 'hidden', + 'name' => 'context', 'value' => $this->page->context->id]); + } + $searchinput = html_writer::tag('form', $contents, $formattrs); + + $wrapperclass = 'search-input-wrapper nav-link'; + if (!$expandable) { + $wrapperclass .= ' expanded expandable'; + } + + return html_writer::tag('div', $searchicon . $searchinput, array('class' => $wrapperclass, 'id' => $id)); + } +} diff --git a/theme/adaptable/scss/card-blocks.scss b/theme/adaptable/scss/card-blocks.scss new file mode 100644 index 0000000..5e10596 --- /dev/null +++ b/theme/adaptable/scss/card-blocks.scss @@ -0,0 +1,103 @@ +$card-group-margin: .25rem; // Assuming Boost default preset is used. +$card-deck-margin: $card-group-margin !default; +$card-gutter : $card-deck-margin * 2; + +body.zoomin { + .dashboard-card-deck:not(.fixed-width-cards) { + @media (min-width: 576px) { + .dashboard-card { + width: calc(100% - #{$card-gutter}); + } + } + @media (min-width: 840px) { + .dashboard-card { + width: calc(50% - #{$card-gutter}); + } + } + @media (min-width: 1100px) { + .dashboard-card { + width: calc(33.33% - #{$card-gutter}); + } + } + @media (min-width: 1360px) { + .dashboard-card { + width: calc(25% - #{$card-gutter}); + } + } + } +} + +body:not(.zoomin) { + .dashboard-card-deck:not(.fixed-width-cards) { + @media (min-width: 576px) { + .dashboard-card { + width: calc(100% - #{$card-gutter}); + } + } + @media (min-width: 768px) { + .dashboard-card { + width: calc(50% - #{$card-gutter}); + } + } + @media (min-width: 1100px) { + .dashboard-card { + width: calc(33.33% - #{$card-gutter}); + } + } + @media (min-width: 1360px) { + .dashboard-card { + width: calc(25% - #{$card-gutter}); + } + } + } +} + +body:not(.zoomin) #page.fullin { + .dashboard-card-deck:not(.fixed-width-cards) { + @media (min-width: 576px) { + .dashboard-card { + width: calc(100% - #{$card-gutter}); + } + } + @media (min-width: 768px) { + .dashboard-card { + width: calc(50% - #{$card-gutter}); + } + } + @media (min-width: 1100px) { + .dashboard-card { + width: calc(33.33% - #{$card-gutter}); + } + } + @media (min-width: 1360px) { + .dashboard-card { + width: calc(25% - #{$card-gutter}); + } + } + } +} + +body.zoomin #page.fullin { + .dashboard-card-deck:not(.fixed-width-cards) { + @media (min-width: 576px) { + .dashboard-card { + width: calc(50% - #{$card-gutter}); + } + } + @media (min-width: 840px) { + .dashboard-card { + width: calc(33.33% - #{$card-gutter}); + } + } + @media (min-width: 1100px) { + .dashboard-card { + width: calc(25% - #{$card-gutter}); + } + } + @media (min-width: 1360px) { + .dashboard-card { + width: calc(20% - #{$card-gutter}); + } + } + } +} diff --git a/theme/adaptable/settings.php b/theme/adaptable/settings.php new file mode 100644 index 0000000..cc2610b --- /dev/null +++ b/theme/adaptable/settings.php @@ -0,0 +1,75 @@ +<?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/>. + +/** + * Settings + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +require_once(__DIR__.'/libs/admin_confightmleditor.php'); +require_once(__DIR__.'/lib.php'); + +$settings = null; +$ADMIN->add('themes', new admin_category('theme_adaptable', get_string('configtitle', 'theme_adaptable'))); + +// Adaptable theme settings page. +$asettings = new theme_adaptable_admin_settingspage_tabs('themesettingadaptable', + get_string('configtabtitle', 'theme_adaptable'), + 39 +); +if ($ADMIN->fulltree) { + include(dirname(__FILE__) . '/settings/array_definitions.php'); + include(dirname(__FILE__) . '/settings/colors.php'); + include(dirname(__FILE__) . '/settings/fonts.php'); + include(dirname(__FILE__) . '/settings/buttons.php'); + include(dirname(__FILE__) . '/settings/header.php'); + include(dirname(__FILE__) . '/settings/header_menus.php'); + include(dirname(__FILE__) . '/settings/header_user.php'); + include(dirname(__FILE__) . '/settings/header_social.php'); + include(dirname(__FILE__) . '/settings/navbar_settings.php'); + include(dirname(__FILE__) . '/settings/navbar_styles.php'); + include(dirname(__FILE__) . '/settings/navbar_links.php'); + include(dirname(__FILE__) . '/settings/header_navbar_menu.php'); + include(dirname(__FILE__) . '/settings/category_headers.php'); + include(dirname(__FILE__) . '/settings/alert_box.php'); + include(dirname(__FILE__) . '/settings/block_settings.php'); + include(dirname(__FILE__) . '/settings/block_regions.php'); + include(dirname(__FILE__) . '/settings/marketing_blocks.php'); + include(dirname(__FILE__) . '/settings/frontpage_ticker.php'); + include(dirname(__FILE__) . '/settings/frontpage_slider.php'); + include(dirname(__FILE__) . '/settings/frontpage_courses.php'); + include(dirname(__FILE__) . '/settings/footer.php'); + include(dirname(__FILE__) . '/settings/layout.php'); + include(dirname(__FILE__) . '/settings/layout_responsive.php'); + include(dirname(__FILE__) . '/settings/login.php'); + include(dirname(__FILE__) . '/settings/dash_block_regions.php'); + include(dirname(__FILE__) . '/settings/course_formats.php'); + include(dirname(__FILE__) . '/settings/user.php'); + include(dirname(__FILE__) . '/settings/templates.php'); + include(dirname(__FILE__) . '/settings/print.php'); + include(dirname(__FILE__) . '/settings/analytics.php'); + include(dirname(__FILE__) . '/settings/custom_css.php'); +} +$ADMIN->add('theme_adaptable', $asettings); +require_once(dirname(__FILE__) . '/settings/importexport_settings.php'); diff --git a/theme/adaptable/settings/adaptable_admin_setting_configtemplate.php b/theme/adaptable/settings/adaptable_admin_setting_configtemplate.php new file mode 100644 index 0000000..507aeb0 --- /dev/null +++ b/theme/adaptable/settings/adaptable_admin_setting_configtemplate.php @@ -0,0 +1,132 @@ +<?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/>. + +/** + * Template admin setting. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Template admin setting. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class adaptable_admin_setting_configtemplate extends admin_setting_configtextarea { + + /** + * @var $templatename The name of the template. + */ + private $templatename; + + /** + * Constructor + * + * @param string $name + * @param string $visiblename + * @param string $description + * @param mixed $defaultsetting string or array + * @param string $templatename + * @param mixed $paramtype + * @param string $cols The number of columns to make the editor + * @param string $rows The number of rows to make the editor + */ + public function __construct( + $name, $visiblename, $description, $defaultsetting, $templatename, + $paramtype=PARAM_RAW, $cols='60', $rows='8') { + $this->rows = $rows; + $this->cols = $cols; + + $this->templatename = $templatename; + + global $PAGE; + $PAGE->requires->js_call_amd('theme_adaptable/templatepreview', 'init'); + + parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype); + } + + /** + * Returns an XHTML string for the editor + * + * @param string $data + * @param string $query + * @return string XHTML string for the editor + */ + public function output_html($data, $query='') { + global $OUTPUT, $PAGE; + + $default = $this->get_defaultsetting(); + $defaultinfo = $default; + if (!is_null($default) and $default !== '') { + $defaultinfo = "\n".$default; + } + + $context = (object) [ + 'cols' => $this->cols, + 'rows' => $this->rows, + 'id' => $this->get_id(), + 'name' => $this->get_full_name(), + 'value' => $data, + 'forceltr' => $this->get_force_ltr(), + ]; + $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context); + + $element = format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query); + + $sourcerenderer = $PAGE->get_renderer('theme_adaptable', 'mustachesource'); + $originalsource = $sourcerenderer->get_template($this->templatename); + + $overridetemplate = get_config('theme_adaptable', $this->name); + + if (!empty($overridetemplate)) { + $templateoverridden = true; + } else { + $templateoverridden = false; + $overridetemplate = $originalsource; + } + + $mustacherenderer = $PAGE->get_renderer('theme_adaptable', 'mustache'); + + preg_match('/Example context \(json\):([\s\S]*)/', $overridetemplate, $matched); // From 'display.js' in the template tool. + + if (!empty($matched[1])) { + $json = trim(substr($matched[1], 0, strpos($matched[1], '}}'))); + $data = json_decode($json); + + $context = (object) [ + 'templatepreview' => $mustacherenderer->render_from_template($overridetemplate, $data), + 'templateoverridden' => $templateoverridden + ]; + $element .= $OUTPUT->render_from_template('theme_adaptable/adaptable_admin_setting_configtemplate', $context); + } else { + $context = array(); + $element .= $OUTPUT->render_from_template('theme_adaptable/adaptable_admin_setting_configtemplate_nopreview', $context); + } + + $context = (object) [ + 'templatesource' => $originalsource + ]; + $element .= $OUTPUT->render_from_template('theme_adaptable/adaptable_admin_setting_configtemplate_source', $context); + + return $element; + } +} diff --git a/theme/adaptable/settings/adaptable_admin_setting_getprops.php b/theme/adaptable/settings/adaptable_admin_setting_getprops.php new file mode 100644 index 0000000..f26ad08 --- /dev/null +++ b/theme/adaptable/settings/adaptable_admin_setting_getprops.php @@ -0,0 +1,145 @@ +<?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/>. + +/** + * Get properties setting. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Get properties class. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class adaptable_admin_setting_getprops extends admin_setting { + + /** @var string Store properties. */ + private $props; + + /** @var string Return button name. */ + private $returnbuttonname; + + /** @var string Section name. */ + private $settingsectionname; + + /** @var string Save properties. */ + private $saveprops; + + /** @var string Save properties button name. */ + private $savepropsbuttonname; + + /** + * Not a setting, just properties. + * @param string $name Unique ascii name, either 'mysetting' for settings that in config, + * or 'myplugin/mysetting' for ones in config_plugins. + * @param string $heading Heading. + * @param string $information Text in box. + * @param string $props Properties + * @param string $settingsectionname Setting section name + * @param string $returnbuttonname Return button name + * @param string $savepropsbuttonname Save properties button name + * @param string $saveprops Save properties + */ + public function __construct($name, $heading, $information, $props, $settingsectionname, + $returnbuttonname, $savepropsbuttonname, $saveprops) { + $this->nosave = true; + $this->props = $props; + $this->returnbuttonname = $returnbuttonname; + $this->settingsectionname = $settingsectionname; + $this->savepropsbuttonname = $savepropsbuttonname; + $this->saveprops = $saveprops; + parent::__construct($name, $heading, $information, ''); // Last parameter is default. + } + + /** + * Get setting method. + * @return none + */ + public function get_setting() { + return ''; + } + + /** + * Get default settings method. + * @return string '' + */ + public function get_defaultsetting() { + return ''; + } + + /** + * Never write settings + * + * @param string $data setting to write + * + * @return string Always returns an empty string + */ + public function write_setting($data) { + return ''; + } + + /** + * Returns an HTML string + * + * @param string $data Data + * @param string $query Query + * + * @return string Returns an HTML string + */ + public function output_html($data, $query='') { + $return = ''; + + if ($this->saveprops) { + $returnurl = new moodle_url('/admin/settings.php', array('section' => $this->settingsectionname)); + $returnbutton = '<div class="singlebutton"><a class="btn btn-default" href="'.$returnurl->out(true).'">'. + $this->returnbuttonname.'</a></div>'; + $return .= $returnbutton; + $return .= '<div class="well" style="word-break: break-all;">'; + $return .= json_encode($this->props, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS); + $return .= '</div>'; + $return .= $returnbutton; + } else { + $propsexporturl = new moodle_url('/admin/settings.php', array('section' => $this->settingsectionname, + $this->name.'_saveprops' => 1)); + + $propsexportbutton = '<div class="singlebutton"><div><a class="btn btn-default" href="'.$propsexporturl->out(true).'">'. + $this->savepropsbuttonname.'</a></div></div>'; + $table = new html_table(); + $table->head = array($this->visiblename, markdown_to_html($this->description)); + $table->colclasses = array('leftalign', 'leftalign'); + $table->id = 'adminprops_'.$this->name; + $table->attributes['class'] = 'admintable generaltable'; + $table->data = array(); + + foreach ($this->props as $propname => $propvalue) { + $table->data[] = array($propname, '<pre>'.htmlentities($propvalue).'</pre>'); + } + $return .= $propsexportbutton; + $return .= html_writer::table($table); + $return .= $propsexportbutton; + } + + return $return; + } +} \ No newline at end of file diff --git a/theme/adaptable/settings/adaptable_admin_setting_putprops.php b/theme/adaptable/settings/adaptable_admin_setting_putprops.php new file mode 100644 index 0000000..d9d843d --- /dev/null +++ b/theme/adaptable/settings/adaptable_admin_setting_putprops.php @@ -0,0 +1,132 @@ +<?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/>. + +/** + * Put properties with validation setting. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Set properties class. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class adaptable_admin_setting_putprops extends admin_setting_configtextarea { + + /** @var string Name of the theme. */ + private $themename; + /** @var string Name of the 'callable' function to call with the name of the theme and the properties as an array. */ + private $callme; + /** @var string Report back from the parsing 'callable' to inform the user in the text area. */ + private $report = ''; + + /** + * Not a setting, just putting properties. + * + * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in + * config_plugins. + * @param string $visiblename localised + * @param string $description long localised info + * @param string $themename Name of the theme. + * @param string $callme Name of the 'callable' function to call with the name of the theme and the properties as an array. + */ + public function __construct($name, $visiblename, $description, $themename, $callme) { + $this->themename = $themename; + $this->callme = $callme; + parent::__construct($name, $visiblename, $description, ''); // Last parameter is default. + } + + /** + * Get default settings. + * @return string '' + */ + public function get_defaultsetting() { + return ''; + } + + /** + * Write settings. + * + * @param string $data Data + * + * @return mixed Result of write + */ + public function write_setting($data) { + $validated = $this->validate($data); + if ($validated !== true) { + return $validated; + } + + return ($this->config_write($this->name, $this->report) ? '' : get_string('errorsetting', 'admin')); + } + + /** + * Validate data before storage. + * + * @param string $data Data + * + * @return mixed true if alright, string if error found. + */ + public function validate($data) { + $validated = parent::validate($data); // Pass parent validation first. + + if ($validated == true) { + if (!empty($data)) { + // Only attempt decode if we have the start of a JSON string, otherwise will certainly be the saved report. + if ($data[0] == '{') { + $props = json_decode($data, true); + if ($props === null) { + if (function_exists('json_last_error_msg')) { + $validated = json_last_error_msg(); + } else { + // Fall back to numeric error for older PHP version. + $validated = json_last_error(); + } + } else { + $this->report = call_user_func($this->callme, $this->themename, $props); + } + } else { + // Keep what we have. + $this->report = $data; + } + } + } + + return $validated; + } + + /** + * Returns an HTML string + * + * @param string $data Data + * @param string $query Query + * + * @return string Returns an HTML string + */ + public function output_html($data, $query='') { + $return = parent::output_html($data, $query); + + return $return; + } +} diff --git a/theme/adaptable/settings/alert_box.php b/theme/adaptable/settings/alert_box.php new file mode 100644 index 0000000..5c6ea17 --- /dev/null +++ b/theme/adaptable/settings/alert_box.php @@ -0,0 +1,249 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Alert Section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_frontpage_alert', get_string('frontpagealertsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_alert', get_string('alertsettingsheading', 'theme_adaptable'), + format_text(get_string('alertdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Alert General Settings Heading. + $name = 'theme_adaptable/settingsalertgeneral'; + $heading = get_string('alertsettingsgeneral', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Enable or disable alerts. + $name = 'theme_adaptable/enablealerts'; + $title = get_string('enablealerts', 'theme_adaptable'); + $description = get_string('enablealertsdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Alert hidden course. + $name = 'theme_adaptable/alerthiddencourse'; + $title = get_string('alerthiddencourse', 'theme_adaptable'); + $description = get_string('alerthiddencoursedesc', 'theme_adaptable'); + $default = 'warning'; + $choices = array( + 'disabled' => get_string('alertdisabled', 'theme_adaptable'), + 'info' => get_string('alertinfo', 'theme_adaptable'), + 'warning' => get_string('alertwarning', 'theme_adaptable'), + 'success' => get_string('alertannounce', 'theme_adaptable')); + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Strip Tags. + $name = 'theme_adaptable/enablealertstriptags'; + $title = get_string('enablealertstriptags', 'theme_adaptable'); + $description = get_string('enablealertstriptagsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Number of Alerts. + $name = 'theme_adaptable/alertcount'; + $title = get_string('alertcount', 'theme_adaptable'); + $description = get_string('alertcountdesc', 'theme_adaptable'); + $default = THEME_ADAPTABLE_DEFAULT_ALERTCOUNT; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + $alertcount = get_config('theme_adaptable', 'alertcount'); + // If we don't have an an alertcount yet, default to the preset. + if (!$alertcount) { + $alertcount = THEME_ADAPTABLE_DEFAULT_ALERTCOUNT; + } + + for ($alertindex = 1; $alertindex <= $alertcount; $alertindex++) { + // Alert Box Heading. + $name = 'theme_adaptable/settingsalertbox'.$alertindex; + $heading = get_string('alertsettings', 'theme_adaptable', $alertindex); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Enable Alert. + $name = 'theme_adaptable/enablealert'.$alertindex; + $title = get_string('enablealert', 'theme_adaptable', $alertindex); + $description = get_string('enablealertdesc', 'theme_adaptable', $alertindex); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Alert Key. + $name = 'theme_adaptable/alertkey'.$alertindex; + $title = get_string('alertkeyvalue', 'theme_adaptable'); + $description = get_string('alertkeyvalue_details', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + + // Alert Text. + $name = 'theme_adaptable/alerttext'.$alertindex; + $title = get_string('alerttext', 'theme_adaptable'); + $description = get_string('alerttextdesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + // Alert Type. + $name = 'theme_adaptable/alerttype'.$alertindex; + $title = get_string('alerttype', 'theme_adaptable'); + $description = get_string('alerttypedesc', 'theme_adaptable'); + $default = 'info'; + $choices = array( + 'info' => get_string('alertinfo', 'theme_adaptable'), + 'warning' => get_string('alertwarning', 'theme_adaptable'), + 'success' => get_string('alertannounce', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Alert Access. + $name = 'theme_adaptable/alertaccess' . $alertindex; + $title = get_string('alertaccess', 'theme_adaptable'); + $description = get_string('alertaccessdesc', 'theme_adaptable'); + $default = 'global'; + $choices = array( + 'global' => get_string('alertaccessglobal', 'theme_adaptable'), + 'user' => get_string('alertaccessusers', 'theme_adaptable'), + 'admin' => get_string('alertaccessadmins', 'theme_adaptable'), + 'profile' => get_string('alertaccessprofile', 'theme_adaptable')); + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + $name = 'theme_adaptable/alertprofilefield' . $alertindex; + $title = get_string('alertprofilefield', 'theme_adaptable'); + $description = get_string('alertprofilefielddesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + } + + // Colours. + // Alert Course Settings Heading. + $name = 'theme_adaptable/settingsalertcolors'; + $heading = get_string('settingscolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Alert info colours. + $name = 'theme_adaptable/alertcolorinfo'; + $title = get_string('alertcolorinfo', 'theme_adaptable'); + $description = get_string('alertcolorinfodesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3a87ad', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alertbackgroundcolorinfo'; + $title = get_string('alertbackgroundcolorinfo', 'theme_adaptable'); + $description = get_string('alertbackgroundcolorinfodesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#d9edf7', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alertbordercolorinfo'; + $title = get_string('alertbordercolorinfo', 'theme_adaptable'); + $description = get_string('alertbordercolorinfodesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#bce8f1', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alerticoninfo'; + $title = get_string('alerticoninfo', 'theme_adaptable'); + $description = get_string('alerticoninfodesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, 'info-circle'); + $page->add($setting); + + // Alert success colours. + $name = 'theme_adaptable/alertcolorsuccess'; + $title = get_string('alertcolorsuccess', 'theme_adaptable'); + $description = get_string('alertcolorsuccessdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#468847', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alertbackgroundcolorsuccess'; + $title = get_string('alertbackgroundcolorsuccess', 'theme_adaptable'); + $description = get_string('alertbackgroundcolorsuccessdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#dff0d8', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alertbordercolorsuccess'; + $title = get_string('alertbordercolorsuccess', 'theme_adaptable'); + $description = get_string('alertbordercolorsuccessdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#d6e9c6', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alerticonsuccess'; + $title = get_string('alerticonsuccess', 'theme_adaptable'); + $description = get_string('alerticonsuccessdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, 'bullhorn'); + $page->add($setting); + + // Alert warning colours. + $name = 'theme_adaptable/alertcolorwarning'; + $title = get_string('alertcolorwarning', 'theme_adaptable'); + $description = get_string('alertcolorwarningdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#8a6d3b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alertbackgroundcolorwarning'; + $title = get_string('alertbackgroundcolorwarning', 'theme_adaptable'); + $description = get_string('alertbackgroundcolorwarningdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#fcf8e3', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alertbordercolorwarning'; + $title = get_string('alertbordercolorwarning', 'theme_adaptable'); + $description = get_string('alertbordercolorwarningdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#fbeed5', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/alerticonwarning'; + $title = get_string('alerticonwarning', 'theme_adaptable'); + $description = get_string('alerticonwarningdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, 'exclamation-triangle'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/analytics.php b/theme/adaptable/settings/analytics.php new file mode 100644 index 0000000..9503ce9 --- /dev/null +++ b/theme/adaptable/settings/analytics.php @@ -0,0 +1,132 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @copyright 2015 David Bezemer <info@davidbezemer.nl>, www.davidbezemer.nl + * @copyright 2016 COMETE (Paris Ouest University) + * @author David Bezemer <info@davidbezemer.nl>, Bas Brands <bmbrands@gmail.com>, Gavin Henrick <gavin@lts.ie>, COMETE + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Analytics section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_analytics', get_string('analyticssettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_analytics', get_string('analyticssettingsheading', 'theme_adaptable'), + format_text(get_string('analyticssettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Google Analytics Section. + $name = 'theme_adaptable/googleanalyticssettings'; + $heading = get_string('googleanalyticssettings', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Enable Google analytics. + $name = 'theme_adaptable/enableanalytics'; + $title = get_string('enableanalytics', 'theme_adaptable'); + $description = get_string('enableanalyticsdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Anonymize Google analytics. + $name = 'theme_adaptable/anonymizega'; + $title = get_string('anonymizega', 'theme_adaptable'); + $description = get_string('anonymizegadesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Number of Analytics entries. + $name = 'theme_adaptable/analyticscount'; + $title = get_string('analyticscount', 'theme_adaptable'); + $description = get_string('analyticscountdesc', 'theme_adaptable'); + $default = THEME_ADAPTABLE_DEFAULT_ANALYTICSCOUNT; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + // If we don't have an analyticscount yet, default to the preset. + $analyticscount = get_config('theme_adaptable', 'analyticscount'); + if (!$analyticscount) { + $alertcount = THEME_ADAPTABLE_DEFAULT_ANALYTICSCOUNT; + } + + for ($analyticsindex = 1; $analyticsindex <= $analyticscount; $analyticsindex ++) { + $name = 'theme_adaptable/analyticstext' . $analyticsindex; + $title = get_string('analyticstext', 'theme_adaptable'); + $description = get_string('analyticstextdesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_RAW); + $page->add($setting); + + $name = 'theme_adaptable/analyticsprofilefield' . $analyticsindex; + $title = get_string('analyticsprofilefield', 'theme_adaptable'); + $description = get_string('analyticsprofilefielddesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + } + + // Piwik Analytics Section. + $name = 'theme_adaptable/piwiksettings'; + $heading = get_string('piwiksettings', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Enable Piwik analytics. + $name = 'theme_adaptable/piwikenabled'; + $title = get_string('piwikenabled', 'theme_adaptable'); + $description = get_string('piwikenableddesc', 'theme_adaptable'); + $default = false; + $page->add(new admin_setting_configcheckbox($name, $title, $description, $default, true, false)); + + // Piwik site ID. + $name = 'theme_adaptable/piwiksiteid'; + $title = get_string('piwiksiteid', 'theme_adaptable'); + $description = get_string('piwiksiteiddesc', 'theme_adaptable'); + $default = '1'; + $page->add(new admin_setting_configtext($name, $title, $description, $default)); + + // Piwik image track. + $name = 'theme_adaptable/piwikimagetrack'; + $title = get_string('piwikimagetrack', 'theme_adaptable'); + $description = get_string('piwikimagetrackdesc', 'theme_adaptable'); + $default = true; + $page->add(new admin_setting_configcheckbox($name, $title, $description, $default, true, false)); + + // Piwik site URL. + $name = 'theme_adaptable/piwiksiteurl'; + $title = get_string('piwiksiteurl', 'theme_adaptable'); + $description = get_string('piwiksiteurldesc', 'theme_adaptable'); + $default = ''; + $page->add(new admin_setting_configtext($name, $title, $description, $default)); + + // Enable Piwik admins tracking. + $name = 'theme_adaptable/piwiktrackadmin'; + $title = get_string('piwiktrackadmin', 'theme_adaptable'); + $description = get_string('piwiktrackadmindesc', 'theme_adaptable'); + $default = false; + $page->add(new admin_setting_configcheckbox($name, $title, $description, $default, true, false)); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/array_definitions.php b/theme/adaptable/settings/array_definitions.php new file mode 100644 index 0000000..f7a40b8 --- /dev/null +++ b/theme/adaptable/settings/array_definitions.php @@ -0,0 +1,1280 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @copyright 2017-2018 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; +if ($ADMIN->fulltree) { + // Google Fonts. + $fontlist = array( + 'sans-serif' => new lang_string('default'), + 'ABeeZee' => 'ABeeZee', + 'Abel' => 'Abel', + 'Abhaya Libre' => 'Abhaya Libre', + 'Abril Fatface' => 'Abril Fatface', + 'Aclonica' => 'Aclonica', + 'Acme' => 'Acme', + 'Actor' => 'Actor', + 'Adamina' => 'Adamina', + 'Advent Pro' => 'Advent Pro', + 'Aguafina Script' => 'Aguafina Script', + 'Akronim' => 'Akronim', + 'Aladin' => 'Aladin', + 'Alata' => 'Alata', + 'Alatsi' => 'Alatsi', + 'Aldrich' => 'Aldrich', + 'Alef' => 'Alef', + 'Alegreya' => 'Alegreya', + 'Alegreya SC' => 'Alegreya SC', + 'Alegreya Sans' => 'Alegreya Sans', + 'Alegreya Sans SC' => 'Alegreya Sans SC', + 'Aleo' => 'Aleo', + 'Alex Brush' => 'Alex Brush', + 'Alfa Slab One' => 'Alfa Slab One', + 'Alice' => 'Alice', + 'Alike' => 'Alike', + 'Alike Angular' => 'Alike Angular', + 'Allan' => 'Allan', + 'Allerta' => 'Allerta', + 'Allerta Stencil' => 'Allerta Stencil', + 'Allura' => 'Allura', + 'Almarai' => 'Almarai', + 'Almendra' => 'Almendra', + 'Almendra Display' => 'Almendra Display', + 'Almendra SC' => 'Almendra SC', + 'Amarante' => 'Amarante', + 'Amaranth' => 'Amaranth', + 'Amatic SC' => 'Amatic SC', + 'Amethysta' => 'Amethysta', + 'Amiko' => 'Amiko', + 'Amiri' => 'Amiri', + 'Amita' => 'Amita', + 'Anaheim' => 'Anaheim', + 'Andada' => 'Andada', + 'Andika' => 'Andika', + 'Angkor' => 'Angkor', + 'Annie Use Your Telescope' => 'Annie Use Your Telescope', + 'Anonymous Pro' => 'Anonymous Pro', + 'Antic' => 'Antic', + 'Antic Didone' => 'Antic Didone', + 'Antic Slab' => 'Antic Slab', + 'Anton' => 'Anton', + 'Arapey' => 'Arapey', + 'Arbutus' => 'Arbutus', + 'Arbutus Slab' => 'Arbutus Slab', + 'Architects Daughter' => 'Architects Daughter', + 'Archivo' => 'Archivo', + 'Archivo Black' => 'Archivo Black', + 'Archivo Narrow' => 'Archivo Narrow', + 'Aref Ruqaa' => 'Aref Ruqaa', + 'Arima Madurai' => 'Arima Madurai', + 'Arimo' => 'Arimo', + 'Arizonia' => 'Arizonia', + 'Armata' => 'Armata', + 'Arsenal' => 'Arsenal', + 'Artifika' => 'Artifika', + 'Arvo' => 'Arvo', + 'Arya' => 'Arya', + 'Asap' => 'Asap', + 'Asap Condensed' => 'Asap Condensed', + 'Asar' => 'Asar', + 'Asset' => 'Asset', + 'Assistant' => 'Assistant', + 'Astloch' => 'Astloch', + 'Asul' => 'Asul', + 'Athiti' => 'Athiti', + 'Atma' => 'Atma', + 'Atomic Age' => 'Atomic Age', + 'Aubrey' => 'Aubrey', + 'Audiowide' => 'Audiowide', + 'Autour One' => 'Autour One', + 'Average' => 'Average', + 'Average Sans' => 'Average Sans', + 'Averia Gruesa Libre' => 'Averia Gruesa Libre', + 'Averia Libre' => 'Averia Libre', + 'Averia Sans Libre' => 'Averia Sans Libre', + 'Averia Serif Libre' => 'Averia Serif Libre', + 'B612' => 'B612', + 'B612 Mono' => 'B612 Mono', + 'Bad Script' => 'Bad Script', + 'Bahiana' => 'Bahiana', + 'Bahianita' => 'Bahianita', + 'Bai Jamjuree' => 'Bai Jamjuree', + 'Baloo 2' => 'Baloo 2', + 'Baloo Bhai 2' => 'Baloo Bhai 2', + 'Baloo Bhaina 2' => 'Baloo Bhaina 2', + 'Baloo Chettan 2' => 'Baloo Chettan 2', + 'Baloo Da 2' => 'Baloo Da 2', + 'Baloo Paaji 2' => 'Baloo Paaji 2', + 'Baloo Tamma 2' => 'Baloo Tamma 2', + 'Baloo Tammudu 2' => 'Baloo Tammudu 2', + 'Baloo Thambi 2' => 'Baloo Thambi 2', + 'Balsamiq Sans' => 'Balsamiq Sans', + 'Balthazar' => 'Balthazar', + 'Bangers' => 'Bangers', + 'Barlow' => 'Barlow', + 'Barlow Condensed' => 'Barlow Condensed', + 'Barlow Semi Condensed' => 'Barlow Semi Condensed', + 'Barriecito' => 'Barriecito', + 'Barrio' => 'Barrio', + 'Basic' => 'Basic', + 'Baskervville' => 'Baskervville', + 'Battambang' => 'Battambang', + 'Baumans' => 'Baumans', + 'Bayon' => 'Bayon', + 'Be Vietnam' => 'Be Vietnam', + 'Bebas Neue' => 'Bebas Neue', + 'Belgrano' => 'Belgrano', + 'Bellefair' => 'Bellefair', + 'Belleza' => 'Belleza', + 'Bellota' => 'Bellota', + 'Bellota Text' => 'Bellota Text', + 'BenchNine' => 'BenchNine', + 'Bentham' => 'Bentham', + 'Berkshire Swash' => 'Berkshire Swash', + 'Beth Ellen' => 'Beth Ellen', + 'Bevan' => 'Bevan', + 'Big Shoulders Display' => 'Big Shoulders Display', + 'Big Shoulders Text' => 'Big Shoulders Text', + 'Bigelow Rules' => 'Bigelow Rules', + 'Bigshot One' => 'Bigshot One', + 'Bilbo' => 'Bilbo', + 'Bilbo Swash Caps' => 'Bilbo Swash Caps', + 'BioRhyme' => 'BioRhyme', + 'BioRhyme Expanded' => 'BioRhyme Expanded', + 'Biryani' => 'Biryani', + 'Bitter' => 'Bitter', + 'Black And White Picture' => 'Black And White Picture', + 'Black Han Sans' => 'Black Han Sans', + 'Black Ops One' => 'Black Ops One', + 'Blinker' => 'Blinker', + 'Bokor' => 'Bokor', + 'Bonbon' => 'Bonbon', + 'Boogaloo' => 'Boogaloo', + 'Bowlby One' => 'Bowlby One', + 'Bowlby One SC' => 'Bowlby One SC', + 'Brawler' => 'Brawler', + 'Bree Serif' => 'Bree Serif', + 'Bubblegum Sans' => 'Bubblegum Sans', + 'Bubbler One' => 'Bubbler One', + 'Buda' => 'Buda', + 'Buenard' => 'Buenard', + 'Bungee' => 'Bungee', + 'Bungee Hairline' => 'Bungee Hairline', + 'Bungee Inline' => 'Bungee Inline', + 'Bungee Outline' => 'Bungee Outline', + 'Bungee Shade' => 'Bungee Shade', + 'Butcherman' => 'Butcherman', + 'Butterfly Kids' => 'Butterfly Kids', + 'Cabin' => 'Cabin', + 'Cabin Condensed' => 'Cabin Condensed', + 'Cabin Sketch' => 'Cabin Sketch', + 'Caesar Dressing' => 'Caesar Dressing', + 'Cagliostro' => 'Cagliostro', + 'Cairo' => 'Cairo', + 'Caladea' => 'Caladea', + 'Calistoga' => 'Calistoga', + 'Calligraffitti' => 'Calligraffitti', + 'Cambay' => 'Cambay', + 'Cambo' => 'Cambo', + 'Candal' => 'Candal', + 'Cantarell' => 'Cantarell', + 'Cantata One' => 'Cantata One', + 'Cantora One' => 'Cantora One', + 'Capriola' => 'Capriola', + 'Cardo' => 'Cardo', + 'Carme' => 'Carme', + 'Carrois Gothic' => 'Carrois Gothic', + 'Carrois Gothic SC' => 'Carrois Gothic SC', + 'Carter One' => 'Carter One', + 'Catamaran' => 'Catamaran', + 'Caudex' => 'Caudex', + 'Caveat' => 'Caveat', + 'Caveat Brush' => 'Caveat Brush', + 'Cedarville Cursive' => 'Cedarville Cursive', + 'Ceviche One' => 'Ceviche One', + 'Chakra Petch' => 'Chakra Petch', + 'Changa' => 'Changa', + 'Changa One' => 'Changa One', + 'Chango' => 'Chango', + 'Charm' => 'Charm', + 'Charmonman' => 'Charmonman', + 'Chathura' => 'Chathura', + 'Chau Philomene One' => 'Chau Philomene One', + 'Chela One' => 'Chela One', + 'Chelsea Market' => 'Chelsea Market', + 'Chenla' => 'Chenla', + 'Cherry Cream Soda' => 'Cherry Cream Soda', + 'Cherry Swash' => 'Cherry Swash', + 'Chewy' => 'Chewy', + 'Chicle' => 'Chicle', + 'Chilanka' => 'Chilanka', + 'Chivo' => 'Chivo', + 'Chonburi' => 'Chonburi', + 'Cinzel' => 'Cinzel', + 'Cinzel Decorative' => 'Cinzel Decorative', + 'Clicker Script' => 'Clicker Script', + 'Coda' => 'Coda', + 'Coda Caption' => 'Coda Caption', + 'Codystar' => 'Codystar', + 'Coiny' => 'Coiny', + 'Combo' => 'Combo', + 'Comfortaa' => 'Comfortaa', + 'Comic Neue' => 'Comic Neue', + 'Coming Soon' => 'Coming Soon', + 'Concert One' => 'Concert One', + 'Condiment' => 'Condiment', + 'Content' => 'Content', + 'Contrail One' => 'Contrail One', + 'Convergence' => 'Convergence', + 'Cookie' => 'Cookie', + 'Copse' => 'Copse', + 'Corben' => 'Corben', + 'Cormorant' => 'Cormorant', + 'Cormorant Garamond' => 'Cormorant Garamond', + 'Cormorant Infant' => 'Cormorant Infant', + 'Cormorant SC' => 'Cormorant SC', + 'Cormorant Unicase' => 'Cormorant Unicase', + 'Cormorant Upright' => 'Cormorant Upright', + 'Courgette' => 'Courgette', + 'Courier Prime' => 'Courier Prime', + 'Cousine' => 'Cousine', + 'Coustard' => 'Coustard', + 'Covered By Your Grace' => 'Covered By Your Grace', + 'Crafty Girls' => 'Crafty Girls', + 'Creepster' => 'Creepster', + 'Crete Round' => 'Crete Round', + 'Crimson Pro' => 'Crimson Pro', + 'Crimson Text' => 'Crimson Text', + 'Croissant One' => 'Croissant One', + 'Crushed' => 'Crushed', + 'Cuprum' => 'Cuprum', + 'Cute Font' => 'Cute Font', + 'Cutive' => 'Cutive', + 'Cutive Mono' => 'Cutive Mono', + 'DM Mono' => 'DM Mono', + 'DM Sans' => 'DM Sans', + 'DM Serif Display' => 'DM Serif Display', + 'DM Serif Text' => 'DM Serif Text', + 'Damion' => 'Damion', + 'Dancing Script' => 'Dancing Script', + 'Dangrek' => 'Dangrek', + 'Darker Grotesque' => 'Darker Grotesque', + 'David Libre' => 'David Libre', + 'Dawning of a New Day' => 'Dawning of a New Day', + 'Days One' => 'Days One', + 'Dekko' => 'Dekko', + 'Delius' => 'Delius', + 'Delius Swash Caps' => 'Delius Swash Caps', + 'Delius Unicase' => 'Delius Unicase', + 'Della Respira' => 'Della Respira', + 'Denk One' => 'Denk One', + 'Devonshire' => 'Devonshire', + 'Dhurjati' => 'Dhurjati', + 'Didact Gothic' => 'Didact Gothic', + 'Diplomata' => 'Diplomata', + 'Diplomata SC' => 'Diplomata SC', + 'Do Hyeon' => 'Do Hyeon', + 'Dokdo' => 'Dokdo', + 'Domine' => 'Domine', + 'Donegal One' => 'Donegal One', + 'Doppio One' => 'Doppio One', + 'Dorsa' => 'Dorsa', + 'Dosis' => 'Dosis', + 'Dr Sugiyama' => 'Dr Sugiyama', + 'Duru Sans' => 'Duru Sans', + 'Dynalight' => 'Dynalight', + 'EB Garamond' => 'EB Garamond', + 'Eagle Lake' => 'Eagle Lake', + 'East Sea Dokdo' => 'East Sea Dokdo', + 'Eater' => 'Eater', + 'Economica' => 'Economica', + 'Eczar' => 'Eczar', + 'El Messiri' => 'El Messiri', + 'Electrolize' => 'Electrolize', + 'Elsie' => 'Elsie', + 'Elsie Swash Caps' => 'Elsie Swash Caps', + 'Emblema One' => 'Emblema One', + 'Emilys Candy' => 'Emilys Candy', + 'Encode Sans' => 'Encode Sans', + 'Encode Sans Condensed' => 'Encode Sans Condensed', + 'Encode Sans Expanded' => 'Encode Sans Expanded', + 'Encode Sans Semi Condensed' => 'Encode Sans Semi Condensed', + 'Encode Sans Semi Expanded' => 'Encode Sans Semi Expanded', + 'Engagement' => 'Engagement', + 'Englebert' => 'Englebert', + 'Enriqueta' => 'Enriqueta', + 'Epilogue' => 'Epilogue', + 'Erica One' => 'Erica One', + 'Esteban' => 'Esteban', + 'Euphoria Script' => 'Euphoria Script', + 'Ewert' => 'Ewert', + 'Exo' => 'Exo', + 'Exo 2' => 'Exo 2', + 'Expletus Sans' => 'Expletus Sans', + 'Fahkwang' => 'Fahkwang', + 'Fanwood Text' => 'Fanwood Text', + 'Farro' => 'Farro', + 'Farsan' => 'Farsan', + 'Fascinate' => 'Fascinate', + 'Fascinate Inline' => 'Fascinate Inline', + 'Faster One' => 'Faster One', + 'Fasthand' => 'Fasthand', + 'Fauna One' => 'Fauna One', + 'Faustina' => 'Faustina', + 'Federant' => 'Federant', + 'Federo' => 'Federo', + 'Felipa' => 'Felipa', + 'Fenix' => 'Fenix', + 'Finger Paint' => 'Finger Paint', + 'Fira Code' => 'Fira Code', + 'Fira Mono' => 'Fira Mono', + 'Fira Sans' => 'Fira Sans', + 'Fira Sans Condensed' => 'Fira Sans Condensed', + 'Fira Sans Extra Condensed' => 'Fira Sans Extra Condensed', + 'Fjalla One' => 'Fjalla One', + 'Fjord One' => 'Fjord One', + 'Flamenco' => 'Flamenco', + 'Flavors' => 'Flavors', + 'Fondamento' => 'Fondamento', + 'Fontdiner Swanky' => 'Fontdiner Swanky', + 'Forum' => 'Forum', + 'Francois One' => 'Francois One', + 'Frank Ruhl Libre' => 'Frank Ruhl Libre', + 'Freckle Face' => 'Freckle Face', + 'Fredericka the Great' => 'Fredericka the Great', + 'Fredoka One' => 'Fredoka One', + 'Freehand' => 'Freehand', + 'Fresca' => 'Fresca', + 'Frijole' => 'Frijole', + 'Fruktur' => 'Fruktur', + 'Fugaz One' => 'Fugaz One', + 'GFS Didot' => 'GFS Didot', + 'GFS Neohellenic' => 'GFS Neohellenic', + 'Gabriela' => 'Gabriela', + 'Gaegu' => 'Gaegu', + 'Gafata' => 'Gafata', + 'Galada' => 'Galada', + 'Galdeano' => 'Galdeano', + 'Galindo' => 'Galindo', + 'Gamja Flower' => 'Gamja Flower', + 'Gayathri' => 'Gayathri', + 'Gelasio' => 'Gelasio', + 'Gentium Basic' => 'Gentium Basic', + 'Gentium Book Basic' => 'Gentium Book Basic', + 'Geo' => 'Geo', + 'Geostar' => 'Geostar', + 'Geostar Fill' => 'Geostar Fill', + 'Germania One' => 'Germania One', + 'Gidugu' => 'Gidugu', + 'Gilda Display' => 'Gilda Display', + 'Girassol' => 'Girassol', + 'Give You Glory' => 'Give You Glory', + 'Glass Antiqua' => 'Glass Antiqua', + 'Glegoo' => 'Glegoo', + 'Gloria Hallelujah' => 'Gloria Hallelujah', + 'Goblin One' => 'Goblin One', + 'Gochi Hand' => 'Gochi Hand', + 'Gorditas' => 'Gorditas', + 'Gothic A1' => 'Gothic A1', + 'Gotu' => 'Gotu', + 'Goudy Bookletter 1911' => 'Goudy Bookletter 1911', + 'Graduate' => 'Graduate', + 'Grand Hotel' => 'Grand Hotel', + 'Gravitas One' => 'Gravitas One', + 'Great Vibes' => 'Great Vibes', + 'Grenze' => 'Grenze', + 'Grenze Gotisch' => 'Grenze Gotisch', + 'Griffy' => 'Griffy', + 'Gruppo' => 'Gruppo', + 'Gudea' => 'Gudea', + 'Gugi' => 'Gugi', + 'Gupter' => 'Gupter', + 'Gurajada' => 'Gurajada', + 'Habibi' => 'Habibi', + 'Halant' => 'Halant', + 'Hammersmith One' => 'Hammersmith One', + 'Hanalei' => 'Hanalei', + 'Hanalei Fill' => 'Hanalei Fill', + 'Handlee' => 'Handlee', + 'Hanuman' => 'Hanuman', + 'Happy Monkey' => 'Happy Monkey', + 'Harmattan' => 'Harmattan', + 'Headland One' => 'Headland One', + 'Heebo' => 'Heebo', + 'Henny Penny' => 'Henny Penny', + 'Hepta Slab' => 'Hepta Slab', + 'Herr Von Muellerhoff' => 'Herr Von Muellerhoff', + 'Hi Melody' => 'Hi Melody', + 'Hind' => 'Hind', + 'Hind Guntur' => 'Hind Guntur', + 'Hind Madurai' => 'Hind Madurai', + 'Hind Siliguri' => 'Hind Siliguri', + 'Hind Vadodara' => 'Hind Vadodara', + 'Holtwood One SC' => 'Holtwood One SC', + 'Homemade Apple' => 'Homemade Apple', + 'Homenaje' => 'Homenaje', + 'IBM Plex Mono' => 'IBM Plex Mono', + 'IBM Plex Sans' => 'IBM Plex Sans', + 'IBM Plex Sans Condensed' => 'IBM Plex Sans Condensed', + 'IBM Plex Serif' => 'IBM Plex Serif', + 'IM Fell DW Pica' => 'IM Fell DW Pica', + 'IM Fell DW Pica SC' => 'IM Fell DW Pica SC', + 'IM Fell Double Pica' => 'IM Fell Double Pica', + 'IM Fell Double Pica SC' => 'IM Fell Double Pica SC', + 'IM Fell English' => 'IM Fell English', + 'IM Fell English SC' => 'IM Fell English SC', + 'IM Fell French Canon' => 'IM Fell French Canon', + 'IM Fell French Canon SC' => 'IM Fell French Canon SC', + 'IM Fell Great Primer' => 'IM Fell Great Primer', + 'IM Fell Great Primer SC' => 'IM Fell Great Primer SC', + 'Ibarra Real Nova' => 'Ibarra Real Nova', + 'Iceberg' => 'Iceberg', + 'Iceland' => 'Iceland', + 'Imprima' => 'Imprima', + 'Inconsolata' => 'Inconsolata', + 'Inder' => 'Inder', + 'Indie Flower' => 'Indie Flower', + 'Inika' => 'Inika', + 'Inknut Antiqua' => 'Inknut Antiqua', + 'Inria Sans' => 'Inria Sans', + 'Inria Serif' => 'Inria Serif', + 'Inter' => 'Inter', + 'Irish Grover' => 'Irish Grover', + 'Istok Web' => 'Istok Web', + 'Italiana' => 'Italiana', + 'Italianno' => 'Italianno', + 'Itim' => 'Itim', + 'Jacques Francois' => 'Jacques Francois', + 'Jacques Francois Shadow' => 'Jacques Francois Shadow', + 'Jaldi' => 'Jaldi', + 'Jim Nightshade' => 'Jim Nightshade', + 'Jockey One' => 'Jockey One', + 'Jolly Lodger' => 'Jolly Lodger', + 'Jomhuria' => 'Jomhuria', + 'Jomolhari' => 'Jomolhari', + 'Josefin Sans' => 'Josefin Sans', + 'Josefin Slab' => 'Josefin Slab', + 'Jost' => 'Jost', + 'Joti One' => 'Joti One', + 'Jua' => 'Jua', + 'Judson' => 'Judson', + 'Julee' => 'Julee', + 'Julius Sans One' => 'Julius Sans One', + 'Junge' => 'Junge', + 'Jura' => 'Jura', + 'Just Another Hand' => 'Just Another Hand', + 'Just Me Again Down Here' => 'Just Me Again Down Here', + 'K2D' => 'K2D', + 'Kadwa' => 'Kadwa', + 'Kalam' => 'Kalam', + 'Kameron' => 'Kameron', + 'Kanit' => 'Kanit', + 'Kantumruy' => 'Kantumruy', + 'Karla' => 'Karla', + 'Karma' => 'Karma', + 'Katibeh' => 'Katibeh', + 'Kaushan Script' => 'Kaushan Script', + 'Kavivanar' => 'Kavivanar', + 'Kavoon' => 'Kavoon', + 'Kdam Thmor' => 'Kdam Thmor', + 'Keania One' => 'Keania One', + 'Kelly Slab' => 'Kelly Slab', + 'Kenia' => 'Kenia', + 'Khand' => 'Khand', + 'Khmer' => 'Khmer', + 'Khula' => 'Khula', + 'Kirang Haerang' => 'Kirang Haerang', + 'Kite One' => 'Kite One', + 'Knewave' => 'Knewave', + 'KoHo' => 'KoHo', + 'Kodchasan' => 'Kodchasan', + 'Kosugi' => 'Kosugi', + 'Kosugi Maru' => 'Kosugi Maru', + 'Kotta One' => 'Kotta One', + 'Koulen' => 'Koulen', + 'Kranky' => 'Kranky', + 'Kreon' => 'Kreon', + 'Kristi' => 'Kristi', + 'Krona One' => 'Krona One', + 'Krub' => 'Krub', + 'Kulim Park' => 'Kulim Park', + 'Kumar One' => 'Kumar One', + 'Kumar One Outline' => 'Kumar One Outline', + 'Kurale' => 'Kurale', + 'La Belle Aurore' => 'La Belle Aurore', + 'Lacquer' => 'Lacquer', + 'Laila' => 'Laila', + 'Lakki Reddy' => 'Lakki Reddy', + 'Lalezar' => 'Lalezar', + 'Lancelot' => 'Lancelot', + 'Lateef' => 'Lateef', + 'Lato' => 'Lato', + 'League Script' => 'League Script', + 'Leckerli One' => 'Leckerli One', + 'Ledger' => 'Ledger', + 'Lekton' => 'Lekton', + 'Lemon' => 'Lemon', + 'Lemonada' => 'Lemonada', + 'Lexend Deca' => 'Lexend Deca', + 'Lexend Exa' => 'Lexend Exa', + 'Lexend Giga' => 'Lexend Giga', + 'Lexend Mega' => 'Lexend Mega', + 'Lexend Peta' => 'Lexend Peta', + 'Lexend Tera' => 'Lexend Tera', + 'Lexend Zetta' => 'Lexend Zetta', + 'Libre Barcode 128' => 'Libre Barcode 128', + 'Libre Barcode 128 Text' => 'Libre Barcode 128 Text', + 'Libre Barcode 39' => 'Libre Barcode 39', + 'Libre Barcode 39 Extended' => 'Libre Barcode 39 Extended', + 'Libre Barcode 39 Extended Text' => 'Libre Barcode 39 Extended Text', + 'Libre Barcode 39 Text' => 'Libre Barcode 39 Text', + 'Libre Baskerville' => 'Libre Baskerville', + 'Libre Caslon Display' => 'Libre Caslon Display', + 'Libre Caslon Text' => 'Libre Caslon Text', + 'Libre Franklin' => 'Libre Franklin', + 'Life Savers' => 'Life Savers', + 'Lilita One' => 'Lilita One', + 'Lily Script One' => 'Lily Script One', + 'Limelight' => 'Limelight', + 'Linden Hill' => 'Linden Hill', + 'Literata' => 'Literata', + 'Liu Jian Mao Cao' => 'Liu Jian Mao Cao', + 'Livvic' => 'Livvic', + 'Lobster' => 'Lobster', + 'Lobster Two' => 'Lobster Two', + 'Londrina Outline' => 'Londrina Outline', + 'Londrina Shadow' => 'Londrina Shadow', + 'Londrina Sketch' => 'Londrina Sketch', + 'Londrina Solid' => 'Londrina Solid', + 'Long Cang' => 'Long Cang', + 'Lora' => 'Lora', + 'Love Ya Like A Sister' => 'Love Ya Like A Sister', + 'Loved by the King' => 'Loved by the King', + 'Lovers Quarrel' => 'Lovers Quarrel', + 'Luckiest Guy' => 'Luckiest Guy', + 'Lusitana' => 'Lusitana', + 'Lustria' => 'Lustria', + 'M PLUS 1p' => 'M PLUS 1p', + 'M PLUS Rounded 1c' => 'M PLUS Rounded 1c', + 'Ma Shan Zheng' => 'Ma Shan Zheng', + 'Macondo' => 'Macondo', + 'Macondo Swash Caps' => 'Macondo Swash Caps', + 'Mada' => 'Mada', + 'Magra' => 'Magra', + 'Maiden Orange' => 'Maiden Orange', + 'Maitree' => 'Maitree', + 'Major Mono Display' => 'Major Mono Display', + 'Mako' => 'Mako', + 'Mali' => 'Mali', + 'Mallanna' => 'Mallanna', + 'Mandali' => 'Mandali', + 'Manjari' => 'Manjari', + 'Manrope' => 'Manrope', + 'Mansalva' => 'Mansalva', + 'Manuale' => 'Manuale', + 'Marcellus' => 'Marcellus', + 'Marcellus SC' => 'Marcellus SC', + 'Marck Script' => 'Marck Script', + 'Margarine' => 'Margarine', + 'Markazi Text' => 'Markazi Text', + 'Marko One' => 'Marko One', + 'Marmelad' => 'Marmelad', + 'Martel' => 'Martel', + 'Martel Sans' => 'Martel Sans', + 'Marvel' => 'Marvel', + 'Mate' => 'Mate', + 'Mate SC' => 'Mate SC', + 'Maven Pro' => 'Maven Pro', + 'McLaren' => 'McLaren', + 'Meddon' => 'Meddon', + 'MedievalSharp' => 'MedievalSharp', + 'Medula One' => 'Medula One', + 'Meera Inimai' => 'Meera Inimai', + 'Megrim' => 'Megrim', + 'Meie Script' => 'Meie Script', + 'Merienda' => 'Merienda', + 'Merienda One' => 'Merienda One', + 'Merriweather' => 'Merriweather', + 'Merriweather Sans' => 'Merriweather Sans', + 'Metal' => 'Metal', + 'Metal Mania' => 'Metal Mania', + 'Metamorphous' => 'Metamorphous', + 'Metrophobic' => 'Metrophobic', + 'Michroma' => 'Michroma', + 'Milonga' => 'Milonga', + 'Miltonian' => 'Miltonian', + 'Miltonian Tattoo' => 'Miltonian Tattoo', + 'Mina' => 'Mina', + 'Miniver' => 'Miniver', + 'Miriam Libre' => 'Miriam Libre', + 'Mirza' => 'Mirza', + 'Miss Fajardose' => 'Miss Fajardose', + 'Mitr' => 'Mitr', + 'Modak' => 'Modak', + 'Modern Antiqua' => 'Modern Antiqua', + 'Mogra' => 'Mogra', + 'Molengo' => 'Molengo', + 'Molle' => 'Molle', + 'Monda' => 'Monda', + 'Monofett' => 'Monofett', + 'Monoton' => 'Monoton', + 'Monsieur La Doulaise' => 'Monsieur La Doulaise', + 'Montaga' => 'Montaga', + 'Montez' => 'Montez', + 'Montserrat' => 'Montserrat', + 'Montserrat Alternates' => 'Montserrat Alternates', + 'Montserrat Subrayada' => 'Montserrat Subrayada', + 'Moul' => 'Moul', + 'Moulpali' => 'Moulpali', + 'Mountains of Christmas' => 'Mountains of Christmas', + 'Mouse Memoirs' => 'Mouse Memoirs', + 'Mr Bedfort' => 'Mr Bedfort', + 'Mr Dafoe' => 'Mr Dafoe', + 'Mr De Haviland' => 'Mr De Haviland', + 'Mrs Saint Delafield' => 'Mrs Saint Delafield', + 'Mrs Sheppards' => 'Mrs Sheppards', + 'Mukta' => 'Mukta', + 'Mukta Mahee' => 'Mukta Mahee', + 'Mukta Malar' => 'Mukta Malar', + 'Mukta Vaani' => 'Mukta Vaani', + 'Mulish' => 'Mulish', + 'MuseoModerno' => 'MuseoModerno', + 'Mystery Quest' => 'Mystery Quest', + 'NTR' => 'NTR', + 'Nanum Brush Script' => 'Nanum Brush Script', + 'Nanum Gothic' => 'Nanum Gothic', + 'Nanum Gothic Coding' => 'Nanum Gothic Coding', + 'Nanum Myeongjo' => 'Nanum Myeongjo', + 'Nanum Pen Script' => 'Nanum Pen Script', + 'Neucha' => 'Neucha', + 'Neuton' => 'Neuton', + 'New Rocker' => 'New Rocker', + 'News Cycle' => 'News Cycle', + 'Niconne' => 'Niconne', + 'Niramit' => 'Niramit', + 'Nixie One' => 'Nixie One', + 'Nobile' => 'Nobile', + 'Nokora' => 'Nokora', + 'Norican' => 'Norican', + 'Nosifer' => 'Nosifer', + 'Notable' => 'Notable', + 'Nothing You Could Do' => 'Nothing You Could Do', + 'Noticia Text' => 'Noticia Text', + 'Noto Sans' => 'Noto Sans', + 'Noto Sans HK' => 'Noto Sans HK', + 'Noto Sans JP' => 'Noto Sans JP', + 'Noto Sans KR' => 'Noto Sans KR', + 'Noto Sans SC' => 'Noto Sans SC', + 'Noto Sans TC' => 'Noto Sans TC', + 'Noto Serif' => 'Noto Serif', + 'Noto Serif JP' => 'Noto Serif JP', + 'Noto Serif KR' => 'Noto Serif KR', + 'Noto Serif SC' => 'Noto Serif SC', + 'Noto Serif TC' => 'Noto Serif TC', + 'Nova Cut' => 'Nova Cut', + 'Nova Flat' => 'Nova Flat', + 'Nova Mono' => 'Nova Mono', + 'Nova Oval' => 'Nova Oval', + 'Nova Round' => 'Nova Round', + 'Nova Script' => 'Nova Script', + 'Nova Slim' => 'Nova Slim', + 'Nova Square' => 'Nova Square', + 'Numans' => 'Numans', + 'Nunito' => 'Nunito', + 'Nunito Sans' => 'Nunito Sans', + 'Odibee Sans' => 'Odibee Sans', + 'Odor Mean Chey' => 'Odor Mean Chey', + 'Offside' => 'Offside', + 'Old Standard TT' => 'Old Standard TT', + 'Oldenburg' => 'Oldenburg', + 'Oleo Script' => 'Oleo Script', + 'Oleo Script Swash Caps' => 'Oleo Script Swash Caps', + 'Open Sans' => 'Open Sans', + 'Open Sans Condensed' => 'Open Sans Condensed', + 'Oranienbaum' => 'Oranienbaum', + 'Orbitron' => 'Orbitron', + 'Oregano' => 'Oregano', + 'Orienta' => 'Orienta', + 'Original Surfer' => 'Original Surfer', + 'Oswald' => 'Oswald', + 'Over the Rainbow' => 'Over the Rainbow', + 'Overlock' => 'Overlock', + 'Overlock SC' => 'Overlock SC', + 'Overpass' => 'Overpass', + 'Overpass Mono' => 'Overpass Mono', + 'Ovo' => 'Ovo', + 'Oxanium' => 'Oxanium', + 'Oxygen' => 'Oxygen', + 'Oxygen Mono' => 'Oxygen Mono', + 'PT Mono' => 'PT Mono', + 'PT Sans' => 'PT Sans', + 'PT Sans Caption' => 'PT Sans Caption', + 'PT Sans Narrow' => 'PT Sans Narrow', + 'PT Serif' => 'PT Serif', + 'PT Serif Caption' => 'PT Serif Caption', + 'Pacifico' => 'Pacifico', + 'Padauk' => 'Padauk', + 'Palanquin' => 'Palanquin', + 'Palanquin Dark' => 'Palanquin Dark', + 'Pangolin' => 'Pangolin', + 'Paprika' => 'Paprika', + 'Parisienne' => 'Parisienne', + 'Passero One' => 'Passero One', + 'Passion One' => 'Passion One', + 'Pathway Gothic One' => 'Pathway Gothic One', + 'Patrick Hand' => 'Patrick Hand', + 'Patrick Hand SC' => 'Patrick Hand SC', + 'Pattaya' => 'Pattaya', + 'Patua One' => 'Patua One', + 'Pavanam' => 'Pavanam', + 'Paytone One' => 'Paytone One', + 'Peddana' => 'Peddana', + 'Peralta' => 'Peralta', + 'Permanent Marker' => 'Permanent Marker', + 'Petit Formal Script' => 'Petit Formal Script', + 'Petrona' => 'Petrona', + 'Philosopher' => 'Philosopher', + 'Piedra' => 'Piedra', + 'Pinyon Script' => 'Pinyon Script', + 'Pirata One' => 'Pirata One', + 'Plaster' => 'Plaster', + 'Play' => 'Play', + 'Playball' => 'Playball', + 'Playfair Display' => 'Playfair Display', + 'Playfair Display SC' => 'Playfair Display SC', + 'Podkova' => 'Podkova', + 'Poiret One' => 'Poiret One', + 'Poller One' => 'Poller One', + 'Poly' => 'Poly', + 'Pompiere' => 'Pompiere', + 'Pontano Sans' => 'Pontano Sans', + 'Poor Story' => 'Poor Story', + 'Poppins' => 'Poppins', + 'Port Lligat Sans' => 'Port Lligat Sans', + 'Port Lligat Slab' => 'Port Lligat Slab', + 'Pragati Narrow' => 'Pragati Narrow', + 'Prata' => 'Prata', + 'Preahvihear' => 'Preahvihear', + 'Press Start 2P' => 'Press Start 2P', + 'Pridi' => 'Pridi', + 'Princess Sofia' => 'Princess Sofia', + 'Prociono' => 'Prociono', + 'Prompt' => 'Prompt', + 'Prosto One' => 'Prosto One', + 'Proza Libre' => 'Proza Libre', + 'Public Sans' => 'Public Sans', + 'Puritan' => 'Puritan', + 'Purple Purse' => 'Purple Purse', + 'Quando' => 'Quando', + 'Quantico' => 'Quantico', + 'Quattrocento' => 'Quattrocento', + 'Quattrocento Sans' => 'Quattrocento Sans', + 'Questrial' => 'Questrial', + 'Quicksand' => 'Quicksand', + 'Quintessential' => 'Quintessential', + 'Qwigley' => 'Qwigley', + 'Racing Sans One' => 'Racing Sans One', + 'Radley' => 'Radley', + 'Rajdhani' => 'Rajdhani', + 'Rakkas' => 'Rakkas', + 'Raleway' => 'Raleway', + 'Raleway Dots' => 'Raleway Dots', + 'Ramabhadra' => 'Ramabhadra', + 'Ramaraja' => 'Ramaraja', + 'Rambla' => 'Rambla', + 'Rammetto One' => 'Rammetto One', + 'Ranchers' => 'Ranchers', + 'Rancho' => 'Rancho', + 'Ranga' => 'Ranga', + 'Rasa' => 'Rasa', + 'Rationale' => 'Rationale', + 'Ravi Prakash' => 'Ravi Prakash', + 'Recursive' => 'Recursive', + 'Red Hat Display' => 'Red Hat Display', + 'Red Hat Text' => 'Red Hat Text', + 'Red Rose' => 'Red Rose', + 'Redressed' => 'Redressed', + 'Reem Kufi' => 'Reem Kufi', + 'Reenie Beanie' => 'Reenie Beanie', + 'Revalia' => 'Revalia', + 'Rhodium Libre' => 'Rhodium Libre', + 'Ribeye' => 'Ribeye', + 'Ribeye Marrow' => 'Ribeye Marrow', + 'Righteous' => 'Righteous', + 'Risque' => 'Risque', + 'Roboto' => 'Roboto', + 'Roboto Condensed' => 'Roboto Condensed', + 'Roboto Mono' => 'Roboto Mono', + 'Roboto Slab' => 'Roboto Slab', + 'Rochester' => 'Rochester', + 'Rock Salt' => 'Rock Salt', + 'Rokkitt' => 'Rokkitt', + 'Romanesco' => 'Romanesco', + 'Ropa Sans' => 'Ropa Sans', + 'Rosario' => 'Rosario', + 'Rosarivo' => 'Rosarivo', + 'Rouge Script' => 'Rouge Script', + 'Rowdies' => 'Rowdies', + 'Rozha One' => 'Rozha One', + 'Rubik' => 'Rubik', + 'Rubik Mono One' => 'Rubik Mono One', + 'Ruda' => 'Ruda', + 'Rufina' => 'Rufina', + 'Ruge Boogie' => 'Ruge Boogie', + 'Ruluko' => 'Ruluko', + 'Rum Raisin' => 'Rum Raisin', + 'Ruslan Display' => 'Ruslan Display', + 'Russo One' => 'Russo One', + 'Ruthie' => 'Ruthie', + 'Rye' => 'Rye', + 'Sacramento' => 'Sacramento', + 'Sahitya' => 'Sahitya', + 'Sail' => 'Sail', + 'Saira' => 'Saira', + 'Saira Condensed' => 'Saira Condensed', + 'Saira Extra Condensed' => 'Saira Extra Condensed', + 'Saira Semi Condensed' => 'Saira Semi Condensed', + 'Saira Stencil One' => 'Saira Stencil One', + 'Salsa' => 'Salsa', + 'Sanchez' => 'Sanchez', + 'Sancreek' => 'Sancreek', + 'Sansita' => 'Sansita', + 'Sarabun' => 'Sarabun', + 'Sarala' => 'Sarala', + 'Sarina' => 'Sarina', + 'Sarpanch' => 'Sarpanch', + 'Satisfy' => 'Satisfy', + 'Sawarabi Gothic' => 'Sawarabi Gothic', + 'Sawarabi Mincho' => 'Sawarabi Mincho', + 'Scada' => 'Scada', + 'Scheherazade' => 'Scheherazade', + 'Schoolbell' => 'Schoolbell', + 'Scope One' => 'Scope One', + 'Seaweed Script' => 'Seaweed Script', + 'Secular One' => 'Secular One', + 'Sedgwick Ave' => 'Sedgwick Ave', + 'Sedgwick Ave Display' => 'Sedgwick Ave Display', + 'Sen' => 'Sen', + 'Sevillana' => 'Sevillana', + 'Seymour One' => 'Seymour One', + 'Shadows Into Light' => 'Shadows Into Light', + 'Shadows Into Light Two' => 'Shadows Into Light Two', + 'Shanti' => 'Shanti', + 'Share' => 'Share', + 'Share Tech' => 'Share Tech', + 'Share Tech Mono' => 'Share Tech Mono', + 'Shojumaru' => 'Shojumaru', + 'Short Stack' => 'Short Stack', + 'Shrikhand' => 'Shrikhand', + 'Siemreap' => 'Siemreap', + 'Sigmar One' => 'Sigmar One', + 'Signika' => 'Signika', + 'Signika Negative' => 'Signika Negative', + 'Simonetta' => 'Simonetta', + 'Single Day' => 'Single Day', + 'Sintony' => 'Sintony', + 'Sirin Stencil' => 'Sirin Stencil', + 'Six Caps' => 'Six Caps', + 'Skranji' => 'Skranji', + 'Slabo 13px' => 'Slabo 13px', + 'Slabo 27px' => 'Slabo 27px', + 'Slackey' => 'Slackey', + 'Smokum' => 'Smokum', + 'Smythe' => 'Smythe', + 'Sniglet' => 'Sniglet', + 'Snippet' => 'Snippet', + 'Snowburst One' => 'Snowburst One', + 'Sofadi One' => 'Sofadi One', + 'Sofia' => 'Sofia', + 'Solway' => 'Solway', + 'Song Myung' => 'Song Myung', + 'Sonsie One' => 'Sonsie One', + 'Sora' => 'Sora', + 'Sorts Mill Goudy' => 'Sorts Mill Goudy', + 'Source Code Pro' => 'Source Code Pro', + 'Source Sans Pro' => 'Source Sans Pro', + 'Source Serif Pro' => 'Source Serif Pro', + 'Space Mono' => 'Space Mono', + 'Spartan' => 'Spartan', + 'Special Elite' => 'Special Elite', + 'Spectral' => 'Spectral', + 'Spectral SC' => 'Spectral SC', + 'Spicy Rice' => 'Spicy Rice', + 'Spinnaker' => 'Spinnaker', + 'Spirax' => 'Spirax', + 'Squada One' => 'Squada One', + 'Sree Krushnadevaraya' => 'Sree Krushnadevaraya', + 'Sriracha' => 'Sriracha', + 'Srisakdi' => 'Srisakdi', + 'Staatliches' => 'Staatliches', + 'Stalemate' => 'Stalemate', + 'Stalinist One' => 'Stalinist One', + 'Stardos Stencil' => 'Stardos Stencil', + 'Stint Ultra Condensed' => 'Stint Ultra Condensed', + 'Stint Ultra Expanded' => 'Stint Ultra Expanded', + 'Stoke' => 'Stoke', + 'Strait' => 'Strait', + 'Stylish' => 'Stylish', + 'Sue Ellen Francisco' => 'Sue Ellen Francisco', + 'Suez One' => 'Suez One', + 'Sulphur Point' => 'Sulphur Point', + 'Sumana' => 'Sumana', + 'Sunflower' => 'Sunflower', + 'Sunshiney' => 'Sunshiney', + 'Supermercado One' => 'Supermercado One', + 'Sura' => 'Sura', + 'Suranna' => 'Suranna', + 'Suravaram' => 'Suravaram', + 'Suwannaphum' => 'Suwannaphum', + 'Swanky and Moo Moo' => 'Swanky and Moo Moo', + 'Syncopate' => 'Syncopate', + 'Tajawal' => 'Tajawal', + 'Tangerine' => 'Tangerine', + 'Taprom' => 'Taprom', + 'Tauri' => 'Tauri', + 'Taviraj' => 'Taviraj', + 'Teko' => 'Teko', + 'Telex' => 'Telex', + 'Tenali Ramakrishna' => 'Tenali Ramakrishna', + 'Tenor Sans' => 'Tenor Sans', + 'Text Me One' => 'Text Me One', + 'Thasadith' => 'Thasadith', + 'The Girl Next Door' => 'The Girl Next Door', + 'Tienne' => 'Tienne', + 'Tillana' => 'Tillana', + 'Timmana' => 'Timmana', + 'Tinos' => 'Tinos', + 'Titan One' => 'Titan One', + 'Titillium Web' => 'Titillium Web', + 'Tomorrow' => 'Tomorrow', + 'Trade Winds' => 'Trade Winds', + 'Trirong' => 'Trirong', + 'Trocchi' => 'Trocchi', + 'Trochut' => 'Trochut', + 'Trykker' => 'Trykker', + 'Tulpen One' => 'Tulpen One', + 'Turret Road' => 'Turret Road', + 'Ubuntu' => 'Ubuntu', + 'Ubuntu Condensed' => 'Ubuntu Condensed', + 'Ubuntu Mono' => 'Ubuntu Mono', + 'Ultra' => 'Ultra', + 'Uncial Antiqua' => 'Uncial Antiqua', + 'Underdog' => 'Underdog', + 'Unica One' => 'Unica One', + 'UnifrakturCook' => 'UnifrakturCook', + 'UnifrakturMaguntia' => 'UnifrakturMaguntia', + 'Unkempt' => 'Unkempt', + 'Unlock' => 'Unlock', + 'Unna' => 'Unna', + 'VT323' => 'VT323', + 'Vampiro One' => 'Vampiro One', + 'Varela' => 'Varela', + 'Varela Round' => 'Varela Round', + 'Varta' => 'Varta', + 'Vast Shadow' => 'Vast Shadow', + 'Vesper Libre' => 'Vesper Libre', + 'Viaoda Libre' => 'Viaoda Libre', + 'Vibes' => 'Vibes', + 'Vibur' => 'Vibur', + 'Vidaloka' => 'Vidaloka', + 'Viga' => 'Viga', + 'Voces' => 'Voces', + 'Volkhov' => 'Volkhov', + 'Vollkorn' => 'Vollkorn', + 'Vollkorn SC' => 'Vollkorn SC', + 'Voltaire' => 'Voltaire', + 'Waiting for the Sunrise' => 'Waiting for the Sunrise', + 'Wallpoet' => 'Wallpoet', + 'Walter Turncoat' => 'Walter Turncoat', + 'Warnes' => 'Warnes', + 'Wellfleet' => 'Wellfleet', + 'Wendy One' => 'Wendy One', + 'Wire One' => 'Wire One', + 'Work Sans' => 'Work Sans', + 'Yanone Kaffeesatz' => 'Yanone Kaffeesatz', + 'Yantramanav' => 'Yantramanav', + 'Yatra One' => 'Yatra One', + 'Yellowtail' => 'Yellowtail', + 'Yeon Sung' => 'Yeon Sung', + 'Yeseva One' => 'Yeseva One', + 'Yesteryear' => 'Yesteryear', + 'Yrsa' => 'Yrsa', + 'ZCOOL KuaiLe' => 'ZCOOL KuaiLe', + 'ZCOOL QingKe HuangYou' => 'ZCOOL QingKe HuangYou', + 'ZCOOL XiaoWei' => 'ZCOOL XiaoWei', + 'Zeyada' => 'Zeyada', + 'Zhi Mang Xing' => 'Zhi Mang Xing', + 'Zilla Slab' => 'Zilla Slab', + 'Zilla Slab Highlight' => 'Zilla Slab Highlight' + ); + + // Pixels. + $from0to6px = array(); + for ($i = 0; $i < 7; $i++) { + $from0to6px[$i . 'px'] = $i . 'px'; + } + + $from0to8px = array(); + for ($i = 0; $i < 9; $i++) { + $from0to8px[$i . 'px'] = $i . 'px'; + } + + $from0to12px = array(); + for ($i = 0; $i < 13; $i++) { + $from0to12px[$i . 'px'] = $i . 'px'; + } + + $from10to16px = array(); + for ($i = 10; $i < 17; $i++) { + $from10to16px[$i . 'px'] = $i . 'px'; + } + + $from0to20px = array(); + for ($i = 0; $i < 21; $i++) { + $from0to20px[$i . 'px'] = $i . 'px'; + } + + $from10to20px = array(); + for ($i = 10; $i < 21; $i++) { + $from10to20px[$i . 'px'] = $i . 'px'; + } + + $from10to30px = array(); + for ($i = 10; $i < 31; $i++) { + $from10to30px[$i . 'px'] = $i . 'px'; + } + + $from10to30pxnovalueunit = array(); + for ($i = 10; $i < 31; $i++) { + $from10to30pxnovalueunit[$i] = $i . 'px'; + } + + $from0to30px = array(); + for ($i = 0; $i < 31; $i++) { + $from0to30px[$i . 'px'] = $i . 'px'; + } + + $from0to50px = array(); + for ($i = 0; $i < 51; $i++) { + $from0to50px[$i . 'px'] = $i . 'px'; + } + + $from0to100px = array(); + for ($i = 0; $i < 101; $i++) { + $from0to100px[$i . 'px'] = $i . 'px'; + } + + $from14to46px = array(); + for ($i = 14; $i < 47; $i++) { + $from14to46px[$i . 'px'] = $i . 'px'; + } + + $standardfontsize = array( + '8px' => '8px', + '9px' => '9px', + '10px' => '10px', + '11px' => '11px', + '12px' => '12px', + '13px' => '13px', + '14px' => '14px', + '15px' => '15px', + '16px' => '16px', + '18px' => '18px', + '20px' => '20px', + '22px' => '22px', + '24px' => '24px', + '26px' => '26px', + '28px' => '28px', + '32px' => '32px', + '36px' => '36px', + '40px' => '40px', + '44px' => '44px', + '48px' => '48px', + '54px' => '54px', + '60px' => '60px', + '66px' => '66px', + '72px' => '72px', + '80px' => '80px', + '88px' => '88px', + '96px' => '96px' + ); + + $screensizeblock = array( + 'd-block' => get_string('bs4all', 'theme_adaptable'), + 'd-none d-sm-block' => get_string('bs4small', 'theme_adaptable'), + 'd-none d-md-block' => get_string('bs4medium', 'theme_adaptable'), + 'd-none d-lg-block' => get_string('bs4large', 'theme_adaptable'), + 'd-none d-xl-block' => get_string('bs4extralarge', 'theme_adaptable'), + 'd-none' => get_string('bs4none', 'theme_adaptable') + ); + + $screensizeflex = array( + 'd-flex' => get_string('bs4all', 'theme_adaptable'), + 'd-none d-sm-flex' => get_string('bs4small', 'theme_adaptable'), + 'd-none d-md-flex' => get_string('bs4medium', 'theme_adaptable'), + 'd-none d-lg-flex' => get_string('bs4large', 'theme_adaptable'), + 'd-none d-xl-flex' => get_string('bs4extralarge', 'theme_adaptable'), + 'd-none' => get_string('bs4none', 'theme_adaptable') + ); + + // Numbers. + $from20to40 = array(); + for ($i = 20; $i < 41; $i++) { + $from20to40[$i] = $i; + } + + $from0to60inc5 = array(); + for ($i = 0; $i < 61; $i += 5) { + $from0to60inc5[$i] = $i; + } + + $choices0to12 = array(); + for ($i = 0; $i < 13; $i++) { + $choices0to12[$i] = $i; + } + + $from100to900 = array(); + for ($i = 100; $i < 901; $i += 100) { + $from100to900[$i] = $i; + } + + // Percentages. + $from0to2point5percent = array(); + for ($i = 0; $i < 2.6; $i += 0.1) { + $from0to2point5percent[$i . '%'] = $i . '%'; + } + + $from95to100percent = array( + '95%' => '95%', + '96%' => '96%', + '97%' => '97%', + '98%' => '98%', + '99%' => '99%', + '100%' => '100%', + ); + + $from35to80percent = array(); + for ($i = 35; $i < 81; $i++) { + $from35to80percent[$i . '%'] = $i . '%'; + } + + $from35to100percent = array(); + for ($i = 35; $i < 101; $i++) { + $from35to100percent[$i . '%'] = $i . '%'; + } + + $from85to110percent = array(); + for ($i = 85; $i < 111; $i++) { + $from85to110percent[$i . '%'] = $i . '%'; + } + + // Seconds. + $from0to1second = array(); + $floatcount = 0.0; + for ($i = 0; $i <= 10; $i++) { + $from0to1second[floatval($floatcount) . 's'] = floatval($floatcount) . 's'; + $floatcount += 0.1; + } + + // Texts. + $borderstyles = array( + 'none' => 'none', + 'solid' => 'solid', + 'dashed' => 'dashed', + 'dotted' => 'dotted', + 'double' => 'double' + ); + + $htmltarget = array( + '_blank' => get_string('targetnewwindow', 'theme_adaptable'), + '_self' => get_string('targetsamewindow', 'theme_adaptable') + ); + + $marketblockstyles = array( + '' => get_string('nostyle', 'theme_adaptable'), + 'internalmarket' => get_string('bcustyle', 'theme_adaptable'), + 'covtiles' => get_string('coventrystyle', 'theme_adaptable') + ); + + $sliderstyles = array( + 'slider1' => get_string('sliderstyle1', 'theme_adaptable'), + 'slider2' => get_string('sliderstyle2', 'theme_adaptable') + ); + + $bootstrap12 = array( + '0-0-0-0' => get_string('disabled', 'theme_adaptable'), + '12-0-0-0' => '1', + '6-6-0-0' => '6 + 6', + '4-4-4-0' => '4 + 4 + 4', + '3-3-3-3' => '3 + 3 + 3 + 3', + '6-3-3-0' => '6 + 3 + 3', + '3-3-6-0' => '3 + 3 + 6', + '3-6-3-0' => '3 + 6 + 3', + '4-8-0-0' => '4 + 8', + '8-4-0-0' => '8 + 4', + '3-9-0-0' => '3 + 9', + '9-3-0-0' => '9 + 3', + '5-7-0-0' => '5 + 7', + '7-5-0-0' => '7 + 5', + ); + + $bootstrap12defaults = array('3-3-3-3', '4-4-4-0', '3-3-3-3', '0-0-0-0', '0-0-0-0', + '0-0-0-0', '0-0-0-0', '0-0-0-0', '0-0-0-0', '0-0-0-0'); + + $marketingfooterbuilderdefaults = array('3-3-3-3', '0-0-0-0', '0-0-0-0', '0-0-0-0', '0-0-0-0', + '0-0-0-0', '0-0-0-0', '0-0-0-0', '0-0-0-0', '0-0-0-0'); + + // Adaptable Tabbed Layout changes. 0 signifies the course content or dashboard main content. + $courselabel = get_string('tabbedlayouttablabelcourse', 'theme_adaptable'); + $tab1label = get_string('tabbedlayouttablabelcourse1', 'theme_adaptable'); + $tab2label = get_string('tabbedlayouttablabelcourse2', 'theme_adaptable'); + $tabbedlayoutdefaultscourse = array( + '0' => get_string('disabled', 'theme_adaptable'), + '0-1' => $courselabel . ' + ' . $tab1label, + '1-0' => $tab1label . ' + ' . $courselabel, + '0-1-2' => $courselabel . ' + ' . $tab1label . ' + ' . $tab2label, + '1-0-2' => $tab1label . ' + ' . $courselabel . ' + ' . $tab2label, + '1-2-0' => $tab1label . ' + ' . $tab2label . ' + ' . $courselabel, + '0-2-1' => $courselabel . ' + ' . $tab2label . ' + ' . $tab1label, + ); + + $dashboardlabel = get_string('tabbedlayouttablabeldashboard', 'theme_adaptable'); + $tab1label = get_string('tabbedlayouttablabeldashboard1', 'theme_adaptable'); + $tab2label = get_string('tabbedlayouttablabeldashboard2', 'theme_adaptable'); + $tabbedlayoutdefaultsdashboard = array( + '0' => get_string('disabled', 'theme_adaptable'), + '0-1' => $dashboardlabel . ' + ' . $tab1label, + '1-0' => $tab1label . ' + ' . $dashboardlabel, + '0-1-2' => $dashboardlabel . ' + ' . $tab1label . ' + ' . $tab2label, + '1-0-2' => $tab1label . ' + ' . $dashboardlabel . ' + ' . $tab2label, + '1-2-0' => $tab1label . ' + ' . $tab2label . ' + ' . $dashboardlabel, + '0-2-1' => $dashboardlabel . ' + ' . $tab2label . ' + ' . $tab1label, + ); + + $dashboardblockregionposition = array( + 'abovecontent' => get_string('dashblocksabovecontent', 'theme_adaptable'), + 'belowcontent' => get_string('dashblocksbelowcontent', 'theme_adaptable') + ); +} diff --git a/theme/adaptable/settings/block_regions.php b/theme/adaptable/settings/block_regions.php new file mode 100644 index 0000000..6c0d595 --- /dev/null +++ b/theme/adaptable/settings/block_regions.php @@ -0,0 +1,64 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Frontpage Block Regions Section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_frontpage_block_regions', + get_string('frontpageblockregionsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_marketing', get_string('blocklayoutbuilder', 'theme_adaptable'), + format_text(get_string('blocklayoutbuilderdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/frontpageblocksenabled'; + $title = get_string('frontpageblocksenabled', 'theme_adaptable'); + $description = get_string('frontpageblocksenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + // Block region builder. + $noregions = 20; // Number of block regions defined in config.php. + list('imgblder' => $imgblder, 'totalblocks' => $totalblocks) = \theme_adaptable\toolbox::admin_settings_layout_builder( + $page, 'blocklayoutlayoutrow', $bootstrap12defaults, $bootstrap12); + + $page->add(new admin_setting_heading('theme_adaptable_blocklayoutcheck', get_string('layoutcheck', 'theme_adaptable'), + format_text(get_string('layoutcheckdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $checkcountcolor = '#00695C'; + if ($totalblocks > $noregions) { + $mktcountcolor = '#D7542A'; + } + $mktcountmsg = '<span style="color: ' . $checkcountcolor . '">'; + $mktcountmsg .= get_string('layoutcount1', 'theme_adaptable') . '<strong>' . $noregions . '</strong>'; + $mktcountmsg .= get_string('layoutcount2', 'theme_adaptable') . '<strong>' . $totalblocks . '/' . $noregions . '</strong>.'; + + $page->add(new admin_setting_heading('theme_adaptable_layoutblockscount', '', $mktcountmsg)); + + $page->add(new admin_setting_heading('theme_adaptable_layoutbuilder', '', $imgblder)); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/block_settings.php b/theme/adaptable/settings/block_settings.php new file mode 100644 index 0000000..0d720c9 --- /dev/null +++ b/theme/adaptable/settings/block_settings.php @@ -0,0 +1,260 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_blocks', get_string('blocksettings', 'theme_adaptable')); + + // General. + $name = 'theme_adaptable/settingsblocksgeneral'; + $heading = get_string('settingsblocksgeneral', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Show the navigation block on the course page. + $name = 'theme_adaptable/shownavigationblockoncoursepage'; + $title = get_string('shownavigationblockoncoursepage', 'theme_adaptable'); + $description = get_string('shownavigationblockoncoursepagedesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Colours. + $name = 'theme_adaptable/settingscolors'; + $heading = get_string('settingscolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/blockbackgroundcolor'; + $title = get_string('blockbackgroundcolor', 'theme_adaptable'); + $description = get_string('blockbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderbackgroundcolor'; + $title = get_string('blockheaderbackgroundcolor', 'theme_adaptable'); + $description = get_string('blockheaderbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockbordercolor'; + $title = get_string('blockbordercolor', 'theme_adaptable'); + $description = get_string('blockbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#59585D', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockregionbackgroundcolor'; + $title = get_string('blockregionbackground', 'theme_adaptable'); + $description = get_string('blockregionbackgrounddesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, 'transparent', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Borders. + $name = 'theme_adaptable/settingsborders'; + $heading = get_string('settingsborders', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderbordertopstyle'; + $title = get_string('blockheaderbordertopstyle', 'theme_adaptable'); + $description = get_string('blockheaderbordertopstyledesc', 'theme_adaptable'); + $radchoices = $borderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'dashed', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheadertopradius'; + $title = get_string('blockheadertopradius', 'theme_adaptable'); + $description = get_string('blockheadertopradiusdesc', 'theme_adaptable'); + $radchoices = $from0to20px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderbottomradius'; + $title = get_string('blockheaderbottomradius', 'theme_adaptable'); + $description = get_string('blockheaderbottomradiusdesc', 'theme_adaptable'); + $radchoices = $from0to20px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderbordertop'; + $title = get_string('blockheaderbordertop', 'theme_adaptable'); + $description = get_string('blockheaderbordertopdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '1px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderborderleft'; + $title = get_string('blockheaderborderleft', 'theme_adaptable'); + $description = get_string('blockheaderborderleftdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderborderright'; + $title = get_string('blockheaderborderright', 'theme_adaptable'); + $description = get_string('blockheaderborderrightdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockheaderborderbottom'; + $title = get_string('blockheaderborderbottom', 'theme_adaptable'); + $description = get_string('blockheaderborderbottomdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmainbordertopstyle'; + $title = get_string('blockmainbordertopstyle', 'theme_adaptable'); + $description = get_string('blockmainbordertopstyledesc', 'theme_adaptable'); + $radchoices = $borderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'none', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmaintopradius'; + $title = get_string('blockmaintopradius', 'theme_adaptable'); + $description = get_string('blockmaintopradiusdesc', 'theme_adaptable'); + $radchoices = $from0to20px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmainbottomradius'; + $title = get_string('blockmainbottomradius', 'theme_adaptable'); + $description = get_string('blockmainbottomradiusdesc', 'theme_adaptable'); + $radchoices = $from0to20px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmainbordertop'; + $title = get_string('blockmainbordertop', 'theme_adaptable'); + $description = get_string('blockmainbordertopdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmainborderleft'; + $title = get_string('blockmainborderleft', 'theme_adaptable'); + $description = get_string('blockmainborderleftdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmainborderright'; + $title = get_string('blockmainborderright', 'theme_adaptable'); + $description = get_string('blockmainborderrightdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/blockmainborderbottom'; + $title = get_string('blockmainborderbottom', 'theme_adaptable'); + $description = get_string('blockmainborderbottomdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Fonts heading. + $name = 'theme_adaptable/settingsfonts'; + $heading = get_string('settingsfonts', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Block Header Font size. + $name = 'theme_adaptable/fontblockheadersize'; + $title = get_string('fontblockheadersize', 'theme_adaptable'); + $description = get_string('fontblockheadersizedesc', 'theme_adaptable'); + $default = '22px'; + $choices = $standardfontsize; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Block Header Font weight. + $name = 'theme_adaptable/fontblockheaderweight'; + $title = get_string('fontblockheaderweight', 'theme_adaptable'); + $description = get_string('fontblockheaderweightdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 400, $from100to900); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Block Header Font color. + $name = 'theme_adaptable/fontblockheadercolor'; + $title = get_string('fontblockheadercolor', 'theme_adaptable'); + $description = get_string('fontblockheadercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3A454b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Icons heading. + $name = 'theme_adaptable/settingsblockicons'; + $heading = get_string('settingsblockicons', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Add icon to the title. + $name = 'theme_adaptable/blockicons'; + $title = get_string('blockicons', 'theme_adaptable'); + $description = get_string('blockiconsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Block Header Icon size. + $name = 'theme_adaptable/blockiconsheadersize'; + $title = get_string('blockiconsheadersize', 'theme_adaptable'); + $description = get_string('blockiconsheadersizedesc', 'theme_adaptable'); + $default = '20px'; + $choices = $standardfontsize; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/buttons.php b/theme/adaptable/settings/buttons.php new file mode 100644 index 0000000..8b9f6dd --- /dev/null +++ b/theme/adaptable/settings/buttons.php @@ -0,0 +1,238 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Buttons Section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_buttons', get_string('buttonsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_header', get_string('buttonsettingsheading', 'theme_adaptable'), + format_text(get_string('buttondesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/buttonradius'; + $title = get_string('buttonradius', 'theme_adaptable'); + $description = get_string('buttonradiusdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '5px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Buttons background color. + $name = 'theme_adaptable/buttoncolor'; + $title = get_string('buttoncolor', 'theme_adaptable'); + $description = get_string('buttoncolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#51666C', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Buttons background hover color. + $name = 'theme_adaptable/buttonhovercolor'; + $title = get_string('buttonhovercolor', 'theme_adaptable'); + $description = get_string('buttonhovercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#009688', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Buttons text color. + $name = 'theme_adaptable/buttontextcolor'; + $title = get_string('buttontextcolor', 'theme_adaptable'); + $description = get_string('buttontextcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Secondary Buttons background color. + $name = 'theme_adaptable/buttoncolorscnd'; + $title = get_string('buttoncolorscnd', 'theme_adaptable'); + $description = get_string('buttoncolordescscnd', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#51666C', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Secondary Buttons background hover color. + $name = 'theme_adaptable/buttonhovercolorscnd'; + $title = get_string('buttonhovercolorscnd', 'theme_adaptable'); + $description = get_string('buttonhovercolordescscnd', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#009688', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Secondary Buttons text color. + $name = 'theme_adaptable/buttontextcolorscnd'; + $title = get_string('buttontextcolorscnd', 'theme_adaptable'); + $description = get_string('buttontextcolordescscnd', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Cancel Buttons background color. + $name = 'theme_adaptable/buttoncolorcancel'; + $title = get_string('buttoncolorcancel', 'theme_adaptable'); + $description = get_string('buttoncolordesccancel', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ef5350', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Cancel Buttons background hover color. + $name = 'theme_adaptable/buttonhovercolorcancel'; + $title = get_string('buttonhovercolorcancel', 'theme_adaptable'); + $description = get_string('buttonhovercolordesccancel', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#e53935', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Cancel Buttons text color. + $name = 'theme_adaptable/buttontextcolorcancel'; + $title = get_string('buttontextcolorcancel', 'theme_adaptable'); + $description = get_string('buttontextcolordesccancel', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/editonbk'; + $title = get_string('editonbk', 'theme_adaptable'); + $description = get_string('editonbkdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#4caf50', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/editoffbk'; + $title = get_string('editoffbk', 'theme_adaptable'); + $description = get_string('editoffbkdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#f44336', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/editfont'; + $title = get_string('editfont', 'theme_adaptable'); + $description = get_string('editfontdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/editverticalpadding'; + $title = get_string('editverticalpadding', 'theme_adaptable'); + $description = get_string('editverticalpadding', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '4px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/edithorizontalpadding'; + $title = get_string('edithorizontalpadding', 'theme_adaptable'); + $description = get_string('edithorizontalpadding', 'theme_adaptable'); + $radchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '6px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/buttonlogincolor'; + $title = get_string('buttonlogincolor', 'theme_adaptable'); + $description = get_string('buttonlogincolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ef5350', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/buttonloginhovercolor'; + $title = get_string('buttonloginhovercolor', 'theme_adaptable'); + $description = get_string('buttonloginhovercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#e53935', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/buttonlogintextcolor'; + $title = get_string('buttonlogintextcolor', 'theme_adaptable'); + $description = get_string('buttonlogintextcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/buttonloginpadding'; + $title = get_string('buttonloginpadding', 'theme_adaptable'); + $description = get_string('buttonloginpaddingdesc', 'theme_adaptable'); + $radchoices = $from0to8px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/buttonloginheight'; + $title = get_string('buttonloginheight', 'theme_adaptable'); + $description = get_string('buttonloginheightdesc', 'theme_adaptable'); + $radchoices = array( + '16px' => "16px", + '18px' => "18px", + '20px' => "20px", + '22px' => "22px", + '24px' => "24px", + '26px' => "26px", + '28px' => "28px", + '30px' => "30px", + '32px' => "32px", + '34px' => "34px", + ); + $setting = new admin_setting_configselect($name, $title, $description, '24px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/buttonloginmargintop'; + $title = get_string('buttonloginmargintop', 'theme_adaptable'); + $description = get_string('buttonloginmargintopdesc', 'theme_adaptable'); + $radchoices = $from0to12px; + $setting = new admin_setting_configselect($name, $title, $description, '2px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Enable drop shadow on bottom of button. + $name = 'theme_adaptable/buttondropshadow'; + $title = get_string('buttondropshadow', 'theme_adaptable'); + $description = get_string('buttondropshadowdesc', 'theme_adaptable'); + $shadowchoices = array ( + '0px' => get_string('none', 'theme_adaptable'), + '-1px' => get_string('slight', 'theme_adaptable'), + '-2px' => get_string('standard', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, '0px', $shadowchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/category_headers.php b/theme/adaptable/settings/category_headers.php new file mode 100644 index 0000000..f77d326 --- /dev/null +++ b/theme/adaptable/settings/category_headers.php @@ -0,0 +1,135 @@ +<?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/>. + +/** + * Category header settings + * + * @package theme_adaptable + * @copyright © 2019 - G J Barnard + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +// Category headers heading. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_categoryheaders', get_string('categoryheaderssettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_categoryheaders', + get_string('categoryheaderssettingsheading', 'theme_adaptable'), + format_text(get_string('categoryheaderssettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Category headers to use. + $coursecatsoptions = \theme_adaptable\toolbox::get_top_level_categories(); + $name = 'theme_adaptable/categoryhavecustomheader'; + $title = get_string('categoryhavecustomheader', 'theme_adaptable'); + $description = get_string('categoryhavecustomheaderdesc', 'theme_adaptable'); + $default = array(); + $setting = new admin_setting_configmultiselect($name, $title, $description, $default, $coursecatsoptions); + $page->add($setting); + + $tohavecustomheader = get_config('theme_adaptable', 'categoryhavecustomheader'); + if (!empty($tohavecustomheader)) { + $customheaderids = explode(',', $tohavecustomheader); + $topcats = \theme_adaptable\toolbox::get_top_categories_with_children(); + foreach ($customheaderids as $customheaderid) { + $catinfo = $topcats[$customheaderid]; + if (empty($catinfo['children'])) { + $headdesc = get_string('categoryheaderheaderdesc', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'])); + } else { + $childrentext = ''; + $first = true; + foreach ($catinfo['children'] as $catchildid => $catchild) { + if ($first) { + $first = false; + } else { + $childrentext .= ', '; + } + $childrentext .= $catchild.'('.$catchildid.')'; + } + $headdesc = get_string('categoryheaderheaderdescchildren', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'], 'children' => $childrentext)); + } + $page->add(new admin_setting_heading('theme_adaptable_categoryheader'.$customheaderid, + get_string('categoryheaderheader', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'])), format_text($headdesc, FORMAT_MARKDOWN))); + + // Background image. + $name = 'theme_adaptable/categoryheaderbgimage'.$customheaderid; + $title = get_string('categoryheaderbgimage', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + if (empty($catinfo['children'])) { + $description = get_string('categoryheaderbgimagedesc', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + } else { + $description = get_string('categoryheaderbgimagedescchildren', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'], 'children' => $childrentext)); + } + $setting = new admin_setting_configstoredfile($name, $title, $description, 'categoryheaderbgimage'.$customheaderid); + $page->add($setting); + + // Logo. + $name = 'theme_adaptable/categoryheaderlogo'.$customheaderid; + $title = get_string('categoryheaderlogo', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + if (empty($catinfo['children'])) { + $description = get_string('categoryheaderlogodesc', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + } else { + $description = get_string('categoryheaderlogodescchildren', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'], 'children' => $childrentext)); + } + $setting = new admin_setting_configstoredfile($name, $title, $description, 'categoryheaderlogo'.$customheaderid); + $page->add($setting); + + // Custom title. + $name = 'theme_adaptable/categoryheadercustomtitle'.$customheaderid; + $title = get_string('categoryheadercustomtitle', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + if (empty($catinfo['children'])) { + $description = get_string('categoryheadercustomtitledesc', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + } else { + $description = get_string('categoryheadercustomtitledescchildren', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'], + 'children' => $childrentext)); + } + $default = ''; + $setting = new admin_setting_configtext($name, $title, $description, $default); + $page->add($setting); + + // Custom CSS. + $name = 'theme_adaptable/categoryheadercustomcss'.$customheaderid; + $title = get_string('categoryheadercustomcss', 'theme_adaptable', array('id' => $customheaderid, + 'name' => $catinfo['name'])); + if (empty($catinfo['children'])) { + $description = get_string('categoryheadercustomcssdesc', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'])); + } else { + $description = get_string('categoryheadercustomcssdescchildren', 'theme_adaptable', + array('id' => $customheaderid, 'name' => $catinfo['name'], 'children' => $childrentext)); + } + $default = ''; + $setting = new admin_setting_configtextarea($name, $title, $description, $default); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + } + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/colors.php b/theme/adaptable/settings/colors.php new file mode 100644 index 0000000..966f39d --- /dev/null +++ b/theme/adaptable/settings/colors.php @@ -0,0 +1,387 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2016 Jeremy Hopkins (Coventry University) + * @copyright 2015-2016 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Colors section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_color', get_string('colorsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_color', get_string('colorsettingsheading', 'theme_adaptable'), + format_text(get_string('colordesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Main colors heading. + $name = 'theme_adaptable/settingsmaincolors'; + $heading = get_string('settingsmaincolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Site main color. + $name = 'theme_adaptable/maincolor'; + $title = get_string('maincolor', 'theme_adaptable'); + $description = get_string('maincolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3A454b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Site background color. + $name = 'theme_adaptable/backcolor'; + $title = get_string('backcolor', 'theme_adaptable'); + $description = get_string('backcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main region background color. + $name = 'theme_adaptable/regionmaincolor'; + $title = get_string('regionmaincolor', 'theme_adaptable'); + $description = get_string('regionmaincolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Link color. + $name = 'theme_adaptable/linkcolor'; + $title = get_string('linkcolor', 'theme_adaptable'); + $description = get_string('linkcolordesc', 'theme_adaptable'); + $default = '#51666C'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $page->add($setting); + + // Link hover color. + $name = 'theme_adaptable/linkhover'; + $title = get_string('linkhover', 'theme_adaptable'); + $description = get_string('linkhoverdesc', 'theme_adaptable'); + $default = '#009688'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Selection text color. + $name = 'theme_adaptable/selectiontext'; + $title = get_string('selectiontext', 'theme_adaptable'); + $description = get_string('selectiontextdesc', 'theme_adaptable'); + $default = '#000000'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Selection background color. + $name = 'theme_adaptable/selectionbackground'; + $title = get_string('selectionbackground', 'theme_adaptable'); + $description = get_string('selectionbackgrounddesc', 'theme_adaptable'); + $default = '#00B3A1'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Header colors heading. + $name = 'theme_adaptable/settingsheadercolors'; + $heading = get_string('settingsheadercolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Loading bar color. + $name = 'theme_adaptable/loadingcolor'; + $title = get_string('loadingcolor', 'theme_adaptable'); + $description = get_string('loadingcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#00B3A1', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Top header message badge background color. + $name = 'theme_adaptable/msgbadgecolor'; + $title = get_string('msgbadgecolor', 'theme_adaptable'); + $description = get_string('msgbadgecolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#E53935', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Messages main chat window background colour. + $name = 'theme_adaptable/messagingbackgroundcolor'; + $title = get_string('messagingbackgroundcolor', 'theme_adaptable'); + $description = get_string('messagingbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Top header background color. + $name = 'theme_adaptable/headerbkcolor'; + $title = get_string('headerbkcolor', 'theme_adaptable'); + $description = get_string('headerbkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#00796B', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Top header text color. + $name = 'theme_adaptable/headertextcolor'; + $title = get_string('headertextcolor', 'theme_adaptable'); + $description = get_string('headertextcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Bottom header background color. + $name = 'theme_adaptable/headerbkcolor2'; + $title = get_string('headerbkcolor2', 'theme_adaptable'); + $description = get_string('headerbkcolor2desc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#009688', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Bottom header text color. + $name = 'theme_adaptable/headertextcolor2'; + $title = get_string('headertextcolor2', 'theme_adaptable'); + $description = get_string('headertextcolor2desc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Market blocks colors heading. + $name = 'theme_adaptable/settingsmarketingcolors'; + $heading = get_string('settingsmarketingcolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Market blocks border color. + $name = 'theme_adaptable/marketblockbordercolor'; + $title = get_string('marketblockbordercolor', 'theme_adaptable'); + $description = get_string('marketblockbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#e8eaeb', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Market blocks background color. + $name = 'theme_adaptable/marketblocksbackgroundcolor'; + $title = get_string('marketblocksbackgroundcolor', 'theme_adaptable'); + $description = get_string('marketblocksbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, 'transparent', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Overlay tiles colors heading. + $name = 'theme_adaptable/settingsoverlaycolors'; + $heading = get_string('settingsoverlaycolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/rendereroverlaycolor'; + $title = get_string('rendereroverlaycolor', 'theme_adaptable'); + $description = get_string('rendereroverlaycolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3A454b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/rendereroverlayfontcolor'; + $title = get_string('rendereroverlayfontcolor', 'theme_adaptable'); + $description = get_string('rendereroverlayfontcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/tilesbordercolor'; + $title = get_string('tilesbordercolor', 'theme_adaptable'); + $description = get_string('tilesbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3A454b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/covbkcolor'; + $title = get_string('covbkcolor', 'theme_adaptable'); + $description = get_string('covbkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3A454b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/covfontcolor'; + $title = get_string('covfontcolor', 'theme_adaptable'); + $description = get_string('covfontcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/dividingline'; + $title = get_string('dividingline', 'theme_adaptable'); + $description = get_string('dividinglinedesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/dividingline2'; + $title = get_string('dividingline2', 'theme_adaptable'); + $description = get_string('dividingline2desc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Breadcrumb colors heading. + $name = 'theme_adaptable/settingsbreadcrumbcolors'; + $heading = get_string('settingsbreadcrumbcolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Breadcrumb background color. + $name = 'theme_adaptable/breadcrumb'; + $title = get_string('breadcrumbbackgroundcolor', 'theme_adaptable'); + $description = get_string('breadcrumbbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#f5f5f5', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Breadcrumb text color. + $name = 'theme_adaptable/breadcrumbtextcolor'; + $title = get_string('breadcrumbtextcolor', 'theme_adaptable'); + $description = get_string('breadcrumbtextcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#444444', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + + // Messages pop-up colors heading. + $name = 'theme_adaptable/settingsmessagescolors'; + $heading = get_string('settingsmessagescolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Messages pop-up background color. + $name = 'theme_adaptable/messagepopupbackground'; + $title = get_string('messagepopupbackground', 'theme_adaptable'); + $description = get_string('messagepopupbackgrounddesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#fff000', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Messages pop-up text color. + $name = 'theme_adaptable/messagepopupcolor'; + $title = get_string('messagepopupcolor', 'theme_adaptable'); + $description = get_string('messagepopupcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#333333', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Footer colors heading. + $name = 'theme_adaptable/settingsfootercolors'; + $heading = get_string('settingsfootercolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/footerbkcolor'; + $title = get_string('footerbkcolor', 'theme_adaptable'); + $description = get_string('footerbkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#424242', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/footertextcolor'; + $title = get_string('footertextcolor', 'theme_adaptable'); + $description = get_string('footertextcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/footertextcolor2'; + $title = get_string('footertextcolor2', 'theme_adaptable'); + $description = get_string('footertextcolor2desc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/footerlinkcolor'; + $title = get_string('footerlinkcolor', 'theme_adaptable'); + $description = get_string('footerlinkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Forum colors. + $name = 'theme_adaptable/settingsforumheading'; + $heading = get_string('settingsforumheading', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/forumheaderbackgroundcolor'; + $title = get_string('forumheaderbackgroundcolor', 'theme_adaptable'); + $description = get_string('forumheaderbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/forumbodybackgroundcolor'; + $title = get_string('forumbodybackgroundcolor', 'theme_adaptable'); + $description = get_string('forumbodybackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Activity colors. + $name = 'theme_adaptable/activitiesheading'; + $heading = get_string('activitiesheading', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/introboxbackgroundcolor'; + $title = get_string('introboxbackgroundcolor', 'theme_adaptable'); + $description = get_string('introboxbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/course_formats.php b/theme/adaptable/settings/course_formats.php new file mode 100644 index 0000000..48aa7da --- /dev/null +++ b/theme/adaptable/settings/course_formats.php @@ -0,0 +1,582 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015-2017 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +// Course Formats. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_course', get_string('coursesettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_course', get_string('coursesettingsheading', 'theme_adaptable'), + format_text(get_string('coursesettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Course page, wide layout by moving sidebar to bottom. + $page->add(new admin_setting_heading('coursepagesidebarinfooterenabledsection', + get_string('coursepagesidebarinfooterenabledsection', 'theme_adaptable'), + format_text(get_string('coursepagesidebarinfooterenabledsectiondesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/coursepagesidebarinfooterenabled'; + $title = get_string('coursepagesidebarinfooterenabled', 'theme_adaptable'); + $description = get_string('coursepagesidebarinfooterenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + // Show Your progress string in the top of the course. + $name = 'theme_adaptable/showyourprogress'; + $title = get_string('showyourprogress', 'theme_adaptable'); + $description = get_string('showyourprogressdesc', 'theme_adaptable'); + $radchoices = array( + 'none' => get_string('hide', 'theme_adaptable'), + 'inline' => get_string('show', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, '', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course page top slider block region enabled. + $page->add(new admin_setting_heading('theme_adaptable_newsslider_heading', + get_string('coursepagenewssliderblockregionheading', 'theme_adaptable'), + format_text(get_string('coursepagenewssliderblockregionheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/coursepageblocksliderenabled'; + $title = get_string('coursepageblocksliderenabled', 'theme_adaptable'); + $description = get_string('coursepageblocksliderenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + // Activity end block region. + $page->add(new admin_setting_heading('theme_adaptable_activity_bottom_heading', + get_string('coursepageactivitybottomblockregionheading', 'theme_adaptable'), + format_text(get_string('coursepageactivitybottomblockregionheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/coursepageblockactivitybottomenabled'; + $title = get_string('coursepageblockactivitybottomenabled', 'theme_adaptable'); + $description = get_string('coursepageblockactivitybottomenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + // Course block layout settings. + get_string('coursepageblockregionsettings', 'theme_adaptable'); + $page->add(new admin_setting_heading('theme_adaptable_heading', get_string('coursepageblocklayoutbuilder', 'theme_adaptable'), + format_text(get_string('coursepageblocklayoutbuilderdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Course page top / bottom block regions enabled. + $name = 'theme_adaptable/coursepageblocksenabled'; + $title = get_string('coursepageblocksenabled', 'theme_adaptable'); + $description = get_string('coursepageblocksenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + // Heading for adding space between settings. + $page->add(new admin_setting_heading('temp1', '', "<br>")); + + // Course page top block region builder. + $noregions = 4; // Number of block regions defined in config.php. + $totalblocks = 0; + $imgpath = $CFG->wwwroot . '/theme/adaptable/pix/layout-builder/'; + $imgblder = ''; + + $name = 'theme_adaptable/coursepageblocklayoutlayouttoprow1'; + $title = get_string('coursepageblocklayoutlayouttoprow', 'theme_adaptable'); + $description = get_string('coursepageblocklayoutlayouttoprowdesc', 'theme_adaptable'); + $default = 0; + $choices = $bootstrap12; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + $settingname = 'coursepageblocklayoutlayouttoprow1'; + + $courseformatsetting = get_config('theme_adaptable', $settingname); + if (!isset($courseformatsetting)) { + $courseformatsetting = '0-0-0-0'; + } + + if ($courseformatsetting != '0-0-0-0') { + $imgblder .= '<img src="'.$imgpath.$courseformatsetting.'.png" style="padding-top: 5px">'; + } + + $vals = explode('-', $courseformatsetting); + foreach ($vals as $val) { + if ($val > 0) { + $totalblocks++; + } + } + $page->add(new admin_setting_heading('layout_heading1', '', "<h4>" . get_string('layoutcheck', 'theme_adaptable') . "</h4>")); + + $checkcountcolor = '#00695C'; + if ($totalblocks > $noregions) { + $mktcountcolor = '#D7542A'; + } + $mktcountmsg = '<span style="color: ' . $checkcountcolor . '; margin-bottom: 20px;">'; + $mktcountmsg .= get_string('layoutcount1', 'theme_adaptable').'<strong>'.$noregions.'</strong>'; + $mktcountmsg .= get_string('layoutcount2', 'theme_adaptable').'<strong>'.$totalblocks.'/'.$noregions.'</strong></span>.'; + + $page->add(new admin_setting_heading('theme_adaptable_courselayouttopblockscount', '', $mktcountmsg)); + + $page->add(new admin_setting_heading('theme_adaptable_courselayouttopbuilder', '', $imgblder . "<br><br><br><br>")); + + // Course page bottom block region builder. + $noregions = 4; // Number of block regions defined in config.php. + $totalblocks = 0; + $imgpath = $CFG->wwwroot . '/theme/adaptable/pix/layout-builder/'; + $imgblder = ''; + + $name = 'theme_adaptable/coursepageblocklayoutlayoutbottomrow2'; + $title = get_string('coursepageblocklayoutlayoutbottomrow', 'theme_adaptable'); + $description = get_string('coursepageblocklayoutlayoutbottomrowdesc', 'theme_adaptable'); + $default = 0; + $choices = $bootstrap12; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + $settingname = 'coursepageblocklayoutlayoutbottomrow2'; + + $courseformatsetting = get_config('theme_adaptable', $settingname); + if (!isset($courseformatsetting)) { + $courseformatsetting = '0-0-0-0'; + } + + if ($courseformatsetting != '0-0-0-0') { + $imgblder .= '<img src="'.$imgpath.$courseformatsetting.'.png" style="padding-top: 5px">'; + } + + $vals = explode('-', $courseformatsetting); + foreach ($vals as $val) { + if ($val > 0) { + $totalblocks++; + } + } + + $page->add(new admin_setting_heading('layout_heading2', '', "<h4>" . get_string('layoutcheck', 'theme_adaptable') . "</h4>")); + + $checkcountcolor = '#00695C'; + if ($totalblocks > $noregions) { + $mktcountcolor = '#D7542A'; + } + $mktcountmsg = '<span style="color: ' . $checkcountcolor . '">'; + $mktcountmsg .= get_string('layoutcount1', 'theme_adaptable').'<strong>'.$noregions.'</strong>'; + $mktcountmsg .= get_string('layoutcount2', 'theme_adaptable').'<strong>'.$totalblocks.'/'.$noregions.'</strong></span>.'; + + $page->add(new admin_setting_heading('theme_adaptable_courselayoutbottomblockscount', '', $mktcountmsg)); + + $page->add(new admin_setting_heading('theme_adaptable_courselayoutbottombuilder', '', $imgblder . "<br><br>")); + + // Current course section background color. + $name = 'theme_adaptable/coursesectionbgcolor'; + $title = get_string('coursesectionbgcolor', 'theme_adaptable'); + $description = get_string('coursesectionbgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Courses course format heading. + $name = 'theme_adaptable/settingscourses'; + $heading = get_string('settingscourses', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Course section heading background color. + $name = 'theme_adaptable/coursesectionheaderbg'; + $title = get_string('coursesectionheaderbg', 'theme_adaptable'); + $description = get_string('coursesectionheaderbgdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section heading text color. + $name = 'theme_adaptable/sectionheadingcolor'; + $title = get_string('sectionheadingcolor', 'theme_adaptable'); + $description = get_string('sectionheadingcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#3A454b', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Current course section header background color. + $name = 'theme_adaptable/currentcolor'; + $title = get_string('currentcolor', 'theme_adaptable'); + $description = get_string('currentcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#d2f2ef', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Collapsed topics colour settings enabled. + $name = 'theme_adaptable/collapsedtopicscoloursenabled'; + $title = get_string('collapsedtopicscoloursenabled', 'theme_adaptable'); + $description = get_string('collapsedtopicscoloursenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Topics / Weeks course format heading. + $name = 'theme_adaptable/settingstopicsweeks'; + $heading = get_string('settingstopicsweeks', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Course section header border bottom style. + $name = 'theme_adaptable/coursesectionheaderborderstyle'; + $title = get_string('coursesectionheaderborderstyle', 'theme_adaptable'); + $description = get_string('coursesectionheaderborderstyledesc', 'theme_adaptable'); + $radchoices = $borderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'none', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section header border bottom color. + $name = 'theme_adaptable/coursesectionheaderbordercolor'; + $title = get_string('coursesectionheaderbordercolor', 'theme_adaptable'); + $description = get_string('coursesectionheaderbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#F3F3F3', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section header border bottom width. + $name = 'theme_adaptable/coursesectionheaderborderwidth'; + $title = get_string('coursesectionheaderborderwidth', 'theme_adaptable'); + $description = get_string('coursesectionheaderborderwidthdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + ; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section border radius. + $name = 'theme_adaptable/coursesectionheaderborderradiustop'; + $title = get_string('coursesectionheaderborderradiustop', 'theme_adaptable'); + $description = get_string('coursesectionheaderborderradiustopdesc', 'theme_adaptable'); + $radchoices = $from0to50px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section border radius. + $name = 'theme_adaptable/coursesectionheaderborderradiusbottom'; + $title = get_string('coursesectionheaderborderradiusbottom', 'theme_adaptable'); + $description = get_string('coursesectionheaderborderradiusbottomdesc', 'theme_adaptable'); + $radchoices = $from0to50px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section border style. + $name = 'theme_adaptable/coursesectionborderstyle'; + $title = get_string('coursesectionborderstyle', 'theme_adaptable'); + $description = get_string('coursesectionborderstyledesc', 'theme_adaptable'); + $radchoices = $borderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'solid', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section border width. + $name = 'theme_adaptable/coursesectionborderwidth'; + $title = get_string('coursesectionborderwidth', 'theme_adaptable'); + $description = get_string('coursesectionborderwidthdesc', 'theme_adaptable'); + $radchoices = $from0to6px; + ; + $setting = new admin_setting_configselect($name, $title, $description, '1px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section border color. + $name = 'theme_adaptable/coursesectionbordercolor'; + $title = get_string('coursesectionbordercolor', 'theme_adaptable'); + $description = get_string('coursesectionbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#e8eaeb', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course section border radius. + $name = 'theme_adaptable/coursesectionborderradius'; + $title = get_string('coursesectionborderradius', 'theme_adaptable'); + $description = get_string('coursesectionborderradiusdesc', 'theme_adaptable'); + $radchoices = $from0to50px; + $setting = new admin_setting_configselect($name, $title, $description, '0px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Activity display colours. + // Course Activity section heading. + $name = 'theme_adaptable/coursesectionactivitycolors'; + $heading = get_string('coursesectionactivitycolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Use Adaptable icons. + $name = 'theme_adaptable/coursesectionactivityuseadaptableicons'; + $title = get_string('coursesectionactivityuseadaptableicons', 'theme_adaptable'); + $description = get_string('coursesectionactivityuseadaptableiconsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Default icon size. + $name = 'theme_adaptable/coursesectionactivityiconsize'; + $title = get_string('coursesectionactivityiconsize', 'theme_adaptable'); + $description = get_string('coursesectionactivityiconsizedesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '24px'); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course Activity heading colour. + $name = 'theme_adaptable/coursesectionactivityheadingcolour'; + $title = get_string('coursesectionactivityheadingcolour', 'theme_adaptable'); + $description = get_string('coursesectionactivityheadingcolourdesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#0066cc', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course Activity section bottom border width. + // This border was originally used all around an activity but changed to just the bottom. + $name = 'theme_adaptable/coursesectionactivityborderwidth'; + $title = get_string('coursesectionactivityborderwidth', 'theme_adaptable'); + $description = get_string('coursesectionactivityborderwidthdesc', 'theme_adaptable'); + $widthchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '2px', $widthchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course Activity section bottom border style. + $name = 'theme_adaptable/coursesectionactivityborderstyle'; + $title = get_string('coursesectionactivityborderstyle', 'theme_adaptable'); + $description = get_string('coursesectionactivityborderstyledesc', 'theme_adaptable'); + $radchoices = $borderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'dashed', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course Activity section bottom border colour. + $name = 'theme_adaptable/coursesectionactivitybordercolor'; + $title = get_string('coursesectionactivitybordercolor', 'theme_adaptable'); + $description = get_string('coursesectionactivitybordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#eeeeee', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course Activity section left border width. Controls width of all left borders. + $name = 'theme_adaptable/coursesectionactivityleftborderwidth'; + $title = get_string('coursesectionactivityleftborderwidth', 'theme_adaptable'); + $description = get_string('coursesectionactivityleftborderwidthdesc', 'theme_adaptable'); + $widthchoices = $from0to6px; + $setting = new admin_setting_configselect($name, $title, $description, '3px', $widthchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Assign Activity display colours. + $name = 'theme_adaptable/coursesectionactivityassignleftbordercolor'; + $title = get_string('coursesectionactivityassignleftbordercolor', 'theme_adaptable'); + $description = get_string('coursesectionactivityassignleftbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#0066cc', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Assign Activity background colour. + $name = 'theme_adaptable/coursesectionactivityassignbgcolor'; + $title = get_string('coursesectionactivityassignbgcolor', 'theme_adaptable'); + $description = get_string('coursesectionactivityassignbgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Forum Activity display colours. + $name = 'theme_adaptable/coursesectionactivityforumleftbordercolor'; + $title = get_string('coursesectionactivityforumleftbordercolor', 'theme_adaptable'); + $description = get_string('coursesectionactivityforumleftbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#990099', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Forum Activity background colour. + $name = 'theme_adaptable/coursesectionactivityforumbgcolor'; + $title = get_string('coursesectionactivityforumbgcolor', 'theme_adaptable'); + $description = get_string('coursesectionactivityforumbgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Quiz Activity display colours. + $name = 'theme_adaptable/coursesectionactivityquizleftbordercolor'; + $title = get_string('coursesectionactivityquizleftbordercolor', 'theme_adaptable'); + $description = get_string('coursesectionactivityquizleftbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FF3333', $previewconfig); + $page->add($setting); + + // Quiz Activity background colour. + $name = 'theme_adaptable/coursesectionactivityquizbgcolor'; + $title = get_string('coursesectionactivityquizbgcolor', 'theme_adaptable'); + $description = get_string('coursesectionactivityquizbgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#FFFFFF', $previewconfig); + $page->add($setting); + + // Top and bottom margin spacing between activities. + $name = 'theme_adaptable/coursesectionactivitymargintop'; + $title = get_string('coursesectionactivitymargintop', 'theme_adaptable'); + $description = get_string('coursesectionactivitymargintopdesc', 'theme_adaptable'); + $widthchoices = $from0to12px; + $setting = new admin_setting_configselect($name, $title, $description, '2px', $widthchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivitymarginbottom'; + $title = get_string('coursesectionactivitymarginbottom', 'theme_adaptable'); + $description = get_string('coursesectionactivitymarginbottomdesc', 'theme_adaptable'); + $widthchoices = $from0to12px; + $setting = new admin_setting_configselect($name, $title, $description, '2px', $widthchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // SocialWall course format heading. + $name = 'theme_adaptable/socialwall'; + $heading = get_string('socialwall', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Socialwall background color. + $name = 'theme_adaptable/socialwallbackgroundcolor'; + $title = get_string('socialwallbackgroundcolor', 'theme_adaptable'); + $description = get_string('socialwallbackgroundcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Social Wall section border color. + $name = 'theme_adaptable/socialwallbordercolor'; + $title = get_string('socialwallbordercolor', 'theme_adaptable'); + $description = get_string('socialwallbordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#B9B9B9', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Social Wall section border style. + $name = 'theme_adaptable/socialwallbordertopstyle'; + $title = get_string('socialwallbordertopstyle', 'theme_adaptable'); + $description = get_string('socialwallbordertopstyledesc', 'theme_adaptable'); + $radchoices = $borderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'solid', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Social Wall section border width. + $name = 'theme_adaptable/socialwallborderwidth'; + $title = get_string('socialwallborderwidth', 'theme_adaptable'); + $description = get_string('socialwallborderwidthdesc', 'theme_adaptable'); + $radchoices = $from0to12px; + $setting = new admin_setting_configselect($name, $title, $description, '2px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Social Wall section border radius. + $name = 'theme_adaptable/socialwallsectionradius'; + $title = get_string('socialwallsectionradius', 'theme_adaptable'); + $description = get_string('socialwallsectionradiusdesc', 'theme_adaptable'); + $radchoices = $from0to12px; + $setting = new admin_setting_configselect($name, $title, $description, '6px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Social Wall action link color. + $name = 'theme_adaptable/socialwallactionlinkcolor'; + $title = get_string('socialwallactionlinkcolor', 'theme_adaptable'); + $description = get_string('socialwallactionlinkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#51666C', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Social Wall hover link color. + $name = 'theme_adaptable/socialwallactionlinkhovercolor'; + $title = get_string('socialwallactionlinkhovercolor', 'theme_adaptable'); + $description = get_string('socialwallactionlinkhovercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#009688', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course Activity Further Information section heading. + $name = 'theme_adaptable/coursesectionactivityfurtherinformation'; + $heading = get_string('coursesectionactivityfurtherinformation', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivityfurtherinformationassign'; + $title = get_string('coursesectionactivityfurtherinformationassign', 'theme_adaptable'); + $description = get_string('coursesectionactivityfurtherinformationassigndesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivityfurtherinformationquiz'; + $title = get_string('coursesectionactivityfurtherinformationquiz', 'theme_adaptable'); + $description = get_string('coursesectionactivityfurtherinformationquizdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivityfurtherinformationchoice'; + $title = get_string('coursesectionactivityfurtherinformationchoice', 'theme_adaptable'); + $description = get_string('coursesectionactivityfurtherinformationchoicedesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivityfurtherinformationfeedback'; + $title = get_string('coursesectionactivityfurtherinformationfeedback', 'theme_adaptable'); + $description = get_string('coursesectionactivityfurtherinformationfeedbackdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivityfurtherinformationlesson'; + $title = get_string('coursesectionactivityfurtherinformationlesson', 'theme_adaptable'); + $description = get_string('coursesectionactivityfurtherinformationlessondesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/coursesectionactivityfurtherinformationdata'; + $title = get_string('coursesectionactivityfurtherinformationdata', 'theme_adaptable'); + $description = get_string('coursesectionactivityfurtherinformationdatadesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/custom_css.php b/theme/adaptable/settings/custom_css.php new file mode 100644 index 0000000..8150a6e --- /dev/null +++ b/theme/adaptable/settings/custom_css.php @@ -0,0 +1,75 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Custom CSS and JS section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_generic', get_string('customcssjssettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_generic', get_string('genericsettingsheading', 'theme_adaptable'), + format_text(get_string('genericsettingsdescription', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Custom CSS file. + $name = 'theme_adaptable/customcss'; + $title = get_string('customcss', 'theme_adaptable'); + $description = get_string('customcssdesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_configtextarea($name, $title, $description, $default); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Section for javascript to be added e.g. Google Analytics. + $name = 'theme_adaptable/jssection'; + $title = get_string('jssection', 'theme_adaptable'); + $description = get_string('jssectiondesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_configtextarea($name, $title, $description, $default); + $page->add($setting); + + // Section for custom javascript, restricted by profile field. + $name = 'theme_adaptable/jssectionrestricted'; + $title = get_string('jssectionrestricted', 'theme_adaptable'); + $description = get_string('jssectionrestricteddesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_configtextarea($name, $title, $description, $default); + $page->add($setting); + + $name = 'theme_adaptable/jssectionrestrictedprofilefield'; + $title = get_string('jssectionrestrictedprofilefield', 'theme_adaptable'); + $description = get_string('jssectionrestrictedprofilefielddesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + + $name = 'theme_adaptable/jssectionrestricteddashboardonly'; + $title = get_string('jssectionrestricteddashboardonly', 'theme_adaptable'); + $description = get_string('jssectionrestricteddashboardonlydesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/dash_block_regions.php b/theme/adaptable/settings/dash_block_regions.php new file mode 100644 index 0000000..75b2385 --- /dev/null +++ b/theme/adaptable/settings/dash_block_regions.php @@ -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/>. + +/** + * This displays block region settings for the dashboard page. + * + * @package theme_adaptable + * @copyright 2017 Manoj Solanki (Coventry University) + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Frontpage Block Regions Section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_dash_block_regions', + get_string('dashboardblockregionsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_heading', get_string('dashblocklayoutbuilder', 'theme_adaptable'), + format_text(get_string('dashblocklayoutbuilderdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/dashblocksenabled'; + $title = get_string('dashblocksenabled', 'theme_adaptable'); + $description = get_string('dashblocksenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + $name = 'theme_adaptable/dashblocksposition'; + $title = get_string('dashblocksposition', 'theme_adaptable'); + $description = get_string('dashblockspositiondesc', 'theme_adaptable'); + $default = 'abovecontent'; + $choices = $dashboardblockregionposition; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Dashboard block region builder. + $noregions = 20; // Number of block regions defined in config.php. + list('imgblder' => $imgblder, 'totalblocks' => $totalblocks) = \theme_adaptable\toolbox::admin_settings_layout_builder( + $page, 'dashblocklayoutlayoutrow', $bootstrap12defaults, $bootstrap12); + + $page->add(new admin_setting_heading('theme_adaptable_blocklayoutcheck', get_string('layoutcheck', 'theme_adaptable'), + format_text(get_string('layoutcheckdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $checkcountcolor = '#00695C'; + if ($totalblocks > $noregions) { + $mktcountcolor = '#D7542A'; + } + $mktcountmsg = '<span style="color: ' . $checkcountcolor . '">'; + $mktcountmsg .= get_string('layoutcount1', 'theme_adaptable') . '<strong>' . $noregions . '</strong>'; + $mktcountmsg .= get_string('layoutcount2', 'theme_adaptable') . '<strong>' . $totalblocks . '/' . $noregions . '</strong>.'; + + $page->add(new admin_setting_heading('theme_adaptable_dashlayoutblockscount', '', $mktcountmsg)); + + $page->add(new admin_setting_heading('theme_adaptable_dashlayoutbuilder', '', $imgblder)); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/fonts.php b/theme/adaptable/settings/fonts.php new file mode 100644 index 0000000..a80ae1a --- /dev/null +++ b/theme/adaptable/settings/fonts.php @@ -0,0 +1,200 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2018 Jeremy Hopkins (Coventry University) + * @copyright 2015-2018 Fernando Acedo (3-bits.com) + * @copyright 2017-2018 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Fonts Section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_font', get_string('fontsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_font', get_string('fontsettingsheading', 'theme_adaptable'), + format_text(get_string('fontdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Fonts heading. + $name = 'theme_adaptable/settingsfonts'; + $heading = get_string('settingsfonts', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Main Google Font Name. + $name = 'theme_adaptable/fontname'; + $title = get_string('fontname', 'theme_adaptable'); + $description = get_string('fontnamedesc', 'theme_adaptable'); + $default = 'Open Sans'; + $choices = $fontlist; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main Font Subset. + $name = 'theme_adaptable/fontsubset'; + $title = get_string('fontsubset', 'theme_adaptable'); + $description = get_string('fontsubsetdesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_configmulticheckbox($name, $title, $description, $default, array( + 'latin-ext' => "Latin Extended", + 'cyrillic' => "Cyrillic", + 'cyrillic-ext' => "Cyrillic Extended", + 'greek' => "Greek", + 'greek-ext' => "Greek Extended", + 'vietnamese' => "Vietnamese", + 'arabic' => "Arabic", + 'hebrew' => "Hebrew", + 'japanese' => "Japanese", + 'korean' => "Korean", + 'tamil' => "Tamil", + 'thai' => "Thai" + )); + $page->add($setting); + + // Main Font size. + $name = 'theme_adaptable/fontsize'; + $title = get_string('fontsize', 'theme_adaptable'); + $description = get_string('fontsizedesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '95%', $from85to110percent); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main Font weight. + $name = 'theme_adaptable/fontweight'; + $title = get_string('fontweight', 'theme_adaptable'); + $description = get_string('fontweightdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 400, $from100to900); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main Font color. + $name = 'theme_adaptable/fontcolor'; + $title = get_string('fontcolor', 'theme_adaptable'); + $description = get_string('fontcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#333333', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Top Menu Font Size. + $name = 'theme_adaptable/topmenufontsize'; + $title = get_string('topmenufontsize', 'theme_adaptable'); + $description = get_string('topmenufontsizedesc', 'theme_adaptable'); + $radchoices = $standardfontsize; + $setting = new admin_setting_configselect($name, $title, $description, '14px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Navbar Menu Font Size. + $name = 'theme_adaptable/menufontsize'; + $title = get_string('menufontsize', 'theme_adaptable'); + $description = get_string('menufontsizedesc', 'theme_adaptable'); + $radchoices = $standardfontsize; + $setting = new admin_setting_configselect($name, $title, $description, '14px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Navbar Menu Padding. + $name = 'theme_adaptable/menufontpadding'; + $title = get_string('menufontpadding', 'theme_adaptable'); + $description = get_string('menufontpaddingdesc', 'theme_adaptable'); + $radchoices = $from10to30px; + $setting = new admin_setting_configselect($name, $title, $description, '20px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Header Font Name. + $name = 'theme_adaptable/fontheadername'; + $title = get_string('fontheadername', 'theme_adaptable'); + $description = get_string('fontheadernamedesc', 'theme_adaptable'); + $default = 'Roboto'; + $choices = $fontlist; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Header Font weight. + $name = 'theme_adaptable/fontheaderweight'; + $title = get_string('fontheaderweight', 'theme_adaptable'); + $description = get_string('fontheaderweightdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 400, $from100to900); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Header font color. + $name = 'theme_adaptable/fontheadercolor'; + $title = get_string('fontheadercolor', 'theme_adaptable'); + $description = get_string('fontheadercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#333333', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Title Font Name. + $name = 'theme_adaptable/fonttitlename'; + $title = get_string('fonttitlename', 'theme_adaptable'); + $description = get_string('fonttitlenamedesc', 'theme_adaptable'); + $default = 'Roboto Condensed'; + $choices = $fontlist; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Title Font size. + $name = 'theme_adaptable/fonttitlesize'; + $title = get_string('fonttitlesize', 'theme_adaptable'); + $description = get_string('fonttitlesizedesc', 'theme_adaptable'); + $default = '48px'; + $choices = $standardfontsize; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Title Font weight. + $name = 'theme_adaptable/fonttitleweight'; + $title = get_string('fonttitleweight', 'theme_adaptable'); + $description = get_string('fonttitleweightdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 400, $from100to900); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Title font color. + $name = 'theme_adaptable/fonttitlecolor'; + $title = get_string('fonttitlecolor', 'theme_adaptable'); + $description = get_string('fonttitlecolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course font color. + $name = 'theme_adaptable/fonttitlecolorcourse'; + $title = get_string('fonttitlecolorcourse', 'theme_adaptable'); + $description = get_string('fonttitlecolorcoursedesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/footer.php b/theme/adaptable/settings/footer.php new file mode 100644 index 0000000..b7164e1 --- /dev/null +++ b/theme/adaptable/settings/footer.php @@ -0,0 +1,152 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_footer', get_string('footersettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_footer', get_string('footersettingsheading', 'theme_adaptable'), + format_text(get_string('footerdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Show moodle docs link. + $name = 'theme_adaptable/moodledocs'; + $title = get_string('moodledocs', 'theme_adaptable'); + $description = get_string('moodledocsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/footerblocksplacement'; + $title = get_string('footerblocksplacement', 'theme_adaptable'); + $description = get_string('footerblocksplacementdesc', 'theme_adaptable'); + $choices = array( + 1 => get_string('footerblocksplacement1', 'theme_adaptable'), + 2 => get_string('footerblocksplacement2', 'theme_adaptable'), + 3 => get_string('footerblocksplacement3', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $choices); + $page->add($setting); + + // Show Footer blocks. + $name = 'theme_adaptable/showfooterblocks'; + $title = get_string('showfooterblocks', 'theme_adaptable'); + $description = get_string('showfooterblocksdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 1); + $page->add($setting); + + $totalblocks = 0; + $imgpath = $CFG->wwwroot . '/theme/adaptable/pix/layout-builder/'; + $imgblder = ''; + for ($i = 1; $i <= 3; $i++) { + $name = 'theme_adaptable/footerlayoutrow' . $i; + $title = get_string('footerlayoutrow', 'theme_adaptable'); + $description = get_string('footerlayoutrowdesc', 'theme_adaptable'); + $default = $i - 1; + $choices = $bootstrap12; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + $settingname = 'footerlayoutrow' . $i; + + $footersetting = get_config('theme_adaptable', $settingname); + if (!isset($footersetting)) { + $footersetting = '0-0-0-0'; + } + + if ($footersetting != '0-0-0-0') { + $imgblder .= '<img src="'.$imgpath.$footersetting.'.png" style="padding-top: 5px">'; + } + + $vals = explode('-', $footersetting); + foreach ($vals as $val) { + if ($val > 0) { + $totalblocks++; + } + } + } + + $page->add(new admin_setting_heading('theme_adaptable_footerlayoutcheck', get_string('layoutcheck', 'theme_adaptable'), + format_text(get_string('layoutcheckdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $page->add(new admin_setting_heading('theme_adaptable_footerlayoutbuilder', '', $imgblder)); + + $blkcontmsg = get_string('layoutaddcontentdesc1', 'theme_adaptable'); + $blkcontmsg .= $totalblocks; + $blkcontmsg .= get_string('layoutaddcontentdesc2', 'theme_adaptable'); + + $page->add(new admin_setting_heading('theme_adaptable_footerlayoutaddcontent', + get_string('layoutaddcontent', 'theme_adaptable'), format_text($blkcontmsg, FORMAT_MARKDOWN))); + + for ($i = 1; $i <= $totalblocks; $i++) { + $name = 'theme_adaptable/footer' . $i . 'header'; + $title = get_string('footerheader', 'theme_adaptable') . $i; + $description = get_string('footerdesc', 'theme_adaptable') . $i; + $default = ''; + $setting = new admin_setting_configtext($name, $title, $description, $default); + $page->add($setting); + + $name = 'theme_adaptable/footer' . $i . 'content'; + $title = get_string('footercontent', 'theme_adaptable') . $i; + $description = get_string('footercontentdesc', 'theme_adaptable') . $i; + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + } + + // Social icons. + $name = 'theme_adaptable/hidefootersocial'; + $title = get_string('hidefootersocial', 'theme_adaptable'); + $description = get_string('hidefootersocialdesc', 'theme_adaptable'); + $radchoices = array( + 0 => get_string('hide', 'theme_adaptable'), + 1 => get_string('show', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $radchoices); + $page->add($setting); + + // Show Data retention button link. + $name = 'theme_adaptable/gdprbutton'; + $title = get_string('gdprbutton', 'theme_adaptable'); + $description = get_string('gdprbuttondesc', 'theme_adaptable'); + $radchoices = array( + 'none' => get_string('hide', 'theme_adaptable'), + 'inline' => get_string('show', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Footnote. + $name = 'theme_adaptable/footnote'; + $title = get_string('footnote', 'theme_adaptable'); + $description = get_string('footnotedesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/frontpage_courses.php b/theme/adaptable/settings/frontpage_courses.php new file mode 100644 index 0000000..9dd371d --- /dev/null +++ b/theme/adaptable/settings/frontpage_courses.php @@ -0,0 +1,111 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Frontpage courses section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_frontpage_courses', get_string('frontpagecoursesettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_frontpage_courses', + get_string('frontpagesettingsheading', 'theme_adaptable'), + format_text(get_string('frontpagedesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/frontpagerenderer'; + $title = get_string('frontpagerenderer', 'theme_adaptable'); + $description = get_string('frontpagerendererdesc', 'theme_adaptable'); + $choices = array( + 1 => get_string('frontpagerendereroption1', 'theme_adaptable'), + 2 => get_string('frontpagerendereroption2', 'theme_adaptable'), + 3 => get_string('frontpagerendereroption3', 'theme_adaptable'), + 4 => get_string('frontpagerendereroption4', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 2, $choices); + $page->add($setting); + + // Number of tiles per row. + // Number of tiles per row: 12=1 tile / 6=2 tiles / 4 (default)=3 tiles / 3=4 tiles / 2=6 tiles. + $name = 'theme_adaptable/frontpagenumbertiles'; + $title = get_string('frontpagenumbertiles', 'theme_adaptable'); + $description = get_string('frontpagenumbertilesdesc', 'theme_adaptable'); + $choices = array( + 12 => get_string('frontpagetiles1', 'theme_adaptable'), + 6 => get_string('frontpagetiles2', 'theme_adaptable'), + 4 => get_string('frontpagetiles3', 'theme_adaptable'), + 3 => get_string('frontpagetiles4', 'theme_adaptable'), + 2 => get_string('frontpagetiles6', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, 4, $choices); + $page->add($setting); + + // Default image for 'Tiles with overlay' on 'frontpagerenderer' setting. + $name = 'theme_adaptable/frontpagerendererdefaultimage'; + $title = get_string('frontpagerendererdefaultimage', 'theme_adaptable'); + $description = get_string('frontpagerendererdefaultimagedesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, 'frontpagerendererdefaultimage'); + $page->add($setting); + + // Show course contacts. + $name = 'theme_adaptable/tilesshowcontacts'; + $title = get_string('tilesshowcontacts', 'theme_adaptable'); + $description = get_string('tilesshowcontactsdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 1); + $page->add($setting); + + $name = 'theme_adaptable/tilesshowallcontacts'; + $title = get_string('tilesshowallcontacts', 'theme_adaptable'); + $description = get_string('tilesshowallcontactsdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 0); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/tilescontactstitle'; + $title = get_string('tilescontactstitle', 'theme_adaptable'); + $description = get_string('tilescontactstitledesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 1); + $page->add($setting); + + $name = 'theme_adaptable/covhidebutton'; + $title = get_string('covhidebutton', 'theme_adaptable'); + $description = get_string('covhidebuttondesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 0); + $page->add($setting); + + // Show 'Available Courses' label. + $name = 'theme_adaptable/enableavailablecourses'; + $title = get_string('enableavailablecourses', 'theme_adaptable'); + $description = get_string('enableavailablecoursesdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 0, + array( + 'inherit' => get_string('show'), + 'none' => get_string('hide') + )); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/frontpage_slider.php b/theme/adaptable/settings/frontpage_slider.php new file mode 100644 index 0000000..05bfc22 --- /dev/null +++ b/theme/adaptable/settings/frontpage_slider.php @@ -0,0 +1,206 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +// Frontpage Slider. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_frontpage_slider', get_string('frontpageslidersettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_slideshow', get_string('slideshowsettingsheading', 'theme_adaptable'), + format_text(get_string('slideshowdesc', 'theme_adaptable'). + get_string('slideroption2snippet', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/sliderenabled'; + $title = get_string('sliderenabled', 'theme_adaptable'); + $description = get_string('sliderenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 0); + $page->add($setting); + + $name = 'theme_adaptable/sliderfullscreen'; + $title = get_string('sliderfullscreen', 'theme_adaptable'); + $description = get_string('sliderfullscreendesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, 0); + $page->add($setting); + + $name = 'theme_adaptable/slidermargintop'; + $title = get_string('slidermargintop', 'theme_adaptable'); + $description = get_string('slidermargintopdesc', 'theme_adaptable'); + $radchoices = $from0to20px; + $setting = new admin_setting_configselect($name, $title, $description, '20px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slidermarginbottom'; + $title = get_string('slidermarginbottom', 'theme_adaptable'); + $description = get_string('slidermarginbottomdesc', 'theme_adaptable'); + $radchoices = $from0to20px; + $setting = new admin_setting_configselect($name, $title, $description, '20px', $radchoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slideroption2'; + $title = get_string('slideroption2', 'theme_adaptable'); + $description = get_string('slideroption2desc', 'theme_adaptable'); + $radchoices = $sliderstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'nocaptions', $radchoices); + $page->add($setting); + + $slideroption2 = get_config('theme_adaptable', 'slideroption2'); + if (!isset($slideroption2)) { + $slideroption2 = 'slider1'; + } + + if ($slideroption2 == 'slider1') { + $name = 'theme_adaptable/sliderh3color'; + $title = get_string('sliderh3color', 'theme_adaptable'); + $description = get_string('sliderh3colordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/sliderh4color'; + $title = get_string('sliderh4color', 'theme_adaptable'); + $description = get_string('sliderh4colordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slidersubmitcolor'; + $title = get_string('slidersubmitcolor', 'theme_adaptable'); + $description = get_string('slidersubmitcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slidersubmitbgcolor'; + $title = get_string('slidersubmitbgcolor', 'theme_adaptable'); + $description = get_string('slidersubmitbgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#51666C', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + } + + if ($slideroption2 == 'slider2') { + $name = 'theme_adaptable/slider2h3color'; + $title = get_string('slider2h3color', 'theme_adaptable'); + $description = get_string('slider2h3colordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slider2h3bgcolor'; + $title = get_string('slider2h3bgcolor', 'theme_adaptable'); + $description = get_string('slider2h3bgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#000000', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slider2h4color'; + $title = get_string('slider2h4color', 'theme_adaptable'); + $description = get_string('slider2h4colordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#000000', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slider2h4bgcolor'; + $title = get_string('slider2h4bgcolor', 'theme_adaptable'); + $description = get_string('slider2h4bgcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slideroption2submitcolor'; + $title = get_string('slideroption2submitcolor', 'theme_adaptable'); + $description = get_string('slideroption2submitcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slideroption2color'; + $title = get_string('slideroption2color', 'theme_adaptable'); + $description = get_string('slideroption2colordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#51666C', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/slideroption2a'; + $title = get_string('slideroption2a', 'theme_adaptable'); + $description = get_string('slideroption2adesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#51666C', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + } + + // Number of Sliders. + $name = 'theme_adaptable/slidercount'; + $title = get_string('slidercount', 'theme_adaptable'); + $description = get_string('slidercountdesc', 'theme_adaptable'); + $default = THEME_ADAPTABLE_DEFAULT_SLIDERCOUNT; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + // If we don't have an slide yet, default to the preset. + $slidercount = get_config('theme_adaptable', 'slidercount'); + + if (!$slidercount) { + $slidercount = THEME_ADAPTABLE_DEFAULT_SLIDERCOUNT; + } + + for ($sliderindex = 1; $sliderindex <= $slidercount; $sliderindex++) { + $fileid = 'p' . $sliderindex; + $name = 'theme_adaptable/p' . $sliderindex; + $title = get_string('sliderimage', 'theme_adaptable'); + $description = get_string('sliderimagedesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, $fileid); + $page->add($setting); + + $name = 'theme_adaptable/p' . $sliderindex . 'url'; + $title = get_string('sliderurl', 'theme_adaptable'); + $description = get_string('sliderurldesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_URL); + $page->add($setting); + + $name = 'theme_adaptable/p' . $sliderindex . 'cap'; + $title = get_string('slidercaption', 'theme_adaptable'); + $description = get_string('slidercaptiondesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/frontpage_ticker.php b/theme/adaptable/settings/frontpage_ticker.php new file mode 100644 index 0000000..a613df9 --- /dev/null +++ b/theme/adaptable/settings/frontpage_ticker.php @@ -0,0 +1,92 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +// Frontpage Ticker heading. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_frontpage_ticker', get_string('frontpagetickersettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_ticker', get_string('tickersettingsheading', 'theme_adaptable'), + format_text(get_string('tickerdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/enableticker'; + $title = get_string('enableticker', 'theme_adaptable'); + $description = get_string('enabletickerdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enabletickermy'; + $title = get_string('enabletickermy', 'theme_adaptable'); + $description = get_string('enabletickermydesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Ticker Width (fullscreen / fixed width). + $name = 'theme_adaptable/tickerwidth'; + $title = get_string('tickerwidth', 'theme_adaptable'); + $description = get_string('tickerwidthdesc', 'theme_adaptable'); + $options = array( + '' => get_string('tickerwidth', 'theme_adaptable'), + 'width: 100%;' => get_string('tickerfullscreen', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, '', $options); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Number of news ticker sectons. + $name = 'theme_adaptable/newstickercount'; + $title = get_string('newstickercount', 'theme_adaptable'); + $description = get_string('newstickercountdesc', 'theme_adaptable'); + $default = THEME_ADAPTABLE_DEFAULT_TOOLSMENUSCOUNT; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + // If we don't have a menuscount yet, default to the preset. + $newstickercount = get_config('theme_adaptable', 'newstickercount'); + + if (!$newstickercount) { + $newstickercount = THEME_ADAPTABLE_DEFAULT_NEWSTICKERCOUNT; + } + + for ($newstickerindex = 1; $newstickerindex <= $newstickercount; $newstickerindex++) { + $name = 'theme_adaptable/tickertext' . $newstickerindex; + $title = get_string('tickertext', 'theme_adaptable') . ' ' . $newstickerindex; + $description = get_string('tickertextdesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + $name = 'theme_adaptable/tickertext' . $newstickerindex . 'profilefield'; + $title = get_string('tickertextprofilefield', 'theme_adaptable'); + $description = get_string('tickertextprofilefielddesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/header.php b/theme/adaptable/settings/header.php new file mode 100644 index 0000000..cf1c710 --- /dev/null +++ b/theme/adaptable/settings/header.php @@ -0,0 +1,230 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Header heading. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_header', get_string('headersettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_header', get_string('headersettingsheading', 'theme_adaptable'), + format_text(get_string('headerdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Header image. + $name = 'theme_adaptable/headerbgimage'; // TODO - served by 'theme_adaptable_pluginfile'? + $title = get_string('headerbgimage', 'theme_adaptable'); + $description = get_string('headerbgimagedesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, 'headerbgimage'); + $page->add($setting); + + // Select type of login. + $name = 'theme_adaptable/displaylogin'; + $title = get_string('displaylogin', 'theme_adaptable'); + $description = get_string('displaylogindesc', 'theme_adaptable'); + $choices = array( + 'button' => get_string('displayloginbutton', 'theme_adaptable'), + 'box' => get_string('displayloginbox', 'theme_adaptable'), + 'no' => get_string('displayloginno', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'button', $choices); + $page->add($setting); + + // Show username. + $name = 'theme_adaptable/showusername'; + $title = get_string('showusername', 'theme_adaptable'); + $description = get_string('showusernamedesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Logo. + $name = 'theme_adaptable/logo'; + $title = get_string('logo', 'theme_adaptable'); + $description = get_string('logodesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, 'logo'); + $page->add($setting); + + // Page Header Height. + $name = 'theme_adaptable/pageheaderheight'; + $title = get_string('pageheaderheight', 'theme_adaptable'); + $description = get_string('pageheaderheightdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '72px'); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course page header title. + $name = 'theme_adaptable/coursepageheaderhidesitetitle'; + $title = get_string('coursepageheaderhidesitetitle', 'theme_adaptable'); + $description = get_string('coursepageheaderhidesitetitledesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $page->add($setting); + + // Favicon file setting. + $name = 'theme_adaptable/favicon'; + $title = get_string('favicon', 'theme_adaptable'); + $description = get_string('favicondesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, 'favicon'); + $page->add($setting); + + // Site title. + $name = 'theme_adaptable/sitetitle'; + $title = get_string('sitetitle', 'theme_adaptable'); + $description = get_string('sitetitledesc', 'theme_adaptable'); + $radchoices = array( + 'disabled' => get_string('sitetitleoff', 'theme_adaptable'), + 'default' => get_string('sitetitledefault', 'theme_adaptable'), + 'custom' => get_string('sitetitlecustom', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'default', $radchoices); + $page->add($setting); + + // Site title text. + $name = 'theme_adaptable/sitetitletext'; + $title = get_string('sitetitletext', 'theme_adaptable'); + $description = get_string('sitetitletextdesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + // Display Course title in the header. + $name = 'theme_adaptable/enableheading'; + $title = get_string('enableheading', 'theme_adaptable'); + $description = get_string('enableheadingdesc', 'theme_adaptable'); + $radchoices = array( + 'fullname' => get_string('coursetitlefullname', 'theme_adaptable'), + 'shortname' => get_string('coursetitleshortname', 'theme_adaptable'), + 'off' => get_string('hide') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'fullname', $radchoices); + $page->add($setting); + + // Display Breadcrumb or Course title where the breadcrumb normally is. + $name = 'theme_adaptable/breadcrumbdisplay'; + $title = get_string('breadcrumbdisplay', 'theme_adaptable'); + $description = get_string('breadcrumbdisplaydesc', 'theme_adaptable'); + $radchoices = array( + 'breadcrumb' => get_string('breadcrumb', 'theme_adaptable'), + 'fullname' => get_string('coursetitlefullname', 'theme_adaptable'), + 'shortname' => get_string('coursetitleshortname', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'breadcrumb', $radchoices); + $page->add($setting); + + // Course Title Maximum Width. + $name = 'theme_adaptable/coursetitlemaxwidth'; + $title = get_string('coursetitlemaxwidth', 'theme_adaptable'); + $description = get_string('coursetitlemaxwidthdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '20', PARAM_INT); + $page->add($setting); + + // Breadcrumb home. + $name = 'theme_adaptable/breadcrumbhome'; + $title = get_string('breadcrumbhome', 'theme_adaptable'); + $description = get_string('breadcrumbhomedesc', 'theme_adaptable'); + $radchoices = array( + 'text' => get_string('breadcrumbhometext', 'theme_adaptable'), + 'icon' => get_string('breadcrumbhomeicon', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'icon', $radchoices); + $page->add($setting); + + // Breadcrumb separator. + $name = 'theme_adaptable/breadcrumbseparator'; + $title = get_string('breadcrumbseparator', 'theme_adaptable'); + $description = get_string('breadcrumbseparatordesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, 'angle-right'); + $page->add($setting); + + // Choose to display search box or social icons. + $name = 'theme_adaptable/socialorsearch'; + $title = get_string('socialorsearch', 'theme_adaptable'); + $description = get_string('socialorsearchdesc', 'theme_adaptable'); + $radchoices = array( + 'none' => get_string('socialorsearchnone', 'theme_adaptable'), + 'social' => get_string('socialorsearchsocial', 'theme_adaptable'), + 'search' => get_string('socialorsearchsearch', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'search', $radchoices); + $page->add($setting); + + // Search box padding. + $name = 'theme_adaptable/searchboxpadding'; + $title = get_string('searchboxpadding', 'theme_adaptable'); + $description = get_string('searchboxpaddingdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '0px 0px 10px 0px'); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Enable save / cancel overlay at top of page. + $name = 'theme_adaptable/enablesavecanceloverlay'; + $title = get_string('enablesavecanceloverlay', 'theme_adaptable'); + $description = get_string('enablesavecanceloverlaydesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // My courses section. + $page->add(new admin_setting_heading('theme_adaptable_headerstyle_heading', + get_string('headerstyleheading', 'theme_adaptable'), + format_text(get_string('headerstyleheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Adaptable header style selection. + $name = 'theme_adaptable/headerstyle'; + $title = get_string('headerstyle', 'theme_adaptable'); + $description = get_string('headerstyledesc', 'theme_adaptable'); + $radchoices = array( + 'style1' => get_string('headerstyle1', 'theme_adaptable'), + 'style2' => get_string('headerstyle2', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'style1', $radchoices); + $page->add($setting); + + // Page header layout. + $name = 'theme_adaptable/pageheaderlayout'; + $title = get_string('pageheaderlayout', 'theme_adaptable'); + $description = get_string('pageheaderlayoutdesc', 'theme_adaptable'); + $radchoices = array( + 'original' => get_string('pageheaderoriginal', 'theme_adaptable'), + 'alternative' => get_string('pageheaderalternative', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'left', $radchoices); + $page->add($setting); + + // Header 2 search box. + $name = 'theme_adaptable/header2searchbox'; + $title = get_string('header2searchbox', 'theme_adaptable'); + $description = get_string('header2searchboxdesc', 'theme_adaptable'); + $default = 'expandable'; + $radchoices = array( + 'expandable' => get_string('expandable', 'theme_adaptable'), + 'static' => get_string('static', 'theme_adaptable'), + 'disabled' => get_string('disabled', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, $default, $radchoices); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/header_menus.php b/theme/adaptable/settings/header_menus.php new file mode 100644 index 0000000..448b2d5 --- /dev/null +++ b/theme/adaptable/settings/header_menus.php @@ -0,0 +1,137 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_menus', get_string('menusettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_menus', get_string('menusheading', 'theme_adaptable'), + format_text(get_string('menustitledesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Settings for top header menus. + $page->add(new admin_setting_heading('theme_adaptable_menus_visibility', + get_string('menusheadingvisibility', 'theme_adaptable'), + format_text(get_string('menusheadingvisibilitydesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/enablemenus'; + $title = get_string('enablemenus', 'theme_adaptable'); + $description = get_string('enablemenusdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/menuslinkright'; + $title = get_string('menuslinkright', 'theme_adaptable'); + $description = get_string('menuslinkrightdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Links menu icon. Default is "fa-link". + $name = 'theme_adaptable/menuslinkicon'; + $title = get_string('menuslinkicon', 'theme_adaptable'); + $description = get_string('menuslinkicondesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, 'fa-link'); + $page->add($setting); + + $name = 'theme_adaptable/disablemenuscoursepages'; + $title = get_string('disablemenuscoursepages', 'theme_adaptable'); + $description = get_string('disablemenuscoursepagesdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/menusession'; + $title = get_string('menusession', 'theme_adaptable'); + $description = get_string('menusessiondesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, true, true, false); + $page->add($setting); + + $name = 'theme_adaptable/menusessionttl'; + $title = get_string('menusessionttl', 'theme_adaptable'); + $description = get_string('menusessionttldesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '30', PARAM_INT); + $page->add($setting); + + $name = 'theme_adaptable/menuuseroverride'; + $title = get_string('menuuseroverride', 'theme_adaptable'); + $description = get_string('menuuseroverridedesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/menuoverrideprofilefield'; + $title = get_string('menuoverrideprofilefield', 'theme_adaptable'); + $description = get_string('menuoverrideprofilefielddesc', 'theme_adaptable'); + $default = get_string('menuoverrideprofilefielddefault', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_RAW); + $page->add($setting); + + // Number of menus. + $name = 'theme_adaptable/topmenuscount'; + $title = get_string('topmenuscount', 'theme_adaptable'); + $description = get_string('topmenuscountdesc', 'theme_adaptable'); + $default = THEME_ADAPTABLE_DEFAULT_TOPMENUSCOUNT; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + // If we don't have a menuscount yet, default to the preset. + $topmenuscount = get_config('theme_adaptable', 'topmenuscount'); + if (!$topmenuscount) { + $topmenuscount = THEME_ADAPTABLE_DEFAULT_TOPMENUSCOUNT; + } + + for ($topmenusindex = 1; $topmenusindex <= $topmenuscount; $topmenusindex++) { + $page->add(new admin_setting_heading('theme_adaptable_menus' . $topmenusindex, + get_string('newmenuheading', 'theme_adaptable') . $topmenusindex, + format_text(get_string('menusdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/newmenu' . $topmenusindex . 'title'; + $title = get_string('newmenutitle', 'theme_adaptable'); + $description = get_string('newmenutitledesc', 'theme_adaptable'); + $default = get_string('newmenutitledefault', 'theme_adaptable') . ' ' . $topmenusindex; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_RAW); + $page->add($setting); + + $name = 'theme_adaptable/newmenu' . $topmenusindex; + $title = get_string('newmenu', 'theme_adaptable') . $topmenusindex; + $description = get_string('newmenudesc', 'theme_adaptable'); + $setting = new admin_setting_configtextarea($name, $title, $description, '', PARAM_RAW, '50', '10'); + $page->add($setting); + + $name = 'theme_adaptable/newmenu' . $topmenusindex . 'requirelogin'; + $title = get_string('newmenurequirelogin', 'theme_adaptable'); + $description = get_string('newmenurequirelogindesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/newmenu' . $topmenusindex . 'field'; + $title = get_string('newmenufield', 'theme_adaptable'); + $description = get_string('newmenufielddesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/header_navbar_menu.php b/theme/adaptable/settings/header_navbar_menu.php new file mode 100644 index 0000000..eed2e79 --- /dev/null +++ b/theme/adaptable/settings/header_navbar_menu.php @@ -0,0 +1,91 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +// Settings for tools menus. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_header_navbar_menu', get_string('navbarmenusettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_toolsmenu', get_string('toolsmenu', 'theme_adaptable'), + format_text(get_string('toolsmenudesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $page->add(new admin_setting_heading('theme_adaptable_toolsmenu', get_string('toolsmenuheading', 'theme_adaptable'), + format_text(get_string('toolsmenuheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/disablecustommenu'; + $title = get_string('disablecustommenu', 'theme_adaptable'); + $description = get_string('disablecustommenudesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, false, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enabletoolsmenus'; + $title = get_string('enabletoolsmenus', 'theme_adaptable'); + $description = get_string('enabletoolsmenusdesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Number of tools menus. + $name = 'theme_adaptable/toolsmenuscount'; + $title = get_string('toolsmenuscount', 'theme_adaptable'); + $description = get_string('toolsmenuscountdesc', 'theme_adaptable'); + $default = THEME_ADAPTABLE_DEFAULT_TOOLSMENUSCOUNT; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + // If we don't have a menuscount yet, default to the preset. + $toolsmenuscount = get_config('theme_adaptable', 'toolsmenuscount'); + + if (!$toolsmenuscount) { + $toolsmenuscount = THEME_ADAPTABLE_DEFAULT_TOOLSMENUSCOUNT; + } + + for ($toolsmenusindex = 1; $toolsmenusindex <= $toolsmenuscount; $toolsmenusindex++) { + $page->add(new admin_setting_heading('theme_adaptable_menus' . $toolsmenusindex, + get_string('toolsmenuheading', 'theme_adaptable') . $toolsmenusindex, + format_text(get_string('toolsmenudesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/toolsmenu' . $toolsmenusindex . 'title'; + $title = get_string('toolsmenutitle', 'theme_adaptable') . ' ' . $toolsmenusindex; + $description = get_string('toolsmenutitledesc', 'theme_adaptable'); + $default = get_string('toolsmenutitledefault', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_RAW); + $page->add($setting); + + $name = 'theme_adaptable/toolsmenu' . $toolsmenusindex; + $title = get_string('toolsmenu', 'theme_adaptable') . ' ' . $toolsmenusindex; + $description = get_string('toolsmenudesc', 'theme_adaptable'); + $setting = new admin_setting_configtextarea($name, $title, $description, '', PARAM_RAW, '50', '10'); + $page->add($setting); + + $name = 'theme_adaptable/toolsmenu' . $toolsmenusindex . 'field'; + $title = get_string('toolsmenufield', 'theme_adaptable'); + $description = get_string('toolsmenufielddesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/header_social.php b/theme/adaptable/settings/header_social.php new file mode 100644 index 0000000..1eeff9e --- /dev/null +++ b/theme/adaptable/settings/header_social.php @@ -0,0 +1,71 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Social links. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_social', get_string('socialsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_social', get_string('socialheading', 'theme_adaptable'), + format_text(get_string('socialtitledesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/socialsize'; + $title = get_string('socialsize', 'theme_adaptable'); + $description = get_string('socialsizedesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '37px', $from14to46px); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/socialpaddingside'; + $title = get_string('socialpaddingside', 'theme_adaptable'); + $description = get_string('socialpaddingsidedesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 16, $from10to30pxnovalueunit); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/socialpaddingtop'; + $title = get_string('socialpaddingtop', 'theme_adaptable'); + $description = get_string('socialpaddingtopdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '0%', $from0to2point5percent); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/socialtarget'; + $title = get_string('socialtarget', 'theme_adaptable'); + $description = get_string('socialtargetdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '_self', $htmltarget); + $page->add($setting); + + $name = 'theme_adaptable/socialiconlist'; + $title = get_string('socialiconlist', 'theme_adaptable'); + $default = ''; + $description = get_string('socialiconlistdesc', 'theme_adaptable'); + $setting = new admin_setting_configtextarea($name, $title, $description, $default, PARAM_RAW, '50', '10'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/header_user.php b/theme/adaptable/settings/header_user.php new file mode 100644 index 0000000..49da50c --- /dev/null +++ b/theme/adaptable/settings/header_user.php @@ -0,0 +1,158 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_usernav', get_string('usernav', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_usernav', get_string('usernavheading', 'theme_adaptable'), + format_text(get_string('usernavdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Position of the username. + $name = 'theme_adaptable/usernameposition'; + $title = get_string('usernameposition', 'theme_adaptable'); + $description = get_string('usernamepositiondesc', 'theme_adaptable'); + $poschoices = array( + 'left' => get_string('left', 'editor'), + 'right' => get_string('right', 'editor') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'left', $poschoices); + $page->add($setting); + + $name = 'theme_adaptable/hideinforum'; + $title = get_string('hideinforum', 'theme_adaptable'); + $description = get_string('hideinforumdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable My. + $name = 'theme_adaptable/enablemy'; + $title = get_string('enablemy', 'theme_adaptable'); + $description = get_string('enablemydesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable View Profile. + $name = 'theme_adaptable/enableprofile'; + $title = get_string('enableprofile', 'theme_adaptable'); + $description = get_string('enableprofiledesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Edit Profile. + $name = 'theme_adaptable/enableeditprofile'; + $title = get_string('enableeditprofile', 'theme_adaptable'); + $description = get_string('enableeditprofiledesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Calendar. + $name = 'theme_adaptable/enablecalendar'; + $title = get_string('enablecalendar', 'theme_adaptable'); + $description = get_string('enablecalendardesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Private Files. + $name = 'theme_adaptable/enableprivatefiles'; + $title = get_string('enableprivatefiles', 'theme_adaptable'); + $description = get_string('enableprivatefilesdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Grades. + $name = 'theme_adaptable/enablegrades'; + $title = get_string('enablegrades', 'theme_adaptable'); + $description = get_string('enablegradesdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Badges. + $name = 'theme_adaptable/enablebadges'; + $title = get_string('enablebadges', 'theme_adaptable'); + $description = get_string('enablebadgesdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Preferences. + $name = 'theme_adaptable/enablepref'; + $title = get_string('enablepref', 'theme_adaptable'); + $description = get_string('enableprefdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Notes. + $name = 'theme_adaptable/enablenote'; + $title = get_string('enablenote', 'theme_adaptable'); + $description = get_string('enablenotedesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Blog. + $name = 'theme_adaptable/enableblog'; + $title = get_string('enableblog', 'theme_adaptable'); + $description = get_string('enableblogdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Forum posts. + $name = 'theme_adaptable/enableposts'; + $title = get_string('enableposts', 'theme_adaptable'); + $description = get_string('enablepostsdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable My Feedback. + $name = 'theme_adaptable/enablefeed'; + $title = get_string('enablefeed', 'theme_adaptable'); + $description = get_string('enablefeeddesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable Accessibility Tool. + $name = 'theme_adaptable/enableaccesstool'; + $title = get_string('enableaccesstool', 'theme_adaptable'); + $description = get_string('enableaccesstooldesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/importexport_settings.php b/theme/adaptable/settings/importexport_settings.php new file mode 100644 index 0000000..8322101 --- /dev/null +++ b/theme/adaptable/settings/importexport_settings.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Import / Export settings. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$page = new admin_settingpage('theme_adaptable_importexport', get_string('properties', 'theme_adaptable')); +if ($ADMIN->fulltree) { + if (file_exists("{$CFG->dirroot}/theme/adaptable/settings/adaptable_admin_setting_getprops.php")) { + require_once($CFG->dirroot . '/theme/adaptable/settings/adaptable_admin_setting_getprops.php'); + require_once($CFG->dirroot . '/theme/adaptable/settings/adaptable_admin_setting_putprops.php'); + } else if (!empty($CFG->themedir) && file_exists("{$CFG->themedir}/adaptable/settings/adaptable_admin_setting_getprops.php")) { + require_once($CFG->themedir . '/adaptable/settings/adaptable_admin_setting_getprops.php'); + require_once($CFG->themedir . '/adaptable/settings/adaptable_admin_setting_putprops.php'); + } + + $page->add(new admin_setting_heading('theme_adaptable_importexport', + get_string('propertiessub', 'theme_adaptable'), + format_text(get_string('propertiesdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $adaptableexportprops = optional_param('theme_adaptable_getprops_saveprops', 0, PARAM_INT); + $adaptableprops = \theme_adaptable\toolbox::compile_properties('adaptable'); + $page->add(new adaptable_admin_setting_getprops('theme_adaptable_getprops', + get_string('propertiesproperty', 'theme_adaptable'), + get_string('propertiesvalue', 'theme_adaptable'), + $adaptableprops, + 'theme_adaptable_importexport', + get_string('propertiesreturn', 'theme_adaptable'), + get_string('propertiesexport', 'theme_adaptable'), + $adaptableexportprops + )); + + // Import theme settings section (put properties). + $name = 'theme_adaptable/theme_adaptable_putprops_import_heading'; + $heading = get_string('putpropertiesheading', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + $setting = new adaptable_admin_setting_putprops('theme_adaptable_putprops', + get_string('putpropertiesname', 'theme_adaptable'), + get_string('putpropertiesdesc', 'theme_adaptable'), + 'adaptable', + '\theme_adaptable\toolbox::put_properties' + ); + $setting->set_updatedcallback('purge_all_caches'); + $page->add($setting); +} +$ADMIN->add('theme_adaptable', $page); \ No newline at end of file diff --git a/theme/adaptable/settings/layout.php b/theme/adaptable/settings/layout.php new file mode 100644 index 0000000..0cceda0 --- /dev/null +++ b/theme/adaptable/settings/layout.php @@ -0,0 +1,198 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_layout', get_string('layoutsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_layout', get_string('layoutsettingsheading', 'theme_adaptable'), + format_text(get_string('layoutdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Background Image. + $name = 'theme_adaptable/homebk'; + $title = get_string('homebk', 'theme_adaptable'); + $description = get_string('homebkdesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, 'homebk'); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Display block in the Left/Right side. + $name = 'theme_adaptable/blockside'; + $title = get_string('blockside', 'theme_adaptable'); + $description = get_string('blocksidedesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, 0, + array( + 0 => get_string('rightblocks', 'theme_adaptable'), + 1 => get_string('leftblocks', 'theme_adaptable'), + )); + $page->add($setting); + + // Fullscreen width. + $name = 'theme_adaptable/fullscreenwidth'; + $title = get_string('fullscreenwidth', 'theme_adaptable'); + $description = get_string('fullscreenwidthdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '98%', $from95to100percent); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Standard screen width. + $name = 'theme_adaptable/standardscreenwidth'; + $title = get_string('standardscreenwidth', 'theme_adaptable'); + $description = get_string('standardscreenwidthdesc', 'theme_adaptable'); + $choices = array( + 'standard' => '1170px', + 'narrow' => '1000px' + ); + $setting = new admin_setting_configselect($name, $title, $description, 'standard', $choices); + $page->add($setting); + + // Show sidebar when not logged. + $name = 'theme_adaptable/sidebarnotlogged'; + $title = get_string('sidebarnotlogged', 'theme_adaptable'); + $description = get_string('sidebarnotloggeddesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Emoticons size. + $name = 'theme_adaptable/emoticonsize'; + $title = get_string('emoticonsize', 'theme_adaptable'); + $description = get_string('emoticonsizedesc', 'theme_adaptable'); + $default = '16px'; + $choices = $standardfontsize; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Info icon colour. + $name = 'theme_adaptable/infoiconcolor'; + $title = get_string('infoiconcolor', 'theme_adaptable'); + $description = get_string('infoiconcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#5bc0de', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Danger icon colour. + $name = 'theme_adaptable/dangericoncolor'; + $title = get_string('dangericoncolor', 'theme_adaptable'); + $description = get_string('dangericoncolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#d9534f', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Adaptable Tabbed layout changes. + $name = 'theme_adaptable/tabbedlayoutheading'; + $heading = get_string('tabbedlayoutheading', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Course page tabbed layout. + $name = 'theme_adaptable/tabbedlayoutcoursepage'; + $title = get_string('tabbedlayoutcoursepage', 'theme_adaptable'); + $description = get_string('tabbedlayoutcoursepagedesc', 'theme_adaptable'); + $default = 0; + $choices = $tabbedlayoutdefaultscourse; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Have a link back to the course page in the course tabs. + $name = 'theme_adaptable/tabbedlayoutcoursepagelink'; + $title = get_string('tabbedlayoutcoursepagelink', 'theme_adaptable'); + $description = get_string('tabbedlayoutcoursepagelinkdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Course page tab colour selected. + $name = 'theme_adaptable/tabbedlayoutcoursepagetabcolorselected'; + $title = get_string('tabbedlayoutcoursepagetabcolorselected', 'theme_adaptable'); + $description = get_string('tabbedlayoutcoursepagetabcolorselecteddesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#06c', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course page tab colour unselected. + $name = 'theme_adaptable/tabbedlayoutcoursepagetabcolorunselected'; + $title = get_string('tabbedlayoutcoursepagetabcolorunselected', 'theme_adaptable'); + $description = get_string('tabbedlayoutcoursepagetabcolorunselecteddesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#eee', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Course home page tab persistence time. + $name = 'theme_adaptable/tabbedlayoutcoursepagetabpersistencetime'; + $title = get_string('tabbedlayoutcoursepagetabpersistencetime', 'theme_adaptable'); + $description = get_string('tabbedlayoutcoursepagetabpersistencetimedesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '30', PARAM_INT); + $page->add($setting); + + // Dashboard page tabbed layout. + $name = 'theme_adaptable/tabbedlayoutdashboard'; + $title = get_string('tabbedlayoutdashboard', 'theme_adaptable'); + $description = get_string('tabbedlayoutdashboarddesc', 'theme_adaptable'); + $default = 0; + $choices = $tabbedlayoutdefaultsdashboard; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Dashboard page tab colour selected. + $name = 'theme_adaptable/tabbedlayoutdashboardcolorselected'; + $title = get_string('tabbedlayoutdashboardtabcolorselected', 'theme_adaptable'); + $description = get_string('tabbedlayoutdashboardtabcolorselecteddesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#06c', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Dashboard page tab colour unselected. + $name = 'theme_adaptable/tabbedlayoutdashboardcolorunselected'; + $title = get_string('tabbedlayoutdashboardtabcolorunselected', 'theme_adaptable'); + $description = get_string('tabbedlayoutdashboardtabcolorunselecteddesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#eee', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/tabbedlayoutdashboardtab1condition'; + $title = get_string('tabbedlayoutdashboardtab1condition', 'theme_adaptable'); + $description = get_string('tabbedlayoutdashboardtab1conditiondesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW, ''); + $page->add($setting); + + $name = 'theme_adaptable/tabbedlayoutdashboardtab2condition'; + $title = get_string('tabbedlayoutdashboardtab2condition', 'theme_adaptable'); + $description = get_string('tabbedlayoutdashboardtab2conditiondesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW, ''); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/layout_responsive.php b/theme/adaptable/settings/layout_responsive.php new file mode 100644 index 0000000..a579222 --- /dev/null +++ b/theme/adaptable/settings/layout_responsive.php @@ -0,0 +1,168 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015-2017 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_mobile', get_string('responsivesettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_mobile', get_string('responsivesettingsheading', 'theme_adaptable'), + format_text(get_string('responsivesettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Hide Full Header. + $name = 'theme_adaptable/responsiveheader'; + $title = get_string('responsiveheader', 'theme_adaptable'); + $description = get_string('responsiveheaderdesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Hide Social icons. + $name = 'theme_adaptable/responsivesocial'; + $title = get_string('responsivesocial', 'theme_adaptable'); + $description = get_string('responsivesocialdesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/responsivesocialsize'; + $title = get_string('responsivesocialsize', 'theme_adaptable'); + $description = get_string('responsivesocialsizedesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '34px', $from14to46px); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Hide Logo. + $name = 'theme_adaptable/responsivelogo'; + $title = get_string('responsivelogo', 'theme_adaptable'); + $description = get_string('responsivelogodesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Hide course title. + $name = 'theme_adaptable/responsivecoursetitle'; + $title = get_string('responsivecoursetitle', 'theme_adaptable'); + $description = get_string('responsivecoursetitledesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Hide activity / section navigation. + $name = 'theme_adaptable/responsivesectionnav'; + $title = get_string('responsivesectionnav', 'theme_adaptable'); + $description = get_string('responsivesectionnavdesc', 'theme_adaptable'); + $radchoices = array( + 0 => get_string('show', 'theme_adaptable'), + 1 => get_string('hide', 'theme_adaptable'), + ); + $default = 1; + $setting = new admin_setting_configselect($name, $title, $description, $default, $radchoices); + $page->add($setting); + + // Show search icon on small screens. + $name = 'theme_adaptable/responsivesearchicon'; + $title = get_string('responsivesearchicon', 'theme_adaptable'); + $description = get_string('responsivesearchicondesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Hide Ticker. + $name = 'theme_adaptable/responsiveticker'; + $title = get_string('responsiveticker', 'theme_adaptable'); + $description = get_string('responsivetickerdesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Hide breadcrumbs on small screens. + $name = 'theme_adaptable/responsivebreadcrumb'; + $title = get_string('responsivebreadcrumb', 'theme_adaptable'); + $description = get_string('responsivebreadcrumbdesc', 'theme_adaptable'); + $default = 'd-none d-md-flex'; + $choices = $screensizeflex; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Hide Slider. + $name = 'theme_adaptable/responsiveslider'; + $title = get_string('responsiveslider', 'theme_adaptable'); + $description = get_string('responsivesliderdesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Hide Footer. + $name = 'theme_adaptable/responsivepagefooter'; + $title = get_string('responsivepagefooter', 'theme_adaptable'); + $description = get_string('responsivepagefooterdesc', 'theme_adaptable'); + $default = 'd-none d-lg-block'; + $choices = $screensizeblock; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices); + $page->add($setting); + + // Mobile colors heading. + $name = 'theme_adaptable/settingsmobilecolors'; + $heading = get_string('settingsmobilecolors', 'theme_adaptable'); + $setting = new admin_setting_heading($name, $heading, ''); + $page->add($setting); + + // Mobile menu background color. + $name = 'theme_adaptable/mobilemenubkcolor'; + $title = get_string('mobilemenubkcolor', 'theme_adaptable'); + $description = get_string('mobilemenubkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#F9F9F9', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Mobile sidebar tab background colour. + $name = 'theme_adaptable/mobileslidebartabbkcolor'; + $title = get_string('mobileslidebartabbkcolor', 'theme_adaptable'); + $description = get_string('mobileslidebartabbkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#F9F9F9', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Mobile sidebar tab icon colour. + $name = 'theme_adaptable/mobileslidebartabiconcolor'; + $title = get_string('mobileslidebartabiconcolor', 'theme_adaptable'); + $description = get_string('mobileslidebartabiconcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#000000', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/login.php b/theme/adaptable/settings/login.php new file mode 100644 index 0000000..1d077e2 --- /dev/null +++ b/theme/adaptable/settings/login.php @@ -0,0 +1,121 @@ +<?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/>. + +/** + * Login page settings + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @copyright 2019 G J Barnard (http://moodle.org/user/profile.php?id=442195) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +// Login page heading. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_login', get_string('loginsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_login', get_string('loginsettingsheading', 'theme_adaptable'), + format_text(get_string('logindesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Login page background image. + $name = 'theme_adaptable/loginbgimage'; + $title = get_string('loginbgimage', 'theme_adaptable'); + $description = get_string('loginbgimagedesc', 'theme_adaptable'); + $setting = new admin_setting_configstoredfile($name, $title, $description, 'loginbgimage'); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Login page background style. + $name = 'theme_adaptable/loginbgstyle'; + $title = get_string('loginbgstyle', 'theme_adaptable'); + $description = get_string('loginbgstyledesc', 'theme_adaptable'); + $default = 'cover'; + $setting = new admin_setting_configselect($name, $title, $description, $default, + array( + 'cover' => get_string('stylecover', 'theme_adaptable'), + 'stretch' => get_string('stylestretch', 'theme_adaptable') + ) + ); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Login page background opacity. + $opactitychoices = array( + '0.0' => '0.0', + '0.1' => '0.1', + '0.2' => '0.2', + '0.3' => '0.3', + '0.4' => '0.4', + '0.5' => '0.5', + '0.6' => '0.6', + '0.7' => '0.7', + '0.8' => '0.8', + '0.9' => '0.9', + '1.0' => '1.0' + ); + + $name = 'theme_adaptable/loginbgopacity'; + $title = get_string('loginbgopacity', 'theme_adaptable'); + $description = get_string('loginbgopacitydesc', 'theme_adaptable'); + $default = '0.8'; + $setting = new admin_setting_configselect($name, $title, $description, $default, $opactitychoices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Login page header. + $name = 'theme_adaptable/loginheader'; + $title = get_string('loginheader', 'theme_adaptable'); + $description = get_string('loginheaderdesc', 'theme_adaptable'); + $radchoices = array( + 0 => get_string('hide', 'theme_adaptable'), + 1 => get_string('show', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $radchoices); + $page->add($setting); + + // Login page footer. + $name = 'theme_adaptable/loginfooter'; + $title = get_string('loginfooter', 'theme_adaptable'); + $description = get_string('loginfooterdesc', 'theme_adaptable'); + $radchoices = array( + 0 => get_string('hide', 'theme_adaptable'), + 1 => get_string('show', 'theme_adaptable'), + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $radchoices); + $page->add($setting); + + // Top text. + $name = 'theme_adaptable/logintextboxtop'; + $title = get_string('logintextboxtop', 'theme_adaptable'); + $description = get_string('logintextboxtopdesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + // Bottom text. + $name = 'theme_adaptable/logintextboxbottom'; + $title = get_string('logintextboxbottom', 'theme_adaptable'); + $description = get_string('logintextboxbottomdesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/marketing_blocks.php b/theme/adaptable/settings/marketing_blocks.php new file mode 100644 index 0000000..777a8e6 --- /dev/null +++ b/theme/adaptable/settings/marketing_blocks.php @@ -0,0 +1,100 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ +defined('MOODLE_INTERNAL') || die; + +// Marketing blocks section. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_frontpage_blocks', get_string('frontpageblocksettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_marketing', get_string('marketingsettingsheading', 'theme_adaptable'), + format_text(get_string('marketingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/infobox'; + $title = get_string('infobox', 'theme_adaptable'); + $description = get_string('infoboxdesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + $name = 'theme_adaptable/infobox2'; + $title = get_string('infobox2', 'theme_adaptable'); + $description = get_string('infobox2desc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + + $name = 'theme_adaptable/infoboxfullscreen'; + $title = get_string('infoboxfullscreen', 'theme_adaptable'); + $description = get_string('infoboxfullscreendesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/frontpagemarketenabled'; + $title = get_string('frontpagemarketenabled', 'theme_adaptable'); + $description = get_string('frontpagemarketenableddesc', 'theme_adaptable'); + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/frontpagemarketoption'; + $title = get_string('frontpagemarketoption', 'theme_adaptable'); + $description = get_string('frontpagemarketoptiondesc', 'theme_adaptable'); + $choices = $marketblockstyles; + $setting = new admin_setting_configselect($name, $title, $description, 'covtiles', $choices); + $page->add($setting); + + $page->add(new admin_setting_heading('theme_adaptable_marketingbuilder', + get_string('marketingbuilderheading', 'theme_adaptable'), + format_text(get_string('marketingbuilderdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Marketing block region builder. + list('imgblder' => $imgblder, 'totalblocks' => $totalblocks) = \theme_adaptable\toolbox::admin_settings_layout_builder( + $page, 'marketlayoutrow', $marketingfooterbuilderdefaults, $bootstrap12); + + $page->add(new admin_setting_heading('theme_adaptable_blocklayoutcheck', get_string('layoutcheck', 'theme_adaptable'), + format_text(get_string('layoutcheckdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $page->add(new admin_setting_heading('theme_adaptable_layoutbuilder', '', $imgblder)); + + $blkcontmsg = get_string('layoutaddcontentdesc1', 'theme_adaptable'); + $blkcontmsg .= $totalblocks; + $blkcontmsg .= get_string('layoutaddcontentdesc2', 'theme_adaptable'); + + $page->add(new admin_setting_heading('theme_adaptable_blocklayoutaddcontent', get_string('layoutaddcontent', 'theme_adaptable'), + format_text($blkcontmsg, FORMAT_MARKDOWN))); + + + for ($i = 1; $i <= $totalblocks; $i++) { + $name = 'theme_adaptable/market' . $i; + $title = get_string('market', 'theme_adaptable') . $i; + $description = get_string('marketdesc', 'theme_adaptable'); + $default = ''; + $setting = new adaptable_setting_confightmleditor($name, $title, $description, $default); + $page->add($setting); + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/navbar_links.php b/theme/adaptable/settings/navbar_links.php new file mode 100644 index 0000000..5b4bbbe --- /dev/null +++ b/theme/adaptable/settings/navbar_links.php @@ -0,0 +1,84 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2019 Jeremy Hopkins (Coventry University) + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Navbar links. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_navbar_links', get_string('navbarlinkssettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_navbar', get_string('navbarlinksettingsheading', 'theme_adaptable'), + format_text(get_string('navbarlinksettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Help section. + $page->add(new admin_setting_heading('theme_adaptable_help_heading', + get_string('headernavbarhelpheading', 'theme_adaptable'), + format_text(get_string('headernavbarhelpheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/helptarget'; + $title = get_string('helptarget', 'theme_adaptable'); + $description = get_string('helptargetdesc', 'theme_adaptable'); + $choices = array( + '_blank' => get_string('targetnewwindow', 'theme_adaptable'), + '_self' => get_string('targetsamewindow', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, '_blank', $choices); + $page->add($setting); + + // Number of help links. + $name = 'theme_adaptable/helplinkscount'; + $title = get_string('helplinkscount', 'theme_adaptable'); + $description = get_string('helplinkscountdesc', 'theme_adaptable'); + $default = 2; + $setting = new admin_setting_configselect($name, $title, $description, $default, $choices0to12); + $page->add($setting); + + $helplinkscount = get_config('theme_adaptable', 'helplinkscount'); + + for ($helpcount = 1; $helpcount <= $helplinkscount; $helpcount++) { + // Enable help link. + $name = 'theme_adaptable/enablehelp'.$helpcount; + $title = get_string('enablehelp', 'theme_adaptable', array('number' => $helpcount)); + $description = get_string('enablehelpdesc', 'theme_adaptable', array('number' => $helpcount)); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_URL); + $page->add($setting); + + // Help link title. + $name = 'theme_adaptable/helplinktitle'.$helpcount; + $title = get_string('helplinktitle', 'theme_adaptable', array('number' => $helpcount)); + $description = get_string('helplinktitledesc', 'theme_adaptable', array('number' => $helpcount)); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_TEXT); + $page->add($setting); + + $name = 'theme_adaptable/helpprofilefield'.$helpcount; + $title = get_string('helpprofilefield', 'theme_adaptable', array('number' => $helpcount)); + $description = get_string('helpprofilefielddesc', 'theme_adaptable', array('number' => $helpcount)); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + } + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/navbar_settings.php b/theme/adaptable/settings/navbar_settings.php new file mode 100644 index 0000000..b4fd904 --- /dev/null +++ b/theme/adaptable/settings/navbar_settings.php @@ -0,0 +1,284 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Header Navbar. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_navbar_settings', get_string('navbarsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_navbar_settings', get_string('navbarsettingsheading', 'theme_adaptable'), + format_text(get_string('navbardesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Sticky Navbar at the top. See issue #278. + $name = 'theme_adaptable/stickynavbar'; + $title = get_string('stickynavbar', 'theme_adaptable'); + $description = get_string('stickynavbardesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable/Disable menu items. + $name = 'theme_adaptable/enablehome'; + $title = get_string('home'); + $description = get_string('enablehomedesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablehomeredirect'; + $title = get_string('enablehomeredirect', 'theme_adaptable'); + $description = get_string('enablehomeredirectdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablemyhome'; + $title = get_string('myhome'); + $description = get_string('enablemyhomedesc', 'theme_adaptable', get_string('myhome')); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enableevents'; + $title = get_string('events', 'theme_adaptable'); + $description = get_string('enableeventsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablethiscourse'; + $title = get_string('thiscourse', 'theme_adaptable'); + $description = get_string('enablethiscoursedesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablecoursesections'; + $title = get_string('coursesections', 'theme_adaptable'); + $description = get_string('enablecoursesectionsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablecompetencieslink'; + $title = get_string('enablecompetencieslink', 'theme_adaptable'); + $description = get_string('enablecompetencieslinkdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablezoom'; + $title = get_string('enablezoom', 'theme_adaptable'); + $description = get_string('enablezoomdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/defaultzoom'; + $title = get_string('defaultzoom', 'theme_adaptable'); + $description = get_string('defaultzoomdesc', 'theme_adaptable'); + $choices = array( + 'normal' => get_string('normal', 'theme_adaptable'), + 'wide' => get_string('wide', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'wide', $choices); + $page->add($setting); + + // Show / hide text for the Full screen button. + $name = 'theme_adaptable/enablezoomshowtext'; + $title = get_string('enablezoomshowtext', 'theme_adaptable'); + $description = get_string('enablezoomshowtextdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enableshowhideblocks'; + $title = get_string('enableshowhideblocks', 'theme_adaptable'); + $description = get_string('enableshowhideblocksdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Show / hide text for the show / hide blocks button. + $name = 'theme_adaptable/enableshowhideblockstext'; + $title = get_string('enableshowhideblockstext', 'theme_adaptable'); + $description = get_string('enableshowhideblockstextdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/enablenavbarwhenloggedout'; + $title = get_string('enablenavbarwhenloggedout', 'theme_adaptable'); + $description = get_string('enablenavbarwhenloggedoutdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Settings icon and Edit button. + $name = 'theme_adaptable/editsettingsbutton'; + $title = get_string('editsettingsbutton', 'theme_adaptable'); + $description = get_string('editsettingsbuttondesc', 'theme_adaptable'); + $choices = array( + 'cog' => get_string('editsettingsbuttonshowcog', 'theme_adaptable'), + 'button' => get_string('editsettingsbuttonshowbutton', 'theme_adaptable'), + 'cogandbutton' => get_string('editsettingsbuttonshowcogandbutton', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'cog', $choices ); + $page->add($setting); + + $name = 'theme_adaptable/displayeditingbuttontext'; + $title = get_string('displayeditingbuttontext', 'theme_adaptable'); + $description = get_string('displayeditingbuttontextdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // My courses section. + $page->add(new admin_setting_heading('theme_adaptable_mycourses_heading', + get_string('headernavbarmycoursesheading', 'theme_adaptable'), + format_text(get_string('headernavbarmycoursesheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/enablemysites'; + $title = get_string('mysites', 'theme_adaptable'); + $description = get_string('enablemysitesdesc', 'theme_adaptable'); + $choices = array( + 'excludehidden' => get_string('mysitesexclude', 'theme_adaptable'), + 'includehidden' => get_string('mysitesinclude', 'theme_adaptable'), + 'disabled' => get_string('mysitesdisabled', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'excludehidden', $choices); + $page->add($setting); + + // Custom profile field value for restricting access to my courses menu. + $name = 'theme_adaptable/enablemysitesrestriction'; + $title = get_string('enablemysitesrestriction', 'theme_adaptable'); + $description = get_string('enablemysitesrestrictiondesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_RAW); + $page->add($setting); + + $name = 'theme_adaptable/mycoursesmenulimit'; + $title = get_string('mycoursesmenulimit', 'theme_adaptable'); + $description = get_string('mycoursesmenulimitdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '20', PARAM_INT); + $page->add($setting); + + $name = 'theme_adaptable/mysitesmaxlength'; + $title = get_string('mysitesmaxlength', 'theme_adaptable'); + $description = get_string('mysitesmaxlengthdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '30', $from20to40); + $page->add($setting); + + $name = 'theme_adaptable/mysitessortoverride'; + $title = get_string('mysitessortoverride', 'theme_adaptable'); + $description = get_string('mysitessortoverridedesc', 'theme_adaptable'); + $choices = array( + 'off' => get_string('mysitessortoverrideoff', 'theme_adaptable'), + 'strings' => get_string('mysitessortoverridestrings', 'theme_adaptable'), + 'profilefields' => get_string('mysitessortoverrideprofilefields', 'theme_adaptable'), + 'profilefieldscohort' => get_string('mysitessortoverrideprofilefieldscohort', 'theme_adaptable'), + 'myoverview' => get_string('mysitessortoverridemyoverview', 'theme_adaptable'), + 'last' => get_string('mysitessortoverridelast', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'myoverview', $choices); + $page->add($setting); + + $name = 'theme_adaptable/mysitessortoverridefield'; + $title = get_string('mysitessortoverridefield', 'theme_adaptable'); + $description = get_string('mysitessortoverridefielddesc', 'theme_adaptable'); + $default = ''; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_RAW); + $page->add($setting); + + $name = 'theme_adaptable/mysitesmenudisplay'; + $title = get_string('mysitesmenudisplay', 'theme_adaptable'); + $description = get_string('mysitesmenudisplaydesc', 'theme_adaptable'); + $displaychoices = array( + 'shortcodenohover' => get_string('mysitesmenudisplayshortcodenohover', 'theme_adaptable'), + 'shortcodehover' => get_string('mysitesmenudisplayshortcodefullnameonhover', 'theme_adaptable'), + 'fullnamenohover' => get_string('mysitesmenudisplayfullnamenohover', 'theme_adaptable'), + 'fullnamehover' => get_string('mysitesmenudisplayfullnamefullnameonhover', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'shortcodehover', $displaychoices); + $page->add($setting); + + $name = 'theme_adaptable/chiddenicon'; + $title = get_string('chiddenicon', 'theme_adaptable'); + $description = get_string('chiddenicondesc', 'theme_adaptable'); + $default = 'eye-slash'; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_TEXT); + $page->add($setting); + + $name = 'theme_adaptable/cfrozenicon'; + $title = get_string('cfrozenicon', 'theme_adaptable'); + $description = get_string('cfrozenicondesc', 'theme_adaptable'); + $default = 'snowflake-o'; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_TEXT); + $page->add($setting); + + $name = 'theme_adaptable/cneveraccessedicon'; + $title = get_string('cneveraccessedicon', 'theme_adaptable'); + $description = get_string('cneveraccessedicondesc', 'theme_adaptable'); + $default = 'exclamation-circle'; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_TEXT); + $page->add($setting); + + $name = 'theme_adaptable/cdefaulticon'; + $title = get_string('cdefaulticon', 'theme_adaptable'); + $description = get_string('cdefaulticondesc', 'theme_adaptable'); + $default = 'graduation-cap'; + $setting = new admin_setting_configtext($name, $title, $description, $default, PARAM_TEXT); + $page->add($setting); + + // This course section. + $page->add(new admin_setting_heading('theme_adaptable_thiscourse_heading', + get_string('headernavbarthiscourseheading', 'theme_adaptable'), + format_text(get_string('headernavbarthiscourseheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Display participants. + $name = 'theme_adaptable/displayparticipants'; + $title = get_string('displayparticipants', 'theme_adaptable'); + $description = get_string('displayparticipantsdesc', 'theme_adaptable'); + $radchoices = array( + 0 => get_string('hide', 'theme_adaptable'), + 1 => get_string('show', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $radchoices); + $page->add($setting); + + // Display Grades. + $name = 'theme_adaptable/displaygrades'; + $title = get_string('displaygrades', 'theme_adaptable'); + $description = get_string('displaygradesdesc', 'theme_adaptable'); + $radchoices = array( + 0 => get_string('hide', 'theme_adaptable'), + 1 => get_string('show', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 1, $radchoices); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/navbar_styles.php b/theme/adaptable/settings/navbar_styles.php new file mode 100644 index 0000000..ace814c --- /dev/null +++ b/theme/adaptable/settings/navbar_styles.php @@ -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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015 Jeremy Hopkins (Coventry University) + * @copyright 2015 Fernando Acedo (3-bits.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Header Navbar. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_navbar_styles', get_string('navbarstyles', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_navbar_styles', get_string('navbarstylesheading', 'theme_adaptable'), + format_text(get_string('navbarstylesdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Main menu background color. + $name = 'theme_adaptable/menubkcolor'; + $title = get_string('menubkcolor', 'theme_adaptable'); + $description = get_string('menubkcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#ffffff', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main menu text color. + $name = 'theme_adaptable/menufontcolor'; + $title = get_string('menufontcolor', 'theme_adaptable'); + $description = get_string('menufontcolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#222222', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main menu hover color. + $name = 'theme_adaptable/menuhovercolor'; + $title = get_string('menuhovercolor', 'theme_adaptable'); + $description = get_string('menuhovercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#00B3A1', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Main menu bottom border color. + $name = 'theme_adaptable/menubordercolor'; + $title = get_string('menubordercolor', 'theme_adaptable'); + $description = get_string('menubordercolordesc', 'theme_adaptable'); + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, '#00B3A1', $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/navbardisplayicons'; + $title = get_string('navbardisplayicons', 'theme_adaptable'); + $description = get_string('navbardisplayiconsdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $name = 'theme_adaptable/navbardisplaysubmenuarrow'; + $title = get_string('navbardisplaysubmenuarrow', 'theme_adaptable'); + $description = get_string('navbardisplaysubmenuarrowdesc', 'theme_adaptable'); + $default = false; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Dropdown border radius. + $name = 'theme_adaptable/navbardropdownborderradius'; + $title = get_string('navbardropdownborderradius', 'theme_adaptable'); + $description = get_string('navbardropdownborderradiusdesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '0px', $from0to20px); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Dropdown Menu Item Link background hover colour. + $name = 'theme_adaptable/navbardropdownhovercolor'; + $title = get_string('navbardropdownhovercolor', 'theme_adaptable'); + $description = get_string('navbardropdownhovercolordesc', 'theme_adaptable'); + $default = '#EEE'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Dropdown Menu Item Link text colour. + $name = 'theme_adaptable/navbardropdowntextcolor'; + $title = get_string('navbardropdowntextcolor', 'theme_adaptable'); + $description = get_string('navbardropdowntextcolordesc', 'theme_adaptable'); + $default = '#007'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Dropdown Menu Item Link text hover colour. + $name = 'theme_adaptable/navbardropdowntexthovercolor'; + $title = get_string('navbardropdowntexthovercolor', 'theme_adaptable'); + $description = get_string('navbardropdowntexthovercolordesc', 'theme_adaptable'); + $default = '#000'; + $previewconfig = null; + $setting = new admin_setting_configcolourpicker($name, $title, $description, $default, $previewconfig); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + // Dropdown transition time. + $name = 'theme_adaptable/navbardropdowntransitiontime'; + $title = get_string('navbardropdowntransitiontime', 'theme_adaptable'); + $description = get_string('navbardropdowntransitiontimedesc', 'theme_adaptable'); + $setting = new admin_setting_configselect($name, $title, $description, '0.2s', $from0to1second); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/print.php b/theme/adaptable/settings/print.php new file mode 100644 index 0000000..809866d --- /dev/null +++ b/theme/adaptable/settings/print.php @@ -0,0 +1,69 @@ +<?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/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright © 2020 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// Print settings. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_print', get_string('printsettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_print', get_string('printsettingsheading', 'theme_adaptable'), + format_text(get_string('printsettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + $name = 'theme_adaptable/printpageorientation'; + $title = get_string('printpageorientation', 'theme_adaptable'); + $description = get_string('printpageorientationdesc', 'theme_adaptable'); + $choices = array( + 'landscape' => get_string('landscape', 'theme_adaptable'), + 'portrait' => get_string('portrait', 'theme_adaptable') + ); + $setting = new admin_setting_configselect($name, $title, $description, 'landscape', $choices); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/printbodyfontsize'; + $title = get_string('printbodyfontsize', 'theme_adaptable'); + $description = get_string('printbodyfontsizedesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '11pt', PARAM_TEXT); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/printmargin'; + $title = get_string('printmargin', 'theme_adaptable'); + $description = get_string('printmargindesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '2cm 1cm 2cm 2cm', PARAM_TEXT); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $name = 'theme_adaptable/printlineheight'; + $title = get_string('printlineheight', 'theme_adaptable'); + $description = get_string('printlineheightdesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '1.2', PARAM_TEXT); + $setting->set_updatedcallback('theme_reset_all_caches'); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/settings/templates.php b/theme/adaptable/settings/templates.php new file mode 100644 index 0000000..580bfa6 --- /dev/null +++ b/theme/adaptable/settings/templates.php @@ -0,0 +1,84 @@ +<?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/>. + +/** + * Template settings. + * + * @package theme_adaptable + * @copyright 2020 Gareth J Barnard + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +// Templates. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_templates', get_string('templatessettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_templates_heading', get_string('templatesheading', 'theme_adaptable'), + format_text(get_string('templatesheadingdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + static $templates = array( + 'mod_forum/forum_post_email_htmlemail' => 'mod_forum/forum_post_email_htmlemail', + 'mod_forum/forum_post_email_htmlemail_body' => 'mod_forum/forum_post_email_htmlemail_body', + 'mod_forum/forum_post_email_textemail' => 'mod_forum/forum_post_email_textemail', + 'mod_forum/forum_post_emaildigestbasic_htmlemail' => 'mod_forum/forum_post_emaildigestbasic_htmlemail', + 'mod_forum/forum_post_emaildigestbasic_textemail' => 'mod_forum/forum_post_emaildigestbasic_textemail', + 'mod_forum/forum_post_emaildigestfull_htmlemail' => 'mod_forum/forum_post_emaildigestfull_htmlemail', + 'mod_forum/forum_post_emaildigestfull_textemail' => 'mod_forum/forum_post_emaildigestfull_textemail' + ); + $name = 'theme_adaptable/templatessel'; + $title = get_string('templatessel', 'theme_adaptable'); + $description = get_string('templatesseldesc', 'theme_adaptable'); + $default = array(); + $setting = new admin_setting_configmultiselect($name, $title, $description, $default, $templates); + $page->add($setting); + + $asettings->add($page); + + $overridetemplates = get_config('theme_adaptable', 'templatessel'); + if ($overridetemplates) { + if (file_exists("{$CFG->dirroot}/theme/adaptable/settings/adaptable_admin_setting_configtemplate.php")) { + require_once($CFG->dirroot.'/theme/adaptable/settings/adaptable_admin_setting_configtemplate.php'); + } else if (!empty($CFG->themedir) && + file_exists("{$CFG->themedir}/adaptable/settings/adaptable_admin_setting_configtemplate.php")) { + require_once($CFG->themedir.'/adaptable/settings/adaptable_admin_setting_configtemplate.php'); + } + + $overridetemplates = explode(',', $overridetemplates); + foreach ($overridetemplates as $overridetemplate) { + $overridetemplatesetting = str_replace('/', '_', $overridetemplate); + $temppage = new admin_settingpage('theme_adaptable_templates_'.$overridetemplatesetting, + get_string('overridetemplate', 'theme_adaptable', $overridetemplate)); + + $name = 'theme_adaptable/activatetemplateoverride_'.$overridetemplatesetting; + $title = get_string('activatetemplateoverride', 'theme_adaptable', $overridetemplate); + $description = get_string('activatetemplateoverridedesc', 'theme_adaptable', + array('template' => $overridetemplate, 'setting' => $overridetemplatesetting)); + $setting = new admin_setting_configcheckbox($name, $title, $description, false); + $temppage->add($setting); + + $name = 'theme_adaptable/overriddentemplate_'.$overridetemplatesetting; + $title = get_string('overriddentemplate', 'theme_adaptable', $overridetemplate); + $description = get_string('overriddentemplatedesc', 'theme_adaptable', $overridetemplate); + $default = ''; + $setting = new adaptable_admin_setting_configtemplate($name, $title, $description, $default, $overridetemplate); + $temppage->add($setting); + + $asettings->add($temppage); + } + } +} diff --git a/theme/adaptable/settings/user.php b/theme/adaptable/settings/user.php new file mode 100644 index 0000000..d9dbcc8 --- /dev/null +++ b/theme/adaptable/settings/user.php @@ -0,0 +1,74 @@ +<?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/>. + +/** + * User settings + * + * @package theme_adaptable + * @copyright © 2019 - Coventry University + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +// User profile. +if ($ADMIN->fulltree) { + $page = new admin_settingpage('theme_adaptable_user', get_string('usersettings', 'theme_adaptable')); + + $page->add(new admin_setting_heading('theme_adaptable_user', get_string('usersettingsheading', 'theme_adaptable'), + format_text(get_string('usersettingsdesc', 'theme_adaptable'), FORMAT_MARKDOWN))); + + // Custom course title. + $name = 'theme_adaptable/customcoursetitle'; + $title = get_string('customcoursetitle', 'theme_adaptable'); + $description = get_string('customcoursetitledesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_TEXT); + $page->add($setting); + + // Custom course subtitle. + $name = 'theme_adaptable/customcoursesubtitle'; + $title = get_string('customcoursesubtitle', 'theme_adaptable'); + $description = get_string('customcoursesubtitledesc', 'theme_adaptable'); + $setting = new admin_setting_configtext($name, $title, $description, '', PARAM_TEXT); + $page->add($setting); + + // Enable or disable tabbed profile. + $name = 'theme_adaptable/enabletabbedprofile'; + $title = get_string('enabletabbedprofile', 'theme_adaptable'); + $description = get_string('enabletabbedprofiledesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable or disable tabbed profile edit profile link. + $name = 'theme_adaptable/enabledtabbedprofileeditprofilelink'; + $title = get_string('enabledtabbedprofileeditprofilelink', 'theme_adaptable'); + $description = get_string('enabledtabbedprofileeditprofilelinkdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + // Enable or disable tabbed profile user preferences link. + $name = 'theme_adaptable/enabledtabbedprofileuserpreferenceslink'; + $title = get_string('enabledtabbedprofileuserpreferenceslink', 'theme_adaptable'); + $description = get_string('enabledtabbedprofileuserpreferenceslinkdesc', 'theme_adaptable'); + $default = true; + $setting = new admin_setting_configcheckbox($name, $title, $description, $default, true, false); + $page->add($setting); + + $asettings->add($page); +} diff --git a/theme/adaptable/style/adaptable.css b/theme/adaptable/style/adaptable.css new file mode 100644 index 0000000..acc9f26 --- /dev/null +++ b/theme/adaptable/style/adaptable.css @@ -0,0 +1,2535 @@ +/* +* This file is part of Adaptable theme for moodle +* +* 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/>. +* +* +* Adaptable main CSS file +* +* @package theme_adaptable +* @copyright 2015-2019 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @copyright 2018-2019 Manoj Solanki (Coventry University) +* +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +/* Main elements selectors */ +html, +body { + color: [[setting:fontcolor]]; + font-family: [[setting:fontname]], sans-serif; + font-size: [[setting:fontsize]]; + font-weight: [[setting:fontweight]] !important; + line-height: normal; + margin: 0; + padding: 0; +} + +#page-my-index, /* Overridden from Boost theme to ignore background colour set on dashboard. */ +body { + background-color: [[setting:backcolor]]; + background-size: 100% auto; +} + +/* Headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0 0 10px; + color: [[setting:fontheadercolor]]; + font-family: [[setting:fontheadername]], sans-serif; + font-weight: [[setting:fontheaderweight]] !important; +} + +/* Other text tags */ +input, +button, +select, +textarea { + color: [[setting:fontcolor]]; + font-family: [[setting:fontname]], sans-serif; + font-size: [[setting:fontsize]]; + font-weight: [[setting:fontweight]] !important; +} + +/* Links */ +a, +a:visited, +.tabtree .tabrow0 li a { + color: [[setting:linkcolor]]; + text-decoration: none; +} + +a:hover, +a:active { + color: [[setting:linkhover]]; + text-decoration: none; +} + +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +/* Lists */ +li { + line-height: 28px; +} + +#adaptable-page-header-wrapper .dropdown-menu > li > a:hover, +#adaptable-page-header-wrapper .dropdown-menu > li > a:focus, +#adaptable-page-header-wrapper .dropdown-submenu:hover > a, +#adaptable-page-header-wrapper .dropdown-submenu:focus > a { + background-image: none; +} + +/* Text Selection */ +::selection { + color: [[setting:selectiontext]]; + background-color: [[setting:selectionbackground]]; +} + +::-moz-selection { + color: [[setting:selectiontext]]; + background-color: [[setting:selectionbackground]]; +} + +/* Forms */ +fieldset { + min-width: 0; + /* width: 90%; */ +} + +.mform fieldset { + margin-left: 0; +} + +.mform fieldset.collapsible legend a.fheader { + margin-left: 0; +} + +.form-inline { + margin: 5px 0; +} + +.col-form-label.d-inline { + float: right; + margin: 3px 5px 5px 0; +} + +@-moz-document url-prefix() { + fieldset { + display: table-cell; + } + + textarea { + box-sizing: border-box; + } + + textarea[cols] { + width: 100%; + } +} + +select { + width: auto; +} + + +/* main headings */ +#page-content { + padding: 12px 0 20px; + position: relative; + width: auto; +} + +.headingblock, h2.main, +#site-news-forum h2, +#frontpage-course-list h2, +#frontpage-category-names h2, +#frontpage-category-combo h2, +.course_category_tree .category > .info > .categoryname { + font-family: [[setting:fontheadername]], sans-serif !important; + font-weight: [[setting:fontheaderweight]] !important; + color: [[setting:fontheadercolor]]; + font-size: 24px; + border-bottom: 0 solid #eee; + margin-bottom: 20px; +} + +#nologo, #nologo a, +.generalbox h2, +h3.sectionname, +div.tabtree li.selected a span, +.forumpost .subject, +.blog_entry div.subject a, +h2.headingblock, +h2.main, +h3.main, +h2.main a, +h3.main a, +div.loginpanel h2, +div.signuppanel h2 { + font-weight: bold; +} + +.well h4 { + margin-left: 30px; +} + +#header2 { + background-color: transparent; + border-bottom: none; + clear: both; + color: [[setting:headertextcolor2]]; + margin-bottom: 0; + min-height: [[setting:pageheaderheight]]; + padding-top: 0; +} + +/* Override default boost max height for header2. */ +#header2 .navbar { + max-height: 150px; +} + +#header2 .row { + margin: 0 0 0 -15px; + min-height: [[setting:pageheaderheight]]; +} + +#adaptable-page-header-wrapper #header2 i, +#adaptable-page-header-wrapper #page-header i { + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.10); +} + +/* Page */ +#page { + clear: both; + margin: 0; + padding: 0; + max-width: inherit; + -ms-flex-direction: column; + flex-direction: column; + display: -ms-flexbox; + display: flex; + height: 100%; + [[setting:homebkg]] +} + +.container.outercont { + -ms-flex-negative: 0; + flex-shrink: 0; +} + +/* Header */ +#adaptable-page-header-wrapper { + background-color: [[setting:headerbkcolor2]]; + width: 100%; + z-index: 10; +} + +#page-header { + background-color: transparent; + border-bottom: none; + clear: both; + margin-bottom: 0; + min-height: [[setting:pageheaderheight]]; + padding-top: 0; +} + +#above-header .fa { + color: [[setting:headertextcolor]]; +} + +#logo { + margin-bottom: 2px; + margin-left: 0; + margin-top: 0; + max-width: 250px; +} + +#logocontainer { + float: left; + margin: 5px 15px 10px 0; +} + +#sitetitle, +#sitetitle a, +#sitetitle p, +#sitetitle h1, +#sitetitle h2, +#sitetitle h3, +#sitetitle h4, +#sitetitle h5, +#sitetitle h6 { + color: [[setting:fonttitlecolor]]; + + font-family: [[setting:fonttitlename]], sans-serif; + font-size: [[setting:fonttitlesize]]; + font-weight: [[setting:fonttitleweight]] !important; + + line-height: 120%; + margin: 0; + overflow: visible; + position: relative; + text-overflow: ellipsis; + width: auto; + vertical-align: middle; +} + +#sitetitle img { + max-height: 107px; + max-width: 100%; + width: auto; +} + +#coursesearchbox2 { + border-radius: 0; + box-shadow: none; + min-width: 240px; + padding: 8px 10px 5px; + width: auto; + background: #fff url([[pix:theme|search2]]) no-repeat 98% 50%; +} + +#coursesearch2 { + padding-top: 23px; +} + +.socialbox { + margin-bottom: 15px; + margin-top: 15px; + vertical-align: middle; +} + +.socialbox a { + color: [[setting:headertextcolor2]] !important; +} + +.socialicons { + padding-top: [[setting:socialpaddingtop]]; +} + +.socialicons a { + padding-left: [[setting:socialpaddingsidehalf]]px; + padding-right: [[setting:socialpaddingsidehalf]]px); +} + +.socialicons a:first-of-type { + padding-left: [[setting:socialpaddingside]]px; +} + +.socialicons a:last-of-type { + padding-right: [[setting:socialpaddingside]]px; +} + +.socialicons a i { + /* Make at least 29px, values look odd but on inspection they are needed to be so for FontAwesome. */ + font-size: [[setting:socialsize]]; + line-height: [[setting:socialsize]]; +} + +.headermenu { + float: right; + padding-top: 0; + padding-right: 0; + font-size: 12px; + color: #ccc; + text-align: right; + margin-left: 15px; +} + +.headermenu span.fa.fa-angle-down { + font-size: 13px; + padding-left: 5px; +} + +.headermenu .usermendrop a { + font-family: [[setting:fontname]], sans-serif; + font-size: [[setting:topmenufontsize]]; +} + +.headermenu .usermendrop a:hover { + text-decoration: none !important; +} + +.usermendrop span { + font-size: [[setting:topmenufontsize]]; + padding-right: 5px; + height: 30px; +} + +#page-header .userimg { + float: right; + margin-bottom: 20px; + margin-left: 10px; + margin-right: 15px; +} + +.usermenu2.nav { + margin-bottom: 0; + color: [[setting:headertextcolor]]; + font-size: [[setting:topmenufontsize]]; + padding-left: 10px; +} + +.usermenu2.nav a.dropdown-toggle { + font-family: [[setting:fontname]], sans-serif; + font-size: [[setting:topmenufontsize]]; + color: [[setting:headertextcolor]]; +} + +.usermenu2 .fa.fa-comments { + padding-right: 3px; +} + +/* Badges */ +.badge { + padding: .4rem .6em; +} + +a.component-expand .badge { + margin: 0.8em 0.8em 0.4em 0.8em; +} + +.badge, +.count-container { + background-color: [[setting:msgbadgecolor]]; +} + +.badge-primary { + color: #fff; + background-color: #1177d1; } + +.badge-primary[href]:hover, +.badge-primary[href]:focus { + color: #fff; + text-decoration: none; + background-color: #0d5ca2; +} + +.badge-secondary { + color: #212529; + background-color: #ced4da; +} + +.badge-secondary[href]:hover, +.badge-secondary[href]:focus { + color: #212529; + text-decoration: none; + background-color: #b1bbc4; +} + +.badge-success { + color: #fff; + background-color: #5cb85c; +} + +.badge-success[href]:hover, +.badge-success[href]:focus { + color: #fff; + text-decoration: none; + background-color: #449d44; +} + +.badge-info { + color: #212529; + background-color: #5bc0de; +} + +.badge-info[href]:hover, +.badge-info[href]:focus { + color: #212529; + text-decoration: none; + background-color: #31b0d5; +} + +.badge-warning { + color: #212529; + background-color: #f0ad4e; +} + +.badge-warning[href]:hover, +.badge-warning[href]:focus { + color: #212529; + text-decoration: none; + background-color: #ec971f; +} + +.badge-danger { + color: #fff; + background-color: #d9534f; +} + +.badge-danger[href]:hover, +.badge-danger[href]:focus { + color: #fff; + text-decoration: none; + background-color: #c9302c; +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +.badge-light[href]:hover, +.badge-light[href]:focus { + color: #212529; + text-decoration: none; + background-color: #dae0e5; +} + +.badge-dark { + color: #fff; + background-color: #373a3c; +} + +.badge-dark[href]:hover, +.badge-dark[href]:focus { + color: #fff; + text-decoration: none; + background-color: #1f2021; +} + + +.slidewrap { + margin-top: [[setting:slidermargintop]]; + margin-bottom: [[setting:slidermarginbottom]]; +} + +#frontblockregion { + margin-bottom: 20px; + margin-top: 20px; + background-color: [[setting:blockregionbackgroundcolor]]; +} + +#marketblocks { + background-color: [[setting:marketblocksbackgroundcolor]]; + padding-bottom: 10px; +} + +#marketblocks .internalmarket { + padding: 10px 15px 30px; + text-align: center; + border: 1px solid [[setting:marketblockbordercolor]]; + border-top: 0; +} + +.marketimage { + display: block; + width: 100%; + border: 0; +} + +#marketblocks h3 { + font-size: 24px; + line-height: 48px; + font-weight: normal; + margin: 10px auto; +} + +.marketrow { + margin-top: 20px; +} + +img.emoticon { + height: [[setting:emoticonsize]]; + width: [[setting:emoticonsize]]; +} + +.personpic { + text-align: center; +} + +#person img { + border: 6px solid #fff; + max-width: 130px; + display: block; + margin: 0 auto; +} + +.persontitle { + padding-top: 40px; +} + +span.fa-chevron-right.fa { + font-size: 10px; +} + +#theinfo .span3 { + text-align: right; + padding-top: 35px; +} + +#theinfo2 .span3 { + text-align: right; + padding-top: 35px; +} + +#frontpage-course-list h2 { + display: [[setting:enableavailablecourses]]; +} + + +#page-footer { + background-color: [[setting:footerbkcolor]]; + margin: auto 0 0; + text-align: left; + color: [[setting:footertextcolor]] !important; + border-top: 0; + font-size: 110%; + word-wrap: break-word; +} + +#page-footer h3 { + color: [[setting:footertextcolor]] !important; + word-wrap: break-word; +} + +#page-footer a { + color: [[setting:footerlinkcolor]] !important; +} + +#page-footer .block-list > li { + padding: 0.3em 0; +} + +.block-list > li { + line-height: 1.2; +} + +.white li a, .base-tabs a.white, .block-list a.white { + color: #fff; +} + +#page-footer li a span + span { + color: #abb5bf; + padding-left: 5px; +} + +.fa.fa-chevron-right.icon-right-open-mini { + font-size: 10px; +} + +#page-footer .block-list > li a { + padding: 0; +} + +#page-footer .blockplace3 { + display: none; +} + +#page-footer .blockplace2 { + display: none; +} + +#page-site-index #page-footer .blockplace2 { + display: block; +} + +.tool_dataprivacy { + display: [[setting:gdprbutton]]; +} + +.screen-reader-text { + left: -9999px; + position: absolute; + top: -9999px; +} + +.clear { + clear: both; +} + +.headermenu input[name=username], +.headermenu input[name=password] { + height: 12px; + padding-bottom: 4px; +} + +#page-footer .logininfo { + margin: 0 auto 0; + font-size: 90%; + width: auto; +} + +#page-footer .info { + line-height: 21px; + margin: 0 auto; + margin-top: 34px; + padding-top: 10px; + padding-bottom: 10px; + border-top: 1px solid [[setting:dividingline2]]; + font-size: 90%; + color: [[setting:footertextcolor2]]; +} + +#page-footer .info a { + color: [[setting:footerlinkcolor]] !important; +} + +.info nav ul li { + border: medium none; + display: inline-block; + float: left; +} + +.mrl, .mhl, .mal { + margin-right: 20px !important; +} +.mtm, .mvm, .mam { + margin-top: 10px !important; +} + +.info nav ul li a { + border-right: 1px solid #666; + margin: auto; + padding: 0.1em .8em; +} + +#page-footer ol, #page-footer ul { + list-style: none outside none; + margin: 0 0; +} + +#page-my-index #page-navbar { + display: inline; +} + +.navbar .nav-collapse.in { + border-top: none; +} + +.navbar .nav { + display: block; + float: left; + left: 0; + margin: 0 0 0 4px; + position: relative; +} + +.navbar .nav > li > a { + margin: 0 0 0 0 !important; +} + +langmenu a:hover { + background-color: none !important; + text-decoration: none !important; +} + +/* Breadcrumb */ +#page-navbar { + margin-top: 10px; +} + +.breadcrumb { + background: none !important; + background-color: [[setting:breadcrumb]] !important; + padding: 5px; +} + +.breadcrumb li { + text-shadow: none; + margin: 5px 0; +} + +.breadcrumb li span a { + text-decoration: none; +} + +.breadcrumb li span.divider { + display: none !important; +} + +.breadcrumb > a { + display: inline-block; + margin: 5px 0; +} + +.breadcrumb .fa { + display: inline; + margin: 12px 0; +} + +.separator { + margin: 9px; +} + +.separator i { + margin-left: 5px; +} + +.breadcrumb ul i, +.breadcrumb li.lastli span { + color: [[setting:breadcrumbtextcolor]]; + text-decoration: none; +} + +.breadcrumb li a.firstli { + padding-left: 15px; + padding-right: 20px; +} + +.breadcrumb li span.divider { + display: none !important; + border-bottom: 0 solid [[setting:maincolor]]; +} + +.breadcrumb-button { + margin-top: 1px; +} + +.arrow-top { + border-bottom: 18px solid transparent; + border-left: 17px solid [[setting:breadcrumb]]; + border-top: 17px solid transparent; + height: 0; + position: absolute; + right: -17px; + top: 0; + width: 0; + z-index: 1; +} + +.arrow-bottom { + border-bottom: 18px solid transparent; + border-left: 17px solid #fff; + border-top: 17px solid transparent; + height: 0; + position: absolute; + right: -18px; + top: 0; + width: 0; +} + +/* Messages popup. */ +#newmessageoverlay #newmessagetext { + background-color: [[setting:messagepopupbackground]]; + color: [[setting:messagepopupcolor]]; + border-radius: 5px 5px 0 0; +} + +#newmessageoverlay { + border: 3px solid [[setting:messagepopupbackground]]; +} + +.nav .dropdown-menu li a { + border-bottom-color: none !important; +} + +.pagelayout-redirect #content { + width: 60%; + background-color: #fff; + margin: 20px auto; + padding: 1em; + border: 1px solid #dadada; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.15); +} + +div.redirectmessage { + width: 60%; + margin: 10px auto 0 auto; + border: none solid transparent; + font-size: 13px; + background: #fff url([[pix:theme|ajax-loader]]) no-repeat 50% 95%; + padding: 20px 20px 40px; +} + +.singleselect { + width: inherit; +} + +#mode select, +#forummenu select { + height: 36px; +} + +[data-region='post-action'] { + margin: 5px 5px 0 0; +} + +#page-grade-export-xls-index #region-main table { + display: block; + overflow: auto; + width: 100%; +} + +.gradingform_guide .criterion .description { + width: 100%; + padding: 10px 5px 10px 5px; +} + +#notice h4 { + font-size: 24px; + margin-bottom: 20px; + font-weight: bold !important; +} + +#notice.box { + border: none; + padding: 15px; + margin-top: 100px; + font-size: 18px; + color: [[setting:alertcolorinfo]]; + background-color: [[setting:alertbackgroundcolorinfo]]; + border-color: [[setting:alertbordercolorinfo]]; +} + +#notice, +.redirectmessage2 { + text-align: center; + border-color: #e5e5e5; + padding: 5px; +} + +.pagelayout-redirect #page-content { + margin: 20px auto 0; + text-align: center; + width: 80%; +} + +.pagelayout-redirect #page { + background: none repeat scroll 0 0 rgba(0, 0, 0, 0); + box-shadow: none; + max-width: inherit; + padding: 0; +} + +#page-mod-forum-post #id_subject { + width: 100%; +} + +#plugins-control-panel, #admindeviceselector { + display: block; + overflow: auto; +} + +#admindeviceselector .cell img { + max-width: 420px; +} + +span2.mceEditor iframe, table.mceLayout2 iframe { + max-width: 500px !important; +} + +.editor_atto_menu .open ul.dropdown-menu { + min-width: 3.5em; +} + +.form-item .form-label, .mform .fitem div.fitemtitle, .userprofile dl.list dt, .form-horizontal .control-label { + width: 250px; +} + +.form-item .form-label, .mform .fitem div.fitemtitle { + float: left; +} + +.form-item .form-setting, +.form-item .form-description, +.mform .fitem .felement, +#page-mod-forum-search .c1, +.mform .fdescription.required, +.userprofile dl.list dd, +.form-horizontal .controls { +/* margin-left: 260px; */ +} + +.form-item .form-label, +.mform .fitem div.fitemtitle { + text-align: right; +} + + +.form-label label { + font-family: [[setting:fontheadername]], sans-serif !important; + font-weight: [[setting:fontheaderweight]] !important; + color: [[setting:fontheadercolor]]; + font-size: 18px; + padding-bottom: 10px; +} + +.form-colorpicker input { + display: inline; +} + +.settingsform .form-item { + border-bottom: 1px solid [[setting:blockbordercolor]]; + padding: 10px 0; + margin: 10px 0; +} + + +input[type="radio"], input[type="checkbox"] { + /* margin-left: 15px; */ + margin-right: 1px; +} + +#adminsettings h3 { + font-family: [[setting:fontheadername]], sans-serif !important; + font-weight: [[setting:fontheaderweight]] !important; + color: [[setting:fontheadercolor]]; + font-size: 24px; + display: block; + line-height: 40px; + margin-bottom: 20px; + margin-top: 40px; + padding: 0; + width: 100%; +} + +#region-main { + background-color: [[setting:regionmaincolor]]; + border: none; +} + +#region-main +.pagelayout-mydashboard #region-main { + padding: 0 5px; /* Override Boost. */ +} + +/* lang menu */ +.langmenu a { + color: [[setting:headertextcolor]]; +} + +.langmenu a:hover { + color: [[setting:headertextcolor]]; + background-color: none; +} + +/* alert messages */ + +.alert .close { + text-decoration: none; +} + +.customalert { + font-weight: 600; + font-size: 14px; + text-align: center; +} + +.customalert a { + text-decoration: underline; +} + +.customalert a:hover { + color: red; +} + + +.customalert p { + display: inline; +} + +#beta { + min-height: 50px; + background-color: #CC0000; +} + +#beta h3 { + line-height: 48px; + color: #fff !important; + font-size: 24px; + font-weight: bolder; + text-align: center; +} + +/* Alerts */ +.alert { + margin-bottom: 0; +} + +/* Course alerts */ +.adaptable-alert-info { + background-color: [[setting:alertbackgroundcolorinfo]]; + border-width: 1px; + border-color: [[setting:alertbordercolorinfo]]; + color: [[setting:alertcolorinfo]]; +} + +.adaptable-alert-info a, +.adaptable-alert-info a:hover { + color: [[setting:alertcolorinfo]]; +} + +.adaptable-alert-success { + background-color: [[setting:alertbackgroundcolorsuccess]]; + border-width: 1px; + border-color: [[setting:alertbordercolorsuccess]]; + color: [[setting:alertcolorsuccess]]; +} +.adaptable-alert-success a, +.adaptable-alert-success a:hover { + color: [[setting:alertcolorsuccess]]; +} + +.adaptable-alert-warning { + background-color: [[setting:alertbackgroundcolorwarning]]; + border-width: 1px; + border-color: [[setting:alertbordercolorwarning]]; + color: [[setting:alertcolorwarning]]; +} +.adaptable-alert-warning a, +.adaptable-alert-warning a:hover { + color: [[setting:alertcolorwarning]]; +} + +/* admin errors */ +.adminerror { + background-color: #f2dede; + border-color: #ebcccc; + border-radius: 0.25rem; + color: #a94442; + margin-bottom: 1rem; + margin-top: 50px; + padding: 0.75rem 1.25rem; +} + +.notifytiny { + background-color: #f2dede; + border-color: #ebcccc; + color: #a94442; + border-radius: 0.25rem; + margin: 1rem; + font-size: 14px; + font-family: monospace; + padding: 0.75rem 1.25rem; +} + +.navbar .singlebutton, +.navbar input, +.navbar .singlebutton div, +.path-mod-data .navbar form { + margin: 0 0; + line-height: 18px; +} + +#adaptable-page-header-wrapper .navbar #pre-login-form input { + margin: 5px; + padding: 2px; +} + +#adaptable-page-header-wrapper .navbar #pre-login-form .btn-login { + margin: 3px 1px 1px 10px; + padding: 0px 15px; + height: 30px; +} + +.navbar input#navsearchbox { + margin-right: 5px; +} + + +@media (min-width: 576px) { + #adaptable-page-header-wrapper #header2 .search-input-wrapper.expandable > form { + margin: 0; + } + + #adaptable-page-header-wrapper #header2 .search-input-wrapper.expandable { + padding: 0; + position: relative; + } + + #adaptable-page-header-wrapper #header2 .search-input-wrapper.expandable > div { /* The button! */ + background-color: [[setting:backcolor]]; + color: [[setting:fontcolor]]; + height: 24px; + position: absolute; + right: 2px; + top: 2px; + width: 24px; + } + + #adaptable-page-header-wrapper #header2 .search-input-wrapper.expandable > div .icon { + margin-top: 0; + } +} + +@media (max-width: 575.98px) { + .search-input-wrapper.expanded { + width: 32px; + } +} + +#adaptable-page-header-wrapper #header2 .search-input-wrapper>div .icon { + margin-top: 8px; +} + +.navbar .singlebutton input { + padding: 4px 15px; + margin-left: 5px; +} + +.navbar #coursesearchnavbar, +.navbar .wikisearch { + margin-top: 3px; +} + +.navbar .forumsearch input[type=text] { + padding: 0; +} + +/* Show modal pop-up. */ +#notice { + margin: 30px auto; + min-width: 220px; + width: 70%; +} + +/* Modal styles */ +.moodle-dialogue-base .closebutton::after { + color: #777; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: .8; + filter: alpha(opacity=80); +} + +.modal { +/* border: 1px solid #999;*/ + border-radius: 6px; + box-shadow: 0 3px 7px rgba(0, 0, 0, .3); + background-clip: padding-box; + outline: 0; +} + +.modal.fade { + transition: opacity .3s linear, top .3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-title h5 { + font-size: 24px; + font-weight: bold; +} + +.modal-header { + padding: 9px 15px; + border-bottom: none !important; + background-color: transparent; +} + +.modal-header h4 { + font-weight: bold; +} + +.modal-header .close { + background-color: transparent !important; + color: #777 !important; + font-size: 1.5rem; + height: auto; + line-height: 1; + margin: 0; + padding: 3px 0 0; +} + +.modal-header .close:hover, +.modal-header .close:focus { + color: #000 !important; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + overflow-y: auto; + padding: 15px; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-content { + background-color: transparent !important; + border: none !important; +} + +.modal-dialog .modal-content { + background-color: #fff !important; + border: 1px solid [[setting:maincolor]] !important; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: transparent !important; + border-top: none; + border-radius: 0 0 6px 6px; + box-shadow: none !important; +} + +.modal-footer:after, +.modal-footer:before { + display: table; + content: ""; + line-height: 0; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn+.btn { + margin-left: 5px; + margin-bottom: 0; +} + +.modal-footer .btn-group .btn+.btn { + margin-left: -1px; +} + +.modal-footer .btn-block+.btn-block { + margin-left: 0; +} + +.modal-footer::after, .modal-footer::before { + content: ""; + display: table; + line-height: 0; +} + +/* Modal file Picker */ +.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd, +.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd.yui3-widget-hd { + background:none; + background-color: [[setting:maincolor]]; + border-bottom: 1px solid #bbbbbb; +/* border-radius: 10px 10px 0 0; */ + color: #ffffff !important; + font-family: [[setting:fontheadername]], sans-serif !important; + font-size: 15px; + font-weight: normal; + letter-spacing: 1px; + margin: 0; + padding: 0.7em; + text-align: center; + text-shadow: 1px 1px 1px #666666 !important; +} + +.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd h1, +.moodle-dialogue h3 { + font-family: [[setting:fontheadername]], sans-serif !important; + color: #ffffff !important; + text-shadow: 1px 1px 1px #666666 !important; + padding: 0; + text-align: center; +} + +#page-navbar .breadcrumb-button { + display: inline; +} + +#gridshadebox_content ul.gtopics { + margin: 12px 1px 0 1px; +} + +.user-enroller-panel { + z-index: 2; +} + +#gridshadebox_content { + background-color: #fff; + border: 1px solid #999; + border-radius: 6px; + outline: 0; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + background-clip: padding-box; + min-height: 200px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +button.close { + -webkit-appearance: none; + background: none; + border: 0; + cursor: pointer; + padding: 0; +} + +.close { + float: right; + font-size: 21px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} + +#region-main .generaltable { + overflow-x: auto; + overflow-y: visible; +} + +div.no-overflow, .yui3-datatable { + overflow-y: visible; + overflow-x: auto; +} + +.paging { + word-wrap: break-word; +} + +.path-course-view .completionprogress { + z-index: 2; +} + +/* Search box */ +#page-header .searchbox { + bottom: 0; + padding: [[setting:searchboxpadding]]; + position: absolute; +} + +#page-header .searchbox.pagelayoutoriginal { + right: 0; +} + +#page-header .searchbox.pagelayoutalternative { + left: 0; +} + +.greybox { + border: 1px solid #ccc; +} + +.search-box__input { + width: 280px; +} + +button.search-box__button { + background-image: none; +} + +.search-box__button { + margin: 2px !important; + transition: background-color .2s ease-in-out; +} + +.search-box__button:hover { + background: [[setting:buttonhovercolor]]; + opacity: 0.9; + border: 1px solid rgba(#000, .05); + box-shadow: 1px 1px 2px rgba(#fff, .2); + color: [[setting:buttontextcolor]]; + text-decoration: none; + text-shadow: -1px -1px 0 [[setting:buttonhovercolor]]; + transition: all 150ms linear; +} + +.search-box { + margin: 0 auto; + width: 312px; +} + +.bg-white { + background-color: #fff; +} + +.pas { + padding: 2px 4px; + float: right; +} + +.no-border { + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +#search-1 { + padding: 5px 3px 5px 5px !important; + margin: 0 !important; + font-size: 14px; + color: #444; +} + +#search-1:focus { + border: none !important; + box-shadow: none; +} + +.icon-search { + font-size: 1.4em; +} + +/* #navwrap { + background-color: [[setting:menubkcolor]]; +} */ + +div.questionbankwindow.block { + overflow: auto; +} + +.que .formulation { + background-color: [[setting:currentcolor]]; + border-color: #bce8f1; + color: [[setting:fontcolor]]; +} + +.container { + transition: width .5s ease-in-out; +} + +#region-main { + transition: all .5s ease-in-out; +} + +#block-region-side-post { + padding-right: 0; + transition: all .5s ease-in-out; +} + +#zoominicon { + cursor: pointer; + padding-left: 4px !important; + padding-right: 4px !important; +} + +.showhideblocksdesc { + padding-left: 1px; +} + +#showsidebaricon { + display: none; +} + +@media(max-width: 991px) { + #region-main { + flex: 0 0 100%; + max-width: 100%; + } + #block-region-side-post { + background-color: #fff; + overflow: hidden; + padding: 0; + position: fixed; + min-width: 90%; + right: -90%; + transition: right 0.75s; + z-index: 11; + } + .header-style1 #block-region-side-post { + height: calc(100% - 44px); + top: 44px; + } + .header-style1 .pageheadershown #block-region-side-post { + height: calc(100% - 104px); + top: 104px; + } + .header-style2 #block-region-side-post { + height: calc(100% - 72px); + top: 72px; + } + #block-region-side-post .block-controls { + display: none; + } + #block-region-side-post.sidebarshown { + border-left: 2px solid [[setting:mobileslidebartabbkcolor]]; + overflow-y: auto; + right: 0; + } + #showsidebaricon { + background-color: [[setting:mobileslidebartabbkcolor]]; + border-bottom: 4px solid [[setting:mobileslidebartabbkcolor]]; + border-left: 4px solid [[setting:mobileslidebartabbkcolor]]; + border-top: 4px solid [[setting:mobileslidebartabbkcolor]]; + border-radius: 45px 0 0 45px; + color: [[setting:mobileslidebartabiconcolor]]; + display: block; + padding: 6px; + position: fixed; + right: 0; + top: 305px; + transition: right 0.75s; + z-index: 11; + } + .sidebarshown #showsidebaricon { + right: 90%; + } +} + +.fselect #id_courses { + width: 100% !important; +} + +.moodlewidth { + padding-right: 4px !important; + padding-left: 4px !important; +} + +.collapsibleregioncaption { + cursor: pointer; + text-decoration: underline; +} + +.overview .name a { + text-decoration: underline; +} + +.cimbox { + background-color: [[setting:blockbackgroundcolor]]; + background-position: center center; + background-repeat: no-repeat; + background-size: contain; + height: 175px; + position: relative; + margin-right: -1px; + transition: all .5s ease-in-out; + z-index: 8 +} + +.coursebox-content { + padding: 5px 15px 10px; + text-align: center; +} + +.coursebox.hover .summary { + color: [[setting:rendereroverlayfontcolor]]; + display: none; + left: 0; + min-height: 50px; + overflow: auto; + padding: 20px; + position: absolute; + text-align: center; + top: 0; +} + +.coursebox.hover .summary p { + color: [[setting:rendereroverlayfontcolor]]; +} + +.coursebox.panel.hover { + height: 100%; +} + +.coursebox.panel.hover .panel-body .coursebox-content h3 { + font-size: 20px; + font-weight: normal; + line-height: normal; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.coursebox.panel.hover:hover .panel-body { + background-color: [[setting:rendereroverlaycolor]]; + transition: all .5s ease-in-out; +} + +.coursebox.panel.hover:hover .panel-body .coursebox-content h3, +.coursebox.panel.hover:focus .panel-body .coursebox-content h3 { + color: [[setting:rendereroverlayfontcolor]]; +} + +.coursebox.hover .teachers li a, +.coursebox.hover .summary > a:link, +.coursebox.hover .summary > a:visited { + color: [[setting:rendereroverlayfontcolor]]; +} + +.coursebox.hover .teachers { + color: [[setting:rendereroverlayfontcolor]]; + padding-left: 2px; + padding-top: 2px; + text-align: left; +} + +.coursebox.hover:hover .summary { + display: block; + height: auto; + min-width: 100%; + width: auto; +} + +.coursebox.panel.hover:hover .panel-body .cimbox { + opacity: .3; + transition: all .5s ease-in-out; +} + +.panel.hover .coursebtn.submit.btn { + border: 1px solid #eee !important; + box-shadow: none; + display: block; + float: none; + font-weight: 300; + margin: 0 auto; + padding-left: 0; + padding-right: 0; + position: inherit; + width: 90%; +} + +.coursebox.hover .boxfooter { + clear: none; + margin-bottom: 0; +} + +.coursebox .content .courseimage img { + max-width: 300px; +} + +.paging .paging-morelink { + clear: both; +} + +.dir-rtl.path-mod-forum .indent { + margin-right: 3%; + margin-left: 0; +} + +.path-mod-forum .indent { + margin-left: 3%; +} + +.gradingform_guide.editor .addcriterion input, +.gradingform_guide.editor .addcomment input { + background-color: #00aeef; + display: block; + font-weight: bold; + text-decoration: none; +} + +.gradingform_guide.editor .criterion .controls .delete input { + width: 20px; + height: 16px; + background: transparent url([[pix:t/delete]]) no-repeat center top; + margin-top: 4px; +} + +.gradingform_guide.editor .moveup input { + width: 20px; + height: 15px; + background: transparent url([[pix:t/up]]) no-repeat center top; + margin-top: 4px; +} + +.gradingform_guide.editor .movedown input { + width: 20px; + height: 15px; + background: transparent url([[pix:t/down]]) no-repeat center top; + margin-top: 4px; +} + +.gradingform_guide.editor .movedown input:hover { + background: #00aeef url([[pix:t/up]]) no-repeat center top; +} + +.gradingform_guide.editor .controls .delete input:hover { + background: #00aeef url([[pix:t/up]]) no-repeat center top; +} + +.gradingform_guide.editor .addcriterion input, +.gradingform_guide.editor .addcomment input { + padding-left: 10px; + line-height: 20px; + height: 36px; + background-position: 5px 12px; + color: #fff; + font-weight: normal; +} + +.gradingform_guide.editor .addcomment, +.gradingform_guide.editor .addcriterion { + margin-top: 10px; +} + +.footer-inner .pull-right { + text-align: right; +} + +#page-footer .validators, +#page-footer .purgecaches, +#page-footer .performanceinfo, +#page-footer #load { + display: block; + margin-bottom: 20px; + text-align: center; +} + +.purgecaches { + clear: both; +} + +#page-footer .performanceinfo { + margin: 10px 0; +} + +.purgecaches a:link { + color: #fff; +} + +.helplink { + text-align: center; +} + +/* Pace Settings */ +.pace { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background-color: [[setting:loadingcolor]]; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 3px; +} + +/* Hidden admin blocks */ +.hidden-blocks .block { + float: right; + width: 250px; + margin-left: 10px; +} + +.hidden-blocks { + background-color: rgba(255, 154, 154, 0.5); + padding: 7px 7px; +} + +.hidden-blocks h3 { + border-bottom: 1px solid #e8eaeb; + text-shadow: 1px 1px 2px #ffffff; + color: #ff0000; + margin-left: 10px; +} + +.hidden-blocks .block-region .block { + width: 25%; + margin: 0 10px +} + +.hidden-blocks .block-region .block .header { + border-top: 0; +} + +/* Quiz navigation. + Published by Tim Hunt in the moodle forum + + Some styles are modified for Adaptable + Important!: Some colours are hardcoded +*/ + +.que .formulation { + background-color: transparent !important; + border: 1px solid [[setting:maincolor]] !important; +} + +.que .info { + background-color: transparent !important; + border: 1px solid [[setting:maincolor]] !important; +} + +.path-mod-quiz #mod_quiz_navblock .qn_buttons { + margin-right: -14px; +} + +.path-mod-quiz #mod_quiz_navblock .qnbutton { + font-size: 14px; + line-height: 25px !important; + font-weight: bold; + background: none; + background-color: #eee; + height: 45px; + width: 35px; + border-radius: 4px; + border: 0; + margin: 0 5px 5px 0; + color: [[setting:linkcolor]] !important; +} + +.path-mod-quiz #mod_quiz_navblock .qnbutton .thispageholder { + border: 1px solid #999; + border-radius: 4px; + z-index: 1; +} + +.path-mod-quiz #mod_quiz_navblock .qnbutton.thispage .thispageholder { + border: 3px solid #1f536b; +} + +.path-mod-quiz #mod_quiz_navblock .qnbutton.flagged .thispageholder { + background: url([[pix:theme|mod/quiz/flag-on]]) 20px 0 no-repeat; +} + +.path-mod-quiz #mod_quiz_navblock .qnbutton .trafficlight { + border: 0; + background: #fff none center 4px / 10px no-repeat scroll; + height: 20px; + margin-top: 25px; + border-radius: 0 0 4px 4px; +} + +/* Not yet answered */ +.path-mod-quiz #mod_quiz_navblock .qnbutton.notyetanswered .trafficlight, +.path-mod-quiz #mod_quiz_navblock .qnbutton.invalidanswer .trafficlight { + background-color: #fff; +} + +/* Correct answer */ +.path-mod-quiz #mod_quiz_navblock .qnbutton.correct .trafficlight { + background-color: #8bc34a; +} + +/* Blocked */ +.path-mod-quiz #mod_quiz_navblock .qnbutton.blocked .trafficlight { + background-color: #000; +} + +/* Wrong answer */ +.path-mod-quiz #mod_quiz_navblock .qnbutton.notanswered .trafficlight, +.path-mod-quiz #mod_quiz_navblock .qnbutton.incorrect .trafficlight { + background-color: #f44336; +} + +/* Partially correct */ +.path-mod-quiz #mod_quiz_navblock .qnbutton.partiallycorrect .trafficlight { + background-color: #ff9800; +} + +/* Answered */ +.path-mod-quiz #mod_quiz_navblock .qnbutton.complete .trafficlight, +.path-mod-quiz #mod_quiz_navblock .qnbutton.answersaved .trafficlight, +.path-mod-quiz #mod_quiz_navblock .qnbutton.requiresgrading .trafficlight { + background-color: #999; +} + +/* Fix user image in quiz report */ +#page-mod-quiz-report table#attempts td, +#page-mod-quiz-report table.quizresponseanalysis td { + max-width: 100%; + min-width: 30px; +} + +/* Fix quiz page width */ +#page-mod-quiz-attempt .container { + width: 100% !important; +} + + +/* Fix glossary print option */ +#page-mod-glossary-view .glossarycontrol { + margin: -40px 5px; +} + + +.rcs-search { + margin: auto 0 0 5px; +} + +/* Fix Video Player icons */ +.vjs-icon-play::before, +.video-js .vjs-big-play-button::before, +.video-js .vjs-play-control::before, +.vjs-icon-fullscreen-enter::before, +.video-js .vjs-fullscreen-control::before { + font-family: videojs; +} + +/* Fix activity restriction dialog (and others?) */ +.moodle-dialogue-content[role="dialog"] .row { + margin-left: inherit; +} + +/* Edit hover dropdown background */ +.moodle-actionmenu.show[data-enhanced] .menu a:hover { + background-color: #c0c0c0; +} + +.moodle-actionmenu.show[data-enhanced] .menu { + height: auto; +} + +/* img assignment */ +.path-mod-assign [data-region="user-info"] .img-rounded { + height: 60px; +} + +/* New Messages / Notifications topbar (moodle 3.2 and higher) */ +/* messages icons margin */ +#nav-notification-popover-container, +#nav-message-popover-container { + margin-left: 8px; +} + +/* messages icons size */ +.popover-region-toggle .icon img{ + width: 24px; + height: 24px; +} + +/* New message drawer (moodle 3.6 and higher) */ +.message-drawer { + box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.08); + border-left: 1px solid [[setting:headerbkcolor]]; +} + +.rounded-circle { + max-width: none !important; +} + +/* Set icon colour to white to fit default Adaptable colours) +Change the filter selector for other colours */ +.message-drawer button img.icon { + filter: grayscale(100%) brightness(5); + + max-width: none !important; + margin-left: -5px; + padding-right: 0; +} + +.message-app.drawer .btn-block { /* M3.7+ */ + margin-left: 0; +} + +/* Dimmed text (courses or users) */ +.dimmed, +a.dimmed, +a.dimmed:link, +a.dimmed:visited, +a.dimmed_text, +a.dimmed_text:link, +a.dimmed_text:visited, +.dimmed_text, +.dimmed_text a, +.dimmed_text a:link, +.dimmed_text a:visited, +.usersuspended, +.usersuspended a, +.usersuspended a:link, +.usersuspended a:visited, +.dimmed_category, +.dimmed_category a { + color: #999999 !important; +} + +.activity img.iconlarge { + height: [[setting:coursesectionactivityiconsize]]; + width: [[setting:coursesectionactivityiconsize]]; + vertical-align: middle; +} + +.section li.activity { + padding-top: 0; + padding-bottom: 0; + clear: both; +} + +/* Plugins installation status */ +.status-upgrade { + background-color: #d9edf7; +} + +.status-new { + background-color: #dff0d8; +} + + +/* Activity chooser styling. */ +.moodle-dialogue-base .moodle-dialogue.chooserdialogue { + min-width: 90%; + height: 100%; +} + +.choosercontainer #chooseform .option label { + display: block; + padding: 0.4em 0 0.7em 0; + border-bottom: 1px solid #FFFFFF; + font-weight: normal; +} + +.jschooser .choosercontainer #chooseform .alloptions .option .typename { + display: inline-block; + width: 65%; + font-size: 1.2em; +} + +/* Only set these options if we're showing the js container */ +.jsenabled .choosercontainer #chooseform .alloptions { + max-width: 95%; +} + +.chooserdialogue .moodle-dialogue-wrap { + margin-top: 15px; + margin-left: -3px; + background-color: #fff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0px; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); +} + +.moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd, +.moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd.yui3-widget-hd { + background-image: none; + color: #565656; + padding: 10px; + border-bottom: 1px solid #f5f5f5; + border-radius: 0px; + text-align: left; + font-size: 16px; + min-height: 20px; +} + +.moodle-dialogue-base .moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons { + margin-top: 5px; + margin-right: 5px; + padding: 0; + background-image: none; +} + +.choosercontainer #chooseform .selected .typesummary, +.choosercontainer #chooseform .selected .instruction { + display: none; +} +/* End Activity chooser styling */ + +#savediscardsection { + width: 100%; + min-width: 100%; + min-height: 30px; + padding: 10px; + top: 10px; + background-color: #EEEEEE; + opacity: 0.8; + filter: alpha(opacity=80); + position: fixed; /* For IE8 and earlier */ +} + +img.smallicon { + height: 16px; + width: 16px; + vertical-align: middle; +} + +#page-course-resources table.mod_index td img { + height: 36px; + width: 36px; + vertical-align: middle; +} + +#page-course-resources #region-main table.generaltable { + display: table; +} + +#profilepic img, +.userpicture { + display: inline; + margin: 0 4px 0 0; + background-color: #fff; + border-radius: 50%; +} + +#adaptable-page-header-wrapper #above-header .userpicture { + width: 25px ; + height: 25px; +} + +#adaptable-page-header-wrapper #header2 .userpicture { + width: 35px ; + height: 35px; +} + +.comment-message .picture { + width: 30px; + float: left; +} + +.tag_list .inline-list a.label-info[href] { + color: #fff !important; +} + +.searchbox abbr[title]{ + border-bottom:none; + cursor:pointer; + text-decoration:none; +} + +.groupmanagementtable .row { + margin-left: 0px; +} + +/* Added to style the grid of letters etc on participants page */ +.userlist .initialbar .letter { + height: 1.8em; + min-width: 1.8em; + border: 0; +} + +.userlist .initialbar .letter.active { + line-height: 2em; +} + +.userlist .initialbar { + margin-bottom: 10px; +} + +.userlist .initialbar .letter:hover { + color: #FFFFFF !important; +} + +/* Filepicker area. */ +.filepicker-filelist .filepicker-container, .filemanager.fm-noitems .fm-empty-container { + +} + +/* Adaptable 2.0 */ +.p-0 { + background-color: transparent !important; +} + +.row-fluid .col-9 { + width: 74.46808511%; + *width: 74.41489362%; + max-width: none; + flex: none; + min-height: inherit; +} + +.row-fluid .col-3 { + width: 24.40425532%; + *width: 24.35106383%; +} + +@media (max-width: 767px) { + .row-fluid .col-3 { + flex: none; + max-width: 100%; + width: 100%; + } + + .row-fluid .col-9 { + flex: none; + max-width: none; + width: 100%; + } +} + +.form-inline input.form-control[type=text] { + min-width: 0; + border-radius: 2px; + height: 32px; +} + +#page-login-index .card-header { + display: none; +} + +/* Block action classes lifted from previously used bootstrapbase parent theme. */ + +.block.hidden .header .block_action .block-hider-hide { + display: none; +} +.block.hidden .header .block_action .block-hider-show { + display: inline; +} + +.block .header .block_action { + padding: 3px 2px; + float: right; +} + +.block .header .block_action > * { + margin-left: 3px; +} + +.block .header .block_action .block-hider-show, +.block .header .block_action .block-hider-hide { + cursor: pointer; +} + +.block .header .block_action .block-hider-show { + display: none; +} + +.jsenabled .block.hidden .content { + display: none; + opacity: 0; +} + +.jsenabled .block .content { + opacity: 1; + display: block; + -webkit-transition: ease [[setting:navbardropdowntransitiontime]]; + -moz-transition: ease [[setting:navbardropdowntransitiontime]]; + -ms-transition: ease [[setting:navbardropdowntransitiontime]]; + -o-transition: ease [[setting:navbardropdowntransitiontime]]; + transition: ease [[setting:navbardropdowntransitiontime]]; +} + +/* Transform default value text in settings to Italic. */ +.form-item .form-setting .form-defaultinfo { + font-style: italic; +} + +.form-item .form-setting .form-defaultinfo { + padding-left: 5px; +} + +/* Dialog box styling. */ +.moodle-dialogue-base .moodle-dialogue-wrap .container, +.moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .container + { + width: 100%; +} + +.moodle-dialogue-base button.closebutton, +.moodle-dialogue-base .chooserdialogue button.closebutton, +.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .closebutton, +.moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons button.yui3-button.closebutton + { + opacity: 1; + line-height: 1.2em; + border-radius: 3px; + background-color: #FFF; + width: 26px; + padding:0; + margin:0; + +} + +.moodle-dialogue-base .closebutton::after, +.moodle-dialogue-base .chooserdialogue .closebutton::after +.moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons button.yui3-button.closebutton:after + { + display: inline-block; + vertical-align: top; + opacity: 1; + background: none; + color: #777; +} + +.form-group textarea.form-control { + margin: 0 0 0.7rem 0; +} +.moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons { + padding:0.6em; +} + +.moodle-dialogue-base .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons { + padding: 1em; +} + +.settingsform fieldset { + margin-bottom: 10px; +} + +.settingsform .form-item .form-label { + padding-left: 0; +} + +.modal.modal-in-page { + z-index: 1; +} + +/* Backup / restore and Course import page. */ +.path-backup .mform .grouped_settings.section_level { + padding: 10px 0 0 10px; +} + +.path-backup .backup-section .header { + margin-bottom: 20px; +} + +.backup-section .ics-results { + margin-top: 15px; +} + +.path-backup .detail-pair .detail-pair-label { + width: 20%; +} + +.path-backup .detail-pair-value .ics-search input[type="submit"] { + margin: 10px 0 0; +} + +.path-backup .detail-pair .detail-pair-value input[type="submit"] { + margin: 10px 0 0; +} + +/* Course restore */ +.detail-pair-label, +.detail-pair-value { + font-size: 1rem !important; +} + +.path-backup .form-group { + margin-bottom: 0; +} + +.path-backup .form-check .icon-post { + margin-left: .5rem; +} + +.path-backup .mform .root_setting:nth-of-type(odd), +.path-backup .mform .grouped_settings:nth-of-type(odd) { + background-color: transparent; +} + +/* File Picker */ +.fp-setlicense select { + display: block; +} + +.form-control-static .icon +.detail-pair-value .icon { + font-size: 24px !important; +} + +.subheader h3 { + font-size: 1.6rem !important; + margin-top: 70px !important; +} + +.path-backup .backup_progress { + padding: 7px 5px; + border-radius: 8px; + background-color: #f5f5f5; +} + +.backup_stage .backup_stage_current, +.backup_stage .backup_stage_next { + color: [[setting:maincolor]]; +} + +.path-backup .detail-pair .detail-pair-value input[type="submit"] { + margin: 0; +} + +/* info icon */ +.text-info { + color: [[setting:infoiconcolor]] !important; + margin-top: 9px; /* fix vertical alignment */ +} + +/* Highlight results */ +.highlight { + background-color: #ff0; + color: #000 !important; + +} +/* danger icon */ +.text-danger, +.notconnected, +.que .validationerror, +.text-error { + color: [[setting:dangericoncolor]] !important; + margin-right: 1px !important; + margin-top: 8px !important; +} + +/* Success / Fail icon */ +.text-success, +.green, +.notifysuccess, +.connected, +.detail-pair-value .text-danger, +.form-control-static .text-danger, +.notconnected, +.que .validationerror, +.text-error { + font-size: 24px !important; +} + +/* Login Page */ +/* Background Image */ +/*rtl:ignore*/ +#page-login-index .outercont { + -ms-flex-negative: 0; + flex-shrink: 0; + left: 50%; + padding: 0 15px; + position: relative; + margin: 0; + transform: translateX(-50%); +} + +#page-login-index #page-footer { + margin: auto 0 0 !important; +} + +#page-login-index .fullin.narrow .container, +#page-login-index .fullin.standard .container { + width: 100% !important; +} + +#page-login-index { + [[setting:loginbgimage]] + background-position: 0 0; + background-repeat: no-repeat; + [[setting:loginbgstyle]] +} + +#page-login-index #page { + background:none; +} + +.progressbar_container { + max-width: none; +} + +[[setting:loginbgopacity]] diff --git a/theme/adaptable/style/backup-restore.css b/theme/adaptable/style/backup-restore.css new file mode 100644 index 0000000..ebb0ca7 --- /dev/null +++ b/theme/adaptable/style/backup-restore.css @@ -0,0 +1,28 @@ +/* + * 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/>. + * + * + * Adaptable Backup/Restore style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +.path-backup .wibbler { + width: auto; +} diff --git a/theme/adaptable/style/blocks.css b/theme/adaptable/style/blocks.css new file mode 100644 index 0000000..9a0fee0 --- /dev/null +++ b/theme/adaptable/style/blocks.css @@ -0,0 +1,781 @@ +/* +* This file is part of Adaptable theme for moodle +* +* 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/>. +* +* +* Adaptable block styles sheet +* +* @package theme_adaptable +* @copyright 2015-2019 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @copyright 2018-2019 Manoj Solanki (Coventry University) +* +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +.block-region-front .block { + border: none; +} + +.block-region-front .block .header { + border: none; +} + +.block .content a.dimmed { + font-style: italic; +} + +.block_timeline .col-sm { + flex-basis: 0%; + flex-grow: 1; + max-width: 100%; +} + +/* Reset border values */ +[class^="block-side-"] .card-body, +.block { + background-color: [[setting:blockbackgroundcolor]]; + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; + margin-bottom: 2px; +} + +.block { + padding: .5rem; +} + +/* Block titles */ +.block .header, +.block > .card-body, +#dockeditempanel .dockeditempanel_hd { + border-width: [[setting:blockheaderbordertop]] [[setting:blockheaderborderright]] [[setting:blockheaderborderbottom]] [[setting:blockheaderborderleft]]; + border-color: [[setting:blockbordercolor]]; + border-style: [[setting:blockheaderbordertopstyle]]; + padding: 5px; + border-radius: [[setting:blockheadertopradius]] [[setting:blockheadertopradius]] [[setting:blockheaderbottomradius]] [[setting:blockheaderbottomradius]]; +} + +/* Block header */ +.block .header, +#dockeditempanel .dockeditempanel_hd { + background: [[setting:blockheaderbackgroundcolor]]; +} + +.block .header { + padding-right: 0; +} + +/* Block title */ +.block .title h2 { + color: [[setting:fontblockheadercolor]]; + font-size: [[setting:fontblockheadersize]]; + font-weight: [[setting:fontblockheaderweight]] !important; + margin-bottom: 0; + text-shadow: none; + text-transform: none; +} + +.block .header .block_action { + margin: 0; + margin-top: -5px; +} + + +.block .header .action-menu { + border: none !important; + background-color: transparent !important; +} + +.block .header .action-menu a { + color: [[setting:fontblockheadercolor]] !important; +} + +/* Block content */ +.block > .content, +#dockeditempanel .dockeditempanel_bd { + border-width: [[setting:blockmainbordertop]] [[setting:blockmainborderright]] [[setting:blockmainborderbottom]] [[setting:blockmainborderleft]]; + border-color: [[setting:blockbordercolor]]; + border-style: [[setting:blockmainbordertopstyle]]; + padding: 10px 5px; + font-weight: normal; + background: [[setting:blockbackgroundcolor]]; + border-radius: [[setting:blockmaintopradius]] [[setting:blockmaintopradius]] [[setting:blockmainbottomradius]] [[setting:blockmainbottomradius]]; +} + +#dockeditempanel .dockeditempanel_bd { + margin-left: 5px; +} + +.block-region-front .block .content { + padding: 10px 5px; +} + +.block .block-hider-show, +.block .block-hider-hide { + width: 18px; +} + +/** Some specific block styles for calendar block, login block, my profile block, etc. **/ +/* Navigation block */ +.jsenabled .block_settings .block_tree .tree_item.branch, +.block_navigation .block_tree .tree_item.branch { + color: [[setting:linkcolor]] !important; +} + +/* Fix wrong indentation */ +.block_navigation .block_tree p.hasicon img { + padding-right: 0; +} + +.block_navigation ul.block_tree > li > ul, +.block_navigation ul.block_tree > li > ul > li p.tree_item.branch { + padding-left: 0; +} + +.block_navigation ul.block_tree > li > ul > li p.hasicon { + padding-left: 1px; +} +.block_navigation ul.block_tree > li > p[aria-expanded="true"]:before { + content: "\f0e4"; + font-family: "FontAwesome"; + text-indent: -3px; +} + +/* Course Timeline block */ +.block .content .block-timeline a:hover { + background-color: [[setting:navbardropdownhovercolor]] !important; + color: [[setting:navbardropdowntexthovercolor]] !important; +} + +/* Recently accessed courses block */ +.block_recentlyaccessedcourses .paging-bar-container { + margin-top: -3rem; +} + +/* Login block */ +.block_login label { + font-size: 14px; + color: #666; + margin-bottom: 5px; + margin-top: 5px; + font-weight: 400; +} + +.block_login .username label:before { + content: "\f007"; + font-family: "FontAwesome"; + margin-right: 5px; + color: #999; +} + +.block_login .password label:before { + content: "\f084"; + font-family: "FontAwesome"; + margin-right: 5px; + color: #999; +} + +.block_login2 .btn, +.block_login2 .content .footer a { + background-image: none; + background-color: #5cb85c; + border-color: #30ADD1; + border-image: none; + border-radius: 4px; + border-style: solid; + border-width: 1px; + box-shadow: none; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: 14px; + padding: 4px 12px; + text-align: center; + text-shadow: none; + vertical-align: middle; + margin: 10px 0; + transition: 0.25s; + width: 100%; +} + +.block_login2 .btn:hover { + background-color: #388f9d !important; +} + +#page-login-index2 #loginbtn { + background-color: #4f9e4f; + text-shadow: none; +} + +.block_login2 .content .footer div:nth-child(1) a, +#page-login-index2 .forgetpass a { + color: #fff; + text-align: center; + background-color: #ee981e; + border-color: #ee981e; + background-image: none !important; + font-family: 'Open Sans', sans-serif; + font-weight: 600; +} + +#page-login-index2 .forgetpass a { + display: block; + margin-bottom: 10px; + margin-left: 5px; + margin-top: 10px; + max-width: 260px; + text-shadow: none; +} + + +#page-login-index2 .forgetpass { + clear: both; + float: right; + text-align: left; + width: 66%; +} + +.loginbox .loginpanel .desc { + clear: both; +} + +.block_login .content { + margin-left: auto; + margin-right: auto; + max-width: 100%; +} + +.block_login .c1.btn input { + display: block; + text-align: center; + width: 96%; +} + +.loginbox .loginform .form-input input { + width: 50%; +} + +.loginbox .loginform .form-input { + float: right; + width: 66%; +} + +.loginbox .loginform .form-label { + width: 33%; +} + +/* Admin block */ +.block_adminblock .content { + margin: 0 auto; +} + +/* Course Overview block */ +.block_course_overview .content { + margin: 0; +} + +.btn.coursemenubtn { + background-color: transparent !important; + border: none !important; + border-radius: 0% !important; + color: #000000 !important; +} + +.btn.coursemenubtn:hover { + background-color: #f0f0f0 !important; + border-radius: 100% !important; +} + +.coursemenubtn .fa { + color: #000000 !important; +} + + +/* Calendar block */ +.block_calendar_month table.minicalendar.calendartable td { + border: none !important; +} + +.block_calendar_month table.minicalendar.calendartable th { + border: none !important; + border-radius: 0; + background-color: #e0e0e0; +} + +.block_calendar_month table.minicalendar { + margin-bottom: 0 !important; +} + +.block_calendar_month table.minicalendar.calendartable th abbr { + border: none !important; + text-decoration: none; +} + +.block .calendar-controls .previous:before { + content: "\f053"; + font-family: "FontAwesome"; +} + +.block .calendar-controls .next:before { + content: "\f054"; + font-family: "FontAwesome"; +} + +.block .calendar-controls .previous .arrow, +.block .calendar-controls .next .arrow { + display: none; +} + + +/* Comments block */ +.block_comments div.content li { + list-style-type: none; +} + +.block_tree_box hr { + background: transparent; + border: none; + border-top: 1px solid #eee; +} + +/* Site Info block */ +.site-info-container { + background-color: [[setting:maincolor]]; + padding: 5px 10px; + height: 120px +} + +.site-info-value { + color: #ffffff; + font-size: 48px; + display: block; + text-align: right; +} + +.site-info-label { + color: #ffffff; + font-size: 18px; + display: block; + text-align: right; +} + +/* Server Info block */ +.server-info-container { + background-color: #39c121; + padding: 5px 10px; + height: 120px +} + +/* Quiz editing block header */ +.questionbankwindow.block { + padding: 0; +} + +#page-mod-quiz-edit .questionbankwindow div.header { + background-color: #eee; + color: inherit; +} + +#page-mod-quiz-edit .questionbankwindow div.header a { + color: inherit; +} + +#mod_quiz_navblock input { + white-space: normal; +} + +/* Hidden blocks */ +.block.hidden .header { + border-bottom: 0; +} + +.block.invisible:hover { + opacity: 1; +} + +body.has_dock div#dock { + z-index: 3; +} + +/* Squared dot */ +.block_settings .block_tree li.item_with_icon > p img { + left: 3px; + top: 2px; +} + +/* Add or hide block icons */ +.showblockicons .block .header .title h2:before { + content: "\f0c9"; + display: inline-block; + font-family: "FontAwesome" !important; + font-size: [[setting:blockiconsheadersize]]; + font-style: normal; + font-variant: normal; + font-weight: 900 !important; + padding-right: 8px; + text-rendering: auto; +} + +/* Block Icons */ +.showblockicons .block_news_items.block .header .title h2:before { + content: "\f0a1"; +} + +.showblockicons .block_navigation.block .header .title h2:before { + content: "\f0e8"; +} + +.showblockicons .block_calendar_upcoming.block .header .title h2:before, +.showblockicons .block_calendar_month.block .header .title h2:before { + content: "\f133"; +} + +.showblockicons .block_course_list.block .header .title h2:before { + content: "\f108"; +} + +.showblockicons .block_completionstatus.block .header .title h2:before, +.showblockicons .block_selfcompletion.block .header .title h2:before { + content: "\f0e4"; +} + +.showblockicons .block_blog_menu.block .header .title h2:before { + content: "\f02d"; +} + +.showblockicons .block_quiz_results.block .header .title h2:before { + content: "\f080"; +} + +.showblockicons .block_quiz_navblock.block .header .title h2:before { + content: "\f126"; +} + +.showblockicons .block_glossary_random.block .header .title h2:before { + content: "\f0eb"; +} + +.showblockicons .block_book_toc.block .header .title h2:before { + content: "\f02d"; +} + +.showblockicons .block_participants.block .header .title h2:before, +.showblockicons .block_online_users.block .header .title h2:before { + content: "\f0c0"; +} + +.showblockicons .block_html.block .header .title h2:before { + content: "\f022"; +} + +.showblockicons .block_section_links.block .header .title h2:before { + content: "\f02e"; +} + +.showblockicons .block_activity_modules.block .header .title h2:before { + content: "\f12e"; +} + +.showblockicons .block_comments.block .header .title h2:before { + content: "\f075"; +} + +.showblockicons .block_settings.block .header .title h2:before { + content: "\f085"; +} + +.showblockicons .block_admin_bookmarks.block .header .title h2:before { + content: "\f02e"; +} + +.showblockicons .block_blog_tags.block .header .title h2:before, +.showblockicons .block_tags.block .header .title h2:before { + content: "\f02c"; +} + +.showblockicons .block_private_files.block .header .title h2:before { + content: "\f07b"; +} + +.showblockicons .block_block_mentees.block .header .title h2:before { + content: "\f0c0"; +} + +.showblockicons .block_messages.block .header .title h2:before { + content: "\f0e0"; +} + +.showblockicons .block_community.block .header .title h2:before { + content: "\f0ac"; +} + +.showblockicons .block_recent_activity.block .header .title h2:before { + content: "\f017"; +} + +.showblockicons .block_rss_client.block .header .title h2:before { + content: "\f09e"; +} + +.showblockicons .block_search_forums.block .header .title h2:before, +.showblockicons .block_globalsearch.block .header .title h2:before { + content: "\f002"; +} + +.showblockicons .block_myprofile.block .header .title h2:before { + content: "\f007"; +} + +.showblockicons .block_adminblock.block .header .title h2:before { + content: "\f009"; +} + +.showblockicons .block_feedback.block .header .title h2:before { + content: "\f087"; +} + +.showblockicons .block_flickr.block .header .title h2:before { + content: "\f03e"; +} + +.showblockicons .block_youtube.block .header .title h2:before { + content: "\f145"; +} + +.showblockicons .block_course_badges.block .header .title h2:before, +.showblockicons .block_badges.block .header .title h2:before { + content: "\f0a3"; +} + +.showblockicons .block_twitter_search.block .header .title h2:before { + content: "\f099"; +} + +.showblockicons .block_overview.block .header .title h2:before { + content: "\f03a"; +} + +.showblockicons .block_timeline.block .header .title h2:before { + content: "\f062"; +} + +.showblockicons .block_starredcourses.block .header .title h2:before { + content: "\f005"; +} + +.showblockicons .block_lp.block .header .title h2:before { + content: "\f277"; +} + +/* Overriden from Boost. */ +.block_tree .tree_item.branch { + margin-left: 0; +} + +.block_settings .block_tree .tree_item.branch { + padding-left: 5px; +} + +.block_settings .block_tree p.hasicon { + padding-left: 5px; +} + +/* Add slightly extra margin on right. */ +.block_settings .block_tree p.hasicon .icon { + margin-right: 4px; +} + +/* Circle icon size decreased */ +.block_settings .fa-circle { + font-size: 10px; + margin-right: .25rem; +} + +/* Tabbed layout for course and dashboard */ + +main.tabcontentcontainer { + min-height: 400px; + padding: 0; + background: #fff; + margin: 5px 0; +} + +#dashboardtabcontainer section.adaptable-tab-section { + display: none; + padding: 20px 0 0; + border-top: 4px solid [[setting:tabbedlayoutdashboardcolorselected]]; +} + +#coursetabcontainer section.adaptable-tab-section { + display: none; + padding: 20px 0 0; + border-top: 4px solid [[setting:tabbedlayoutcoursepagetabcolorselected]]; +} + +#dashboardtabcontainer input.dashboardtab, +#coursetabcontainer input.coursetab { + display: none; +} + +.tabcontentcontainer div.linktab, +.tabcontentcontainer label.dashboardtab, +.tabcontentcontainer label.coursetab { + position: relative; + display: inline-block; + padding: 15px 15px 25px; + margin: 0 4px 0 0; + border-bottom: 0; + cursor: pointer; + font-weight: 400; + transition: background-color 0.2s ease; +} + +.tabcontentcontainer label.dashboardtab { + background-color: [[setting:tabbedlayoutdashboardcolorunselected]]; +} + +.tabcontentcontainer div.linktab, +.tabcontentcontainer label.coursetab { + background-color: [[setting:tabbedlayoutcoursepagetabcolorunselected]]; +} + +.tabcontentcontainer div.linktab { + line-height: 1; + vertical-align: top; +} + +.tabcontentcontainer div.linktab a { + color: inherit; +} + +.tabcontentcontainer > dashboardtab.label:hover::after, +.tabcontentcontainer > dashboardtab.input:focus + label::after, +.tabcontentcontainer > dashboardtab.input:checked + label::after { + background-color: [[setting:tabbedlayoutdashboardcolorselected]]; +} + +.tabcontentcontainer > coursetab.label:hover::after, +.tabcontentcontainer > coursetab.input:focus + label::after, +.tabcontentcontainer > coursetab.input:checked + label::after { + background-color: [[setting:tabbedlayoutcoursepagetabcolorselected]]; +} + +.tabcontentcontainer div.linktab, +.tabcontentcontainer input.dashboardtab:checked + label { + border-color: [[setting:tabbedlayoutdashboardcolorselected]]; + background-color: [[setting:tabbedlayoutdashboardcolorselected]]; + border-top: 4px solid [[setting:tabbedlayoutdashboardcolorselected]]; /* Override this in custom css to change the selected tab colour */ + color: #FFF; +} + +.tabcontentcontainer input.coursetab:checked + label { + border-color: [[setting:tabbedlayoutcoursepagetabcolorselected]]; + background-color: [[setting:tabbedlayoutcoursepagetabcolorselected]]; + border-top: 4px solid [[setting:tabbedlayoutcoursepagetabcolorselected]]; /* Override this in custom css to change the selected tab colour */ + color: #FFF; +} + +#tab-content:checked ~ #adaptable-course-tab-content, +#tab1:checked ~ #adaptable-course-tab-1, +#tab2:checked ~ #adaptable-course-tab-2 { + display: block; +} + +#dashboard-tab-content:checked ~ #adaptable-dashboard-tab-content, +#dashboard-tab1:checked ~ #adaptable-dashboard-tab-1, +#dashboard-tab2:checked ~ #adaptable-dashboard-tab-2 { + display: block; +} + +.tabcontentcontainer .tab-panel { + padding: 30px 0; + margin-right: 5px; + margin-bottom: 5px; +} + +.tabcontentcontainer .tabset { + max-width: 100%; + min-height: 200px; +} + +#dashboard-tab-content:checked ~ #adaptable-dashboard-tab-content.adaptable-tab-section.tab-panel { + padding-left: 15px; + padding-right: 15px; +} + +@media (min-width: 1024px) { + + .tabcontentcontainer div.linktab, + .tabcontentcontainer dashboardtab.input[type="radio"], + .tabcontentcontainer coursetab.input[type="radio"] { + font-size: 1.4em; + } + + .tabcontentcontainer div.linktab { + font-size: 3em; + padding: 10px 15px; + } + + .tabcontentcontainer label.dashboardtab, + .tabcontentcontainer label.coursetab { + font-size: 1.25em; + padding: 20px 40px; + } +} + +@media (max-width: 1023px) { + .tabcontentcontainer div.linktab { + font-size: 1.6em; + padding: 10px 15px; + } + + .tabcontentcontainer label.dashboardtab, + .tabcontentcontainer label.coursetab { + padding: 12px; + } +} + +@media (max-width: 460px) { + .tabcontentcontainer div.linktab { + font-size: 1.5em; + padding: 12px; + } + + .tabcontentcontainer label.dashboardtab, + .tabcontentcontainer label.coursetab { + font-size: 0.95em; + } + + .tabcontentcontainer div.linktab, + .tabcontentcontainer label.dashboardtab, + .tabcontentcontainer label.coursetab { + width: 100%; /* Make tabs full width at this resolution. */ + margin: 2px 0; + } + + .tabcontentcontainer label.dashboardtab:before, + .tabcontentcontainer label.coursetab:before { + content: "\f101"; + padding-right: 5px; + font-family: "FontAwesome"; + } + + #coursetabcontainer section.adaptable-tab-section, + #dashboardtabcontainer section.adaptable-tab-section { + border: 0; + } + +} diff --git a/theme/adaptable/style/bootstrap.css b/theme/adaptable/style/bootstrap.css new file mode 100644 index 0000000..866bfe7 --- /dev/null +++ b/theme/adaptable/style/bootstrap.css @@ -0,0 +1,28 @@ +/* + * 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/>. + * + * + * Adaptable Bootstrap override styles style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +.progress { + width: 100%; +} diff --git a/theme/adaptable/style/browser.css b/theme/adaptable/style/browser.css new file mode 100644 index 0000000..0f70dcf --- /dev/null +++ b/theme/adaptable/style/browser.css @@ -0,0 +1,30 @@ +/* + * 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/>. + * + * + * Adaptable Browser specific override styles style sheet + * + * @package theme_adaptable + * @copyright 2020 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +/* Safari */ +.safari .section .activity .activityinstance, +.safari .section .activity .activityinstance > a { + width: 100%; +} diff --git a/theme/adaptable/style/button.css b/theme/adaptable/style/button.css new file mode 100644 index 0000000..14e13af --- /dev/null +++ b/theme/adaptable/style/button.css @@ -0,0 +1,459 @@ +/* +* This file is part of Adaptable theme for moodle +* +* 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/>. +* +* +* Adaptable buttons CSS file +* +* @package theme_adaptable +* @copyright 2015-2019 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @copyright 2018-2019 Manoj Solanki (Coventry University) +* +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +/* Default style for all the buttons */ +.btn, +.btn a { + background-image: none; + border: 1px solid #ccc; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #b3b3b3; + border-radius: [[setting:buttonradius]]; + box-shadow: 0 [[setting:buttondropshadow]] 0 0 rgba(0, 0, 0, 0.5) inset; + cursor: pointer; + font-family: [[setting:fontname]]; + font-size: 14px; + line-height: 20px; + margin-bottom: 0; + padding: 6px 12px 6px; + text-align: center; + text-shadow: none; +} + +/* Primary button */ +.btn-primary, +.backup-restore .singlebutton button[type="submit"], +.path-mod-workshop .assessment-summary.graded .singlebutton input[type="submit"], +.path-mod-workshop .example-summary.graded .singlebutton input[type="submit"], +.btn-default, +[data-role="next"], +[data-role="previous"], +input[type="submit"], +input[type="button"], +#searchform_button, +a.submit, +#notice .singlebutton + .singlebutton input, +#page-login-index2 .forgetpass a, +input.form-submit, +input#id_submitbutton, +input#id_submitbutton2, +.path-admin .buttons input[type="submit"], +.nav-pills > .active > a, +td.submit input, +div.discussionsubscription > a.discussiontoggle, +.btn-outline-secondary, +.paging-morelink a, +.btn-link, +a#adaptable-message-user-button, + +/* Add Restrictions Dialogue */ +#availability_addrestriction_date.btn.btn-secondary, +#availability_addrestriction_grade.btn.btn-secondary, +#availability_addrestriction_profile.btn.btn-secondary, +#availability_addrestriction_list_.btn.btn-secondary, +#availability_addrestriction_completion.btn.btn-secondary { + font-family: [[setting:fontname]]; + background-image: none; + background-color: [[setting:buttoncolor]]; + background-size: 100% 200%; + text-shadow: none; + box-shadow: 0 [[setting:buttondropshadow]] 0 0 rgba(0, 0, 0, 0.5) inset; + border: 0; + border-radius: [[setting:buttonradius]]; + color: [[setting:buttontextcolor]] !important; + cursor: pointer; + padding: .4rem 1rem; + -ie-transition: background .2s ease-out; + -o-transition: background .2s ease-out; + transition: background .2s ease-out; +} + +.btn-primary:hover { + background-color: [[setting:buttonhovercolor]] !important; + text-decoration: none !important; + color: [[setting:buttontextcolor]] !important; +} + +/* Secondary button */ +.btn-secondary { + background-color: [[setting:buttoncolorscnd]]; + color: [[setting:buttontextcolorscnd]]; + min-height: 32px; + text-decoration: none !important; +} + +a.btn-secondary { + color: [[setting:buttontextcolorscnd]]; +} + +.btn-secondary:hover { + background-color: [[setting:buttonhovercolorscnd]]; + color: [[setting:buttontextcolorscnd]]; + text-decoration: none !important; +} + +.btn-sm { + margin-right: 3px; +} + +/* Action buttons */ +#notice .singlebutton + .singlebutton input, +submit.buttons input[name="cancel"] { + background-color: [[setting:editoffbk]] !important; + text-shadow: none; +} + +input[type="submit"]:hover, +input[type="button"]:hover, +#searchform_button:hover, +a.submit:hover, +#notice .singlebutton + .singlebutton input:hover, +.submit.buttons input[name="cancel"]:hover, +#page-login-index2 .forgetpass a:hover, +.submitbutton:hover, +.nav-pills > .active > a:hover, .nav-pills > .active > a:focus, +.btn-outline-secondary:hover, +.btn-link:hover, +div.discussionsubscription > a.discussiontoggle:hover, +a#adaptable-message-user-button:hover, + +/* Add Restrictions Dialogue */ +#availability_addrestriction_date.btn.btn-secondary:hover, +#availability_addrestriction_grade.btn.btn-secondary:hover, +#availability_addrestriction_profile.btn.btn-secondary:hover, +#availability_addrestriction_list_.btn.btn-secondary:hover, +#availability_addrestriction_completion.btn.btn-secondary:hover { + background: none; + background-position: 0 -100%; + background-color: [[setting:buttonhovercolor]] !important; + text-decoration: none !important; + color: [[setting:buttontextcolor]] !important; +} + +input[type="submit"]:focus, +input[type="button"]:focus, +#searchform_button:focus, +a.submit:focus, #notice .singlebutton + .singlebutton input:focus, +.submit.buttons input[name="cancel"]:focus, +#page-login-index2 .forgetpass a:focus { + background-color: [[setting:buttonhovercolor]] !important; + background-position: 0 -100%; + text-decoration: none !important; + color: [[setting:buttontextcolor]] !important; +} + +button.close { + text-shadow: none !important; + color: #777; + opacity: 1 !important; + box-shadow: none !important; +} + +#edittingbutton .btn { + color: [[setting:editfont]] !important; +} + +#edittingbutton { + padding-right: 10px; + line-height: 22px; +} + +.navbar #edittingbutton .singlebutton { + line-height: inherit; +} + +.navbar #edittingbutton .singlebutton:first-child { + margin-right: 6px; +} + +#edittingbutton #coursesearchnavbar .form-group .btn { + margin: 0 5px 0px 5px; + padding: 4px 12px; +} + +#edittingbutton .helptooltip a { + background-color: transparent !important; + box-shadow: none !important; + border: none !important; + border-radius: 0 !important; +} + +.path-mod-workshop .assessment-summary.graded .singlebutton input[type="submit"]:hover, +.path-mod-workshop .example-summary.graded .singlebutton input[type="submit"]:hover, +input.form-submit:hover, +input#id_submitbutton:hover, +input#id_submitbutton2:hover, +.path-admin .buttons input[type="submit"]:hover, +td.submit input:hover, +input.form-submit:focus, +input#id_submitbutton:focus, +input#id_submitbutton2:focus, +.path-admin .buttons input[type="submit"]:focus, +td.submit input:focus, +input.form-submit:active, +input#id_submitbutton:active, +input#id_submitbutton2:active, +.path-admin .buttons input[type="submit"]:active, +td.submit input:active, +input.form-submit.active, +input#id_submitbutton.active, +input#id_submitbutton2.active, +.path-admin .buttons input[type="submit"].active, +td.submit input.active, input.form-submit.disabled, +input#id_submitbutton.disabled, +input#id_submitbutton2.disabled, +.path-admin .buttons input[type="submit"].disabled, +td.submit input.disabled, +input.form-submit[disabled], +input#id_submitbutton[disabled], +input#id_submitbutton2[disabled], +.path-admin .buttons input[type="submit"][disabled], +td.submit input[disabled] { + background-image: none; + background-color: [[setting:buttonhovercolor]] !important; + color: [[setting:buttontextcolor]] !important; + box-shadow: 0 [[setting:buttondropshadow]] 0 0 rgba(0, 0, 0, 0.5) inset; +} + +#edittingbutton .btn, +#edittingbutton input[type="submit"] { + background-image: none; + background-color: [[setting:editonbk]]; + border-radius: [[setting:buttonradius]]; + box-shadow: 0 [[setting:buttondropshadow]] 0 0 rgba(0, 0, 0, 0.5) inset; + color: [[setting:editfont]] !important; + height: 24px; + line-height: 14px; + padding: [[setting:editverticalpadding]] [[setting:edithorizontalpadding]]; +} + +#adaptable-page-header-wrapper #main-navbar #edittingbutton a { + color: [[setting:editfont]] !important; +} + +.editing #edittingbutton .btn, +.editing #edittingbutton input[type="submit"], +.editing #edittingbutton a:hover, +.purgecaches a, +.tool_dataprivacy a { + color: [[setting:editfont]]; + background-image: none; + background-color: [[setting:editoffbk]]; + padding: [[setting:editverticalpadding]] [[setting:edithorizontalpadding]]; + text-decoration: none !important; + box-shadow: 0 [[setting:buttondropshadow]] 0 0 rgba(0, 0, 0, 0.5) inset; + border-radius: [[setting:buttonradius]]; +} + +.fp-btn-choose { + margin-bottom: 10px; +} + +/* Set Cancel button to a link instead a button to improve usability */ +.submit.buttons input[name="cancel"], +[data-role="end"], +[data-role="cancel"], +.cancel a, +#id_cancel, +input[name="addcancel"], +button[name="resetbutton"] { + font-family: [[setting:fontname]]; + font-size: 14px; + line-height: 20px; + height: 36px; + cursor: pointer; + color: [[setting:buttontextcolorcancel]] !important; + background-image: none !important; + background-color: [[setting:buttoncolorcancel]] !important; + box-shadow: none; + border: none; +} + +/* Set Cancel hover to a link with the same colour that Cancel but underlined */ +.cancel a:hover, +[data-role="end"]:hover, +[data-role="cancel"]:hover, +input[name="addcancel"]:hover, +button[name="resetbutton"]:hover, +#id_cancel:hover { + color: [[setting:buttontextcolorcancel]] !important; + background-color: [[setting:buttonhovercolorcancel]] !important; + text-decoration: none !important; +} + +/* Disabled button hardcoded to grey. */ +.btn-primary.disabled, +.btn-primary[disabled] { + background-image: none; + background-color: #e0e0e0; + color: #bdbdbd; +} + +#notice .singlebutton + .singlebutton input, +.submit.buttons input[name="cancel"] { + color: [[setting:editfont]] !important; + background-image: none; + background-color: [[setting:editoffbk]] !important; + text-shadow: none; +} + +/* Login */ +.btn-login { + background-image: none; + background-color: [[setting:buttonlogincolor]]; + border: none; + border-radius: [[setting:buttonradius]]; + box-shadow: 0 [[setting:buttondropshadow]] 0 0 rgba(0, 0, 0, 0.5) inset; + color: [[setting:buttonlogintextcolor]]; + cursor: pointer; + padding: [[setting:buttonloginpadding]] 15px; + text-shadow: none; + transition: background 0.2s ease-out 0s; + margin-top: [[setting:buttonloginmargintop]]; + margin-right:2px; + height:[[setting:buttonloginheight]]; +} + +.btn-login:hover { + background-image: none; + background-color: [[setting:buttonloginhovercolor]]; + color: [[setting:buttonlogintextcolor]]; + text-decoration: none; +} + +/* Login page button */ +#loginbtn, +#guestlogin .btn { + height: 50px; + font-size: larger; +} + +.continuebutton { + margin: 10px 0; +} + +/* Complete activity button */ +.path-course-view li.activity span.autocompletion img, +.path-course-view li.activity form.togglecompletion .btn { + background-color: transparent; + box-shadow: none; + border: none; +} + +/* Caret */ +.btn .caret { + border-top-color: [[setting:buttontextcolor]] !important; +} + +/* Message Drawer styles (moodle 3.6 and higher) */ + +/* Hide messages drawer unused buttons */ +.message-drawer .btn.hidden { + display: none !important; +} + +/* Display buttons with same height than the rest */ +.message-drawer .btn-block { + min-height: 36px; +} + +/* Set action buttons to action colour */ +.message-drawer button[data-action="confirm-delete-conversation"], +.message-drawer button[data-action="confirm-block"] { + background-color: [[setting:editoffbk]] !important; + text-shadow: none !important; + color: #ffffff !important; +} + +/* Remove border from progress icon button styling that is present due to previous styles + defined for a links used with button styling. */ +a.btn[data-toggle="popover"] { + margin-left: 5px; + border: none; + background-color: transparent !important; +} + +/* btn-group */ +.btn-group .btn { + margin-right: 5px; +} + +.discussiontoggle { + margin-right: 5px; +} + +/* .btn-link styles. */ +.btn-link { + background-color: transparent; + color: [[setting:linkcolor]] !important; + box-shadow: none; +} + +.btn-link:hover, +.btn-link:focus { + background-color: transparent !important; + color: [[setting:linkhover]] !important; +} + +.btn-link .icon { + margin-right: 0; +} + +.btn-link .menu-action-text .icon { + margin-top: 8px; +} + + + +.discussiontoggle { + padding: 10px; +} + +/* btn-icon */ +.btn-icon { + padding-top: 8px !important; +} + +/* Forum subscribed button */ +div.discussionsubscription { + margin-top: -2px !important; +} + +/* Forum Move discussion button */ +#forummenu .btn-secondary { + margin-left: 5px; +} + +.fp-upload-btn { + margin-top: 1rem; +} + +/* Message button margin (applies to all the elements) */ +#page-user-profile .node_category li, +.path-user .node_category li { + margin-top: 15px; +} diff --git a/theme/adaptable/style/cardblocks.css b/theme/adaptable/style/cardblocks.css new file mode 100644 index 0000000..9305451 --- /dev/null +++ b/theme/adaptable/style/cardblocks.css @@ -0,0 +1,63 @@ +@media (min-width: 576px) { + body.zoomin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(100% - 0.5rem); } } + +@media (min-width: 840px) { + body.zoomin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(50% - 0.5rem); } } + +@media (min-width: 1100px) { + body.zoomin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(33.33% - 0.5rem); } } + +@media (min-width: 1360px) { + body.zoomin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(25% - 0.5rem); } } + +@media (min-width: 576px) { + body:not(.zoomin) .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(100% - 0.5rem); } } + +@media (min-width: 768px) { + body:not(.zoomin) .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(50% - 0.5rem); } } + +@media (min-width: 1100px) { + body:not(.zoomin) .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(33.33% - 0.5rem); } } + +@media (min-width: 1360px) { + body:not(.zoomin) .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(25% - 0.5rem); } } + +@media (min-width: 576px) { + body:not(.zoomin) #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(100% - 0.5rem); } } + +@media (min-width: 768px) { + body:not(.zoomin) #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(50% - 0.5rem); } } + +@media (min-width: 1100px) { + body:not(.zoomin) #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(33.33% - 0.5rem); } } + +@media (min-width: 1360px) { + body:not(.zoomin) #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(25% - 0.5rem); } } + +@media (min-width: 576px) { + body.zoomin #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(50% - 0.5rem); } } + +@media (min-width: 840px) { + body.zoomin #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(33.33% - 0.5rem); } } + +@media (min-width: 1100px) { + body.zoomin #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(25% - 0.5rem); } } + +@media (min-width: 1360px) { + body.zoomin #page.fullin .dashboard-card-deck:not(.fixed-width-cards) .dashboard-card { + width: calc(20% - 0.5rem); } } diff --git a/theme/adaptable/style/categorycustom.css b/theme/adaptable/style/categorycustom.css new file mode 100644 index 0000000..57cd867 --- /dev/null +++ b/theme/adaptable/style/categorycustom.css @@ -0,0 +1,29 @@ +/* +* 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/>. +* +* +* Adaptable Category Custom Style sheet +* +* @package theme_adaptable +* @copyright © 2019 - G J Barnard +* @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +* +*/ + +/* Category Custom CSS Settings */ + +[[setting:catgorycustomcss]] diff --git a/theme/adaptable/style/core.css b/theme/adaptable/style/core.css new file mode 100644 index 0000000..c700b78 --- /dev/null +++ b/theme/adaptable/style/core.css @@ -0,0 +1,44 @@ +/* + * 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/>. + * + * + * Adaptable core override styles style sheet + * + * @package theme_adaptable + * @copyright 2020 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +/* Calendar links on the page. */ +a.skip-block, +a.skip { + left: -1000em; +} + +.modchoosercontainer .btn { + border: none; + box-shadow: none; +} + +.modchoosercontainer .btn-icon { + padding-top: 0 !important; +} + +body.h5p-embed #page-content { + display: block; + padding: 0; +} diff --git a/theme/adaptable/style/course.css b/theme/adaptable/style/course.css new file mode 100644 index 0000000..b34c7dd --- /dev/null +++ b/theme/adaptable/style/course.css @@ -0,0 +1,1006 @@ +/* +* This file is part of Adaptable theme for moodle +* +* 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/>. +* +* +* Adaptable course CSS file +* +* @package theme_adaptable +* @copyright 2015-2019 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @copyright 2018-2019 Manoj Solanki (Coventry University) +* +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +/* ****************** */ +/* Common settings */ +/* ****************** */ + +/* Remove 'Your Progress' in Course page (only 3.6 or lower). */ +#completionprogressid { + display: [[setting:showyourprogress]]; + margin-bottom: 15px; +} + +#page-coursetitle h1 { + font-size: 1.5rem; + text-align: center; +} + +#coursetitle, +#sitetitle #coursetitle { + color: [[setting:fonttitlecolorcourse]]; +} + +/* ************************ */ +/* Course Tiles */ +/* ************************ */ + +/* Remove 'Available courses' label. */ +#frontpage-available-course-list h2 { + display: [[setting:enableavailablecourses]]; +} + +/* Display the course list container */ + +#frontpage-available-course-list { + display: block; + flex-flow: row wrap; + justify-content: space-between; + flex-grow: 1; + flex-wrap: wrap; +} + +#page-site-index .courses { + display: flex; + flex-flow: row wrap; +} + +.paging-morelink { + display: block; + clear:both; + width: 100%; +} + +/* Media breakpoints for courses list area */ +@media (min-width: 992px) { + #page-site-index .courses { + max-width: 960px; + } +} +@media (min-width: 1200px) { + #page-site-index .courses { + max-width: 100%; + } +} + +/* Course tiles */ +.coursebox.panel .panel-body { + border: 4px solid [[setting:tilesbordercolor]]; + margin: 2px; +} + +.coursebox.panel, +.coursebox.panel .panel-body { + position: relative; +} + +.coursebox .boxfooter { + clear: both; + margin-bottom: 30px; +} + +.coursebox .coursebtn { + bottom: 10px; + left: 0; + margin: 0 auto; + position: absolute; + right: 0; +} + +/* 1 tile. */ +.coursebox.col-12 .coursebtn { + width: 20%; +} + +/* 2 tiles. */ +.coursebox.col-6 .coursebtn { + width: 40%; +} + +/* 3 tiles. */ +.coursebox.col-4 .coursebtn { + width: 50%; +} + +/* 4 tiles. */ +.coursebox.col-3 .coursebtn { + width: 50%; +} + +/* 6 tiles. */ +.coursebox.col-2 .coursebtn { + width: 80%; +} + +.coursebox.panel ul.teachers li { + display: [[setting:tilesshowallcontacts]]; +} + +.coursebox.panel ul.teachers li:first-child { + display: block; +} + +.coursebox .panel-heading { + padding: 12px 10px 10px; +} + +.coursebox .panel-heading a { + font-size: 105%; +} + +.courses .coursebox.even { + background-color: #eee !important; +} + +.coursebox > .info > .coursename a { + background-size: 14px; + font-size: 14px; + font-weight: normal; +} + +.coursebox > .info > h3.name, +.course_category_tree .category > .info .name { + font-size: 120%; + font-weight: normal; + margin: 0; +} + +.coursebox .content { + clear: none; +} + +.coursebox { + padding: 0; + border: 0; + border-radius: 0; + flex-grow: 2 !important; + flex-shrink: 2 !important; + flex-basis: 90% !important; +} + +#page .covtiles.panel { + background: transparent !important; + border: 0; + position: relative; +} + +#page .covtiles.panel .panel-body { + border: 1px solid #e4e4e4; + border-radius: 2px; + box-shadow: 0 1px 2px #c1c1c1; + padding: 5px; + position: relative; +} + +#marketblocks.covtiles { + overflow: visible; +} + +#marketblocks .covtiles { + border-radius: 2px; + box-shadow: 0 1px 2px #c1c1c1; + position: relative; + border: 5px solid #fff; +} + +#page .covtiles.panel:hover .panel-body { + background-color: transparent !important; +} + +#marketblocks.covtiles .internalmarket { + background-color: transparent; + border: 0; + padding: 10px 5px; + text-align: left; + position: relative; +} + +#marketblocks.covtiles h3 { + margin-bottom: 0; +} + +#marketblocks.covtiles a.submit { + margin-top: 10px; + display: inline-block; +} + +#page .covtiles.panel .cimbox { + background-position: 50% 0 !important; + background-size: cover !important; + height: 200px; + overflow: hidden; +} + +#page .covtiles.panel .cimbox, +#page .covtiles.panel:hover .cimbox, +#page .covtiles.panel:hover { + opacity: 1; +} + +.covtiles.coursebox.panel.hover .panel-body .coursebox-content h3, +#marketblocks.covtiles h4 { + background: [[setting:covbkcolor]]; + border: 5px solid #fff; + border-left: 0; + border-right: 0; + border-bottom: 0; + color: [[setting:covfontcolor]] !important; + font-size: 18px; + font-weight: 800; + left: -5px; + margin: 0; + padding: 5px 10px; + text-align: left; + width: auto; + max-width: 100%; +} + +.covtiles.coursebox.panel.hover .panel-body .coursebox-content h3 a, +#marketblocks.covtiles h4 a, +.covtiles.coursebox.panel.hover .panel-body .coursebox-content h3 a:hover, +#marketblocks.covtiles h4 a:hover { + color: [[setting:covfontcolor]] !important; +} + +.covtiles .coursebox-content { + position: relative; + padding: 0 0; +} + +.coursebox.hover.covtiles2 .summary { + display: block !important; + background: [[setting:covbkcolor]]; + bottom: 0; + left: 0; + padding: 0; + right: 0; + top: inherit; + width: 98%; + height: 0; + min-height: 0; + max-height: 0; + overflow: hidden; + opacity: 1; + transition: max-height 1.0s !important; +} + +.coursebox.hover.covtiles2:hover .summary { + height: auto; + min-height: 30px; + padding: 10px 1%; + max-height: 1000px; + opacity: 1; + transition: max-height 1.0s !important; +} + +.covtiles.panel.hover .coursebtn.submit.btn { + display: block; + margin-top: 10px; +} + +.coursebox.hover.covtiles .summary { + background: [[setting:covbkcolor]]; + color: [[setting:covfontcolor]] !important; + display: block; + position: relative; + text-align: left; + min-width: auto; +} + +#page .covtiles.panel .panel-body { + padding-bottom: 38px; +} + +.covtiles .coursebox-content { + background-color: [[setting:covbkcolor]]; + bottom: 5px; + height: 38px; + left: 5px; + right: 4px; + position: absolute; + overflow: hidden; + transition: height .5s ease-in-out; + width: auto; + z-index: 9; /* Override .cimbox class. */ +} + +.covtiles .panel-body .coursebox-content:hover, +.covtiles .panel-body .coursebox-content:focus { + height: calc(100% - 5px); +} + +/* ************************ */ +/* Topics / Weekly format */ +/* ************************ */ + +.course-content ul.topics li.section { + padding-bottom: 0; +} + +#page .course-content ul li.section.main { + background-color: [[setting:coursesectionbgcolor]]; + border: [[setting:coursesectionborderwidth]] [[setting:coursesectionborderstyle]] [[setting:coursesectionbordercolor]]; + margin-bottom: 10px; + border-radius: [[setting:coursesectionborderradius]]; +} + +.site-topic ul, +.course-content ul { + margin-left: 0.1em; +} + +.site-topic ul.section, +.course-content ul.section { + margin: 0; + padding-left: 0.2em; +} + +#newdiscussionform { + text-align: center; +} + +#searchforums { + margin: 5px 0 5px 5px; +} + +.course-content .current { + background-color: transparent; + border-left: 0px solid #ccc; + border-right: 0px dashed #ccc; + margin-top: 0px !important; +} + +.panel-body { + overflow: hidden; +} + +.panel-body .courseimage img { + float: right; + margin-bottom: 10px; + max-width: 60px; + margin-left: 5px; +} + +.course-content ul li.section .side { + display: none; +} + +.editing .course-content ul li.section .side { + display: inherit; +} + +.course-content ul.topics li.section .content, +.course-content ul.weeks li.section .content { + margin: 0; +} + +.editing .course-content ul.topics li.section .content, +.editing .course-content ul.weeks li.section .content { + margin: 0; +} + +.course-content ul.topics li.section .content .summarytext, +.course-content ul.weeks li.section .content .summarytext { + margin-left: 15px; +} + +.section-handle { + margin: 0; + position: relative; + top: 12px; +} + +.course-content ul.topics li.section .left { + padding-top: 2px; + text-align: right; +} + +.course-content ul li.section.main { + margin: 0; +} + +.sectionname { + background-color: [[setting:coursesectionheaderbg]]; + border-bottom: [[setting:coursesectionheaderborderstyle]] [[setting:coursesectionheaderbordercolor]] [[setting:coursesectionheaderborderwidth]]; + border-bottom-left-radius: [[setting:coursesectionheaderborderradiusbottom]]; + border-bottom-right-radius: [[setting:coursesectionheaderborderradiusbottom]]; + border-radius: [[setting:coursesectionheaderborderradiustop]]; + color: [[setting:sectionheadingcolor]]; + margin-bottom: 20px; + padding: 3px 10px; +} + +.format-onetopic .onetopic .tab_content { + background-color: [[setting:coursesectionheaderbg]]; +} + +/* Alter nav on single-section for better onetopic course format support */ +.single-section .nav { + margin-bottom: 0; +} + +.single-section .nav-tabs { + border-bottom: none; +} + +.path-course-view .completionprogress { + color: #666; + float: none; + font-size: 90%; + padding: 0 0 10px; +} + +#gridshadebox_content .content .summary, +#gridshadebox_content .content ul.section { + padding: 0 20px; +} + +/* Current Section */ +#page .course-content ul li.section.main.current{ + background-color: [[setting:currentcolor]]; +} + +.format-onetopic .onetopic .nav-tabs .nav-link .tab_content .sectionname { + background-color: inherit; +} + +.format-onetopic .onetopic .nav-tabs .nav-link.active .tab_content, +.format-onetopic .onetopic .nav-tabs .nav-link.active .tab_content .sectionname { + background-color: [[setting:currentcolor]] !important; +} + +.course-content ul li.section.hidden .sectionname, +.course-content ul li.section.hidden .content > div { + margin-right: 40px; +} + +/* ************* */ +/* Social Wall */ +/* ************* */ + +/* All sections--------------------------------------------------------------------------- */ +/* Add padding, background, and word-wrap on all sections*/ +.format-socialwall .topics { + padding: 2em; + border-radius: [[setting:socialwallsectionradius]]; + background-color: [[setting:socialwallbackgroundcolor]]; + word-wrap: break-word; + font-family: [[setting:fontname]]; +} + +/*Add padding, background, and border to all sections */ +.format-socialwall .section.main.clearfix#section-0, +.format-socialwall .section.main.clearfix#section-2, +.format-socialwall .topics > form, +.format-socialwall #counttotalpost, +.format-socialwall .topics .tl-post, +.format-socialwall .tl-post .tl-post-actionarea { + padding: 1em; + background: #fff; + border: [[setting:socialwallborderwidth]] [[setting:socialwallbordertopstyle]] [[setting:socialwallbordercolor]]; + border-radius: [[setting:socialwallsectionradius]]; +} + +/* Section 0 ------------------------------------------------------------------------------- */ +/* Combine section 0, section 2, search form, and post count */ +.format-socialwall .section.main.clearfix#section-0 { + border-bottom: 0; + margin-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.format-socialwall .section.main.clearfix#section-2 { + border-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom: 0; + margin-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.format-socialwall .topics > form { + border-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.format-socialwall #counttotalpost { + border-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +/* Make title background white */ +.format-socialwall .section.main.clearfix#section-0 h3.sectionname { + background: #fff; +} + +/* Section 2------------------------------------------------------------------------------- */ +/* Make activity/resource link inactive */ +.format-socialwall #postform input#sw-addactivitylink { + pointer-events: none; +} + +/* Text area resizing--------------------------------------------------------------------- */ +/* Allow the comment starting text area to expand to the width of the parent element */ +.format-socialwall div.felement.ftextarea { + max-width: 100%; +} +/* Set the starting text areas of post and comment to the maximum width, and stop them from expanding futher than their parent element */ +.format-socialwall .sw-texarea, .format-socialwall .tl-commentform .tl-commenttext { + box-sizing: border-box; + min-width: 100%; + max-width: 100%; +} + +/* Post----------------------------------------------------------------------------------- */ +/* Add space between post text and author, and post text and attached files */ +.format-socialwall .tl-posttext .text_to_html { + padding: 2em 0 1.5em; +} + +/* Remove border between post author picture and post */ +.format-socialwall .tl-text { + border-left: 0; +} + +/* Resize, re-align, and recolour post time edited ago */ +.format-socialwall .tl-edited-wrapper { + font-size: 0.8em; + text-align: right; + color: [[setting:fontcolor]]; + background: #fff; +} + +/* Comment--------------------------------------------------------------------------------- */ +/* Add space at the bottom of each comment */ +.format-socialwall .tl-comment { + padding-bottom: 1em; +} + +/* Add space between comment author name and comment text */ +.format-socialwall .tl-text .tl-authorname { + padding-bottom: 1.5em; +} + +/* Add space between comment text and time ago */ +.format-socialwall .tl-comment .tl-text div:nth-child(2) { + padding-bottom: 1.5em; +} + +/* Remove space at the bottom of replywrapper */ +.format-socialwall .tl-comment-replywrapper { + padding-bottom: 0; +} + +/* Moves the required text to the right */ +div.fdescription.required { + float: right; +} + +/* Post attachment------------------------------------------------------------------------- */ +/* Add space between attached files and post text or post author, and space between attached files and post action area */ +.format-socialwall ul.section.tl-postattachment { + padding: 2em 0 2.5em; + margin: 0; +} + +/* Present attached files horizontally */ +.format-socialwall ul.section.tl-postattachment div { + float: left; +} + +/* Post action area------------------------------------------------------------------------ */ +/* Add space around post counts and timeago */ +.format-socialwall .tl-counts, .format-socialwall .tl-timeago { + font-size: 0.8em; + color: [[setting:fontcolor]]; + padding: 1em 0 1.5em; +} + +/* Add space after each count (likes and comments) */ +.format-socialwall .tl-counts span { + padding-right: 1.5em; +} + +/* Add space at the end of action area and remove right, bottom, left border */ +.format-socialwall .tl-post .tl-post-actionarea { + padding: 0 0 1em; + border-left: 0; + border-right: 0; + border-bottom: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +/* Post action links---------------------------------------------------------------------- */ +/* Make the color of action link grey */ +.format-socialwall .tl-actionlink a { + color: [[setting:socialwallactionlinkcolor]]; +} + +/* Add space after like */ +.format-socialwall .tl-actionlink a.like { + padding-right: 3em; +} + +/* Add space after likenomore and change color to light blue */ +.format-socialwall .tl-actionlink a.likenomore { + padding-right: 3em; + font-weight: bold; +} + +/* Change color on hover over to all action link from grey to light blue */ +.format-socialwall .tl-actionlink a:hover { + font-weight: bold; + color: [[setting:socialwallactionlinkhovercolor]]; + text-decoration: none; +} + +/* Before the like action link----------------------------------------------------- */ +/* Add a hollow thumbs up icon before like */ +.format-socialwall .like:before { + font-family: "Font Awesome 5 Free"; + content: "\f087"; + position: relative; + padding-right: .15em; +} + +/* Add a full thumbs up icon before like */ +.format-socialwall .likenomore:before { + font-family: "Font Awesome 5 Free"; + content: "\f164"; + position: relative; + padding-right: .15em; +} + +/* Add a hollow comment icon before like */ +.format-socialwall .tl-actionlink a:nth-child(3):before { + font-family: "Font Awesome 5 Free"; + content: "\f0e5"; + position: relative; + padding-right: .15em; +} + +/* Profile picture------------------------------------------------------------------ */ +/* Make the profile picture circular */ +.format-socialwall img.userpicture { + border: 0.25em solid #ffffff; + border-radius: 50%; + box-shadow: 0 0.05em 0.05em rgba(0,0,0,0.05); +} + +.activity_footer, +.section_footer { + margin-top: 2em; + padding: 1em 0; + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; + clear: both; + display: block; +} + +.section_footer .prevnext { + font-style: normal; + text-decoration: none; + display: inline-block; + width: 100%; +} + +@media (min-width: 768px) { + .section_footer .previous_activity, + .section_footer .previous_section, + .section_footer .next_activity, + .section_footer .next_section { + width: 50%; + vertical-align: top; + } + +} + +.section_footer > .jumpmenu { + width: 100%; +} + +.next_activity, +.next_section { + text-align: right; + min-height: 1px; +} + +.jumpnav > .jumpmenu { + text-align: center; + margin-top: 1em; +} +.jumpnav > .jumpmenu #jump-to-activity.custom-select { + width: auto; +} + +.previous_activity, +.previous_section { + min-height: 1px; +} + +.nav_icon { + padding: .3em; + text-align: center; + font-size: 1.5em; + line-height: 2em; + display: table-cell; + min-width: 50px; +} + +.previous_activity .nav_icon, +.previous_section .nav_icon { + margin-right: .3em; + border-right: 1px solid #eee; +} +.previous_activity .text, +.previous_section .text { + padding-left: 9px; +} +.next_activity .nav_icon, +.next_section .nav_icon { + margin-left: .3em; + border-left: 1px solid #eee; +} +.next_activity .text, +.next_section .text { + padding-right: 9px; +} + +.activity_footer .text, +.section_footer .text { + display: table-cell; + width: 10007px; + vertical-align: top; + padding-top: 9px; + color: #565656; +} + +.nav_guide { + letter-spacing: .1em; + text-transform: uppercase; + font-style: normal; +} + +/* Activity styling */ +.block_site_main_menu .mod-indent .activity, +.block_site_main_menu .main-menu-content .activity { + padding: 5px 0px 5px 0px; +} + +.section-modchooser { + padding: 5px 10px; +} + +.section li.modtype_assign div.activity-wrapper { + -webkit-box-shadow: -[[setting:coursesectionactivityleftborderwidth]] 0 0 0 [[setting:coursesectionactivityassignleftbordercolor]]; + box-shadow: -[[setting:coursesectionactivityleftborderwidth]] 0 0 0 [[setting:coursesectionactivityassignleftbordercolor]]; + border-bottom: [[setting:coursesectionactivityborderwidth]] [[setting:coursesectionactivityborderstyle]] [[setting:coursesectionactivitybordercolor]]; + background-color: [[setting:coursesectionactivityassignbgcolor]]; +} + +.section li.modtype_forum div.activity-wrapper { + -webkit-box-shadow: -[[setting:coursesectionactivityleftborderwidth]] 0 0 0 [[setting:coursesectionactivityforumleftbordercolor]]; + box-shadow: -[[setting:coursesectionactivityleftborderwidth]] 0 0 0 [[setting:coursesectionactivityforumleftbordercolor]]; + border-bottom: [[setting:coursesectionactivityborderwidth]] [[setting:coursesectionactivityborderstyle]] [[setting:coursesectionactivitybordercolor]]; + background-color: [[setting:coursesectionactivityforumbgcolor]]; +} + +.section li.modtype_quiz div.activity-wrapper { + -webkit-box-shadow: -[[setting:coursesectionactivityleftborderwidth]] 0 0 0 [[setting:coursesectionactivityquizleftbordercolor]]; + box-shadow: -[[setting:coursesectionactivityleftborderwidth]] 0 0 0 [[setting:coursesectionactivityquizleftbordercolor]]; + border-bottom: [[setting:coursesectionactivityborderwidth]] [[setting:coursesectionactivityborderstyle]] [[setting:coursesectionactivitybordercolor]]; + background-color: [[setting:coursesectionactivityquizbgcolor]]; +} + +.activity img.activityicon { + margin-right: 0; +} + +/* Label Info */ +.section .activity .availabilityinfo { + margin-left: 30px; + margin-top: 1em; +} + +.availabilityinfo .label-info { + background-color: [[setting:alertbackgroundcolorinfo]]; + border-color: [[setting:alertbordercolorinfo]]; + color: [[setting:alertcolorinfo]]; + font-size: 14px; + padding: 10px; +} + +.availabilityinfo .label-info a, +.availabilityinfo .label-info a:hover { + color: [[setting:alertcolorinfo]] !important; +} + + +/* Activity further information styling */ +.ad-activity-meta-container { + padding: 5px 0 0 0; + margin: 0px 10px 0 30px; +} + +.ad-activity-due-date { + background-color: #555555; + color: #ffffff; + padding: 7px; + margin: 0 10px 0 0; + line-height: 3em; + border-radius: 0.5em; + font-size: 0.9em; + font-weight: 500; + white-space: nowrap; +} + +.ad-activity-due-date a { + color: #fff; +} + +.ad-activity-mod-feedback { + padding: 5px 0; +} + +.ad-activity-mod-feedback i { + font-size: 1.1em; +} + +.ad-activity-action { + margin: 0 7px; +} + +.ad-activity-action i { + margin-right: 0.35rem; +} + +.ad-activity-date-overdue { + background-color: #f8d7da; +} + +.ad-activity-date-overdue a { + color: #721c24; +} + +.ad-activity-date-overdue .text-warning { + color: #721C24 !important; +} + +.ad-activity-date-submitted { + background-color: #d4edda; +} + +.ad-activity-date-submitted a { + color: #155724; +} + +.ad-activity-date-nearly-due { + background-color: #fff3cd; +} + +.ad-activity-date-nearly-due a { + color: #856404; +} + +.ad-activity-mod-feedback i, +.ad-activity-mod-engagement i, +.ad-activity-due-date i { + font-size: 1.1em; +} + +li.activity div.actions-right { + float: right; padding: 5px; +} + +.ad-activity-wrapper { + margin: [[setting:coursesectionactivitymargintop]] 0 [[setting:coursesectionactivitymarginbottom]] 0; + padding: 5px 10px 5px 0; +} + +.section .activity .mod-indent-outer { + width: 100%; +} + +.section .label .mod-indent-outer { + padding-left: 0; +} + +.section .activity.modtype_label.label { + padding: 0; +} + +.topics .section .activity .mod-indent-outer { + margin-left: 15px; +} + +.course-content ul.topics li.section .summary { + margin: 5px 5px 5px 10px; +} + +.section .activity .actions-right .actions { + position: absolute; + right: 17px; + top: 15px; +} + +.section .activity .activityinstance { + min-width: 60%; + margin-left: 5px; +} + +.course-content ul.gtopics li.section .content { + margin: 0 10px; +} + +.course-content ul.ftopics li.section .content { + margin: 0 10px; +} + +.section li.activity { + padding: 0; + margin-bottom: 5px; +} + +.course-content .single-section .section-navigation { + padding: .1em; +} + +/* region-main-settings-menu is used on pages other than course pages, but included here to keep the settings together. */ +.context-header-settings-menu, +.region-main-settings-menu { + height: 2em; + margin: 4px 0 0; +} + +#region-main-settings-menu > div { + position: static; + right: auto; + margin: 0; +} + +.section .activity .contentwithoutlink > a, +.section .activity .activityinstance > a, +.section .activity .contentwithoutlink .inplaceeditable > a, +.section .activity .activityinstance .inplaceeditable > a { + color: [[setting:coursesectionactivityheadingcolour]]; +} + +.section .activity .contentwithoutlink > a > span, +.section .activity .activityinstance > a > span, +.section .activity .contentwithoutlink .inplaceeditable > a > span, +.section .activity .activityinstance .inplaceeditable > a > span { + font-size: 1.1em; + padding-left: 8px; + font-weight: 800; +} diff --git a/theme/adaptable/style/custom.css b/theme/adaptable/style/custom.css new file mode 100644 index 0000000..282a3ee --- /dev/null +++ b/theme/adaptable/style/custom.css @@ -0,0 +1,32 @@ +/* +* 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/>. +* +* +* Adaptable Custom Style sheet +* +* @package theme_adaptable +* @copyright 2015-2016 Jeremy Hopkins (Coventry University) +* @copyright 2015-2017 Fernando Acedo (3-bits.com) +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +* +*/ + +/* Custom CSS Settings */ +/* Add the CSS styles from the CSS custom box */ + + + +[[setting:customcss]] diff --git a/theme/adaptable/style/extras.css b/theme/adaptable/style/extras.css new file mode 100644 index 0000000..a4398f2 --- /dev/null +++ b/theme/adaptable/style/extras.css @@ -0,0 +1,1263 @@ +/* +* This file is part of Adaptable theme for moodle +* +* 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/>. +* +* +* Adaptable Extras CSS file +* +* @package theme_adaptable +* @copyright 2015-2019 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @copyright 2018-2019 Manoj Solanki (Coventry University) +* +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +#back-to-top { + background-color: #000; + background-color: rgba(0, 0, 0, 0.25); + border-radius: 5px; + box-shadow: 0 0 1px #fff; + color: #fff; + display: none; + padding: 5px 12px; + position: fixed; + right: 3px; + text-align: center; + text-decoration: none; + text-transform: uppercase; + top: 85%; + transition: all 0.3s ease 0s; + font-size: 1.1em; + cursor: pointer; +} +#back-to-top:hover .fa { + color: [[setting:linkhover]]; +} + +.gradereport-grader-table img { + max-width: none; +} + +.singleselect { + display: inline-block; + max-width: 100%; + width: 100%; +} + +.panel-heading { + border-bottom: 1px solid #000; + padding: 12px 15px 10px; + background-color: #f3f3f3; + border-color: #e8eaeb; + color: #555; +} + +.panel-heading a { + font-size: 110%; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 80%; + display: inline-block; +} + +.panel-body { + padding: 15px; +} + +ul.teachers { + list-style-type: none; + margin: 0; + padding: 0; + font-size: 90%; +} + +.btn.btn-info { + background-image: none !important; + color: #fff; + padding: 8px 15px; + text-shadow: none; +} + + +/* Messages */ +.messaging-area-container .messaging-area .contacts-area .contacts .contact.selected { + background-color: #e9ebeb; + color: #333; +} + +.messaging-area-container .messaging-area .contacts-area .contacts .contact.selected .information .lastmessage { + color: #333; +} + +.messaging-area-container .messaging-area .contacts-area .contacts .contact:hover .information .lastmessage { + color: #333; +} + +.messaging-area-container .messaging-area .contacts-area .tabs .tab.selected { + color: [[setting:linkcolor]] !important; + +} + +.messaging-area-container .messaging-area .contacts-area .tabs .tab { + background-color: transparent; +} + +.messaging-area-container .messaging-area .messages-area .messages .message .content.right { + background-color: #dcf8c6; + border-radius: 5px; + border: 1px solid #c0c0c0; + box-shadow: 2px 2px 1px 0 rgba(0, 0, 0, 0.2); +} + +.messaging-area-container .messaging-area .messages-area .messages .message .content.left { + background-color: #fff; + border-radius: 5px; + border: 1px solid #c0c0c0; + box-shadow: 2px 2px 1px 0 rgba(0, 0, 0, 0.2); +} + +.messaging-area-container .messaging-area .contacts-area .contacts .contact:hover { + background-color: #e9ebeb; + color: #333; +} + +.messaging-area-container .messaging-area .messages-area .messages .message .content .text p { + font-size: 1.2em; +} + +.messaging-area-container .messaging-area .messages-area .messages .message .content .timesent { + color: #808080; + float: right; + font-size: 1em; +} + +.messaging-area-container .messaging-area .messages-area .messages { + background-color: [[setting:messagingbackgroundcolor]]; +} + +.messaging-area-container .messaging-area .messages-area .response { + padding: 0 10px 9px; +} + +.information .lastmessage, +.information .lastmessage:hover { + color: #e9ebeb; +} + +.msg-title, +.msg-sender, +.msg-time { + color: [[setting:fontcolor]]; +} + +.msg-time { + float: right; + font-size: 1em; +} + +.messaging-area-container .messaging-area .messages-area .messages-header .name-container .status { + font-size: 12px; + font-weight: normal; + line-height: 16px; +} + +.messaging-area-container .messaging-area .messages-area .messages-header .btn { + height: 25px; + line-height: 25px; + padding: 0 5px; + width: auto; + color: [[setting:linkcolor]] !important; + background: none !important; + border: 0; + box-shadow: none; + font-size: 18px; +} + +.messaging-area-container .messaging-area .messages-area .messages-header .btn .messages-delete, +.messaging-area-container .messaging-area .messages-area .messages-header .btn .cancel-messages-delete { + height: 25px; + line-height: 25px; + padding: 0 5px; + width: auto; + font-size: 14px; +} + +.messaging-area-container .messaging-area .messages-area .response { + border-top: none; +} + +.messaging-area-container .messaging-area .messages-area .messages .blocktime { + color: [[setting:maincolor]]; + font-weight: bold; + font-size: 16px; + background-color: rgba(255, 255, 255, 0.6); + border-radius: 4px; +} + +.messaging-area-container .messaging-area .contacts-area .tabs .tab { + color: [[setting:linkcolor]]; +} + +.messaging-area-container .messaging-area .contacts-area .tabs .tab:hover { + color: [[setting:linkhover]]; +} + +.messaging-area-container .messaging-area .messages-area .messages-header .actions { + font-weight: normal; + padding-right: 15px; + position: absolute; + right: 0; + top: 0; +} + +.messaging-area-container .status .online-text { + color: #51666C; +} + +.messages-area .actions a:hover { + text-decoration: none; +} + +/* Messages Pop-Up (moodle 3.2+) */ +#newmessageoverlay { + width: 276px; + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + background-color: #ffffff; + background-clip: padding-box; + border-radius: 6px; + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + white-space: normal; + position: fixed; + top: inherit; + left: inherit; + bottom: 5px; + right: 5px; + display: block; +} + +#newmessageoverlay.top { + margin-top: -10px; +} + +#newmessageoverlay.right { + margin-left: 10px; +} + +#newmessageoverlay.bottom { + margin-top: 10px; +} + +#newmessageoverlay.left { + margin-left: -10px; +} + +#newmessageoverlay.top > .arrow { + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + bottom: -11px; +} + +#newmessageoverlay.top > .arrow:after { + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #fff; +} + +#newmessageoverlay.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); +} + +#newmessageoverlay.right > .arrow:after { + content: " "; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: #fff; +} + +#newmessageoverlay.bottom > .arrow { + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + top: -11px; +} + +#newmessageoverlay.bottom > .arrow:after { + content: " "; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: #fff; +} + +#newmessageoverlay.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); +} + +#newmessageoverlay.left > .arrow:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: #ffffff; + bottom: -10px; +} + +#newmessageoverlay a { + text-decoration: underline; +} + +#newmessageoverlay #newmessagetext { + margin: 0; + padding: 8px 14px; + font-size: 14px; + font-weight: normal; + line-height: 18px; +} + +#newmessageoverlay #usermessage { + padding: 9px 14px; + border: 1px dotted rgba(0, 0, 0, 0.2); + border-radius: 4px; + margin-top: 5px; + background-color: #ffffff; +} + +#newmessageoverlay #newmessagelinks { + margin: 5px 14px; +} + +.box.message .messagearea .messagehistory .box.center table.message_user_pictures td#user1, +.box.message .messagearea .messagehistory .box.center table.message_user_pictures td#user2 { + display: block; + height: 160px; +} + +.box.message .messagearea .messagehistory .box.center table.message_user_pictures td#user1 img.userpicture, +.box.message .messagearea .messagehistory .box.center table.message_user_pictures td#user2 img.userpicture { + border-radius: 50%; +} + +.box.message .messagearea .messagehistory .box.center table.message_user_pictures td#user1 img.userpicture { + border: 3px solid #d6e9c6; +} + +.box.message .messagearea .messagehistory .box.center table.message_user_pictures td#user2 img.userpicture { + border: 3px solid #bce8f1; +} + +.box.message .messagearea .messagehistory .messagehistorytype { + color: #fff; + padding: 5px 15px; + border-bottom: 1px solid #ddd; + border: 0; +} + +.box.message .messagearea .messagehistory .messagehistorytype > li { + float: left; + margin-bottom: -1px; +} + +.box.message .messagearea .messagehistory .messagehistorytype > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} + +.box.message .messagearea .messagehistory .messagehistorytype > li > a:hover { + border-color: #eee #eee #ddd; +} + +.box.message .messagearea .messagehistory .messagehistorytype > li.active > a, +.box.message .messagearea .messagehistory .messagehistorytype > li.active > a:hover, +.box.message .messagearea .messagehistory .messagehistorytype > li.active > a:focus { + color: #555; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} + +.box.message .messagearea .messagehistory .messagehistorytype.nav-justified { + width: 100%; + padding: 10px 0 0 0; + border-bottom: 0; +} + +.box.message .messagearea .messagehistory .messagehistorytype.nav-justified > li { + float: none; +} + +.box.message .messagearea .messagehistory .messagehistorytype.nav-justified > li > a { + text-align: center; + margin-bottom: 5px; +} + +.box.message .messagearea .messagehistory .messagehistorytype.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} + +#intro.generalbox { + background-color: [[setting:introboxbackgroundcolor]]; +} + +/* Forum */ +.forumpost .maincontent .left { + padding: 0 10px 0 10px; +} + +.forumpost .row .left { + width: auto; +} + +.forumpost .options .commands { + margin-left: 0; + float: right; +} + +.forumpost .subject { + font-weight: bold; +} + +.forumsearch input[type=text] { + margin-bottom: 0 !important; +} + +/* Further styling for forum block. */ +.forumpost .userpicture { + width: 50px!important; + height: 50px!important; + display: inline; + padding: 1px; + margin: 0 10px 0 0; + background-color: #fff; + border-radius: 50%; + border: 5px solid #fff; +} + +.forumpost .picture img.userpicture { + margin: 0px 10px 0px 10px; +} + +.forumpost .header .topic { + margin: 0px 0px 10px 10px; +} + +.forumpost { + background-color: [[setting:forumbodybackgroundcolor]]; +} + +.forumpost .header { + padding: 10px 0px 10px 0px; + background-color: [[setting:forumheaderbackgroundcolor]]; + border-radius: 0 0 0 0; +} + +.showblockicons .block_sitenews .header .title h2:before { + content: "\f1ea"; + font-family: "FontAwesome"; +} + +/* Lesson Activity */ +#page-mod-lesson-edit .singleselect { + width: auto; +} + +.path-mod-lesson .answeroption .fcheckbox label, +.path-mod-lesson .mform .fitem.answeroptiongroup fieldset.fgroup label { + float: left; + padding-left: 40px; +} + +.lessonbutton a { + background-color: [[setting:buttoncolor]]; + color: [[setting:buttontextcolor]] !important; + padding: 5px; + margin: 10px; + box-shadow: 0 -2px 0 0 rgba(0, 0, 0, 0.5) inset; + border: 0; + border-radius: [[setting:buttonradius]]; + cursor: pointer; + padding: 8px 15px 8px; + transition: background .2s ease-out; +} + +.lessonbutton a:hover { + background-image: none; + background-color: [[setting:buttonhovercolor]]; + color: [[setting:buttontextcolor]] !important; + box-shadow: 0 -2px 0 0 rgba(0, 0, 0, 0.5) inset; +} + +.box { + margin-top: 15px; +} + +.block .content .box { + margin-top: 0; +} + +/* Forum Activity */ +#page-mod-forum-discuss .discussioncontrol.displaymode { + text-align: left; +} + +/* #page-mod-forum-discuss .discussioncontrols .discussioncontrol { + float: none; + width: 100%; +} */ + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect { + width: 100%; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect { + display: block; + width: auto; + height: 38px; + padding: 8px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect:focus { + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect::-moz-placeholder { + color: #999; + opacity: 1; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect:-ms-input-placeholder { + color: #999; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect::-webkit-input-placeholder { + color: #999; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect[disabled], +#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect[readonly], +fieldset[disabled] #page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect { + cursor: not-allowed; + background-color: #eee; + opacity: 1; +} + +textarea#page-mod-forum-discuss .discussioncontrols .discussioncontrol .singleselect select.singleselect { + height: auto; +} + +/* For forum page, to add padding to each discussion in the table. Applies to Moodle 3.6 only. */ +.path-mod-forum .forumheaderlist tbody .discussion td { + padding-top: 0.5em; + padding-bottom: 0.5em; +} + +/* Remove margin for form elements where it's not needed. */ +#page-mod-forum-discuss .discussioncontrols .form-inline, +.forumsearch .form-inline { + margin: 0; +} + +#page-mod-forum-discuss .discussioncontrols input { + margin: 0; + padding: .6rem 1rem; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect { + display: block; + width: 100%; + height: 38px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + margin: 0 10px 0 0; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + width: auto; + float: left; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect:focus { + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect::-moz-placeholder { + color: #999; + opacity: 1; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect:-ms-input-placeholder { + color: #999; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect::-webkit-input-placeholder { + color: #999; +} + +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect[disabled], +#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect[readonly], +fieldset[disabled] #page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect { + cursor: not-allowed; + background-color: #eee; + opacity: 1; +} + +textarea#page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion select.urlselect { + height: auto; +} + +@media (max-width: 575px) { + #page-mod-forum-discuss .discussioncontrols .discussioncontrol { + float: none; + } +} + +@media (min-width: 576px) { + #page-mod-forum-discuss .discussioncontrols .discussioncontrol, + #page-mod-forum-discuss .discussioncontrols .discussioncontrol.movediscussion.movediscussion { + float: left; + } +} + +#page-mod-forum-discuss .forumpost .row .left.picture img, +#page-site-index .forumpost .row .left.picture img { + border: 5px solid #fff; + border-radius: 270px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + height: 50px; + padding: 1px; + vertical-align: middle; + width: 50px; + margin-right: 10px; +} + +.forumpost .subject { + font-size: 1.8em; + line-height: normal; + font-weight: 400; + /* padding: 15px 0 10px; */ +} + +.forumpost .content { + clear: both; + padding: 0 15px; +} + +.forumpost .row.maincontent .left { + float: none; +} + +/* #page-mod-forum-discuss .forumpost .row .options .commands { + color: #fafafa; + display: inline-block; + float: right; +} */ + +#page-mod-forum-discuss .forumpost .row .options .commands a { + font-size: 12px; + border: 1px solid #b4b4b7; + background-color: #f7f7f7; + color: #000000 !important; + padding: 9px 12px 9px; + line-height: 22px; + margin: 0 6px; + text-decoration: none; + border-radius: 3px; + transition: 0.25s; +} + +#page-mod-forum-discuss .forumpost .row .options .commands a:hover { + background: #1990d5; + color: #ffffff !important; + font-weight: 400; +} + +.options.clearfix { + padding: 15px 5px; +} + +.signuppanel .row .btn { + display: none; +} + +#changenumsections .smallicon { + width: 24px; +} + +#id_category, +#id_parent { +/* width: 100%; */ + max-width: 100%; +} + +/* News Ticker */ +#ticker, +#ticker_container { + font-size: 16px; +} + +#ticker-wrap { + position: relative; + margin-bottom: 15px; + margin-top: 15px; + background: #eee; + line-height: 40px; + [[setting:tickerwidth]] +} + +#page-site-index #ticker-wrap { + display: block; +} + +#ticker-announce { + background-color: [[setting:maincolor]]; + padding: 0 15px; + color: #eee; + margin-right: 15px; + margin-left: -15px; + text-transform: uppercase; + font-weight: 600; +} + +#ticker-wrap #controls { + padding-right: 15px; +} + +#ticker-wrap #controls .fa { + color: [[setting:maincolor]]; + cursor: pointer; + margin-top: 10px; +} + +#ticker-wrap #newscontent { + max-width: 75%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#ticker-wrap #newscontent #news { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#play_trigger a, #pause_trigger a { + color: [[setting:maincolor]] !important; +} + +/* Slider */ +/* Browser Resets */ +.flex-container a:active, +.flexslider a:active, +.flex-container a:focus, +.flexslider a:focus { + outline: none; +} + +.slides, +.flex-control-nav, +.flex-direction-nav { + margin: 0; + padding: 0; + list-style: none; +} + +/* IMPORTANT! FlexSlider Necessary Styles */ +.flexslider { + margin: 0; + padding: 0; +} + +.flexslider .slides > li { + display: none; + -webkit-backface-visibility: hidden; +} + +/* Hide the slides before the JS is loaded. Avoids image jumping */ +.flexslider .slides img { + width: 100%; + display: block; +} + +.flex-pauseplay span { + text-transform: capitalize; +} + +/* Clearfix for the .slides element */ +.slides:after { + content: "."; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; +} + +html[xmlns] .slides { + display: block; +} + +* html .slides { + height: 1%; +} + +/* No JavaScript Fallback */ +/* If you are not using another script, such as Modernizr, make sure you * include js that eliminates this class on page load */ +.no-js .slides > li:first-child { + display: block; +} + +/* FlexSlider Default Theme *********************************/ +.flexslider { + margin: 0; + background: #fff; + position: relative; + zoom: 1; +} +.flex-viewport { + max-height: 2000px; + -webkit-transition: all 1s ease; + -moz-transition: all 1s ease; + transition: all 1s ease; +} +.dir-rtl .flex-viewport { + direction: ltr; + max-height: 2000px; + -webkit-transition: all 1s ease; + -moz-transition: all 1s ease; + transition: all 1s ease; +} +.loading .flex-viewport { + max-height: 300px; +} +.flexslider .slides { + zoom: 1; +} + +/* Direction Nav */ +.flex-direction-nav a { + width: 30px; + height: 30px; + display: block; + position: absolute; + top: 50%; + cursor: pointer; + text-indent: -9999px; + opacity: 0; + -webkit-transition: all .3s ease; + margin: 0 10px; +} + +.flex-direction-nav .flex-next { + background: #333 url([[pix:theme|next]]) no-repeat 50% 50%; + right: 0; + opacity: .2; +} + +.flex-direction-nav .flex-prev { + background: #333 url([[pix:theme|previous]]) no-repeat 50% 50%; + left: 0; + opacity: .2; +} + +.flexslider:hover .flex-next { + opacity: 1; +} + +.flexslider:hover .flex-prev { + opacity: 1; +} + +.flex-direction-nav .disabled { + opacity: 0.3 !important; + filter: alpha(opacity=30); + cursor: default; +} + +/* Direction Nav (Blog Slider) */ +.image-slider .flex-direction-nav a { + opacity: 0; +} + +.image-slider .flex-direction-nav .flex-next { + background: #333 url([[pix:theme|next]]) no-repeat 50% 50%; + right: 0; +} + +.image-slider .flex-direction-nav .flex-prev { + background: #333 url([[pix:theme|previous]]) no-repeat 50% 50%; + left: 0; +} + +.image-slider:hover .flex-next { + opacity: 0.3; +} + +.image-slider:hover .flex-prev { + opacity: 0.3; +} + +/* Control Nav */ +.flex-control-nav { + width: 100%; + position: absolute; + bottom: 10px; + text-align: center; +} + +.flex-control-nav li { + margin: 0 4px; + display: inline-block; + zoom: 1; + *display: inline; +} + +.flex-control-paging li a { + width: 8px; + height: 8px; + display: block; + background: #666; + background: rgba(0, 0, 0, 0.1); + cursor: pointer; + text-indent: -9999px; + border-radius: 20px; + box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3); +} + +.flex-control-paging li a:hover { + background: #333; + background: rgba(0, 0, 0, 0.7); +} + +.flex-control-paging li a.flex-active { + background-color: #000; + cursor: default; +} + +.flex-control-thumbs { + margin: 5px 0 0; + position: static; + overflow: hidden; +} + +.flex-control-thumbs li { + width: 25%; + float: left; + margin: 0; +} + +.flex-control-thumbs img { + width: 100%; + display: block; + opacity: 0.7; + cursor: pointer; +} + +.flex-control-thumbs img:hover { + opacity: 1; +} + +.flex-control-thumbs .active { + opacity: 1; + cursor: default; +} + +/* Caption style */ +/* IE rgba() hack */ +.flex-caption { + background: none; + -ms-filter: progid: DXImageTransform.Microsoft.gradient(startColorstr=#4C000000, endColorstr=#4C000000); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr=#4C000000, endColorstr=#4C000000); + zoom: 1; +} + +.flex-caption { + bottom: 0px; + background-color: rgba(0, 0, 0, 0.5); + color: #fff; + margin: 0; + padding: 25px 25px 25px 30px; + position: absolute; + right: 0; + width: 50%; +} + +.flex-caption h3 { + color: [[setting:sliderh3color]]; + letter-spacing: 1px; + margin-bottom: 0px; + text-transform: uppercase; + font-size: 1.5em; +} + +.flex-caption h4 { + color: [[setting:sliderh4color]]; + letter-spacing: 1px; + margin-bottom: 20px; + font-size: 1em; +} + +.flex-caption p { + margin: 0 0 15px; +} + +.flex-caption .submit { + margin-right: 10px; + float: right; + color: [[setting:slidersubmitcolor]] !important; + background: [[setting:slidersubmitbgcolor]] +} + +#main-slider { + margin: 0 0 10px; +} + +.slides li { + position: relative; +} + +.flex-control-nav { + margin-bottom: -40px; +} + +/*new slider style */ +/* Direction Nav */ +.slidestyle2 .flex-direction-nav a { + width: 30px; + height: 100%; + display: block; + position: absolute; + top: 0%; + cursor: pointer; + text-indent: -9999px; + opacity: 0; + -webkit-transition: all .3s ease; +} + +.slidestyle2 .flex-direction-nav .flex-next { + height: 35px; + width: 30px; + top: 46%; + background: [[setting:slideroption2a]] url([[pix:theme|next]]) no-repeat 50% 50%; + + right: 15px; + opacity: 1; + background-size: auto 24px; +} + +.slidestyle2 .flex-direction-nav .flex-prev { + height: 35px; + width: 30px; + top: 46%; + background: [[setting:slideroption2a]] url([[pix:theme|previous]]) no-repeat 50% 50%; + left: 15px; + opacity: 1; + background-size: auto 24px; +} + +/* Caption style */ +/* IE rgba() hack */ +.slidestyle2 .flex-caption { + background: none; + -ms-filter: progid: DXImageTransform.Microsoft.gradient(startColorstr=#4C000000, endColorstr=#4C000000); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr=#4C000000, endColorstr=#4C000000); + zoom: 1; +} + +.slidestyle2 .flex-caption { + top: 35%; + color: inherit; + margin: 0; + padding: 0; + position: absolute; + right: 0; + width: 100%; + text-align: center; + transition: all 1s ease-in-out; +} + +.slidestyle2 .flex-caption .span6 { + margin-left: 6%; +} + +/*CSS transitions on larger screens */ +.slidestyle2 .flex-caption h3 { + background: [[setting:slider2h3bgcolor]]; + color: [[setting:slider2h3color]]; + display: block; + font-size: 16px; + font-weight: 800; + line-height: 20px; + margin: 0; + margin-left: 30px; + padding: 5px 15px; + float: left; + text-transform: uppercase; + max-width: 1170px; + clear: both; +} + +.slidestyle2 .flex-caption h4 { + font-size: 22px; + line-height: 32px; + background-color: [[setting:slider2h4bgcolor]]; + clear: both; + padding: 15px; + text-align: left; + margin-bottom: 0; + color: [[setting:slider2h4color]]; + font-weight: normal; + text-decoration: none !important; +} + +.slidestyle2 a:hover .flex-caption h4, +.slidestyle2 a:hover .flex-caption h3, +.slidestyle2 a:hover h4, +.slidestyle2 *:hover { + text-decoration: none !important; +} + +.slidestyle2 .flex-caption p { + margin: 0 0 15px; + color: #333; +} + +.slidestyle2 .flex-caption a.submit { + display: block; + font-size: 16px; + margin: 0; + margin-left: 30px; + float: right; + padding: 8px 15px; + text-transform: uppercase; + text-align: left; + border: 0px solid #ab3423; + background: [[setting:slideroption2color]] !important; + color: [[setting:slideroption2submitcolor]] !important; + border-radius: 0; + box-shadow: none; +} + +.slidestyle2 .flex-direction-nav { + display: inherit; +} + +.slidestyle2 .flex-control-nav.flex-control-paging { + display: none; +} + +.slidestyle2 #main-slider { + margin: 0; +} + +/* Fullscreen button */ +.fullin.narrow .container, +.fullin.standard .container { + width: [[setting:fullscreenwidth]]; +} + +.sbll { + display: none; +} + +.fullin .sbll { + display: inherit; +} + +.fullin .hbll { + display: none; +} + +@media (min-width: 992px) { + .zoomin #block-region-side-post { + display: none; + height: 0; + margin-left: 0; + min-height: 0; + overflow: hidden; + width: 0; + } + + .zoomin #region-main { + flex: 0 0 100%; + max-width: 100%; + } +} + +.sbl { + display: none; +} + +.zoomin .sbl { + display: inherit; +} + +.zoomin .hbl { + display: none; +} + +.fpcombocollapse .fp-chevron:before { + content: "\f077"; +} + +.fpcombocollapse.collapsed .fp-chevron:before { + content: "\f078"; +} diff --git a/theme/adaptable/style/form.css b/theme/adaptable/style/form.css new file mode 100644 index 0000000..d18a65e --- /dev/null +++ b/theme/adaptable/style/form.css @@ -0,0 +1,104 @@ +/* + * 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/>. + * + * + * Adaptable Form override styles style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +@media (min-width: 768px) { + .availability-dialogue .moodle-dialogue { + width: 740px !important; + } + + .availability-dialogue .list-unstyled li.row div:nth-child(1) { + /* col-6 -> col4 */ + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + padding-bottom: 4px; + } + + .availability-dialogue .list-unstyled li.row div:nth-child(2) { + /* col-6 -> col8 */ + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } +} + +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail .col-sm-9, +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail_body .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail_body .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail_body .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail_body .col-sm-9, +#admin-activatetemplateoverride_mod_forum_forum_post_email_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_textemail .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_email_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_textemail .col-sm-9, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-9, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-9, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-9, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_textemail .col-sm-9, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_textemail .col-sm-9 { + flex: 0 0 100%; + max-width: 100%; +} + +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail_body .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_htmlemail_body .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail_body .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_htmlemail_body .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_email_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_email_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestbasic_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_htmlemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_textemail .col-sm-3, +#admin-activatetemplateoverride_mod_forum_forum_post_emaildigestfull_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_textemail .col-sm-3, +#admin-overriddentemplate_mod_forum_forum_post_emaildigestfull_textemail .col-sm-3 { + text-align: left !important; +} diff --git a/theme/adaptable/style/header.css b/theme/adaptable/style/header.css new file mode 100644 index 0000000..451d945 --- /dev/null +++ b/theme/adaptable/style/header.css @@ -0,0 +1,31 @@ +/* + * 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/>. + * + * + * Adaptable header styles style sheet + * + * @package theme_adaptable + * @copyright © 2019 - TBD + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +.headerbgimage { + background-position: 0 0; + background-repeat: no-repeat; + background-size: cover; +} diff --git a/theme/adaptable/style/menu.css b/theme/adaptable/style/menu.css new file mode 100644 index 0000000..77d378d --- /dev/null +++ b/theme/adaptable/style/menu.css @@ -0,0 +1,684 @@ +/* +* 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/>. +* +* +* Adaptable Menu Style sheet +* +* @package theme_adaptable +* @copyright 2015-2016 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +* +*/ + +/* Navwrap */ +#adaptable-page-header-wrapper #main-navbar { + background-color: [[setting:menubkcolor]]; + border-bottom: 3px solid [[setting:menubordercolor]]; + clear: both; + z-index: 100; +} + +#adaptable-page-header-wrapper #main-navbar .navbar .navbar-nav > li:first-child a.dropdown-toggle { + margin-left: -[[setting:menufontpadding]]; +} + +#adaptable-page-header-wrapper #main-navbar .navbar .navbar-nav > li:last-child a.dropdown-toggle { + margin-right: -[[setting:menufontpadding]]; +} + +/* Remove caret from main navbar menu. */ +#adaptable-page-header-wrapper #main-navbar .dropdown-toggle::after, +#adaptable-page-header-wrapper #nav-drawer .dropdown-toggle::after { + content: none; +} + +/* Used to add padding to top of page when sticky top menu is used */ +/* .page-header-margin { + padding-top: 30px; +} */ + +.adaptable-page-header-wrapper#affix { + top: 0; + width: 100%; + z-index: 1010; +} + +/* General menu styling */ + +/* Sticky navbar */ + +/* The sticky class is added to the navbar with JS when it reaches its scroll position */ +#adaptable-page-header-wrapper .adaptable-navbar-sticky { + position: fixed; + top: 0; + width: 100%; +} + +/* Add some top padding to the page content to prevent sudden quick movement (as the navigation bar gets a new position at the top of the page (position:fixed and top:0) */ +#adaptable-page-header-wrapper .adaptable-navbar-sticky + .adaptable-navbar-sticky-content-padding { + padding-top: 60px; +} + +/* Page content */ +#adaptable-page-header-wrapper .adaptable-navbar-sticky-content-padding { + padding: 16px; +} + + +#adaptable-page-header-wrapper .navbar-nav .dropdown li a i, +#adaptable-page-header-wrapper #above-header a i { + text-align: center; + width: 22px; +} + +a.dropdown-item.i.fa.fa-users.fa-lg { + marging-right: 3px; +} + +/* New main navbar styling */ +#adaptable-page-header-wrapper #main-navbar .navbar-nav > li > a, +#adaptable-page-header-wrapper #main-navbar .navbar-nav > li > div.nav-link, +#adaptable-page-header-wrapper #main-navbar .navbar-nav .context-header-settings-menu .action-menu-trigger a.dropdown-toggle, +#adaptable-page-header-wrapper #main-navbar .navbar-nav .context-header-settings-menu .action-menu-trigger a.dropdown-toggle:hover, +#adaptable-page-header-wrapper #main-navbar .navbar-nav .region-main-settings-menu .action-menu-trigger a.dropdown-toggle, +#adaptable-page-header-wrapper #main-navbar .navbar-nav .region-main-settings-menu .action-menu-trigger a.dropdown-toggle:hover { + background-color: transparent; + color: [[setting:menufontcolor]]; + font-size: [[setting:menufontsize]]; + font-weight: 400; + line-height: 14px; + padding: 2px [[setting:menufontpadding]] 2px [[setting:menufontpadding]]; + text-decoration: none; + text-shadow: none; + text-transform: none; + -moz-transition: all [[setting:navbardropdowntransitiontime]]; + -ms-transition: all [[setting:navbardropdowntransitiontime]]; + -o-transition: all [[setting:navbardropdowntransitiontime]]; + -webkit-transition: all [[setting:navbardropdowntransitiontime]]; + transition: all [[setting:navbardropdowntransitiontime]]; +} + +#adaptable-page-header-wrapper .dropdown-menu ul { + padding: 0; + margin: 0; +} + +#adaptable-page-header-wrapper #dropdownlangmenu0.dropdown-menu { + left: auto; + right: 0; +} + +#adaptable-page-header-wrapper #main-navbar ul.navbar-nav > li > a.nav-link:hover, +#adaptable-page-header-wrapper #main-navbar li div.nav-link:hover, +#adaptable-page-header-wrapper #main-navbar li #edittingbutton input[type="submit"]:hover { + background-color: [[setting:menuhovercolor]]; + text-decoration: none; +} + +#adaptable-page-header-wrapper #main-navbar li .action-menu-trigger a.dropdown-toggle:hover { + background-color: transparent; + color: [[setting:menuhovercolor]]; +} + +#adaptable-page-header-wrapper #main-navbar .input-group { + display: flex; + align-items: center +} + +#adaptable-page-header-wrapper #main-navbar .navbar-nav>.nav-item .dropdown-menu .dropdown-toggle:hover:after { + border-left-color: [[setting:navbardropdowntexthovercolor]]; +} + +#adaptable-page-header-wrapper #above-header li a:hover { + background-color: transparent; +} + +#adaptable-page-header-wrapper #above-header .navbar button { + margin: 3px 1px 1px 0px; + padding: 2px 2px; + font-size: 16px; +} + +.navbar .nav > li:last-child > a { + border-right: 0 !important; +} + +#adaptable-page-header-wrapper .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); } + +/* New top header menu styling (includes #aboveheader css previously in adaptable.css). */ + +#above-header { + background-color: [[setting:headerbkcolor]]; + border-bottom: 1px solid [[setting:dividingline]]; +} + +#adaptable-page-header-wrapper #above-header .dropdown-item { + padding: .3rem 0.65rem; +} + +#adaptable-page-header-wrapper #above-header .navbar-nav > li > a, +#adaptable-page-header-wrapper #header2 .navbar-nav > div > li > a, +#adaptable-page-header-wrapper #header2 .navbar-nav > li > a, +#adaptable-page-header-wrapper #header2 .navbar-nav > a { + color: [[setting:headertextcolor]]; + font-size: [[setting:topmenufontsize]]; + text-decoration: none; +} + +#adaptable-page-header-wrapper #above-header .navbar .popover-region-toggle i, +#adaptable-page-header-wrapper #header2 .navbar .popover-region-toggle i, +#adaptable-page-header-wrapper #header2 .search-input-wrapper > div { + color: [[setting:headertextcolor]]; + font-size: 1.2em; +} + +#adaptable-page-header-wrapper #header2 .search-input-wrapper { + margin: 0 0.2rem 0 0.2rem; +} + + +#adaptable-page-header-wrapper #above-header .navbar > li { + line-height: 23px; +} + +#adaptable-page-header-wrapper .nav-link { + padding: .1rem .2rem .1rem .2rem; +} + +#adaptable-page-header-wrapper .dropdown-menu .dropdown .dropdown-toggle { + padding: .25rem 1.5rem; +} + +#adaptable-page-header-wrapper #above-header .dropdown-toggle a, +#adaptable-page-header-wrapper #header2 .dropdown-menu a, +#adaptable-page-header-wrapper #header2 .dropdown-toggle a, +#adaptable-page-header-wrapper #above-header .dropdown-menu a { + color: [[setting:linkcolor]]; + font-family: [[setting:fontname]], sans-serif; + font-size: [[setting:topmenufontsize]]; +} + +#adaptable-page-header-wrapper #above-header .dropdown-toggle a i, +#adaptable-page-header-wrapper #header2 .dropdown-menu a i, +#adaptable-page-header-wrapper #header2 .dropdown-toggle a i, +#adaptable-page-header-wrapper #above-header .dropdown-menu a i { + color: [[setting:linkcolor]]; +} + +#adaptable-page-header-wrapper #above-header .linksmenu { + margin: 2px 15px 0 15px; +} + +#adaptable-page-header-wrapper #above-header .langdesc, +#adaptable-page-header-wrapper #above-header .linksdesc, +#adaptable-page-header-wrapper #header2 .langdesc, +#adaptable-page-header-wrapper #header2 .linksdesc { + color: [[setting:headertextcolor]]; + font-size: [[setting:topmenufontsize]]; +} + +#adaptable-page-header-wrapper #above-header .navbar .popover-region i +#adaptable-page-header-wrapper #above-header a i, +#adaptable-page-header-wrapper #header2 a i { + color: [[setting:headertextcolor]]; +} + +#adaptable-page-header-wrapper #header2 .popover-region-container a i { + color: inherit; +} + +#adaptable-page-header-wrapper #header2 a i { + padding-right: 4px; +} + +#adaptable-page-header-wrapper #above-header a.dropdown-toggle { + margin-top: 2px; +} + +/* New menus styles */ + +.newmenus { + position: relative; + margin-left: 20px; + font-size: [[setting:topmenufontsize]]; + height: 26px; +} + +.newmenus .dropdown-menu .dropdown-menu { + left: 100%; +} + +.newmenus .dropdown-toggle { + color: [[setting:headertextcolor]] !important; + background-color: transparent !important; +} + +.newmenus .dropdown-menu li a { + color: [[setting:maincolor]] !important; + font-size: 13px; + font-weight: 400; + padding: 3px 10px; +} + +.newmenus .dropdown-menu li:hover a { + background-color: #eee; +} + +.newmenus .dropdown-toggle:hover, +.newmenus .dropdown-toggle:active, +.pull-left .usermenu2.nav a:hover { + background-color: transparent !important; +} + +.newmenus .dropdown-toggle::after { + font-family:"Font Awesome"; + margin-left:5px; + content:"\f107"; + font-weight: normal; + color: [[setting:headertextcolor]]; +} + +.newmenus .dropdown-menu .dropdown-toggle::after { + color: transparent; +} + +.menutitle { + height: 100%; +} + +/* The Overlay (background) */ +.overlaymenu { + height: 0; + width: 100%; + position: fixed; + z-index: 1090; + left: 0; + top: 0; + background-color: rgb(0,0,0); /* Black fallback color */ + background-color: rgba(0,0,0, 0.9); /* Black w/opacity */ + overflow-x: hidden; /* Disable horizontal scroll */ + transition: 0.5s; /* 0.5 second transition effect to slide in or slide down the overlay (height or width, depending on reveal) */ +} + +.overlaymenu.open { + height: 100%; +} + +.overlaymenu .overlayclosebtn { + position: absolute; + top: 20px; + right: 45px; +} + +.overlay-content { + position: relative; + top: 5%; + width: 100%; + margin-top: 30px; +} + +.overlay-content li > a { + padding: 8px; + text-decoration: none; + font-size: 1.5em; + color: #ffffff !important; + display: block; + transition: 0.3s; /* Transition effects on hover (color) */ +} + +.overlay-content li > a:hover { + /* color: [[setting:linkhover]] !important; */ + color: [[setting:linkhover]]; +} + +.overlay-content ul.overlaylist { + list-style: none; + text-align: left; + max-width: 90%; + margin: 0 auto 30px; +} + +.overlay-content ul.overlaylist .level-0 a { + font-size: 2em; + margin-bottom: 1em; +} + +.overlay-content ul.overlaylist .level-2 a:before { + content: "-"; + margin-right: 15px; +} +.overlay-content ul.overlaylist .level-3 a:before { + content: "."; + margin-right: 15px; + margin-left: 15px; +} + +/* When the height of the screen is less than 450 pixels, change the font-size of the links and position the close button again, so they don't overlap */ +@media (max-height: 450px) { + .overlaymenu a {font-size: 20px} + .overlaymenu .closebtn { + font-size: 40px; + top: 15px; + right: 35px; + } +} + +/* Styling the top menu link when it's configured not to appear on the right. */ +#adaptable-page-header-wrapper .topmenuleft { + float: left; + margin: 0; + padding: 10px 0px 0 0; +} + +/* New dropdown-related classes. These are mainly for settings. */ + +.navbar { + -webkit-box-shadow: none; + box-shadow: none; +} + +/* #adaptable-page-header-wrapper .navwrap .navbar > li > a { + color: [[setting:menufontcolor]] /* !important */; +/* line-height: 40px; + float: none; + padding: 0 [[setting:menufontpadding]] 0 [[setting:menufontpadding]]; + text-decoration: none; + text-shadow: none; + font-weight: 400; + font-size: [[setting:menufontsize]]; + text-transform: none; + border-right: 0; + background-color: transparent !important; +} + +#adaptable-page-header-wrapper .navbar > li > a:hover { + color: [[setting:menufontcolor]]; + background-color: [[setting:menuhovercolor]] !important; + text-decoration: none; +} */ + +.navbar li.dropdown.open > .dropdown-toggle, +.navbar li.dropdown.active > .dropdown-toggle, +.navbar li.dropdown.open.active > .dropdown-toggle { + font-size: [[setting:menufontsize]]; +} + +#adaptable-page-header-wrapper .navbar .dropdown-menu { + font-size: [[setting:menufontsize]]; + border-radius: [[setting:navbardropdownborderradius]]; + -webkit-transition: all [[setting:navbardropdowntransitiontime]]; + -moz-transition: all [[setting:navbardropdowntransitiontime]]; + -ms-transition: all [[setting:navbardropdowntransitiontime]]; + -o-transition: all [[setting:navbardropdowntransitiontime]]; + transition: all [[setting:navbardropdowntransitiontime]]; +} + +#region-main-settings-menu .dropdown-menu { + max-height: calc(100vh - 200px); + overflow-y: auto; +} + +.adaptable-navbar-sticky #region-main-settings-menu .dropdown-menu { + max-height: calc(100vh - 44px); +} + +.dropdown-menu.fade { + display: block; + opacity: 0; + pointer-events: none; +} + +.show > .dropdown-menu.fade { + pointer-events: auto; + opacity: 1; +} + +/* Bootstrap hides/shows dropdowns onclick via the class .show */ +.dropdown-menu.show { + display: block !important; /* SHAME - using important as this seems to be overwriten elsewhere. */ +} + +#adaptable-page-header-wrapper .nav .dropdown-menu li a { + color: [[setting:linkcolor]]; + background-color: none; +} + +.dropdown-item:hover, +.dropdown-item:focus, +.dropdown-item:focus-within, +.dropdown-item:active, +.dropdown-item.active, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a, +.block a.dropdown-item.active { + background-color: [[setting:navbardropdownhovercolor]]; + color: [[setting:navbardropdowntexthovercolor]]; +} + +.dropdown-item:focus-within a { + color: [[setting:navbardropdowntexthovercolor]]; +} + +.dropdown-menu a, +.dropdown-menu > .active, +.dropdown-menu > .active > a, +.dropdown-submenu > a +#adaptable-page-header-wrapper .dropdown-menu li a { + color: [[setting:navbardropdowntextcolor]]; +} + +.dropdown-item:hover > a, +.dropdown-item:focus > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus, +#adaptable-page-header-wrapper .dropdown-menu li a:hover { + background-color: [[setting:navbardropdownhovercolor]]; + background-image: none !important; + color: [[setting:navbardropdowntexthovercolor]]; +} + +.dropdown-submenu>.dropdown-menu { + border-radius: [[setting:navbardropdownborderradius]]; +} + +#adaptable-page-header-wrapper #main-navbar .navbar-nav > .nav-item .dropdown-menu .dropdown-toggle:after { + display: block; + content: " "; + float: right; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + border-width: 5px 0 5px 5px; + border-left-color: [[setting:linkcolor]]; + margin-top: 9px; + margin-right: -12px; +} + +#adaptable-page-header-wrapper #main-navbar .navbar-nav .nav-item .dropdown-menu > .nav-item .dropdown-menu { + top: -1px; +} + +/* New bootstrap related css classes for hover menus. The below added for Adaptable for Moodle 3.6 onwards. */ + +/*=-====Bootstrapthemes.co btco-hover-menu=====*/ + +#adaptable-page-header-wrapper .navbar-nav { + margin-bottom: 0; +} + +#adaptable-page-header-wrapper .navbar .popover-region-toggle { + width: inherit; +} + +#adaptable-page-header-wrapper .navbar .popover-region { + margin: 0px 0 0 4px; +} + +.navbar-light .navbar-nav .nav-link { + color: rgb(64, 64, 64); +} +.btco-hover-menu a , .navbar > li > a { +/* text-transform: capitalize; */ +/* padding: 10px 15px; */ +} + +/* Drop-down menu for hover. */ +#adaptable-page-header-wrapper .btco-hover-menu { + background: none; + margin: 0; + padding: 0; + min-height: 30px +} + +#adaptable-page-header-wrapper #main-navbar .action-menu .dropdown-toggle::after { + display: inline-block; + content: normal; + width: 0; + height: 0; + margin-left: .255em; + vertical-align: .255em; + content: ""; + border-top: .3em solid; + border-right: .3em solid transparent; + border-bottom: 0; + border-left: .3em solid transparent; +} + +#adaptable-page-header-wrapper #main-navbar .action-menu a.dropdown-toggle i { + padding: 0; +} + + +@media (max-width: 991px) { + #adaptable-page-header-wrapper .btco-hover-menu .show > .dropdown-toggle::after{ + transform: rotate(-90deg); + } +} + +@media (min-width: 991px) { + #adaptable-page-header-wrapper .btco-hover-menu .collapse ul li { + position:relative; + } + + #adaptable-page-header-wrapper .btco-hover-menu .collapse ul li:hover > ul, + #adaptable-page-header-wrapper .btco-hover-menu .collapse ul ul.dropdown-menu li:hover > ul { + display: block; + } + + #adaptable-page-header-wrapper .btco-hover-menu .collapse ul ul.dropdown-menu { + display: none; + margin-top: 0; + min-width: 180px; + padding: 0; + position: absolute; + } + + #adaptable-page-header-wrapper .btco-hover-menu .collapse ul ul.dropdown-menu ul { + display: none; + left: 100%; + min-width: 180px; + position: absolute; + top: 0; + } + + #adaptable-page-header-wrapper .btco-hover-menu .collapse ul ul.dropdown-menu ul ul { + left: -100%; + z-index: 1; + } + + #adaptable-page-header-wrapper #above-header .btco-hover-menu .collapse ul ul.dropdown-menu { + min-width: 60px; + } + + #adaptable-page-header-wrapper #above-header .btco-hover-menu .collapse ul [id^='langmenu'] { + min-width: 135px; + } +} + +/* Increase width slightly of default nav-drawer. */ +[data-region="drawer"] { + width: 300px; +} + +/* Adjust height according to header style. */ +.header-style1 [data-region="drawer"] { + height: calc(100% - 105px); + top: 105px; +} + +.header-style2 [data-region="drawer"] { + height: calc(100% - 46px); + top: 46px; +} + +@media screen and (min-width: 768px) { + .header-style2 [data-region="drawer"] { + height: calc(100% - 72px); + top: 72px; + } +} + +/* When the nav-drawer is closed take it out of the dom so it is not read by screenreaders. */ +#nav-drawer.closed #nav-drawer-inner { + display: none; +} + +.nav-link img.userpicture { + margin-right: 0; +} + + +#adaptable-page-header-wrapper button[aria-controls="nav-drawer"] { + font-size: 1.2em; +} + +#adaptable-page-header-wrapper #main-navbar .nav-item .dropdown-toggle i { + width: 20px; +} + +#adaptable-page-header-wrapper #main-navbar .nav-item i.fa-circle { + font-size: 10px; + margin-right: .25rem; +} + +#adaptable-page-header-wrapper #main-navbar .nav-item i.only { + padding-right: 0; +} + +#adaptable-page-header-wrapper #adaptable-page-header-nav-drawer button { + border: 0; + background: none; + font-size: 1.5em; + color: [[setting:headertextcolor]]; +} + +#adaptable-page-header-nav-drawer button { + cursor: pointer; +} + +#adaptable-page-header-wrapper .icon { + height: 14px; + margin-right: 0; + padding-right: 4px; + width: 14px; +} + +#adaptable-page-header-wrapper .context-header-settings-menu .icon { + width: auto; +} diff --git a/theme/adaptable/style/messages.css b/theme/adaptable/style/messages.css new file mode 100644 index 0000000..d7fbce6 --- /dev/null +++ b/theme/adaptable/style/messages.css @@ -0,0 +1,89 @@ +/* + * 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/>. + * + * + * Adaptable Messages style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +@media screen and (min-width: 768px) { + /* M3.7+ */ + .message-app.drawer, + .message-app.drawer .btn, + .message-app.drawer .btn a, + .message-app.drawer .form-control { + font-size: 105%; + } + + /* M3.7+ */ + .message-app.drawer .btn { + height: auto; + } + + .message-app.drawer { /* M3.7+ */ + width: 400px; + } + + .message-app.drawer.hidden { /* M3.7+ */ + right: -400px; + } +} + +[data-region='message-drawer'] [data-region='view-overview'] .input-group .input-group-text { + border-radius: 25px 0 0 25px; +} + +[data-region='message-drawer'] [data-region='view-overview'] .input-group [data-region='view-overview-search-input'] { + border-radius: 0 25px 25px 0; +} + +[data-region='message-drawer'] [data-region='search-input'] { + border-radius: 25px; + margin-right: 4px; +} + +[data-region='message-drawer'] [data-region='header-container'] [data-region='contact-request-count'] { + color: #fff; +} + +[data-region='message-drawer'] [data-region='header-container'] [data-region='contact-request-count'], +[data-region='message-drawer'] [data-region='body-container'] [data-region='section-unread-count'], +[data-region='message-drawer'] [data-region='body-container'] [data-region='unread-count'] { + background-color: red !important; + border-radius: 25px; + height: 25px; +} + +[data-region='message-drawer'] [data-region='body-container'] [data-region='last-message-date'] { + padding-top: 0.1rem !important; +} + +[data-region='message-drawer'] [data-region='body-container'] .btn-link { + background-color: #fff; + border-radius: 0; + color: inherit !important; +} + +[data-region='message-drawer'] [data-region='content-messages-footer-container'] .btn-link { + align-items: center; + display: flex; + justify-content: center; + padding: 1px 1px 0 0 !important; +} diff --git a/theme/adaptable/style/navigation.css b/theme/adaptable/style/navigation.css new file mode 100644 index 0000000..51358e5 --- /dev/null +++ b/theme/adaptable/style/navigation.css @@ -0,0 +1,49 @@ +/* + * 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/>. + * + * + * Adaptable Navigation style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +.nav-item { + margin-bottom: auto; + margin-top: auto; + vertical-align: middle; +} + +@media (max-width: 767px) { + .nomobilenavigation .activity-navigation .text, + .nomobilenavigation .section_footer .text { + display: none; + } + + .nomobilenavigation .next_section { + float: right; + } + + .nomobilenavigation .activity-navigation .col-md-6 { + width: 50%; + } + + .nomobilenavigation .section_footer .prevnext { + width: auto; + } +} diff --git a/theme/adaptable/style/notifications.css b/theme/adaptable/style/notifications.css new file mode 100644 index 0000000..83d4700 --- /dev/null +++ b/theme/adaptable/style/notifications.css @@ -0,0 +1,80 @@ +/* + * 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/>. + * + * + * Adaptable Notifications override styles style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +.popover-region-container { + background-color: #fff; + border: 1px solid #ddd; + box-shadow: 5px 5px 15px 1px #000; + height: 500px; + opacity: 1; + position: absolute; + right: 0; + top: 30px; + transition: height 0.25s ease 0s; + visibility: visible; + width: 360px; + z-index: 100; +} + +.count-container { + right: -5px; +} + +.popover-region-toggle::before, +.popover-region-toggle::after { + visibility: hidden; +} + +.popover-region-content-container { + height: calc(100% - 100px); +} + +.popover-region-header-container, +.popover-region-footer-container { + height: 50px; + line-height: 50px; +} + +.popover-region-header-text, +.popover-region-seeall-text { + font-size: 125%; + line-height: 50px; +} + +.content-item-container.notification .content-item-body .notification-message { + font-size: 100%; +} + +.content-item-container .content-item-footer .timestamp, +.content-item-container .view-more { + font-size: 70%; +} + +@media (min-width: 768px) { + .popover-region-container { + font-size: 125%; + width: 400px; + } +} diff --git a/theme/adaptable/style/print.css b/theme/adaptable/style/print.css new file mode 100644 index 0000000..56fa308 --- /dev/null +++ b/theme/adaptable/style/print.css @@ -0,0 +1,133 @@ +/* +* 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/>. +* +* +* Adaptable print media style sheet +* +* @package theme_adaptable +* @copyright 2015-2018 Jeremy Hopkins (Coventry University) +* @copyright 2015-2018 Fernando Acedo (3-bits.com) +* @copyright 2017-2018 Manoj Solanki (Coventry University) +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +* +*/ + +/* Hide some elements in Print view */ +@media print { + /* Remove things */ + #above-header, + .socialbox, + #navwrap, + #page-navbar, + #showsidebaricon, + .purgecaches, + #back-to-top { + display: none !important; + } + + #region-main { + margin: 0 !important; + } + + .page-context-header { + margin: 0 !important; + } + + /* Remove sidebar */ + #block-region-side-post, + #block-region-side-pre { + display: none !important; + } + + /* Remove Adaptable block regions */ + #frontblockregion { + display: none !important; + } + + /* Remove progress link */ + #completionprogressid { + display: none !important; + } + + /* Remove student image from printout */ + img.userpicture { + display: none !important; + } + + /* Grades */ + .grade-navigation, + #graded_users_selector, + .view_users_selector { + display: none !important; + } + + /* Remove selection boxes from printout of user grade report */ + .form-search input, + .form-inline input, + .form-horizontal input, + .form-search textarea, + .form-inline textarea, + .form-horizontal textarea, + .form-search select, + .form-inline select, + .form-horizontal select, + .form-search .help-inline, + .form-inline .help-inline, + .form-horizontal .help-inline, + .form-search .uneditable-input, + .form-inline .uneditable-input, + .form-horizontal .uneditable-input, + .form-search .input-prepend, + .form-inline .input-prepend, + .form-horizontal .input-prepend, + .form-search .input-append, + .form-inline .input-append, + .form-horizontal .input-append { + display: none !important; + } + + div.singleview_buttons { + display: none !important; + } + + /* Topics / Weeks course format */ + #page .course-content ul li.section.main { + page-break-before: always; + } + + /* Remove footer */ + footer { + display: none !important; + } +} + +/* Print settings */ +@page { + margin: [[setting:printmargin]]; + size: [[setting:printpageorientation]]; +} + +@media print { + body, + li.activity.label, + .file-picker td.label { + font-size: [[setting:printbodyfontsize]]; + } + + body, + li { + line-height: [[setting:printlineheight]]; + } +} diff --git a/theme/adaptable/style/responsive.css b/theme/adaptable/style/responsive.css new file mode 100644 index 0000000..843bfac --- /dev/null +++ b/theme/adaptable/style/responsive.css @@ -0,0 +1,685 @@ +/* +* This file is part of Adaptable theme for moodle +* +* 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/>. +* +* +* Adaptable Responsive style sheet +* +* @package theme_adaptable +* @copyright 2015-2019 Jeremy Hopkins (Coventry University) +* @copyright 2015-2019 Fernando Acedo (3-bits.com) +* @copyright 2018-2019 Manoj Solanki (Coventry University) +* +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +/* ===================================================== +Media Queries - max's in reverse +order for prescidence. +====================================================== */ + +/*===================================================== +1200px +======================================================= */ + +@media (max-width: 1199px) { + .showhideblocksdesc, + .zoomdesc { + display: none; + } + + #adaptable-page-header-wrapper #main-navbar .navbar-nav > li > a { + padding: 2px 4px; + } + + #adaptable-page-header-wrapper #main-navbar .navbar .navbar-nav > li:first-child a { + margin-left: -4px; + } + + #adaptable-page-header-wrapper #main-navbar .navbar .navbar-nav > li:last-child a { + margin-right: -4px; + } +} + +/*===================================================== +979px +======================================================= */ +@media (max-width: 979px) { + #page-header { + min-height: 60px; + } + + .langdesc { + display: inline; + } + .slidestyle2 .flex-caption { + right: -800px; + } + .slidestyle2 .flex-active-slide .flex-caption { + right: 0; + transition: all 1s ease; + } + .slidestyle2 .flex-caption h3 { + max-width: 970px; + } + .slidestyle2 .flex-caption br { + display: none; + } + .slidestyle2 .flex-caption { + top: 25%; + } + .slidestyle2 .flex-direction-nav { + display: none; + } + .slidestyle2 .flex-caption h4 { + font-size: 23px; + line-height: 30px; + } + .slidestyle2 .flex-caption a.submit { + font-size: 14px; + float: none; + white-space: normal; + width: auto; + } + + .navbar { + float: none !important; + } + + .navbar .pull-right { + float: none; + } + + .navbar .nav > li > a { + margin: 0 10px 0 0 !important; + } + + .flex-direction-nav .flex-next { + right: 0 !important; + } + + .flex-direction-nav .flex-prev { + left: 0 !important; + } + + /* Header */ + /* Logo */ + #logocontainer { + display: block !important; + margin: 5px 0; + padding: 0; + clear:both; + width: 100%; + height: auto; + } + + #logo img { + display: block; + margin: 0 auto; + } + + /* Site title */ + #sitetitle { + clear: both; + display: block; + margin: 5px 0; + max-width: 100%; + text-align: center; + padding: 0; + height: auto; + top: 0; + left: 0; + } + + /* Social Icons */ + .socialbox { + display: table; + padding: 5px; + float: none; + clear: both; + margin: 15px auto; + } + + .coursebox .panel-body p { + font-size: 90%; + line-height: 130%; + } + + .enrolmenticons { + display: none; + } + + #edittingbutton a, + #edittingbutton .btn { + margin-left: 10px !important; + } + + .searchbox { + padding-bottom: 5px; + } + + .search-box, + .searchbox { + margin: 0; + width: 100%; + } + + #search-1 { + width: calc(100% - 39px); + } + + .hbl, .sbl, .hbll, .sbll { + display: none !important; + } + + .nav-collapse .nav { + width: 100%; + margin-left: 0 !important; + } + + #adaptable-page-header-wrapper #main-navbar { + border-bottom: 0; + } + + #page-header a img { + margin: 0 auto; + float: none; + display: block; + } + + .nav .dropdown-menu li a { + color: [[setting:menufontcolor]]; + } + + .nav-collapse.active { + background-color: [[setting:mobilemenubkcolor]]; + height: auto; + top: 6px; + padding-bottom: 4px; + } + + #edittingbutton { + margin-left: 10px; + } + + .menutitle { + float: none; + } + + /* New styles for Adaptable 2. */ + #adaptable-page-header-wrapper .nav-link { + padding: inherit; + } + + #adaptable-page-header-wrapper .fa { + height: 24px; + margin-right: .5rem; + width: 24px; + } + + #adaptable-page-header-wrapper .topmenuleft { + float: left !important; + margin: 0; + padding: 5px 5px 0 0; + } + + #adaptable-page-header-wrapper .navbar .popover-region { + margin: 4px 0 0 4px; + } + + .has-page-header.page-header-margin.header-style1 #page { + margin-top: 30px; + } + + .mobiletheme.page-header-margin.header-style1.has-header-bg .headerbgimage { + background-image: none !important; + } + + .page-header-margin.header-style2 #page { + margin-top: 84px; + } + + /* Course activity and section layout updates to remove any extra padding on smaller screens. */ + .section .activity .activityinstance { + min-width: 100%; + } + + .activity-meta-container { + margin: 0 0 0 20px; + } + + #region-main, + .pagelayout-mydashboard #region-main { + padding: 0 15px; + } +} + +/*===================================================== +767px +======================================================= */ +@media (max-width: 767px) { + #page-login-index2 .forgetpass { + clear: both; + float: none; + margin: 0 auto; + min-width: 200px; + text-align: center; + width: 50%; + } + + #ticker-announce { + float: none; + margin: 0; + text-align: center; + } + + #ticker-wrap #newscontent { + line-height: normal; + max-width: inherit; + overflow: inherit; + padding: 10px 10px 10px; + white-space: normal; + } + + #ticker-wrap #newscontent #news { + white-space: normal; + } + + #ticker-wrap #controls { + display: none; + } + + .slidestyle2 .flex-caption { + background-color: transparent !important; + left: 0; + position: relative; + top: auto; + } + + .slidestyle2 .flex-caption a.submit { + float: none; + margin: 0; + text-align: center; + width: auto; + } + + .slidestyle2 .flex-caption h3 { + background-color: #000; + margin: 0 !important; + } + + .slidestyle2 .flex-caption h4 { + font-size: 18px; + line-height: 22px; + padding: 10px 0; + } + + /* 1 tile. */ + .coursebox.col-12 .coursebtn { + width: 30% !important; + } + + /* 2 tiles. */ + .coursebox.col-6 .coursebtn { + width: 55% !important; + } + + /* 3 tiles. */ + .coursebox.col-4 .coursebtn { + width: 70% !important; + } + + /* 4 tiles. */ + .coursebox.col-3 .coursebtn { + width: 80% !important; + } + + /* 6 tiles. */ + .coursebox.col-2 .coursebtn { + width: 95% !important; + } + + #page-site-index a.submit { + display: block; + margin: 5px auto; + text-align: center; + white-space: normal; + width: auto; + } + + .headermenu2 { + clear: both; + float: none; + padding: 10px 0; + text-align: center; + } + + .flex-caption { + font-size: 90% !important; + padding: 5px 0 !important; + width: 100% !important; + } + + .flex-caption p { + font-size: 90% !important; + line-height: 1.4em; + margin: 0 5px !important; + } + + .flex-caption h3 { + font-size: 1.1em !important; + line-height: 2em; + margin: 0 5px !important; + } + + a.submit { + display: block; + margin: 5px auto; + text-align: center; + width: 80%; + } + + .block .header { + margin: 0 !important; + } + + #page-footer { + margin: 50px 0; + } + + #edittingbutton a, + #edittingbutton .btn { + margin-left: -10px !important; + } + + .navbar .nav > li > a { + margin: 0 10px 0 4px !important; + } + + #social-connect { + clear: both; + float: none; + width: auto; + } + + .message .contactselector { + float: none; + width: auto; + } + + .message .messagearea { + border-left: none; + float: none; + min-height: 200px; + padding-left: 1%; + width: auto; + } + + .navbar .nav-collapse.in > .nav > li > a { + border-radius: 0; + padding-left: 0 !important; + } + + .userhead { + height: 20px; + } + + .userprofile dl.list dt { + width: 110px !important; + } + + .userprofile dl.list dd { + margin-left: 120px !important; + } + + .newmenu1, + .newmenu2, + .newmenu3, + .newmenu4, + .newmenu5, + .newmenu6, + .newmenu7, + .newmenu8, + .newmenu9, + .newmenu10, + .newmenu11, + .newmenu12, + .newmenu13, + .newmenu14, + .newmenu15, + .newmenu16 { + display: none; + } + + /* Links and Language menu, hide text on small screens. */ + #above-header .langdesc, + #above-header .linksdesc { + display: none; + } + + #above-header { + min-height: 37px; + } + + #header2, + #header2 .row, + #adaptable-page-header-wrapper > #header2 > div > div, + #adaptable-page-header-wrapper #header2 .navbar-nav { + min-height: 46px; + } + + .page-header-margin.header-style2 #page { + margin-top: 62px; + } + + .socialbox a { + color: [[setting:headertextcolor2]]; + } + + .socialbox a i { + /* Make at least 29px, values look odd but on inspection they are needed to be so for FontAwesome. */ + font-size: [[setting:responsivesocialsize]]; + line-height: [[setting:responsivesocialsize]]; + } + .custom-select { + max-width:100%; + } +} + + +/*===================================================== +576px +======================================================= */ +@media (max-width: 576px) { + .page-header-margin.header-style2 #page { + margin-top: 38px; + } +} + +/*===================================================== +480px +======================================================= */ +@media (max-width: 480px) { + .flex-caption { + width: auto !important; + padding: 5px 0 !important; + position: relative !important; + margin-top: -1px !important; + background-color: #000 !important; + } + + #page-content { + padding: 0; + width: auto; + margin-top: 15px; + } + + #page-header .userimg { + display: none; + } + + #coursesearchbox2 { + display: none; + } +} + +/* Mins in ascending order. + +/*===================================================== +768px +======================================================= */ +@media (min-width: 768px) { + body.drawer-open-left { + margin-left: 0; + } +} + +/*===================================================== +1201px or higher / nozoom +======================================================= */ + +@media (min-width: 1200px) { + .standard .container { + max-width: 100%; + width: 1170px; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .standard .container { + max-width: 100%; + width: 960px; + } +} + +@media (min-width: 1000px) { + .narrow .container { + max-width: 100%; + width: 1000px; + } +} + +@media (max-width: 992px) and (max-width: 999px) { + .narrow .container { + max-width: 100%; + width: 960px; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + #adaptable-page-header-wrapper .container, + .container { + max-width: 100%; + width: 724px; + } +} + +@media (min-width: 576px) and (max-width: 767px) { + .container { + max-width: 100%; + } +} + + +/*===================================================== +Other. +======================================================= */ +@media (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:portrait) { + #coursesearchbox2 { + min-width: 140px; + margin-top: 5px; + } +} + +@media (min-width: 481px) { + /* Activity chooser styling. Desktop styling. */ + .jsenabled .choosercontainer #chooseform .alloptions { + overflow-x: hidden; + overflow-y: auto; + max-width: 50%; + max-height: -webkit-calc(100vh - 15em) !important; + max-height: calc(100vh - 15em) !important; + } + + .jsenabled .choosercontainer #chooseform .instruction, + .jsenabled .choosercontainer #chooseform .typesummary { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 50%; + margin: 0; + padding: 1.8em; + background-color: #fff; + overflow-x: hidden; + overflow-y: auto; + } + + /* Selected option settings */ + .jsenabled .choosercontainer #chooseform .instruction { + display: block; + font-size: 1.2em; + } + + .choosercontainer #chooseform .selected .typesummary, + .choosercontainer #chooseform .selected .instruction { + display: block; + font-size: 1.1em; + } + + .choosercontainer #chooseform .selected { + background-color: #fff; + } + + /* Styling for overall activity chooser box */ + + .moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd, + .moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd.yui3-widget-hd { + padding: 20px; + padding-right: 30px; + text-align: center; + font-size: 36px; + min-height: 40px; + } + + .moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons { + margin-top: 10px; + margin-right: 10px; + } + + .moodle-dialogue-base .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-hd .yui3-widget-buttons button.yui3-button.closebutton:hover { + opacity: 1; + filter: alpha(opacity=100); + } + + .chooserdialogue .moodle-dialogue-wrap .moodle-dialogue-bd { + padding: 0; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + } + + /* End Activity chooser styling. */ + +} + +@media (min-width: 768px) { + #above-header .linksmenu { + margin-left: 0px; + } + + .adaptable-drawer { + display: none; + } +} diff --git a/theme/adaptable/style/tabs.css b/theme/adaptable/style/tabs.css new file mode 100644 index 0000000..bb17e58 --- /dev/null +++ b/theme/adaptable/style/tabs.css @@ -0,0 +1,71 @@ +/* + * 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/>. + * + * + * Adaptable User styles style sheet + * + * @package theme_adaptable + * @copyright © 2019 - Coventry University + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + + +.tabcontentcontainer input.adaptabletab:checked + label { + background-color: [[setting:headerbkcolor]]; + border-color: [[setting:headerbkcolor]]; + color: [[setting:headertextcolor]]; +} + +.tabcontentcontainer label.adaptabletab { + background-color: #eee; + border-bottom: 0; + cursor: pointer; + display: inline-block; + font-weight: 400; + margin: 0 4px 0 0; + padding: 1em; + position: relative; + transition: background-color .2s ease; +} + +/* Tab panel content. */ +#adaptable_profile_tree .adaptable-tab.tab-panel { + border: 1px solid rgba(0,0,0,.125); /* Same colour as profile card border. */ + border-top: 4px solid [[setting:headerbkcolor]]; +} + +/* Remove borders on cards in the tabs. */ +.adaptable-tab.tab-panel .node_category { + border-color: transparent; +} + + +/* CSS for the one topic format tabs. */ +.onetopic .nav-tabs .nav-link { + border: 1px solid #eee; + border-bottom-color: transparent; + margin: 2px; /* Magic number so tabs can have border. */ +} + +/* Give active tabs brand colour background. */ +.onetopic .nav-tabs .nav-link.active, +.onetopic .nav-tabs .nav-item.show .nav-link { + background-color: [[setting:headerbkcolor]]; + border-color: transparent; + color: [[setting:headertextcolor]]; +} diff --git a/theme/adaptable/style/user.css b/theme/adaptable/style/user.css new file mode 100644 index 0000000..5376218 --- /dev/null +++ b/theme/adaptable/style/user.css @@ -0,0 +1,115 @@ +/* + * 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/>. + * + * + * Adaptable User styles style sheet + * + * @package theme_adaptable + * @copyright 2019 G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +.path-user .userprofile > .page-context-header, +.path-user .userprofile > .description { + display: none; +} + +#page-user-profile .node_category li, +.path-user .node_category li { + margin-bottom: 5px; +} + +#page-user-profile .node_category li.aduseropt { + margin: 0; +} + +#page-user-profile .node_category li.aduseropt dl { + margin-bottom: 0; +} + +#adaptable_profile_tree .ucol1 .node_category.contact { + margin-top: 3.5em; /* Magic number to line up with tabs. */ +} + +#adaptable_profile_tree .ucol1 .adaptableuserpicture { + padding-bottom: 30px; + position: relative; +} + +#adaptable_profile_tree .ucol1 .adaptableuserpicture a { + left: 0; + position: absolute; + right: 0; + top: -80px; +} + +#adaptable_profile_tree .ucol1 .adaptableuserpicture a img.userpicture { + margin-right: 0; +} + + +.path-user #adaptable_profile_tree .node_category .editprofile { + text-align: left; +} + +#adaptable_profile_tree .ucol1 .adaptableuserpicture a, +.path-user #adaptable_profile_tree .ucol1 .node_category .editprofile, +#adaptable_profile_tree .ucol1 .contentnode { + text-align: center; +} + +#adaptable_profile_tree .ucol1 .firstname, +#adaptable_profile_tree .ucol1 .fullname, +#adaptable_profile_tree .ucol1 .lastname { + font-weight: bold; +} + +#adaptable_profile_tree .ucol1 .firstname, +#adaptable_profile_tree .ucol1 .fullname { + font-size: 150%; +} + +#adaptable_profile_tree .ucol1 .lastname { + font-size: 140%; +} + +.adaptablemyeditprofile .col-md-3, +.adaptablemyeditprofile .col-md-9 { + flex: 0 0 100%; + max-width: 100%; +} + +.adaptablemyeditprofile .col-md-3 span.float-sm-right { + float: none !important; +} + +.adaptablemyeditprofile .col-md-3 label.col-form-label.d-inline { + float: left !important; +} + +.adaptablemyeditprofile .col-md-3 .text-info, +.adaptablemyeditprofile .col-md-3 label.col-form-label.d-inline { + margin: 0; +} + +.adaptablemyeditprofile .col-md-3 .btn { + height: 26px; +} + +.adaptablemyeditprofile .fp-restrictions { + text-align: left; +} diff --git a/theme/adaptable/templates/adaptable_admin_setting_configtemplate.mustache b/theme/adaptable/templates/adaptable_admin_setting_configtemplate.mustache new file mode 100644 index 0000000..c176f18 --- /dev/null +++ b/theme/adaptable/templates/adaptable_admin_setting_configtemplate.mustache @@ -0,0 +1,41 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/adaptable_admin_setting_configtemplate + + Template which defines the preview bit of the adaptable_admin_setting_configtemplate admin setting. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * templatepreview + * templateoverridden + + Example context (json): + { + "templatepreview": "HTML", + "templateoverridden": true + } +}} +<h3>{{#templateoverridden}}{{#str}}overriddentemplatepreview, theme_adaptable{{/str}}{{/templateoverridden}}{{^templateoverridden}}{{#str}}originaltemplatepreview, theme_adaptable{{/str}}{{/templateoverridden}}</h3> +<div class="jumbotron"> + {{{templatepreview}}} +</div> \ No newline at end of file diff --git a/theme/adaptable/templates/adaptable_admin_setting_configtemplate_nopreview.mustache b/theme/adaptable/templates/adaptable_admin_setting_configtemplate_nopreview.mustache new file mode 100644 index 0000000..04f0cc0 --- /dev/null +++ b/theme/adaptable/templates/adaptable_admin_setting_configtemplate_nopreview.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/adaptable_admin_setting_configtemplate_nopreview + + Template which defines the preview bit of the adaptable_admin_setting_configtemplate admin setting. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + } +}} +<h3>{{#str}}overriddentemplatenopreview, theme_adaptable{{/str}}</h3> diff --git a/theme/adaptable/templates/adaptable_admin_setting_configtemplate_source.mustache b/theme/adaptable/templates/adaptable_admin_setting_configtemplate_source.mustache new file mode 100644 index 0000000..b4c1475 --- /dev/null +++ b/theme/adaptable/templates/adaptable_admin_setting_configtemplate_source.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/adaptable_admin_setting_configtemplate_source + + Template which defines the preview bit of the adaptable_admin_setting_configtemplate admin setting. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * templatesource + + Example context (json): + { + "templatesource": "Mustache Source String" + } +}} +<h3>{{#str}}originaltemplatesource, theme_adaptable{{/str}}</h3> +<pre> +{{templatesource}} +</pre> diff --git a/theme/adaptable/templates/adaptable_admin_setting_tabs.mustache b/theme/adaptable/templates/adaptable_admin_setting_tabs.mustache new file mode 100644 index 0000000..13d07da --- /dev/null +++ b/theme/adaptable/templates/adaptable_admin_setting_tabs.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/adaptable_admin_setting_tabs + + + Example context (json): + { + "versioninfo": "Release 3.0.3, version 2020073104 on Moodle 3.9.4 (Build: 20210118)", + "tabs": [ + { + "name": "tab1", + "active": 0, + "displayname": "Inactive tab1", + "html": "<p>Tab 1 content</p>" + }, + { + "name": "tab2", + "active": 1, + "displayname": "Active tab2", + "html": "<p>Tab 2 content</p>" + } + ] + } +}} +<h3>{{versioninfo}}</h3> +{{#versioncheck}} +<div class="alert alert-warning mb-3"> +{{{versioncheck}}} +</div> +{{/versioncheck}} +<ul class="nav nav-tabs" role="tablist"> + {{#tabs}} + <li class="nav-item"> + <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab" + {{#active}}aria-selected="true"{{/active}} + {{^active}}aria-selected="false" tabindex="-1"{{/active}}>{{displayname}}</a> + </li> + {{/tabs}} +</ul> +<div class="tab-content mt-3"> + {{#tabs}} + <div class="tab-pane {{#active}}active{{/active}}" id="{{name}}" role="tabpanel"> + {{{html}}} + </div> + {{/tabs}} +</div> + diff --git a/theme/adaptable/templates/core/modal.mustache b/theme/adaptable/templates/core/modal.mustache new file mode 100644 index 0000000..6df44d8 --- /dev/null +++ b/theme/adaptable/templates/core/modal.mustache @@ -0,0 +1,67 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/modal + + Moodle modal template. + + The purpose of this template is to render a modal + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * title A cleaned string (use clean_text()) to display. + * body HTML content for the boday + * footer HTML content for the footer + + Example context (json): + { + "title": "Example modal", + "body": "Some example content for the body", + "footer": "Footer text, right here!" + } +}} + +<div class="modal moodle-has-zindex" data-region="modal-container" aria-hidden="true" role="dialog"> + <div class="modal-dialog {{$classes}}{{classes}}{{/classes}}" role="document" data-region="modal" aria-labelledby="{{uniqid}}-modal-title" tabindex="0"> + <div class="modal-content"> + <div class="modal-header {{$headerclasses}}{{headerclasses}}{{/headerclasses}}" data-region="header"> + + {{$header}} + <h5 id="{{uniqid}}-modal-title" class="modal-title" data-region="title">{{$title}}{{title}}{{/title}}</h5> + {{/header}} + <button type="button" class="close" data-action="hide" aria-label={{#quote}}{{#str}}closebuttontitle{{/str}}{{/quote}}> + <i class="fa fa-times-circle-o" aria-hidden="true"></i> + </button> + </div> + <div class="modal-body" data-region="body"> + {{$body}} + {{{body}}} + {{/body}} + </div> + <div class="modal-footer" data-region="footer"> + {{$footer}} + {{{footer}}} + {{/footer}} + </div> + </div> + </div> +</div> diff --git a/theme/adaptable/templates/core/preferences_groups.mustache b/theme/adaptable/templates/core/preferences_groups.mustache new file mode 100644 index 0000000..24d60a4 --- /dev/null +++ b/theme/adaptable/templates/core/preferences_groups.mustache @@ -0,0 +1,45 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/core/preferences_groups + + Example context (json): + { + "groups": "[preferences_group Object]", + "nodes": "navigation_node_collection Object", + "title": "Group title", + "get_title": "User account", + "action": "https://domain.example/user/editadvanced.php?id=2&course=1", + "get_content": "Edit profile" + } +}} +<div class="row"> + {{#groups}} + <div class="col-md-4"> + <div class="card mb-3"> + <div class="card-body"> + <h4 class="card-title">{{title}}</h4> + <div class="card-text"> + {{#nodes}} + <div><a {{#get_title}}title="{{get_title}}"{{/get_title}} href="{{{action}}}">{{get_content}}</a></div> + {{/nodes}} + </div> + </div> + </div> + </div> + {{/groups}} +</div> \ No newline at end of file diff --git a/theme/adaptable/templates/core/progress_bar.mustache b/theme/adaptable/templates/core/progress_bar.mustache new file mode 100644 index 0000000..c498f1e --- /dev/null +++ b/theme/adaptable/templates/core/progress_bar.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/core/progress_bar + + Progress bar. + + Example context (json): + { + "id": "progressbar_test", + "width": "500" + } +}} +<div id="{{id}}" class="row progressbar_container"> + <div class="col-12"> + <p id="{{id}}_status" class="text-xs-center"></p> + <progress id="{{id}}_bar" class="progress progress-striped progress-animated" value="0" max="100"></progress> + <p id="{{id}}_estimate" class="text-xs-center"></p> + </div> +</div> + +{{! We must not use the JS helper otherwise this gets executed too late. }} +<script> +(function() { + var el = document.getElementById('{{id}}'), + progressBar = document.getElementById('{{id}}_bar'), + statusIndicator = document.getElementById('{{id}}_status'), + estimateIndicator = document.getElementById('{{id}}_estimate'); + + el.addEventListener('update', function(e) { + var msg = e.detail.message, + percent = e.detail.percent, + estimate = e.detail.estimate; + + statusIndicator.textContent = msg; + progressBar.setAttribute('value', Math.round(percent)); + if (percent === 100) { + progressBar.classList.add('progress-success'); + estimateIndicator.textContent = '100%'; + } else { + if (estimate) { + estimateIndicator.textContent = estimate + ' - ' + percent + '%'; + } else { + estimateIndicator.textContent = '' + percent + '%'; + } + progressBar.classList.remove('progress-success'); + } + }); +})(); +</script> diff --git a/theme/adaptable/templates/core_course/activity_navigation.mustache b/theme/adaptable/templates/core_course/activity_navigation.mustache new file mode 100644 index 0000000..7df2e53 --- /dev/null +++ b/theme/adaptable/templates/core_course/activity_navigation.mustache @@ -0,0 +1,84 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/core_course/activity_navigation + + Displays the activity navigation + + Context variables required for this template: + * prevlink Object - The action link data for the previous activity link. Corresponds with the core/action_link context. + * nextlink Object - The action link data for the next activity link. Corresponds with the core/action_link context. + * activitylist Object - The data for the activity selector menu. Corresponds with the core/url_select context. + + Example context (json): + { + "prevlink": { + "disabled": false, + "url": "#", + "id": "test-id-1", + "classes": "btn btn-link", + "attributes": [ + { + "name": "title", + "value": "Activity A" + } + ], + "text": "◄ Activity A" + }, + "nextlink": { + "disabled": false, + "url": "#", + "id": "test-id-2", + "classes": "btn btn-link", + "attributes": [ + { + "name": "title", + "value": "Activity C" + } + ], + "text": "Activity C ►" + }, + "activitylist": { + "id": "url_select_test", + "formid": "url_select_form", + "action": "#", + "options": [ + {"name": "Jump to...", "value": "#0"}, + {"name": "Activity A", "value": "#1"}, + {"name": "Activity B", "value": "#2"}, + {"name": "Activity C", "value": "#3"} + ] + } + } +}} +<nav class="activity_footer activity-navigation"> + <div class="row"> + <div class="col-md-6"> + <div class="float-left"> + {{#prevlink}}{{> core/action_link }}{{/prevlink}} + </div> + </div> + <div class="col-md-6"> + <div class="float-right"> + {{#nextlink}}{{> core/action_link }}{{/nextlink}} + </div> + </div> + </div> +</nav> +<div class="jumpnav"> + {{#activitylist}}{{> core/url_select }}{{/activitylist}} +</div> \ No newline at end of file diff --git a/theme/adaptable/templates/core_message/message_drawer.mustache b/theme/adaptable/templates/core_message/message_drawer.mustache new file mode 100644 index 0000000..8744f1b --- /dev/null +++ b/theme/adaptable/templates/core_message/message_drawer.mustache @@ -0,0 +1,77 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template core_message/message_drawer + + This template will render the message drawer. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + { + } +}} +{{< core/drawer}} + {{$drawercontent}} + <div class="drawer-top p-2 border-bottom bg-messageheader"> + {{#str}} messages, message {{/str}} + <a id="message-drawer-close-{{uniqid}}" class="pull-right" href="#" role="button"> + {{#pix}} i/window_close, core, {{#str}} togglemessagemenu, message {{/str}} {{/pix}} + </a> + </div> + <div id="message-drawer-{{uniqid}}" class="message-app" data-region="message-drawer" role="region"> + <div class="header-container position-relative" data-region="header-container"> + {{> core_message/message_drawer_view_contacts_header }} + {{> core_message/message_drawer_view_conversation_header }} + {{> core_message/message_drawer_view_overview_header }} + {{> core_message/message_drawer_view_search_header }} + {{> core_message/message_drawer_view_settings_header }} + </div> + <div class="body-container position-relative" data-region="body-container"> + {{> core_message/message_drawer_view_contact_body }} + {{> core_message/message_drawer_view_contacts_body }} + {{> core_message/message_drawer_view_conversation_body }} + {{> core_message/message_drawer_view_group_info_body }} + {{> core_message/message_drawer_view_overview_body }} + {{> core_message/message_drawer_view_search_body }} + {{> core_message/message_drawer_view_settings_body }} + </div> + <div class="footer-container position-relative" data-region="footer-container"> + {{> core_message/message_drawer_view_conversation_footer }} + {{> core_message/message_drawer_view_overview_footer }} + </div> + </div> + {{/drawercontent}} +{{/core/drawer}} + +{{#js}} +require(['jquery', 'core_message/message_drawer', 'core_message/message_popover'], function($, MessageDrawer, Popover) { + var root = $('#message-drawer-{{uniqid}}'); + MessageDrawer.init(root); + + var toggle = $('#message-drawer-close-{{uniqid}}'); + Popover.init(toggle); +}); +{{/js}} \ No newline at end of file diff --git a/theme/adaptable/templates/core_message/message_popover.mustache b/theme/adaptable/templates/core_message/message_popover.mustache new file mode 100644 index 0000000..c1f2774 --- /dev/null +++ b/theme/adaptable/templates/core_message/message_popover.mustache @@ -0,0 +1,60 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template core_message/message_popover + + This template will render the message popover for the navigation bar. + + Classes required for JS: + * none + + Data attributes required for JS: + * All data attributes are required + + Context variables required for this template: + * userid The logged in user id + * urls The URLs for the popover + + Example context (json): + { + "unreadcount": 3 + } + +}} +<div class="pull-right popover-region popover-region-messages collapsed"> + <a id="message-drawer-toggle-{{uniqid}}" class="nav-link d-inline-block popover-region-toggle position-relative" href="#" + role="button"> + {{#pix}} t/message, core, {{#str}} togglemessagemenu, message {{/str}} {{/pix}} + <div class="count-container {{^unreadcount}}hidden{{/unreadcount}}" data-region="count-container" + aria-label="{{#str}} unreadconversations, core_message, {{unreadcount}} {{/str}}">{{unreadcount}}</div> + </a> +</div> + +{{#js}} +require( +[ + 'jquery', + 'core_message/message_popover' +], +function( + $, + Popover +) { + var toggle = $('#message-drawer-toggle-{{uniqid}}'); + Popover.init(toggle); +}); +{{/js}} diff --git a/theme/adaptable/templates/header.mustache b/theme/adaptable/templates/header.mustache new file mode 100644 index 0000000..ba0bfce --- /dev/null +++ b/theme/adaptable/templates/header.mustache @@ -0,0 +1,43 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/header + + This template renders the header. + + Example context (json): + { + "settingsmenu": "settings_html", + "hasnavbar": false, + "navbar": "navbar_if_available", + "headerclasses": "d-none d-md-flex" + } +}} + +<div id="page-header" class="col-12 pt-3 pb-3 {{{headerclasses}}}"> + <div class="d-flex flex-fill flex-wrap align-items-center"> + <div id="page-navbar" class="mr-auto"> + {{{navbar}}} + </div> + {{#headeractions}} + <div class="header-actions-container flex-shrink-0" data-region="header-actions-container"> + <div class="header-action ml-2">{{{.}}}</div> + </div> + {{/headeractions}} + </div> +</div> + diff --git a/theme/adaptable/templates/headerloginform.mustache b/theme/adaptable/templates/headerloginform.mustache new file mode 100644 index 0000000..852203f --- /dev/null +++ b/theme/adaptable/templates/headerloginform.mustache @@ -0,0 +1,36 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/headerloginform + + Header style one. + + Example context (json): + { + "url": "http://localhost", + "token": "", + "displayloginbox": true + } +}} +<form id="pre-login-form" class="form-inline my-2 my-lg-0" action="{{{url}}}" method="post"> + <input type="hidden" name="logintoken" value="{{token}}"/> + {{#displayloginbox}} + <input type="text" name="username" placeholder="{{#str}}loginplaceholder, theme_adaptable{{/str}}" size="11"> + <input type="password" name="password" placeholder="{{#str}}passwordplaceholder, theme_adaptable{{/str}}" size="11"> + {{/displayloginbox}} + <button class="btn-login" type="submit">{{#str}}logintextbutton, theme_adaptable{{/str}}</button> +</form> diff --git a/theme/adaptable/templates/headernavbar.mustache b/theme/adaptable/templates/headernavbar.mustache new file mode 100644 index 0000000..fe4b3e8 --- /dev/null +++ b/theme/adaptable/templates/headernavbar.mustache @@ -0,0 +1,161 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/headernavbar + + Header style one. + + Example context (json): + { + "disablecustommenu": false, + "navigationmenudrawer": "", + "searchurl": "http://localhost", + "toolsmenudrawer": true, + "navigationmenu": "", + "toolsmenu": false, + "showcog": true, + "coursemenucontent": "", + "othermenucontent": "", + "pageheadingbutton": "", + "showhideblocks": true, + "showhideblockszoomside": "", + "showhideblockszoominicontitle": "", + "showhideblockshidetitle": "", + "showhideblocksshowtitle": "", + "showhideblocksicontype": "train", + "showhideblockstext": true, + "showhideblockszoominicontitle": "", + "enablezoom": true, + "enablezoomshowtext": true + } +}} +<div id="nav-drawer" data-region="drawer" class="d-print-none moodle-has-zindex closed" aria-hidden="true" tabindex="-1"> + <div id="nav-drawer-inner"> + <nav class="list-group"> + <ul class="list-unstyled components"> + {{{navigationmenudrawer}}} + {{^disablecustommenu}} + <li> + {{{output.custom_menu_drawer}}} + </li> + {{/disablecustommenu}} + {{#toolsmenudrawer}} + <li> + {{{toolsmenudrawer}}} + </li> + {{/toolsmenudrawer}} + </ul> + </nav> + + <nav class="list-group m-t-1"> + {{{output.context_mobile_settings_menu}}} + <a class="list-group-item list-group-item-action " href="{{{searchurl}}}"> + <div class="m-l-0"> + <div class="media"> + <span class="media-left"> + <i class="icon fa fa-wrench fa-fw" aria-hidden="true"></i> + </span> + <span class="media-body ">{{#str}}administrationsite{{/str}}</span> + </div> + </div> + </a> + </nav> + </div> +</div> + +<div id="main-navbar" class="d-none d-lg-block"> + <div class="container"> + <div class="navbar navbar-expand-md btco-hover-menu"> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{#str}}togglenavigation, theme_adaptable{{/str}}"> + <span class="navbar-toggler-icon"></span> + </button> + + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + + <nav aria-label="{{#str}}sitelinkslabel, theme_adaptable{{/str}}"> + <ul class="navbar-nav"> + {{{navigationmenu}}} + {{^disablecustommenu}} + {{{output.custom_menu}}} + {{/disablecustommenu}} + {{#toolsmenu}} + {{{output.tools_menu}}} + {{/toolsmenu}} + </ul> + </nav> + + <ul class="navbar-nav ml-auto"> + {{#showcog}} + {{#coursemenucontent}} + <li class="nav-item mr-2"> + <div class="context-header-settings-menu"> + {{{coursemenucontent}}} + </div> + </li> + {{/coursemenucontent}} + + {{#othermenucontent}} + <li class="nav-item mr-2"> + <div id="region-main-settings-menu" class="region-main-settings-menu"> + {{{othermenucontent}}} + </div> + </li> + {{/othermenucontent}} + {{/showcog}} + + {{#pageheadingbutton}} + <li class="nav-item mx-0 my-auto"> + <div id="edittingbutton"> + {{{pageheadingbutton}}} + </div> + </li> + {{/pageheadingbutton}} + + {{#showhideblocks}} + <li class="nav-item mr-1"> + <div id="zoominicon" class="{{showhideblockszoomside}} nav-link" title="{{showhideblockszoominicontitle}}" data-hidetitle="{{showhideblockshidetitle}}" data-showtitle="{{showhideblocksshowtitle}}"> + <i class="fa fa-lg fa-{{showhideblocksicontype}}" aria-hidden="true"></i> + {{#showhideblockstext}} + <span class="showhideblocksdesc">{{showhideblockszoominicontitle}}</span> + {{/showhideblockstext}} + </div> + </li> + {{/showhideblocks}} + + {{#enablezoom}} + <li class="nav-item mx-0 hbll"> + <a class="nav-link moodlewidth" href="javascript:void(0);" title="{{#str}}fullscreen, theme_adaptable{{/str}}"> + <i class="fa fa-expand fa-lg" aria-hidden="true"></i> + {{#enablezoomshowtext}} + <span class="zoomdesc">{{#str}}fullscreen, theme_adaptable{{/str}}</span> + {{/enablezoomshowtext}} + </a> + </li> + <li class="nav-item mx-0 sbll"> + <a class="nav-link moodlewidth" href="javascript:void(0);" title="{{#str}}standardview, theme_adaptable{{/str}}"> + <i class="fa fa-compress fa-lg" aria-hidden="true"></i> + {{#enablezoomshowtext}} + <span class="zoomdesc">{{#str}}standardview, theme_adaptable{{/str}}</span> + {{/enablezoomshowtext}} + </a> + </li> + {{/enablezoom}} + </ul> + </div> + </div> + </div> +</div> diff --git a/theme/adaptable/templates/headersearch.mustache b/theme/adaptable/templates/headersearch.mustache new file mode 100644 index 0000000..f697c0a --- /dev/null +++ b/theme/adaptable/templates/headersearch.mustache @@ -0,0 +1,38 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/headersearch + + Header search. + + Example context (json): + { + "pagelayout": "pagelayoutoriginal", + "url": "http://localhost" + } +}} +<div class="searchbox {{pagelayout}} d-none d-lg-block"> + <form action="{{{url}}}"> + <label class="hidden" for="search-1" style="display: none;">{{#str}}searchcourses, theme_adaptable{{/str}}</label> + <div class="search-box grey-box bg-white clear-fix"> + <input placeholder="{{#str}}searchcourses, theme_adaptable{{/str}}" accesskey="6" class="search_tour bg-white no-border left search-box__input ui-autocomplete-input" type="text" name="search" id="search-1" autocomplete="off"> + <button title="{{#str}}searchcourses, theme_adaptable{{/str}}" type="submit" class="no-border bg-white pas search-box__button"> + <abbr class="fa fa-search" title="{{#str}}searchcourses, theme_adaptable{{/str}}"></abbr> + </button> + </div> + </form> +</div> diff --git a/theme/adaptable/templates/headersocial.mustache b/theme/adaptable/templates/headersocial.mustache new file mode 100644 index 0000000..81fd070 --- /dev/null +++ b/theme/adaptable/templates/headersocial.mustache @@ -0,0 +1,30 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/headersocial + + Header social. + + Example context (json): + { + "classes": "", + "pageheaderoriginal": true + } +}} +<div class="socialbox socialicons {{classes}} {{#pageheaderoriginal}}text-right{{/pageheaderoriginal}}{{^pageheaderoriginal}}text-left{{/pageheaderoriginal}}"> + {{{output.socialicons}}} +</div> diff --git a/theme/adaptable/templates/headerstyleone.mustache b/theme/adaptable/templates/headerstyleone.mustache new file mode 100644 index 0000000..c1c773d --- /dev/null +++ b/theme/adaptable/templates/headerstyleone.mustache @@ -0,0 +1,152 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/headerstyleone + + Header style one. + + Example context (json): + { + "headerbg": "", + "sitelogo": "", + "sitetitle": "Adaptable", + "shownavbar": true, + "responsivesearchicon": "", + "coursesearch": "", + "nonavbar": false, + "langmenu": false, + "loginoruser": "", + "menuslinkright": false, + "pageheaderoriginal": true, + "socialorsearch": "" + } +}} +<header id="adaptable-page-header-wrapper"{{{headerbg}}}> + <div id="above-header"> + <div class="container"> + <nav class="navbar navbar-expand btco-hover-menu"> + {{#shownavbar}} + <div id="adaptable-page-header-nav-drawer" data-region="drawer-toggle" class="d-lg-none mr-3"> + <button id="drawer" aria-expanded="false" aria-controls="nav-drawer" type="button" class="nav-link float-sm-left mr-1" data-side="left"> + <i class="fa fa-bars fa-fw" aria-hidden="true"></i> + <span class="sr-only">{{#str}}sidepanel{{/str}}</span> + </button> + </div> + {{/shownavbar}} + + <div class="collapse navbar-collapse"> + {{^menuslinkright}} + <div class="my-auto m-1">{{{output.get_top_menus}}}</div> + {{/menuslinkright}} + <ul class="navbar-nav ml-auto my-auto"> + <li class="pull-left"> + {{{output.user_menu}}} + </li> + + {{#menuslinkright}} + <li class="my-auto m-1">{{{output.get_top_menus}}}</li> + {{/menuslinkright}} + + <li class="nav-item mx-md-1 my-auto{{responsivesearchicon}}"> + <a class="nav-link" href="{{coursesearch}}"> + <i class="icon fa fa-search fa-fw " title="{{#str}}search, theme_adaptable{{/str}}" aria-label="{{#str}}search, theme_adaptable{{/str}}"></i> + </a> + </li> + + {{! Remove Messages and Notifications icons when no navbar. }} + {{^nonavbar}} + <li class="my-auto mx-md-1">{{{output.navbar_plugin_output}}}</li> + {{/nonavbar}} + + {{#langmenu}} + <li class="nav-item dropdown ml-2 my-auto">{{{output.lang_menu}}}</li> + {{/langmenu}} + + {{{output.page_heading_menu}}} + <li class="nav-item"> + {{{loginoruser}}} + </li> + + </ul> + </div> + </nav> + </div> + </div> + + <div id="page-header" class="container {{responsiveheader}}"> + <div class="row"> + {{#pageheaderoriginal}} + <div class="col-lg-4 p-0 my-auto"> + {{#sitelogo}} + <div class="d-flex justify-content-start bd-highlight"> + {{{sitelogo}}} + </div> + {{/sitelogo}} + <div id="course-header"> + {{{output.course_header}}} + </div> + </div> + <div class="col-lg-8"> + {{#sitetitle}} + <div class="d-flex justify-content-end bd-highlight"> + {{{sitetitle}}} + </div> + {{/sitetitle}} + {{! Remove Search Box or Social icons when no navbar. }} + {{^nonavbar}} + {{{socialorsearch}}} + {{/nonavbar}} + </div> + {{/pageheaderoriginal}} + + {{^pageheaderoriginal}} + <div class="col-lg-8"> + {{#sitetitle}} + <div class="d-flex justify-content-start bd-highlight"> + {{{sitetitle}}} + </div> + {{/sitetitle}} + {{! Remove Search Box or Social icons when no navbar. }} + {{^nonavbar}} + {{{socialorsearch}}} + {{/nonavbar}} + </div> + <div class="col-lg-4 p-0 my-auto"> + {{#sitelogo}} + <div class="d-flex justify-content-end bd-highlight"> + {{{sitelogo}}} + </div> + {{/sitelogo}} + </div> + {{/pageheaderoriginal}} + </div> + {{^pageheaderoriginal}} + <div class="row"> + <div class="col-12 p-0 my-auto"> + <div id="course-header"> + {{{output.course_header}}} + </div> + </div> + </div> + {{/pageheaderoriginal}} + </div> + + {{#shownavbar}} + {{> theme_adaptable/headernavbar}} + {{/shownavbar}} + +</header> diff --git a/theme/adaptable/templates/headerstyletwo.mustache b/theme/adaptable/templates/headerstyletwo.mustache new file mode 100644 index 0000000..40e720f --- /dev/null +++ b/theme/adaptable/templates/headerstyletwo.mustache @@ -0,0 +1,103 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/headerstyletwo + + Header style two. + + Example context (json): + { + "headerbg": "", + "sitelogo": "", + "sitetitle": "Adaptable", + "shownavbar": true, + "topmenus": "", + "responsivesearchicon": "", + "coursesearch": "", + "nonavbar": false, + "langmenu": false, + "loginoruser": "" + } +}} +<header id="adaptable-page-header-wrapper"{{{headerbg}}}> + <div id="header2" class="container pt-2"> + <div class="row"> + <div class="d-none d-lg-block col-lg-4"> + <div class="d-flex justify-content-start bd-highlight"> + {{#sitelogo}}{{{sitelogo}}}{{/sitelogo}} + {{#sitetitle}}{{{sitetitle}}}{{/sitetitle}} + <div id="course-header"> + {{{output.course_header}}} + </div> + </div> + </div> + + <div class="col-lg-8 p-0 my-auto"> + <nav class="navbar navbar-expand btco-hover-menu"> + {{#shownavbar}} + <div id="adaptable-page-header-nav-drawer" data-region="drawer-toggle" class="d-lg-none mr-3"> + <button id="drawer" aria-expanded="false" aria-controls="nav-drawer" type="button" class="nav-link float-sm-left mr-1" data-side="left"> + <i class="fa fa-bars fa-fw" aria-hidden="true"></i> + <span class="sr-only">{{#str}}sidepanel{{/str}}</span> + </button> + </div> + {{/shownavbar}} + + <div class="collapse navbar-collapse"> + <ul class="navbar-nav ml-auto"> + + <li class="my-auto"> + {{{output.search_box}}} + </li> + + <li class="my-auto m-1">{{{topmenus}}}</li> + + <li class="pull-left mr-2 my-auto"> + {{{output.user_menu}}} + </li> + + <li class="nav-item mx-md-1 my-auto{{responsivesearchicon}}"> + <a class="nav-link" href="{{coursesearch}}"> + <i class="icon fa fa-search fa-fw " title="{{#str}}search, theme_adaptable{{/str}}" aria-label="{{#str}}search, theme_adaptable{{/str}}"></i> + </a> + </li> + + {{! Remove Messages and Notifications icons when no navbar. }} + {{^nonavbar}} + <li class="my-auto mx-1">{{{output.navbar_plugin_output}}}</li> + {{/nonavbar}} + + {{#langmenu}} + {{{langmenu}}} + {{/langmenu}} + + {{{output.page_heading_menu}}} + <li class="nav-item"> + {{{loginoruser}}} + </li> + </ul> + </div> + </nav> + </div> + </div> + </div> + + {{#shownavbar}} + {{> theme_adaptable/headernavbar}} + {{/shownavbar}} + +</header> diff --git a/theme/adaptable/templates/overlaymenu.mustache b/theme/adaptable/templates/overlaymenu.mustache new file mode 100644 index 0000000..3904d7f --- /dev/null +++ b/theme/adaptable/templates/overlaymenu.mustache @@ -0,0 +1,64 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/overlaymenu + + Example context (json): + { + "showright": false, + "menuslinkicon": "fa-link", + "rows": { + "span": 12, + "menus": { + "menuitems": "Markup" + } + }, + "showlinktext": false + } +}} +{{#showright}} + <span class="nav-link" id="openoverlaymenu"> + <i class="fa {{{menuslinkicon}}} fa-lg"></i><span class="linksdesc">{{#showlinktext}}{{#str}}linksmenu, theme_adaptable{{/str}}{{/showlinktext}}</span> + </span> +{{/showright}} + +{{^showright}} + <span class="nav-link" id="openoverlaymenu"> + <i class="fa fa-link fa-lg"></i><span class="linksdesc">{{#showlinktext}}{{#str}}linksmenu, theme_adaptable{{/str}}{{/showlinktext}}</span> + </span> +{{/showright}} + +<div id="conditionalmenu" class="overlaymenu"> + <span id="overlaymenuclose" class="btn btn-default pull-right overlayclosebtn"> + <i class="fa fa-close"></i> + </span> + <div class="overlay-content"> + {{#rows}} + <div class="row-fluid"> + {{#menus}} + <div class="col-{{span}}"> + <ul class="overlaylist"> + {{#menuitems}} + {{> theme_adaptable/overlaymenuitem }} + {{/menuitems}} + </ul> + </div> + {{/menus}} + </div> + {{/rows}} + </div> +</div> \ No newline at end of file diff --git a/theme/adaptable/templates/overlaymenuitem.mustache b/theme/adaptable/templates/overlaymenuitem.mustache new file mode 100644 index 0000000..652e13f --- /dev/null +++ b/theme/adaptable/templates/overlaymenuitem.mustache @@ -0,0 +1,30 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/overlaymenuitem + + Example context (json): + { + "class": "level-1", + "url": "URL", + "title": "Title", + "texr": "Text" + } +}} +<li class="{{class}}"> + <a href="{{{url}}}" title={{{title}}}>{{{text}}}</a> +</li> \ No newline at end of file diff --git a/theme/adaptable/templates/savediscard.mustache b/theme/adaptable/templates/savediscard.mustache new file mode 100644 index 0000000..47e0f58 --- /dev/null +++ b/theme/adaptable/templates/savediscard.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/savediscard + + Save / Discard floating buttons template. + + Example context (json): + { + "topmargin": "5px", + "discardtext": "Cancel", + "savetext": "Save" + } +}} + +{{#js}} +require(['theme_adaptable/savebutton'], function(mod) { + mod.init(); +}); +{{/js}} + +<div id="savediscardsection" style="margin-top: {{{ topmargin }}};"> +<input id="adminresetbutton" class="form-submit" style="float: right; margin-right: 20px;" type="reset" + form="adminsettings" value=" {{{ discardtext }}}" /> +<input id="adminsubmitbutton" style="float: right; margin-right: 20px;" type="submit" form="adminsettings" value=" {{{ savetext }}}" /> +</div> diff --git a/theme/adaptable/templates/tabs.mustache b/theme/adaptable/templates/tabs.mustache new file mode 100644 index 0000000..8c93688 --- /dev/null +++ b/theme/adaptable/templates/tabs.mustache @@ -0,0 +1,63 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/tabs + + Custom tabs. + + Example context (json): + { + "containerid": "userprofiletabs", + "tabs": [ + { + "name": "tab1", + "displayname": "Tab1", + "content": "<p>Tab 1 content</p>", + "selected": true + }, + { + "name": "tab2", + "displayname": "Tab2", + "content": "<p>Tab 2 content</p>", + "selected": false + } + ] + } +}} +<style> +#{{containerid}} input.adaptabletab, +#{{containerid}} div.adaptable-tab { + display: none; +} + +{{#tabs}} +#{{name}}:checked~#adaptable-tab-{{name}} { + display: block; +} +{{/tabs}} +</style> +<div id="{{containerid}}" class="tabcontentcontainer"> + {{#tabs}} + <input id="{{name}}" type="radio" name="tabs" class="adaptabletab"{{#selected}} checked="checked"{{/selected}}> + <label for="{{name}}" class="adaptabletab">{{displayname}}</label> + {{/tabs}} + {{#tabs}} + <div id="adaptable-tab-{{name}}" class="adaptable-tab tab-panel py-3"> + {{{content}}} + </div> + {{/tabs}} +</div> \ No newline at end of file diff --git a/theme/adaptable/templates/tool_usertours/tourstep.mustache b/theme/adaptable/templates/tool_usertours/tourstep.mustache new file mode 100644 index 0000000..3ef3000 --- /dev/null +++ b/theme/adaptable/templates/tool_usertours/tourstep.mustache @@ -0,0 +1,65 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/tool_usertours/tourstep + + Template used as a template to render step bubbles with their content + in a user tour. + + Classes required for JS: + * arrow + * popover-title + * popover-content + * popover-navigation + * popover-title + + Data attributes required for JS: + * data-role=prev + * data-role=next + * data-role=pause-resume + * data-pause-text + * data-resume-text + * data-role=end + * data-placeholder=body + * data-placeholder=title + + Context variables required for this template: + * None + + Example context (json): + { + } + +}} +<div class="modal-dialog" role="document" data-role="flexitour-step"> + <div class="modal-content"> + <div class="tooltip-arrow" data-role="arrow"></div> + <div class="modal-header"> + <h5 class="modal-title" data-placeholder="title"></h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close" data-role="end"> + <i class="fa fa-times-circle-o" aria-hidden="true"></i> + </button> + </div> + <div class="modal-body" data-placeholder="body"> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-role="previous">{{# str }} previous, moodle {{/ str }}</button> + <button type="button" class="btn btn-primary" data-role="next">{{# str }} next, moodle {{/ str }}</button> + <button class="btn btn-secondary" data-role="end"> {{# str }} endtour, tool_usertours {{/ str }} </button> + </div> + </div> +</div> diff --git a/theme/adaptable/templates/usermenu.mustache b/theme/adaptable/templates/usermenu.mustache new file mode 100644 index 0000000..d43c0e4 --- /dev/null +++ b/theme/adaptable/templates/usermenu.mustache @@ -0,0 +1,56 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see <http://www.gnu.org/licenses/>. +}} +{{! + @template theme_adaptable/usermenu + + A custom bootstrap dropdown for Adaptable's user menu. + // TODO - This should use a core template at some point in the future. + + Example context (json): + { + "username": "A Student", + "showusername": true, + "usernamepositionleft": true, + "userpic": "URL", + "userprofilemenu": "Markup" + } +}} + +<a class="nav-link dropdown-toggle my-auto" role="button" href="#" +id="usermenu" data-toggle="dropdown" +aria-haspopup="true" aria-expanded="false" +aria-controls="usermenu-dropdown" +aria-label="{{#str}}usermenu, theme_adaptable{{/str}}" +title="{{{username}}}"> + {{#showusername}} + {{#usernamepositionleft}} + <span class="d-none d-md-inline-block mx-1">{{username}}</span> + {{/usernamepositionleft}} + {{/showusername}} + {{{userpic}}} + {{#showusername}} + {{^usernamepositionleft}} + <span class="d-none d-md-inline-block mx-1">{{username}}</span> + {{/usernamepositionleft}} + {{/showusername}} +</a> + +<div class="dropdown-menu dropdown-menu-right" role="menu" +id="usermenu-dropdown" +aria-labelledby="usermenu" > + {{{userprofilemenu}}} +</div> diff --git a/theme/adaptable/tests/PHPUNIT_COMMANDS.txt b/theme/adaptable/tests/PHPUNIT_COMMANDS.txt new file mode 100644 index 0000000..586b54c --- /dev/null +++ b/theme/adaptable/tests/PHPUNIT_COMMANDS.txt @@ -0,0 +1,13 @@ +Ref: https://docs.moodle.org/dev/PHPUnit + +composer install --dev + +php admin/tool/phpunit/cli/init.php + +vendor/bin/phpunit theme_adaptable_toolbox_testcase theme/adaptable/tests/adaptabletoolbox_test.php +or +vendor\bin\phpunit theme_adaptable_toolbox_testcase theme/adaptable/tests/adaptabletoolbox_test.php + +vendor/bin/phpunit --group theme_adaptable +or +vendor\bin\phpunit --group theme_adaptable diff --git a/theme/adaptable/tests/adaptabletoolbox_test.php b/theme/adaptable/tests/adaptabletoolbox_test.php new file mode 100644 index 0000000..7ef86f0 --- /dev/null +++ b/theme/adaptable/tests/adaptabletoolbox_test.php @@ -0,0 +1,108 @@ +<?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/>. + +/** + * Define unit tests for the toolbox class. + * + * @package theme_adaptable + * @copyright © 2018 G J Barnard. + * @author G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +/** + * Toolbox unit tests for the Adaptable theme. + * @group theme_adaptable + * @copyright Copyright (c) 2017 Manoj Solanki (Coventry University) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_adaptable_toolbox_testcase extends advanced_testcase { + + protected function setUp() { + $this->resetAfterTest(true); + + set_config('theme', 'adaptable'); + } + + public function test_to_add_property() { + // Ref: http://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit. + // and http://php.net/manual/en/reflectionmethod.invoke.php. + $reflectionmethod = new ReflectionMethod('\theme_adaptable\toolbox', 'to_add_property'); + $reflectionmethod->setAccessible(true); + + // Correct ones.... + $this->assertTrue($reflectionmethod->invoke(null, 'p2url')); + $this->assertTrue($reflectionmethod->invoke(null, 'p2cap')); + $this->assertTrue($reflectionmethod->invoke(null, 'sliderh3color')); + $this->assertTrue($reflectionmethod->invoke(null, 'sliderh4color')); + $this->assertTrue($reflectionmethod->invoke(null, 'slidersubmitcolor')); + $this->assertTrue($reflectionmethod->invoke(null, 'slidersubmitbgcolor')); + $this->assertTrue($reflectionmethod->invoke(null, 'slider2h3color')); + $this->assertTrue($reflectionmethod->invoke(null, 'slider2h3bgcolor')); + $this->assertTrue($reflectionmethod->invoke(null, 'slider2h4color')); + $this->assertTrue($reflectionmethod->invoke(null, 'slider2h4bgcolor')); + $this->assertTrue($reflectionmethod->invoke(null, 'slideroption2submitcolor')); + $this->assertTrue($reflectionmethod->invoke(null, 'slideroption2color')); + $this->assertTrue($reflectionmethod->invoke(null, 'slideroption2a')); + + $this->assertTrue($reflectionmethod->invoke(null, 'alerttext2')); + $this->assertTrue($reflectionmethod->invoke(null, 'alertkey2')); + $this->assertTrue($reflectionmethod->invoke(null, 'alerttype3')); + $this->assertTrue($reflectionmethod->invoke(null, 'alertaccess7')); + $this->assertTrue($reflectionmethod->invoke(null, 'alertprofilefield11')); + + $this->assertTrue($reflectionmethod->invoke(null, 'analyticstext5')); + $this->assertTrue($reflectionmethod->invoke(null, 'analyticsprofilefield7')); + + $this->assertTrue($reflectionmethod->invoke(null, 'newmenu3title')); + $this->assertTrue($reflectionmethod->invoke(null, 'newmenu2')); + $this->assertTrue($reflectionmethod->invoke(null, 'newmenu4requirelogin')); + $this->assertTrue($reflectionmethod->invoke(null, 'newmenu1field')); + + $this->assertTrue($reflectionmethod->invoke(null, 'toolsmenu5title')); + $this->assertTrue($reflectionmethod->invoke(null, 'toolsmenu5')); + + $this->assertTrue($reflectionmethod->invoke(null, 'tickertext4')); + $this->assertTrue($reflectionmethod->invoke(null, 'tickertext4profilefield')); + + // Incorrect ones.... + $this->assertFalse($reflectionmethod->invoke(null, 'p2ur1')); + $this->assertFalse($reflectionmethod->invoke(null, 'p4cab')); + + $this->assertFalse($reflectionmethod->invoke(null, 'settingsalertbox12')); + $this->assertFalse($reflectionmethod->invoke(null, 'alerttext245')); + $this->assertFalse($reflectionmethod->invoke(null, 'alertkay2')); + $this->assertFalse($reflectionmethod->invoke(null, 'alertkey')); + $this->assertFalse($reflectionmethod->invoke(null, 'alerttype3const')); + $this->assertFalse($reflectionmethod->invoke(null, 'alertaccess7denied')); + $this->assertFalse($reflectionmethod->invoke(null, 'alertprofilefields11')); + + $this->assertFalse($reflectionmethod->invoke(null, 'analyticstext5string')); + $this->assertFalse($reflectionmethod->invoke(null, 'analyticsprofilefields7')); + + $this->assertFalse($reflectionmethod->invoke(null, 'newmenu3titles')); + $this->assertFalse($reflectionmethod->invoke(null, 'newmenus2')); + $this->assertFalse($reflectionmethod->invoke(null, 'newmenu4requirelogins')); + $this->assertFalse($reflectionmethod->invoke(null, 'newmenulfield')); + + $this->assertFalse($reflectionmethod->invoke(null, 'toolsmenu5tutle')); + $this->assertFalse($reflectionmethod->invoke(null, 'toolsmenus')); + + $this->assertFalse($reflectionmethod->invoke(null, 'tickertext4s')); + $this->assertFalse($reflectionmethod->invoke(null, 'tickertext4profilesfield')); + } +} diff --git a/theme/adaptable/version.php b/theme/adaptable/version.php new file mode 100644 index 0000000..6a0744e --- /dev/null +++ b/theme/adaptable/version.php @@ -0,0 +1,48 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Version details + * + * @package theme_adaptable + * @copyright 2015-2019 Jeremy Hopkins (Coventry University) + * @copyright 2015-2019 Fernando Acedo (3-bits.com) + * @copyright 2017-2019 Manoj Solanki (Coventry University) + * @copyright 2019-onwards G J Barnard - {@link http://moodle.org/user/profile.php?id=442195} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ + +defined('MOODLE_INTERNAL') || die; + +// The theme name. +$plugin->component = 'theme_adaptable'; + +// Adaptable version date (YYYYMMDDrr where rr is the release number). +$plugin->version = 2020073105; + +$plugin->requires = 2020061500.00; // 3.9 (Build: 20200615). + +// Adaptable version using SemVer (https://semver.org). +$plugin->release = '3.0.4'; + +// Adaptable maturity (do not use ALPHA or BETA versions in production sites). +$plugin->maturity = MATURITY_RC; + +// Adaptable dependencies (Only Boost as it's the parent theme). +$plugin->dependencies = array( + 'theme_boost' => 2020061500 +); -- GitLab